From 0a6c8ad287b5e8400c4da2aa28958c5e972fb42c Mon Sep 17 00:00:00 2001 From: Ian Webster Date: Fri, 21 Aug 2026 09:29:54 -0700 Subject: [PATCH 1/2] Save scan instructions before Deep workers start --- .../_bundled_plugin/scripts/workbench_cli.py | 3 +- .../_bundled_plugin/scripts/workbench_db.py | 3 +- sdk/typescript/src/api.ts | 37 ++++---- sdk/typescript/tests-ts/api.test.ts | 14 ++- .../tests-ts/deep-scan-workbench.test.ts | 95 ++++++++++++++++++- 5 files changed, 130 insertions(+), 22 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/workbench_cli.py b/sdk/typescript/_bundled_plugin/scripts/workbench_cli.py index 2fc1c8f86..00f3ad3fb 100644 --- a/sdk/typescript/_bundled_plugin/scripts/workbench_cli.py +++ b/sdk/typescript/_bundled_plugin/scripts/workbench_cli.py @@ -143,6 +143,7 @@ def parse_args(description: str) -> argparse.Namespace: register_cli_scan.add_argument("--scan-dir", required=True) register_cli_scan.add_argument("--repository", required=True) register_cli_scan.add_argument("--recipe-json", required=True) + register_cli_scan.add_argument("--user-context") register_cli_scan.add_argument("--parent-scan-id") register_cli_scan.add_argument("--archive-existing", action="store_true") register_cli_scan.add_argument("--archived-scan-dir") @@ -314,7 +315,7 @@ def parse_args(description: str) -> argparse.Namespace: if arguments.count("--user-context-stdin") != 1 or "--user-context" in arguments: parser.error("pass exactly one user-context transport") index = arguments.index("--user-context-stdin") - arguments[index : index + 1] = ["--user-context", sys.stdin.read()] + arguments[index] = "--user-context=" + sys.stdin.buffer.read().decode("utf-8") return parser.parse_args(arguments) diff --git a/sdk/typescript/_bundled_plugin/scripts/workbench_db.py b/sdk/typescript/_bundled_plugin/scripts/workbench_db.py index 5584d3a13..403271d7a 100644 --- a/sdk/typescript/_bundled_plugin/scripts/workbench_db.py +++ b/sdk/typescript/_bundled_plugin/scripts/workbench_db.py @@ -1691,10 +1691,11 @@ def register_cli_scan(connection: sqlite3.Connection, args: argparse.Namespace) scan_dir=scan_dir, ) connection.execute( - "UPDATE scans SET recipe_json = ?, parent_scan_id = ? WHERE id = ?", + "UPDATE scans SET recipe_json = ?, parent_scan_id = ?, user_context = ? WHERE id = ?", ( json.dumps(recipe, allow_nan=False, separators=(",", ":"), sort_keys=True), parent_scan_id, + args.user_context, scan_id, ), ) diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index 8331229f3..f6fc932e6 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -751,22 +751,27 @@ export class CodexSecurity { signal, failureMessage: "Could not save the Codex Security scan", }; - const registration = await workbench(workbenchOptions, [ - "register-cli-scan", - "--repository", - repo, - "--scan-dir", - scanDir, - "--recipe-json", - JSON.stringify(recipe), - ...(options.archiveExisting === true ? ["--archive-existing"] : []), - ...(archivedScanDir === null - ? [] - : ["--archived-scan-dir", archivedScanDir]), - ...(options.parentScanId === undefined - ? [] - : ["--parent-scan-id", options.parentScanId]), - ]); + const registration = await workbench( + workbenchOptions, + [ + "register-cli-scan", + "--repository", + repo, + "--scan-dir", + scanDir, + "--recipe-json", + JSON.stringify(recipe), + ...(options.scanPrompt === undefined ? [] : ["--user-context-stdin"]), + ...(options.archiveExisting === true ? ["--archive-existing"] : []), + ...(archivedScanDir === null + ? [] + : ["--archived-scan-dir", archivedScanDir]), + ...(options.parentScanId === undefined + ? [] + : ["--parent-scan-id", options.parentScanId]), + ], + options.scanPrompt, + ); const scanId = registration["scanId"]; const targetId = registration["targetId"]; const contract = registration["contract"]; diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index a6ecd98bd..10f4cab56 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -2096,7 +2096,7 @@ describe("CodexSecurity orchestration", () => { resolvePluginPython: async () => "/managed/python", prepareOutputDir: async () => scanDir, repositoryRevision: async () => "deadbeef", - runWorkbench: async (_options, args): Promise => { + runWorkbench: async (_options, args, input): Promise => { if (args[0] !== "register-cli-scan") { return { scanId: "scan_example_001", @@ -2104,6 +2104,8 @@ describe("CodexSecurity orchestration", () => { falsePositives: [], }; } + expect(args).not.toContain("--user-context-stdin"); + expect(input).toBeUndefined(); recipe = JSON.parse(args[args.indexOf("--recipe-json") + 1]!); return mockScanRegistration(args); }, @@ -3748,6 +3750,7 @@ describe("CodexSecurity orchestration", () => { }); test("provides authoritative knowledge-base context without retaining its documents", async () => { + const scanPrompt = "Review the synthetic authorization boundary."; const root = await temporaryDirectory(); const repository = join(root, "repository"); const codexHome = join(root, "codex-home"); @@ -3771,7 +3774,7 @@ describe("CodexSecurity orchestration", () => { resolvePluginPython: async () => "/managed/python", prepareOutputDir: async () => scanDir, repositoryRevision: async () => "deadbeef", - runWorkbench: async (_options, args): Promise => { + runWorkbench: async (_options, args, input): Promise => { if (args[0] === "get-scan-feedback") { return { scanId: "scan_example_001", @@ -3780,6 +3783,8 @@ describe("CodexSecurity orchestration", () => { }; } if (args[0] !== "register-cli-scan") return {}; + expect(args).toContain("--user-context-stdin"); + expect(input).toBe(scanPrompt); recipe = JSON.parse(args[args.indexOf("--recipe-json") + 1]!); return mockScanRegistration(args); }, @@ -3803,7 +3808,10 @@ describe("CodexSecurity orchestration", () => { ); await expect( - client.run(repository, { knowledgeBasePaths: [knowledgeBase] }), + client.run(repository, { + knowledgeBasePaths: [knowledgeBase], + scanPrompt, + }), ).resolves.toMatchObject({ threadId: "thread-1" }); expect(existsSync(knowledgeDirectory)).toBe(false); expect(prompt).toContain( diff --git a/sdk/typescript/tests-ts/deep-scan-workbench.test.ts b/sdk/typescript/tests-ts/deep-scan-workbench.test.ts index 5b00da0af..cc60442a9 100644 --- a/sdk/typescript/tests-ts/deep-scan-workbench.test.ts +++ b/sdk/typescript/tests-ts/deep-scan-workbench.test.ts @@ -9,7 +9,8 @@ import { import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, describe, expect, test } from "bun:test"; -import { PLUGIN_ROOT } from "./plugin-root.js"; +import { runWorkbench } from "../src/runtime.js"; +import { loadBundledRuntime, PLUGIN_ROOT } from "./plugin-root.js"; const originalClaimToken = "22222222-2222-4222-8222-222222222222"; const replacementClaimToken = "33333333-3333-4333-8333-333333333333"; @@ -186,6 +187,98 @@ test("recovers an interrupted copied Deep Scan publication", async () => { }); }); +test.each([ + ["supplied", " Review café authentication.\r\n\t"], + ["absent", undefined], +] as const)( + "preserves %s scan instructions from registration through the Deep worker prompt", + async (_label, scanPrompt) => { + const root = await realpath( + await mkdtemp(join(tmpdir(), "codex-security-deep-context-")), + ); + temporaryDirectories.push(root); + const repository = join(root, "repository"); + const scanDir = join(root, "scan"); + await mkdir(repository); + await mkdir(scanDir, { mode: 0o700 }); + await writeFile(join(repository, "source.py"), "# synthetic source\n"); + const python = Bun.which("python3") ?? Bun.which("python"); + expect(python).not.toBeNull(); + const command = (args: string[], input?: string) => + runWorkbench( + { + python: python!, + pluginRoot: PLUGIN_ROOT, + environment: { + ...process.env, + CODEX_SECURITY_STATE_DIR: join(root, "state"), + CODEX_HOME: join(root, "codex-home"), + }, + }, + args, + input, + ); + const registration = await command( + [ + "register-cli-scan", + "--repository", + repository, + "--scan-dir", + scanDir, + "--recipe-json", + JSON.stringify({ + config: {}, + mode: "deep", + repository, + target: { kind: "repository", paths: [] }, + }), + ...(scanPrompt === undefined ? [] : ["--user-context-stdin"]), + ], + scanPrompt, + ); + const scanId = registration["scanId"] as string; + const context = await command(["get-scan", "--scan-id", scanId]); + expect(context["scan"]).toMatchObject({ userContext: scanPrompt ?? null }); + + const begun = await command([ + "begin-deep-scan", + "--scan-id", + scanId, + "--thread-id", + "synthetic-thread", + "--scan-root", + join(root, "scans"), + "--available-parallelism", + "4", + "--workflow-version", + "deep-scan-mcp/v1", + ]); + const deepScan = begun["deepScan"] as Record; + + const templates = + /\/\/ templates\/deep-scan\/discovery\.md\n([\s\S]*?)\/\/ src\/deep-scan\/worker-runner\.ts/u.exec( + await loadBundledRuntime(), + )?.[1]; + expect(templates).toBeDefined(); + const renderDiscoveryPrompt = new Function( + `${templates}\nreturn renderDiscoveryPrompt;`, + )() as (input: Record) => string; + const prompt = renderDiscoveryPrompt({ + ...deepScan, + pluginRoot: PLUGIN_ROOT, + workerLabel: "synthetic-worker", + subagents: 0, + }); + const workerContext = JSON.parse( + /```json\n([\s\S]*?)\n```/u.exec(prompt)![1]!, + ); + expect(workerContext).toMatchObject({ + scanId, + userContext: scanPrompt ?? null, + }); + }, +); + describe("deep scan workbench ownership", () => { test("rejects completion before an SDK-created Deep Scan finishes", async () => { const root = await realpath( From 1f9c816baabaacc6c10a0b3fd92017b660cfc1b2 Mon Sep 17 00:00:00 2001 From: Ian Webster Date: Fri, 21 Aug 2026 13:47:27 -0700 Subject: [PATCH 2/2] test: cover large scan registration payloads --- sdk/typescript/tests-ts/deep-scan-workbench.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/tests-ts/deep-scan-workbench.test.ts b/sdk/typescript/tests-ts/deep-scan-workbench.test.ts index 6c57cd083..262de005a 100644 --- a/sdk/typescript/tests-ts/deep-scan-workbench.test.ts +++ b/sdk/typescript/tests-ts/deep-scan-workbench.test.ts @@ -230,7 +230,9 @@ test.each([ ], JSON.stringify({ recipe: { - config: {}, + config: { + developer_instructions: "Synthetic context. ".repeat(4_000), + }, mode: "deep", repository, target: { kind: "repository", paths: [] },