diff --git a/.github/workflows/prepublication-publish-checks.yml b/.github/workflows/prepublication-publish-checks.yml index f9a59147..77cb3238 100644 --- a/.github/workflows/prepublication-publish-checks.yml +++ b/.github/workflows/prepublication-publish-checks.yml @@ -91,6 +91,7 @@ jobs: - name: Run pre-publication publish worker env: + CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} SECURITY_SCAN_WORKER_TOKEN: ${{ secrets.SECURITY_SCAN_WORKER_TOKEN }} VIRUSTOTAL_API_KEY: ${{ secrets.VT_API_KEY }} diff --git a/scripts/security/prepublication-worker-workflow.test.ts b/scripts/security/prepublication-worker-workflow.test.ts index 6c42e612..24924c02 100644 --- a/scripts/security/prepublication-worker-workflow.test.ts +++ b/scripts/security/prepublication-worker-workflow.test.ts @@ -113,6 +113,7 @@ describe("pre-publication publish worker workflow", () => { "${{ vars.PREPUBLICATION_TRUFFLEHOG_IMAGE || 'ghcr.io/trufflesecurity/trufflehog:3.95.6@sha256:96f8429082cb2d4ae73b1096dcdb2f5aa139881d97042b0c5e5fa226a392e056' }}", }); expect(String(job.env?.PREPUBLICATION_TRUFFLEHOG_IMAGE)).toContain("@sha256:"); + expect(job.env).not.toHaveProperty("CODEX_API_KEY"); expect(job.env).not.toHaveProperty("OPENAI_API_KEY"); expect(job.env).not.toHaveProperty("SECURITY_SCAN_WORKER_TOKEN"); expect(job.env).not.toHaveProperty("CODEX_SECURITY_SCAN_TIMEOUT_MS"); @@ -134,6 +135,7 @@ describe("pre-publication publish worker workflow", () => { expect(steps.find((step) => step.name === "Install SkillSpector")).toBeUndefined(); expect(JSON.stringify(job)).not.toContain("CODEX_SECURITY_SCAN_SHADOW_CLAWSCAN"); expect(runStep?.env).toEqual({ + CODEX_API_KEY: "${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }}", OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}", SECURITY_SCAN_WORKER_TOKEN: "${{ secrets.SECURITY_SCAN_WORKER_TOKEN }}", VIRUSTOTAL_API_KEY: "${{ secrets.VT_API_KEY }}", @@ -144,6 +146,9 @@ describe("pre-publication publish worker workflow", () => { expect(stepUsesSecret(step, "SECURITY_SCAN_WORKER_TOKEN"), stepName).toBe( stepName === "Run pre-publication publish worker", ); + expect(stepUsesSecret(step, "CODEX_API_KEY"), stepName).toBe( + stepName === "Run pre-publication publish worker", + ); expect(stepUsesSecret(step, "OPENAI_API_KEY"), stepName).toBe( stepName === "Run pre-publication publish worker", ); diff --git a/scripts/security/run-prepublication-worker.test.ts b/scripts/security/run-prepublication-worker.test.ts index cea47e3b..1f1d656b 100644 --- a/scripts/security/run-prepublication-worker.test.ts +++ b/scripts/security/run-prepublication-worker.test.ts @@ -643,6 +643,61 @@ JSON } }); + it("preserves the redacted ClawScan judge failure reason", async () => { + const workspace = await tempDir(); + await mkdir(join(workspace, "artifact"), { recursive: true }); + await writeFile(join(workspace, "artifact", "SKILL.md"), "# Demo\n"); + const fakeClawScan = join(workspace, "fake-clawscan"); + await writeFile( + fakeClawScan, + `#!/usr/bin/env bash +set -euo pipefail +output="" +while [ "$#" -gt 0 ]; do + if [ "$1" = "--output" ]; then + output="$2" + break + fi + shift +done +cat > "$output" <<'JSON' +{"schemaVersion":"clawscan-run-v1","profile":"clawhub","scanners":{"clawscan-static":{"status":"completed"},"skillspector":{"status":"completed"},"virustotal":{"status":"completed"}},"judge":{"status":"failed","error":"Codex request was rate limited.","result":null}} +JSON +`, + ); + await chmod(fakeClawScan, 0o755); + const previousCommand = process.env.PREPUBLICATION_CLAWSCAN_COMMAND; + process.env.PREPUBLICATION_CLAWSCAN_COMMAND = fakeClawScan; + + try { + await expect( + runNativeClawScan( + { + job: { + _id: String(attempt.attemptId), + attempts: 1, + hasMaliciousSignal: false, + leaseToken: attempt.claimId, + source: "pre-publication", + targetKind: "skillVersion", + waitForVtUntil: 0, + }, + target: {}, + }, + workspace, + ), + ).resolves.toEqual({ + check: { + status: "failed", + summary: "ClawScan judge status was failed: Codex request was rate limited.", + }, + }); + } finally { + if (previousCommand === undefined) delete process.env.PREPUBLICATION_CLAWSCAN_COMMAND; + else process.env.PREPUBLICATION_CLAWSCAN_COMMAND = previousCommand; + } + }); + it("maps TruffleHog verified-secret exit code to a blocked result", async () => { const workspace = await tempDir(); await mkdir(join(workspace, "artifact"), { recursive: true }); diff --git a/scripts/security/run-prepublication-worker.ts b/scripts/security/run-prepublication-worker.ts index c6cab1dc..76d68600 100644 --- a/scripts/security/run-prepublication-worker.ts +++ b/scripts/security/run-prepublication-worker.ts @@ -405,12 +405,18 @@ function storedAnalysisFromClawScanArtifact(artifact: unknown): { const judge = asRecord(record?.judge); const result = asRecord(judge?.result); const judgeStatus = readString(judge, ["status"]); + const judgeError = readString(judge, ["error"]); const scannerFailures = collectClawScanScannerFailures(asRecord(record?.scanners)); if (scannerFailures.length > 0) { return { error: `ClawScan scanner did not complete: ${scannerFailures.join(", ")}` }; } if (judgeStatus !== "completed") { - return { error: `ClawScan judge status was ${judgeStatus ?? "missing"}` }; + return { + error: [ + `ClawScan judge status was ${judgeStatus ?? "missing"}`, + ...(judgeError ? [judgeError] : []), + ].join(": "), + }; } const verdict = readString(result, ["verdict", "status"]); if (!verdict) return { error: "ClawScan judge did not return a verdict" }; diff --git a/vitest.config.ts b/vitest.config.ts index 548af8a2..2139312e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -41,6 +41,7 @@ export default defineConfig({ "**/dist/**", "**/coverage/**", "**/convex/_generated/**", + ".github/scripts/catalog-feed-schema-version-guard.test.cjs", "packages/clawhub/**", "packages/clawhub-admin/test-artifact/**", "e2e/**",