From a74bfc8cc8071630e6e13c8402758ce8d03ab2e3 Mon Sep 17 00:00:00 2001 From: Dipesh Babu Date: Thu, 6 Aug 2026 18:41:28 -0400 Subject: [PATCH 1/2] Pass registered scan ID to agent prompt --- sdk/typescript/src/api.ts | 22 +++++---- sdk/typescript/tests-ts/api.test.ts | 73 ++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index 3e94f2dd..aa832458 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -633,14 +633,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, - ); - checkOpen(); const expectation: ScanExpectation = { repository: repo, repositoryRevision: await ( @@ -814,6 +806,15 @@ export class CodexSecurity { } activeScan = { id: scanId, options: workbenchOptions }; checkOpen(); + const basePrompt = await scanPrompt( + shellPluginRoot, + normalized, + mode, + scanId, + runtime.configPath !== undefined, + knowledgeBase !== null, + ); + checkOpen(); const feedback = await workbench( { ...workbenchOptions, @@ -1953,6 +1954,7 @@ async function scanPrompt( pluginRoot: string, target: NormalizedTarget, mode: ScanMode, + scanId: string, hasConfigPath = false, hasKnowledgeBase = false, ): Promise { @@ -1969,7 +1971,7 @@ 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 === "deep-security-scan" @@ -1981,7 +1983,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 c8045b47..c65455e2 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -2559,6 +2559,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"; @@ -3986,7 +4057,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", From c7e40ae7106394a551bc8c8b6a3f1f97bf3e23f3 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 11 Aug 2026 17:26:07 -0700 Subject: [PATCH 2/2] fix(scan): validate skills before registration Keep the registered scan ID authoritative while preserving fail-fast plugin validation and covering standard and deep scans with and without feedback. Co-authored-by: Carlos Acosta <93443910+charle-z@users.noreply.github.com> --- sdk/typescript/src/api.ts | 33 ++--- sdk/typescript/tests-ts/api.test.ts | 182 ++++++++++++++++++---------- 2 files changed, 134 insertions(+), 81 deletions(-) diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index ec11829e..4bb081be 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -663,6 +663,19 @@ export class CodexSecurity { `Shell-visible plugin root must be outside CODEX_HOME: ${canonicalShellPluginRoot}`, ); } + const skillName = skillNameFor(normalized, mode); + const skillPath = join(shellPluginRoot, "skills", skillName, "SKILL.md"); + const skillMetadata = await lstat(skillPath).catch(() => null); + if ( + skillMetadata === null || + !skillMetadata.isFile() || + skillMetadata.isSymbolicLink() + ) { + throw new IncompleteScanError( + `Installed plugin is missing scan skill: ${skillName}`, + ); + } + checkOpen(); const expectation: ScanExpectation = { repository: repo, repositoryRevision: await ( @@ -857,10 +870,10 @@ export class CodexSecurity { } activeScan = { id: scanId, options: workbenchOptions }; checkOpen(); - const basePrompt = await scanPrompt( - shellPluginRoot, + const basePrompt = scanPrompt( normalized, mode, + skillName, scanId, runtime.configPath !== undefined, knowledgeBase !== null, @@ -2118,29 +2131,21 @@ function trustedAccessWarning( return `Some cybersecurity requests or findings may be refused because ${access} could not be verified. Check ${action} or apply at ${applicationUrl}.`; } -async function scanPrompt( - pluginRoot: string, +function scanPrompt( target: NormalizedTarget, mode: ScanMode, + skillName: string, scanId: string, hasConfigPath = false, hasKnowledgeBase = false, additionalPrompt?: string, -): Promise { - const skillName = skillNameFor(target, mode); - const skillPath = join(pluginRoot, "skills", skillName, "SKILL.md"); - const metadata = await lstat(skillPath).catch(() => null); - if (metadata === null || !metadata.isFile() || metadata.isSymbolicLink()) { - throw new IncompleteScanError( - `Installed plugin is missing scan skill: ${skillName}`, - ); - } +): string { return [ `Use the installed $codex-security:${skillName} skill at "$CODEX_SECURITY_PLUGIN_ROOT/skills/${skillName}/SKILL.md".`, "Run this Codex Security scan non-interactively.", ...(mode === "deep" ? [ - `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.`, + `The SDK has already registered this scan. Call start_codex_security_deep_scan with ${JSON.stringify({ scanId })}; never pass targetPath or create another scan.`, ] : skillName === "security-scan" ? [ diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 1ef3426f..03b373af 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -2656,76 +2656,124 @@ 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"); - }, - }), - }), + test("rejects a missing scan skill before registering a scan", async () => { + const root = await temporaryDirectory(); + const repository = join(root, "repository"); + const codexHome = join(root, "codex-home"); + const pluginRoot = join(root, "plugin-without-skills"); + const scanDir = join(root, "scan"); + await mkdir(repository); + await mkdir(codexHome); + await mkdir(pluginRoot); + await mkdir(scanDir, { mode: 0o700 }); + const runtime = preparedRuntime(codexHome); + const commands: string[] = []; + const client = new TestClient( + {}, + { + environment: {}, + prepareRuntime: async () => ({ + ...runtime, + plugin: { + ...(runtime["plugin"] as Record), + pluginRoot, + marketplaceRoot: pluginRoot, + installedRoot: pluginRoot, }, - ); + }), + resolvePluginPython: async () => "/managed/python", + prepareOutputDir: async () => scanDir, + repositoryRevision: async () => "deadbeef", + runWorkbench: async (_options: unknown, args: readonly string[]) => { + commands.push(args[0]!); + return args[0] === "register-cli-scan" + ? mockScanRegistration(args) + : {}; + }, + }, + ); - 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.`, + await expect(client.run(repository)).rejects.toThrow( + "Installed plugin is missing scan skill: security-scan", + ); + expect(commands).toEqual([]); + await client.close(); + }); + + test.each([ + ["standard without feedback", "standard", false], + ["standard with feedback", "standard", true], + ["deep without feedback", "deep", false], + ["deep with feedback", "deep", true], + ] as const)( + "uses the registered scan ID in %s", + async (_scenario, mode, withFeedback) => { + const root = await temporaryDirectory(); + const repository = join(root, "repository"); + const codexHome = join(root, "codex-home"); + const scanDir = join(root, "scan"); + const scanId = "123e4567-e89b-12d3-a456-426614174000"; + 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") { + const deepScanArguments = prompt.match( + /start_codex_security_deep_scan with (\{[^\n]+\});/, ); - 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(); + expect(deepScanArguments).not.toBeNull(); + expect(JSON.parse(deepScanArguments![1]!)).toEqual({ scanId }); + } else { + expect(prompt).not.toContain("start_codex_security_deep_scan"); } - } - }); + expect(prompt.includes("false_positive_feedback.json")).toBe( + withFeedback, + ); + await client.close(); + }, + ); test.each([ ["semantic matching fails", "matcher", "matcher unavailable"], @@ -4747,7 +4795,7 @@ describe("CodexSecurity orchestration", () => { ); expect(prompt).toContain("$codex-security:deep-security-scan"); expect(prompt).toContain( - 'start_codex_security_deep_scan with { scanId: "scan_example_001" }', + 'start_codex_security_deep_scan with {"scanId":"scan_example_001"}', ); expect(prompt).not.toContain( "This exhaustive scan authorizes the delegated-worker phases",