diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index cb90e089..b7a90921 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -637,15 +637,6 @@ export class CodexSecurity { `Shell-visible plugin root must be outside CODEX_HOME: ${canonicalShellPluginRoot}`, ); } - const basePrompt = await scanPrompt( - shellPluginRoot, - normalized, - mode, - runtime.configPath !== undefined, - knowledgeBase !== null, - options.scanPrompt, - ); - checkOpen(); const expectation: ScanExpectation = { repository: repo, repositoryRevision: await ( @@ -840,6 +831,16 @@ export class CodexSecurity { } activeScan = { id: scanId, options: workbenchOptions }; checkOpen(); + const basePrompt = await scanPrompt( + shellPluginRoot, + normalized, + mode, + scanId, + runtime.configPath !== undefined, + knowledgeBase !== null, + options.scanPrompt, + ); + checkOpen(); const feedback = await workbench( { ...workbenchOptions, @@ -2023,6 +2024,7 @@ async function scanPrompt( pluginRoot: string, target: NormalizedTarget, mode: ScanMode, + scanId: string, hasConfigPath = false, hasKnowledgeBase = false, additionalPrompt?: string, @@ -2040,11 +2042,11 @@ async function scanPrompt( "Run this Codex Security scan non-interactively.", ...(mode === "deep" ? [ - 'The SDK has already registered this scan. Call start_codex_security_deep_scan with { scanId: "$CODEX_SECURITY_SCAN_ID" }; never pass targetPath or create another scan.', + `The SDK has already registered this scan. Call start_codex_security_deep_scan with { scanId: ${JSON.stringify(scanId)} }; never pass targetPath or create another scan.`, ] : skillName === "security-scan" ? [ - 'The SDK has already registered this scan. Use exactly "$CODEX_SECURITY_SCAN_ID" and "$CODEX_SECURITY_SCAN_DIR"; never call a scan-start or completion tool, and leave finalization to the SDK.', + `The SDK has already registered this scan. Use exactly ${JSON.stringify(scanId)} and "$CODEX_SECURITY_SCAN_DIR"; never call a scan-start or completion tool, and leave finalization to the SDK.`, ] : []), ...(skillName === "security-scan" @@ -2060,7 +2062,7 @@ async function scanPrompt( 'Use "$PYTHON" as for every plugin helper; replace any literal python or python3 helper invocation with this exact interpreter.', 'Repository root: "$CODEX_SECURITY_REPOSITORY"', 'Use this exact scan directory for all scan output: "$CODEX_SECURITY_SCAN_DIR"', - 'Use exactly "$CODEX_SECURITY_SCAN_ID" as the scan ID in the manifest, findings, and coverage.', + `Use exactly ${JSON.stringify(scanId)} as the scan ID in the manifest, findings, and coverage.`, 'Use exactly "$CODEX_SECURITY_TARGET_ID" as scan.target.targetId; do not derive a different target ID.', 'Use exactly "$CODEX_SECURITY_TARGET_DISPLAY_NAME" as scan.target.displayName; do not infer a display name from the Git remote.', 'Use exactly "$CODEX_SECURITY_TARGET_KIND" as scan.target.kind; do not infer the target kind from the checkout.', diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index ec8076ae..98d5aa32 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -2632,6 +2632,77 @@ describe("CodexSecurity orchestration", () => { await client.close(); }); + test("uses the registered scan ID in standard and deep prompts", async () => { + const scanId = "123e4567-e89b-12d3-a456-426614174000"; + + for (const mode of ["standard", "deep"] as const) { + for (const withFeedback of [false, true]) { + const root = await temporaryDirectory(); + const repository = join(root, "repository"); + const codexHome = join(root, "codex-home"); + const scanDir = join(root, "scan"); + await mkdir(repository); + await mkdir(codexHome); + await mkdir(scanDir, { mode: 0o700 }); + let prompt = ""; + const client = new TestClient( + {}, + { + environment: {}, + prepareRuntime: async () => preparedRuntime(codexHome), + resolvePluginPython: async () => "/managed/python", + prepareOutputDir: async () => scanDir, + repositoryRevision: async () => "deadbeef", + runWorkbench: async ( + _options: unknown, + args: readonly string[], + ) => { + if (args[0] === "register-cli-scan") { + return { ...mockScanRegistration(args), scanId }; + } + if (args[0] === "get-scan-feedback") { + return { + scanId, + targetId: "target_sha256_example", + falsePositives: withFeedback + ? [{ reason: "The finding is no longer reproducible." }] + : [], + }; + } + return {}; + }, + createCodex: () => ({ + startThread: () => ({ + id: null, + async runStreamed(input: string) { + prompt = input; + throw new Error("prompt captured"); + }, + }), + }), + }, + ); + + await expect(client.run(repository, { mode })).rejects.toThrow( + "prompt captured", + ); + expect(prompt).toContain( + `Use exactly "${scanId}" as the scan ID in the manifest, findings, and coverage.`, + ); + expect(prompt).not.toContain("$CODEX_SECURITY_SCAN_ID"); + if (mode === "deep") { + expect(prompt).toContain( + `start_codex_security_deep_scan with { scanId: "${scanId}" }`, + ); + } + if (withFeedback) { + expect(prompt).toContain("false_positive_feedback.json"); + } + await client.close(); + } + } + }); + test("rejects feedback from another scan or invalid reviewer feedback", async () => { const scanId = "scan_example_001"; const targetId = "target_sha256_example"; @@ -4319,7 +4390,7 @@ describe("CodexSecurity orchestration", () => { ); expect(prompt).toContain("$codex-security:deep-security-scan"); expect(prompt).toContain( - 'start_codex_security_deep_scan with { scanId: "$CODEX_SECURITY_SCAN_ID" }', + 'start_codex_security_deep_scan with { scanId: "scan_example_001" }', ); expect(prompt).not.toContain( "This exhaustive scan authorizes the delegated-worker phases",