fix(release): keep plugin security packing inert

This commit is contained in:
Vincent Koc
2026-08-20 21:06:51 -07:00
parent bf65391b7e
commit dc15c4f660
5 changed files with 181 additions and 33 deletions
+106 -1
View File
@@ -95,6 +95,7 @@ function writePluginArtifact(params: {
writeFileSync(
join(artifactDir, "plugin-npm-security-artifact.json"),
`${JSON.stringify({
artifactKind: "inert-package-input",
candidateSha: CANDIDATE_SHA,
extensionId: params.extensionId,
packageDir: `extensions/${params.extensionId}`,
@@ -109,6 +110,7 @@ function writePluginArtifact(params: {
);
return {
artifact: {
artifactKind: "inert-package-input" as const,
artifactDir,
candidateSha: CANDIDATE_SHA,
extensionId: params.extensionId,
@@ -222,6 +224,109 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
}
});
it("packs candidate input without running lifecycle or asset build hooks", () => {
const candidateRoot = tempDirs.make("openclaw-plugin-security-inert-pack-");
const outputDir = tempDirs.make("openclaw-plugin-security-inert-output-");
const packageDir = join(candidateRoot, "extensions", "inert");
const packlistHelper = join(candidateRoot, "trusted-packlist-helper.mjs");
const markers = Object.fromEntries(
["asset", "prepare", "prepack", "postpack"].map((name) => [
name,
join(candidateRoot, `${name}-ran`),
]),
);
initGitRepo(candidateRoot);
mkdirSync(packageDir, { recursive: true });
const markerCommand = (marker: string) =>
`node -e "require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'ran')"`;
writeFileSync(
join(packageDir, "package.json"),
`${JSON.stringify({
name: "@openclaw/test-inert-pack",
openclaw: {
assetScripts: { build: markerCommand(markers.asset!) },
release: { publishToNpm: true },
},
scripts: {
postpack: markerCommand(markers.postpack!),
prepack: markerCommand(markers.prepack!),
prepare: markerCommand(markers.prepare!),
},
version: "1.0.0",
})}\n`,
"utf8",
);
writeFileSync(join(packageDir, "index.ts"), "export const inert = true;\n", "utf8");
writeFileSync(
join(packageDir, "openclaw.plugin.json"),
`${JSON.stringify({ id: "inert" })}\n`,
"utf8",
);
writeFileSync(
packlistHelper,
'process.stdout.write(JSON.stringify(["index.ts", "openclaw.plugin.json", "package.json"]));\n',
"utf8",
);
execFileSync("git", ["-C", candidateRoot, "add", "."]);
execFileSync("git", ["-C", candidateRoot, "commit", "--quiet", "-m", "fixture"]);
const candidateSha = execFileSync("git", ["-C", candidateRoot, "rev-parse", "HEAD"], {
encoding: "utf8",
}).trim();
const toolingSha = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
const result = spawnSync(
process.execPath,
[
"--import",
"tsx",
"scripts/plugin-npm-security-prepare.mts",
"prepare",
"--candidate-root",
candidateRoot,
"--candidate-sha",
candidateSha,
"--extension-id",
"inert",
"--output-dir",
outputDir,
"--package-dir",
"extensions/inert",
"--package-name",
"@openclaw/test-inert-pack",
"--tooling-sha",
toolingSha,
],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
NODE_ENV: "test",
OPENCLAW_PLUGIN_SECURITY_TEST_PACKLIST_HELPER: packlistHelper,
},
},
);
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
for (const marker of Object.values(markers)) {
expect(existsSync(marker)).toBe(false);
}
const metadata = JSON.parse(
readFileSync(join(outputDir, "plugin-npm-security-artifact.json"), "utf8"),
) as { artifactKind?: unknown; tarballName?: unknown };
expect(metadata.artifactKind).toBe("inert-package-input");
expect(typeof metadata.tarballName).toBe("string");
const staged = stageScannerRelevantPluginTarballFiles(
join(outputDir, String(metadata.tarballName)),
);
try {
expect(staged.packedFiles).toContain("index.ts");
expect(staged.packedFiles.some((file) => file.startsWith("dist/"))).toBe(false);
} finally {
rmSync(staged.stageDir, { force: true, recursive: true });
}
});
it("rejects malformed, unsafe, duplicate, and excessive packlist entries", () => {
for (const malformed of [
null,
@@ -351,7 +456,7 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
expect(normalizePackedFindingPath("dist/service-malware.js")).toBe("dist/service-malware.js");
});
it("finds malicious final-artifact code, ignores candidate scanner replacements, and fails slow", async () => {
it("finds malicious packed input code, ignores candidate scanner replacements, and fails slow", async () => {
const marker = join(tempDirs.make("openclaw-plugin-npm-security-marker-"), "ran");
const malicious = writePluginArtifact({
extensionId: "malicious",
@@ -281,6 +281,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
it("keeps the trusted security scanner outside the candidate test process", () => {
const pluginWorkflow = readPluginPrereleaseWorkflow();
const pluginSource = readFileSync(".github/workflows/plugin-prerelease.yml", "utf8");
const securityPrepareSource = readFileSync("scripts/plugin-npm-security-prepare.mts", "utf8");
const resolver = pluginWorkflow.jobs["resolve-candidate"];
const securityPlan = pluginWorkflow.jobs["plugin-npm-security-plan"];
const securityPackage = pluginWorkflow.jobs["plugin-npm-security-package"];
@@ -299,7 +300,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
(step: WorkflowStep) => step.name === "Install trusted scanner dependencies",
);
const runSecurityScan = securityScan.steps.find(
(step: WorkflowStep) => step.name === "Scan immutable plugin tarballs",
(step: WorkflowStep) => step.name === "Scan inert plugin package inputs",
);
const uploadReport = securityScan.steps.find(
(step: WorkflowStep) => step.name === "Upload plugin npm security scan report",
@@ -367,6 +368,15 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
expect(securityScan.needs).not.toContain("preflight");
expect(securityPackage.permissions).toEqual({ contents: "read" });
expect(securityPackage.secrets).toBeUndefined();
expect(JSON.stringify(securityPackage)).not.toContain("plugin-npm-publish.sh");
expect(JSON.stringify(securityPackage)).not.toContain("plugin-npm-runtime-build");
expect(JSON.stringify(securityPackage)).not.toContain("generate-npm-package-lock");
expect(securityPrepareSource).not.toContain("plugin-npm-publish.sh");
expect(securityPrepareSource).not.toContain("plugin-npm-runtime-build");
expect(securityPrepareSource).not.toContain("generate-npm-package-lock");
expect(securityPrepareSource).not.toMatch(/\bnpmArgs:\s*\[\s*["'](?:ci|install)["']/u);
expect(securityPrepareSource).toContain('"--ignore-scripts"');
expect(securityPrepareSource).toContain('"--workspaces=false"');
expect(nodeShard.needs).toEqual(["resolve-candidate", "preflight"]);
expect(runNodeShard?.run).toContain('spawnSync("pnpm", ["test", "--", ...configs]');
expect(pluginSource).not.toContain("npm-install-security-scan.release.test.ts");