From 19fce8558ae717ed0674cf956d688a95a87c64fd Mon Sep 17 00:00:00 2001 From: Carlos Acosta <93443910+charle-z@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:07:45 +0000 Subject: [PATCH 1/4] fix(scan): preserve snapshot digest for diff scans --- .../_bundled_plugin/scripts/workbench_db.py | 22 +- .../scripts/workbench_scan_start.py | 9 + .../scripts/workbench_target.py | 8 + .../tests-ts/compact-diff-scan.test.ts | 197 +++++++++++++++++- 4 files changed, 223 insertions(+), 13 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/workbench_db.py b/sdk/typescript/_bundled_plugin/scripts/workbench_db.py index 2bf47326..f45082e3 100644 --- a/sdk/typescript/_bundled_plugin/scripts/workbench_db.py +++ b/sdk/typescript/_bundled_plugin/scripts/workbench_db.py @@ -110,6 +110,7 @@ git_submodule_paths, git_target_metadata, git_worktree_context, + immutable_diff_content_digest, require_git_worktree_head, require_remediation_target, require_scan_target_identity, @@ -337,12 +338,22 @@ def require_diff_target( ) if supplied_base != parent: raise SystemExit("Commit base revision must match the selected commit's parent.") - return {"kind": kind, "baseRevision": parent, "headRevision": head} + return { + "kind": kind, + "baseRevision": parent, + "headRevision": head, + "contentDigest": immutable_diff_content_digest(kind, parent, head), + } base = resolve_git_commit(target, base_revision or "", "Base revision") head = resolve_git_commit(target, head_revision or "", "Head revision") if base == head: raise SystemExit("Base and head revisions must identify different commits.") - return {"kind": kind, "baseRevision": base, "headRevision": head} + return { + "kind": kind, + "baseRevision": base, + "headRevision": head, + "contentDigest": immutable_diff_content_digest(kind, base, head), + } def inspect_setup_values( @@ -504,8 +515,9 @@ def workbench_completion_binding(scan: sqlite3.Row, completed_at: str) -> dict[s if scan["mode"] == "diff": target["baseRevision"] = scan["diff_base_revision"] target["headRevision"] = scan["diff_head_revision"] - if scan["diff_target_kind"] == "working_tree" and scan["diff_content_digest"]: - target["snapshotDigest"] = scan["diff_content_digest"] + diff_target = stored_diff_target(scan) + if diff_target and diff_target.get("contentDigest"): + target["snapshotDigest"] = diff_target["contentDigest"] else: if scan["target_revision"] != "unversioned": target["revision"] = scan["target_revision"] @@ -1622,6 +1634,8 @@ def register_cli_scan(connection: sqlite3.Connection, args: argparse.Namespace) if head != current_head: raise SystemExit("Working-tree HEAD changed before the scan started.") diff_target["contentDigest"] = worktree_content_digest(repository) + else: + diff_target["contentDigest"] = immutable_diff_content_digest("range", base, head) mode = "diff" if diff_target is not None else recipe["mode"] target_identity = scan_target_identity(repository, diff_target) scope_file_count = ( diff --git a/sdk/typescript/_bundled_plugin/scripts/workbench_scan_start.py b/sdk/typescript/_bundled_plugin/scripts/workbench_scan_start.py index 7cb1f621..a463d5ed 100644 --- a/sdk/typescript/_bundled_plugin/scripts/workbench_scan_start.py +++ b/sdk/typescript/_bundled_plugin/scripts/workbench_scan_start.py @@ -20,6 +20,7 @@ from workbench_target import ( directory_content_digest, git_revision, + immutable_diff_content_digest, worktree_content_digest, ) from workbench_validation import optional_text, user_text @@ -83,6 +84,14 @@ def stored_diff_target(row: sqlite3.Row) -> dict[str, str] | None: } if row["diff_content_digest"]: target["contentDigest"] = row["diff_content_digest"] + elif ( + target["kind"] in {"commit", "range"} + and target["baseRevision"] + and target["headRevision"] + ): + target["contentDigest"] = immutable_diff_content_digest( + target["kind"], target["baseRevision"], target["headRevision"] + ) return target diff --git a/sdk/typescript/_bundled_plugin/scripts/workbench_target.py b/sdk/typescript/_bundled_plugin/scripts/workbench_target.py index bd19fb88..3e72c774 100644 --- a/sdk/typescript/_bundled_plugin/scripts/workbench_target.py +++ b/sdk/typescript/_bundled_plugin/scripts/workbench_target.py @@ -80,6 +80,14 @@ def update_digest_field(digest: Any, label: bytes, value: bytes) -> None: digest.update(value) +def immutable_diff_content_digest(kind: str, base_revision: str, head_revision: str) -> str: + payload = "\0".join( + ("codex-security-diff/v1", kind, base_revision, head_revision) + ).encode("utf-8") + digest = hashlib.sha256(payload).hexdigest() + return f"codex-security-snapshot/v1:sha256:{digest}" + + def worktree_content_digest(target: Path) -> str: require_clean_submodule_worktrees(target) repository, pathspec = git_worktree_context(target) diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 87fcae93..72577c72 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -1,6 +1,7 @@ import { execFileSync, spawn, spawnSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { + cpSync, mkdirSync, mkdtempSync, readFileSync, @@ -71,6 +72,36 @@ function python(script: string, ...args: string[]) { ); } +function pythonWithState(stateDir: string, script: string, ...args: string[]) { + const command = + Bun.which("python3") ?? Bun.which("python") ?? Bun.which("py"); + expect(command).not.toBeNull(); + return spawnSync( + command!, + ["-B", join(PLUGIN_ROOT, "scripts", script), ...args], + { + encoding: "utf8", + env: { ...process.env, CODEX_SECURITY_STATE_DIR: stateDir }, + }, + ); +} + +function immutableDiffDigest( + kind: "commit" | "range", + baseRevision: string, + headRevision: string, +): string { + const digest = createHash("sha256") + .update("codex-security-diff/v1\0") + .update(kind) + .update("\0") + .update(baseRevision) + .update("\0") + .update(headRevision) + .digest("hex"); + return `codex-security-snapshot/v1:sha256:${digest}`; +} + function candidate(path: string): JsonObject { return { cwe_ids: [], @@ -281,6 +312,162 @@ describe("compact diff scan", () => { expect(escaped.stderr).toContain("in-scope file row 1"); }); + test("derives canonical digests for immutable and working-tree diffs", () => { + const { repository } = createRepository(); + writeSource(repository, "app.ts", "export const value = 1;\n"); + git(repository, "add", "."); + git(repository, "commit", "-qm", "first"); + const first = git(repository, "rev-parse", "HEAD"); + writeSource(repository, "app.ts", "export const value = 2;\n"); + git(repository, "add", "."); + git(repository, "commit", "-qm", "second"); + const second = git(repository, "rev-parse", "HEAD"); + writeSource(repository, "app.ts", "export const value = 3;\n"); + git(repository, "add", "."); + git(repository, "commit", "-qm", "third"); + const third = git(repository, "rev-parse", "HEAD"); + + const inspect = ( + kind: "commit" | "range" | "working_tree", + baseRevision?: string, + headRevision?: string, + ): JsonObject => { + const args = [ + "inspect-setup", + "--target-path", + repository, + "--scope", + ".", + "--mode", + "diff", + "--diff-target-kind", + kind, + ]; + if (baseRevision !== undefined) + args.push("--diff-base-revision", baseRevision); + if (headRevision !== undefined) + args.push("--diff-head-revision", headRevision); + const result = python("workbench_db.py", ...args); + expect(result.status, result.stderr).toBe(0); + return (JSON.parse(result.stdout) as { diffTarget: JsonObject }).diffTarget; + }; + + const commit = inspect("commit", second, third); + const range = inspect("range", second, third); + const repeatedRange = inspect("range", second, third); + const widerRange = inspect("range", first, third); + expect(commit["contentDigest"]).toBe( + immutableDiffDigest("commit", second, third), + ); + expect(range["contentDigest"]).toBe( + immutableDiffDigest("range", second, third), + ); + expect(repeatedRange["contentDigest"]).toBe(range["contentDigest"]); + expect(commit["contentDigest"]).not.toBe(range["contentDigest"]); + expect(widerRange["contentDigest"]).not.toBe(range["contentDigest"]); + + writeSource(repository, "app.ts", "export const value = 4;\n"); + const workingTree = inspect("working_tree"); + const repeatedWorkingTree = inspect("working_tree"); + expect(workingTree["contentDigest"]).toMatch( + /^codex-security-snapshot\/v1:sha256:[0-9a-f]{64}$/u, + ); + expect(repeatedWorkingTree["contentDigest"]).toBe( + workingTree["contentDigest"], + ); + writeSource(repository, "app.ts", "export const value = 5;\n"); + expect(inspect("working_tree")["contentDigest"]).not.toBe( + workingTree["contentDigest"], + ); + }); + + test("prepares CLI range completion with the canonical snapshot digest", () => { + const { root, repository } = createRepository(); + writeSource(repository, "app.ts", "export const value = 1;\n"); + git(repository, "add", "."); + git(repository, "commit", "-qm", "base"); + const baseRevision = git(repository, "rev-parse", "HEAD"); + writeSource(repository, "app.ts", "export const value = 2;\n"); + git(repository, "add", "."); + git(repository, "commit", "-qm", "head"); + const headRevision = git(repository, "rev-parse", "HEAD"); + const stateDir = join(root, "state"); + const scanDir = join(root, "scan"); + mkdirSync(stateDir); + mkdirSync(scanDir); + + const registration = pythonWithState( + stateDir, + "workbench_db.py", + "register-cli-scan", + "--repository", + repository, + "--scan-dir", + scanDir, + "--recipe-json", + JSON.stringify({ + config: {}, + mode: "standard", + repository, + target: { + kind: "refs", + paths: [], + base: baseRevision, + head: headRevision, + }, + }), + ); + expect(registration.status, registration.stderr).toBe(0); + const scanId = (JSON.parse(registration.stdout) as { scanId: string }) + .scanId; + + cpSync(join(PLUGIN_ROOT, "examples", "completed-scan"), scanDir, { + recursive: true, + }); + const manifestPath = join(scanDir, "scan-manifest.json"); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { + scan: { + id: string; + sealedAt?: string; + artifacts?: unknown; + target: JsonObject; + }; + }; + manifest.scan.id = scanId; + manifest.scan.target["kind"] = "git_diff"; + delete manifest.scan.target["snapshotDigest"]; + delete manifest.scan.sealedAt; + delete manifest.scan.artifacts; + writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`); + + for (const name of ["findings.json", "coverage.json"]) { + const path = join(scanDir, name); + const artifact = JSON.parse(readFileSync(path, "utf8")) as JsonObject; + artifact["scanId"] = scanId; + writeFileSync(path, `${JSON.stringify(artifact)}\n`); + } + writeFileSync(join(scanDir, "report.md"), "# Draft report\n"); + + const prepared = pythonWithState( + stateDir, + "workbench_db.py", + "prepare-scan-completion", + "--scan-id", + scanId, + ); + expect(prepared.status, prepared.stderr).toBe(0); + const target = ( + ( + JSON.parse(readFileSync(manifestPath, "utf8")) as { + scan: { target: JsonObject }; + } + ).scan + ).target; + expect(target["snapshotDigest"]).toBe( + immutableDiffDigest("range", baseRevision, headRevision), + ); + }); + test("runs the compact MCP diff lifecycle through a completed scan", async () => { const { root, repository } = createRepository(); writeSource(repository, "src/guard.py", "allowed = True\n"); @@ -407,16 +594,8 @@ describe("compact diff scan", () => { const target = ( (completed["manifest"] as JsonObject)["scan"] as JsonObject )["target"] as JsonObject; - const digest = createHash("sha256") - .update("codex-security-diff/v1\0") - .update("range") - .update("\0") - .update(baseRevision) - .update("\0") - .update(headRevision) - .digest("hex"); expect(target["snapshotDigest"]).toBe( - `codex-security-snapshot/v1:sha256:${digest}`, + immutableDiffDigest("range", baseRevision, headRevision), ); expect((completed["coverage"] as JsonObject)["inventoryStrategy"]).toBe( "diff", From 2e61662598ed6a2a47cac94a3d6a871004f36807 Mon Sep 17 00:00:00 2001 From: Carlos Acosta <93443910+charle-z@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:24:04 +0000 Subject: [PATCH 2/4] test(scan): enforce private diff scan fixtures --- sdk/typescript/tests-ts/compact-diff-scan.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 72577c72..e5d98813 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -1,6 +1,7 @@ import { execFileSync, spawn, spawnSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { + chmodSync, cpSync, mkdirSync, mkdtempSync, @@ -393,8 +394,10 @@ describe("compact diff scan", () => { const headRevision = git(repository, "rev-parse", "HEAD"); const stateDir = join(root, "state"); const scanDir = join(root, "scan"); - mkdirSync(stateDir); - mkdirSync(scanDir); + mkdirSync(stateDir, { mode: 0o700 }); + mkdirSync(scanDir, { mode: 0o700 }); + chmodSync(stateDir, 0o700); + chmodSync(scanDir, 0o700); const registration = pythonWithState( stateDir, From 896ac7ffda6d270825b5c0bfc917f3f5f35890a1 Mon Sep 17 00:00:00 2001 From: Carlos Acosta <93443910+charle-z@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:24:05 +0000 Subject: [PATCH 3/4] style(scan): format diff digest tests --- sdk/typescript/tests-ts/compact-diff-scan.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index e5d98813..5249bc96 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -350,7 +350,8 @@ describe("compact diff scan", () => { args.push("--diff-head-revision", headRevision); const result = python("workbench_db.py", ...args); expect(result.status, result.stderr).toBe(0); - return (JSON.parse(result.stdout) as { diffTarget: JsonObject }).diffTarget; + return (JSON.parse(result.stdout) as { diffTarget: JsonObject }) + .diffTarget; }; const commit = inspect("commit", second, third); @@ -460,12 +461,10 @@ describe("compact diff scan", () => { ); expect(prepared.status, prepared.stderr).toBe(0); const target = ( - ( - JSON.parse(readFileSync(manifestPath, "utf8")) as { - scan: { target: JsonObject }; - } - ).scan - ).target; + JSON.parse(readFileSync(manifestPath, "utf8")) as { + scan: { target: JsonObject }; + } + ).scan.target; expect(target["snapshotDigest"]).toBe( immutableDiffDigest("range", baseRevision, headRevision), ); From bbcca30d02222cca5bf9293a6fc71098b891e9da Mon Sep 17 00:00:00 2001 From: Carlos Acosta <93443910+charle-z@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:46:38 +0000 Subject: [PATCH 4/4] test(scan): assert persisted diff digest --- .../tests-ts/compact-diff-scan.test.ts | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 5249bc96..d602444d 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -87,6 +87,15 @@ function pythonWithState(stateDir: string, script: string, ...args: string[]) { ); } +function pythonEval(source: string, ...args: string[]) { + const command = + Bun.which("python3") ?? Bun.which("python") ?? Bun.which("py"); + expect(command).not.toBeNull(); + return spawnSync(command!, ["-I", "-B", "-c", source, ...args], { + encoding: "utf8", + }); +} + function immutableDiffDigest( kind: "commit" | "range", baseRevision: string, @@ -422,8 +431,43 @@ describe("compact diff scan", () => { }), ); expect(registration.status, registration.stderr).toBe(0); - const scanId = (JSON.parse(registration.stdout) as { scanId: string }) - .scanId; + const expectedDigest = immutableDiffDigest( + "range", + baseRevision, + headRevision, + ); + const registered = JSON.parse(registration.stdout) as { + scanId: string; + contract: { diffTarget: { contentDigest?: string } }; + }; + const scanId = registered.scanId; + expect(registered.contract.diffTarget.contentDigest).toBe(expectedDigest); + + const database = join(stateDir, "workbench.sqlite3"); + const stored = pythonEval( + [ + "import sqlite3, sys", + "connection = sqlite3.connect(sys.argv[1])", + "value = connection.execute('SELECT diff_content_digest FROM scans WHERE id = ?', (sys.argv[2],)).fetchone()[0]", + "print(value or '')", + ].join("\n"), + database, + scanId, + ); + expect(stored.status, stored.stderr).toBe(0); + expect(stored.stdout.trim()).toBe(expectedDigest); + + const cleared = pythonEval( + [ + "import sqlite3, sys", + "connection = sqlite3.connect(sys.argv[1])", + "connection.execute('UPDATE scans SET diff_content_digest = NULL WHERE id = ?', (sys.argv[2],))", + "connection.commit()", + ].join("\n"), + database, + scanId, + ); + expect(cleared.status, cleared.stderr).toBe(0); cpSync(join(PLUGIN_ROOT, "examples", "completed-scan"), scanDir, { recursive: true, @@ -465,9 +509,7 @@ describe("compact diff scan", () => { scan: { target: JsonObject }; } ).scan.target; - expect(target["snapshotDigest"]).toBe( - immutableDiffDigest("range", baseRevision, headRevision), - ); + expect(target["snapshotDigest"]).toBe(expectedDigest); }); test("runs the compact MCP diff lifecycle through a completed scan", async () => {