From 97cb4a2a8027996a13145816e2f4ca140d8060e8 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:44:27 +0000 Subject: [PATCH 1/8] fix(scan): confine local diff inputs to the selected target --- .../_bundled_plugin/.codex-plugin/plugin.json | 2 +- .../scripts/generate_in_scope_files.py | 19 +- .../scripts/generate_rank_input.py | 49 +++- sdk/typescript/src/version.ts | 2 +- .../tests-ts/diff-rank-input.test.ts | 260 ++++++++++++++++-- 5 files changed, 294 insertions(+), 38 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json b/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json index 93d0724aa..2530914a2 100644 --- a/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json +++ b/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex-security", - "version": "0.1.24", + "version": "0.1.25", "description": "Codex Security workflows for security scans, analysis, and investigation.", "author": { "name": "OpenAI" diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index 42bb48dc6..09bff54a9 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -167,7 +167,11 @@ def generate_diff_in_scope_files( ) -> int: """Reuse the existing diff selection without generating previews or duplicate worklists.""" sys.path.insert(0, str(Path(__file__).resolve().parent)) - from generate_rank_input import git_changed_paths, path_is_excluded + from generate_rank_input import ( + changed_path_parent_is_within_target, + git_changed_paths, + path_is_excluded, + ) from rank_preview import ( DEFAULT_PREVIEW_BYTES, TEXT_CODE_EXTENSIONS, @@ -206,6 +210,19 @@ def generate_diff_in_scope_files( for path, status in eligible: relative = path.relative_to(repository) + if mode != "revisions": + try: + within_target = changed_path_parent_is_within_target( + path, repository + ) + except (OSError, RuntimeError) as error: + raise InventoryError( + "could not inspect a changed Git working-tree path" + ) from error + if not within_target: + raise InventoryError( + "changed Git working-tree paths must stay inside the selected target" + ) if status != "D": if mode == "revisions": contents = revision_blobs[relative] diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 8c4dd3722..006d77a44 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -293,6 +293,33 @@ def path_is_excluded(path: Path) -> bool: return path.name.endswith((".min.js", ".map")) +def changed_path_parent_is_within_target(path: Path, target: Path) -> bool: + """Resolve the nearest existing parent without dereferencing the changed leaf.""" + target = target.resolve(strict=True) + candidate = path.parent + while True: + try: + candidate.lstat() + except (FileNotFoundError, NotADirectoryError): + parent = candidate.parent + if parent == candidate: + return False + candidate = parent + continue + break + + resolved = candidate.resolve(strict=True) + if resolved.is_relative_to(target): + return True + for ancestor in (resolved, *resolved.parents): + try: + if ancestor.samefile(target): + return True + except OSError: + continue + return False + + def windows_stream_component(path: Path) -> str | None: """Return the first NTFS alternate-data-stream component.""" @@ -708,6 +735,17 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: rows: list[JsonRow] = [] for path, status in changed: rel = path.relative_to(repo) + if args.mode != "revisions": + try: + within_target = changed_path_parent_is_within_target(path, repo) + except (OSError, RuntimeError) as error: + raise SystemExit( + "Could not inspect a changed Git working-tree path." + ) from error + if not within_target: + raise SystemExit( + "Changed Git working-tree paths must stay inside the selected target." + ) if status == "D": preview = "" @@ -723,14 +761,9 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: elif path.is_symlink(): preview = "" elif path.is_file(): - try: - path.resolve(strict=True).relative_to(repo) - except (OSError, ValueError): - preview = "" - else: - preview, is_binary = preview_for(path, args.preview_bytes) - if is_binary: - continue + preview, is_binary = preview_for(path, args.preview_bytes) + if is_binary: + continue else: preview = "" rows.append({"path": rel.as_posix(), "area": args.area, "preview": preview}) diff --git a/sdk/typescript/src/version.ts b/sdk/typescript/src/version.ts index c0cf1dfe7..4f12a51fa 100644 --- a/sdk/typescript/src/version.ts +++ b/sdk/typescript/src/version.ts @@ -9,7 +9,7 @@ const PACKAGE_VERSIONS = packageVersions( export const VERSION = PACKAGE_VERSIONS.package; export const CODEX_SDK_VERSION = PACKAGE_VERSIONS.sdk; export const CODEX_EXECUTABLE_VERSION = PACKAGE_VERSIONS.executable; -export const BUNDLED_PLUGIN_VERSION = "0.1.24" as const; +export const BUNDLED_PLUGIN_VERSION = "0.1.25" as const; const PACKAGE_NAME = "@openai/codex-security"; diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index 19e120300..864d692d6 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -1,5 +1,6 @@ import { execFileSync, spawnSync } from "node:child_process"; import { + cpSync, mkdirSync, mkdtempSync, readFileSync, @@ -11,10 +12,31 @@ import { import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { afterEach, expect, test } from "bun:test"; +import { BUNDLED_PLUGIN_VERSION, bootstrapPlugin } from "../src/index.js"; import { PLUGIN_ROOT } from "./plugin-root.js"; const temporaryRoots: string[] = []; +function supportsFileSymlinks(): boolean { + const root = mkdtempSync(join(tmpdir(), "codex-security-symlink-probe-")); + try { + symlinkSync("missing.py", join(root, "broken.py"), "file"); + return true; + } catch (error) { + if ( + process.platform === "win32" && + (error as NodeJS.ErrnoException).code === "EPERM" + ) { + return false; + } + throw error; + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +const fileSymlinksAvailable = supportsFileSymlinks(); + function pythonExecutable(): string | null { return ( process.env["PYTHON"] ?? @@ -44,11 +66,169 @@ function git(repository: string, ...args: string[]): string { ).trim(); } -test("diff previews stay inside the selected repository", () => { +async function upgradeBundledPlugin(root: string): Promise { + expect(BUNDLED_PLUGIN_VERSION).toBe("0.1.25"); + const previous = join(root, "previous-plugin"); + cpSync(PLUGIN_ROOT, previous, { recursive: true }); + const previousManifestPath = join(previous, ".codex-plugin", "plugin.json"); + const previousManifest = JSON.parse( + readFileSync(previousManifestPath, "utf8"), + ) as { version: string }; + previousManifest.version = "0.1.24"; + writeFileSync(previousManifestPath, JSON.stringify(previousManifest)); + + const home = join(root, "codex-home"); + const marketplace = join(home, "sdk-marketplace"); + mkdirSync(home, { mode: 0o700 }); + const runCodex = async (_command: unknown, args: readonly string[]) => { + if (args[1] === "marketplace") { + writeFileSync( + join(home, "config.toml"), + `[marketplaces.codex-security-sdk]\nsource_type = "local"\nsource = ${JSON.stringify(marketplace)}\n`, + ); + return ""; + } + const selected = join(marketplace, "plugins", "codex-security"); + const manifest = JSON.parse( + readFileSync(join(selected, ".codex-plugin", "plugin.json"), "utf8"), + ) as { version: string }; + const installed = join(home, "installed", manifest.version); + rmSync(installed, { recursive: true, force: true }); + mkdirSync(join(home, "installed"), { recursive: true }); + cpSync(selected, installed, { recursive: true }); + return JSON.stringify({ + installedPath: installed, + version: manifest.version, + }); + }; + const options = { + codexCommand: { command: "/synthetic-codex" }, + runCodex, + }; + + const predecessor = await bootstrapPlugin(home, previous, options); + const upgraded = await bootstrapPlugin(home, PLUGIN_ROOT, options); + expect(predecessor.version).toBe("0.1.24"); + expect(upgraded.version).toBe(BUNDLED_PLUGIN_VERSION); + expect(upgraded.installedRoot).not.toBe(predecessor.installedRoot); + const installedMcp = JSON.parse( + readFileSync(join(upgraded.installedRoot, ".mcp.json"), "utf8"), + ) as { mcpServers: Record }; + expect( + installedMcp.mcpServers["codex-security"]?.env_vars?.find( + (name) => name === "CODEX_SAFETY_IDENTIFIER", + ), + ).toBe("CODEX_SAFETY_IDENTIFIER"); + expect( + readFileSync( + join(upgraded.installedRoot, "scripts", "generate_rank_input.py"), + "utf8", + ), + ).toBe( + readFileSync( + join(PLUGIN_ROOT, "scripts", "generate_rank_input.py"), + "utf8", + ), + ); + return upgraded.installedRoot; +} + +function stagedBrokenSymlinkFixture(): { + root: string; + repository: string; + base: string; +} { + const root = realpathSync( + mkdtempSync(join(tmpdir(), "codex-security-diff-broken-link-")), + ); + temporaryRoots.push(root); + const repository = join(root, "repository"); + mkdirSync(repository); + git(repository, "init", "-q"); + writeFileSync(join(repository, "base.py"), "value = 1\n"); + git(repository, "add", "base.py"); + git(repository, "commit", "-qm", "base"); + const base = git(repository, "rev-parse", "HEAD"); + symlinkSync( + "../synthetic-fixture/missing-target.py", + join(repository, "broken.py"), + "file", + ); + git(repository, "add", "broken.py"); + return { root, repository, base }; +} + +test.skipIf(!fileSymlinksAvailable)( + "diff inventory omits a broken symlink leaf", + () => { + const { root, repository, base } = stagedBrokenSymlinkFixture(); + const python = pythonExecutable(); + expect(python).not.toBeNull(); + const output = join(root, "in-scope-files.txt"); + const result = spawnSync( + python!, + [ + "-B", + join(PLUGIN_ROOT, "scripts", "generate_in_scope_files.py"), + "--repo", + repository, + "--scope", + ".", + "--out", + output, + "--diff-base", + base, + "--diff-mode", + "local-patch", + ], + { encoding: "utf8" }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(output, "utf8")).toBe(""); + }, +); + +test.skipIf(!fileSymlinksAvailable)( + "diff rank input keeps a broken symlink leaf without a preview", + () => { + const { root, repository, base } = stagedBrokenSymlinkFixture(); + const python = pythonExecutable(); + expect(python).not.toBeNull(); + const output = join(root, "rank-input.jsonl"); + const result = spawnSync( + python!, + [ + "-B", + join(PLUGIN_ROOT, "scripts", "generate_rank_input.py"), + "make-diff-rank-input", + "--repo", + repository, + "--base", + base, + "--mode", + "local-patch", + "--out", + output, + ], + { encoding: "utf8" }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(JSON.parse(readFileSync(output, "utf8"))).toEqual({ + path: "broken.py", + area: "diff", + preview: "", + }); + }, +); + +test("diff inventory and previews stay inside the selected repository", async () => { const root = realpathSync( mkdtempSync(join(tmpdir(), "codex-security-diff-rank-")), ); temporaryRoots.push(root); + const installedPluginRoot = await upgradeBundledPlugin(root); const repository = join(root, "repository"); const nested = join(repository, "src", "nested"); mkdirSync(nested, { recursive: true }); @@ -83,34 +263,25 @@ test("diff previews stay inside the selected repository", () => { git(repository, "commit", "-qm", "selected changes"); const head = git(repository, "rev-parse", "HEAD"); - const externalFixture = join(root, "synthetic-fixture"); - mkdirSync(externalFixture); - writeFileSync(join(externalFixture, "linked.py"), "synthetic = True\n"); - rmSync(nested, { recursive: true }); - symlinkSync(externalFixture, nested, "junction"); - const python = pythonExecutable(); expect(python).not.toBeNull(); const output = join(root, "rank-input.jsonl"); - const result = spawnSync( - python!, - [ - "-B", - join(PLUGIN_ROOT, "scripts", "generate_rank_input.py"), - "make-diff-rank-input", - "--repo", - repository, - "--base", - base, - "--head", - head, - "--mode", - "local-patch", - "--out", - output, - ], - { encoding: "utf8" }, - ); + const args = [ + "-B", + join(installedPluginRoot, "scripts", "generate_rank_input.py"), + "make-diff-rank-input", + "--repo", + repository, + "--base", + base, + "--head", + head, + "--mode", + "local-patch", + "--out", + output, + ]; + const result = spawnSync(python!, args, { encoding: "utf8" }); expect(result.status, result.stderr).toBe(0); const rows = readFileSync(output, "utf8") @@ -127,7 +298,42 @@ test("diff previews stay inside the selected repository", () => { "value = 2", ); expect(rows.find((row) => row.path === "src/nested/linked.py")?.preview).toBe( - "", + "value = 2", + ); + + const externalFixture = join(root, "synthetic-fixture"); + mkdirSync(externalFixture); + writeFileSync(join(externalFixture, "linked.py"), "synthetic = True\n"); + rmSync(nested, { recursive: true }); + symlinkSync(externalFixture, nested, "junction"); + + const escaped = spawnSync(python!, args, { encoding: "utf8" }); + const inventory = spawnSync( + python!, + [ + "-B", + join(installedPluginRoot, "scripts", "generate_in_scope_files.py"), + "--repo", + repository, + "--scope", + ".", + "--out", + join(root, "in-scope-files.txt"), + "--diff-base", + base, + "--diff-head", + head, + "--diff-mode", + "local-patch", + ], + { encoding: "utf8" }, + ); + expect([escaped.status, inventory.status]).toEqual([1, 2]); + expect(escaped.stderr).toContain( + "Changed Git working-tree paths must stay inside the selected target.", + ); + expect(inventory.stderr).toContain( + "changed Git working-tree paths must stay inside the selected target", ); }); From 33a49c637211cf402cf4a6cd30628edb1396ebec Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:27:30 +0000 Subject: [PATCH 2/8] test(scan): cover replaced parents for deleted paths --- .../tests-ts/diff-rank-input.test.ts | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index 864d692d6..8a9e8adc9 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -231,12 +231,15 @@ test("diff inventory and previews stay inside the selected repository", async () const installedPluginRoot = await upgradeBundledPlugin(root); const repository = join(root, "repository"); const nested = join(repository, "src", "nested"); + const removedParent = join(repository, "removed"); mkdirSync(nested, { recursive: true }); + mkdirSync(removedParent); git(repository, "init", "-q"); writeFileSync(join(repository, "src", "handler.py"), "value = 1\n"); writeFileSync(join(repository, "src", "deleted.py"), "removed = True\n"); writeFileSync(join(repository, "src", "entry.py"), "handler.py"); writeFileSync(join(nested, "linked.py"), "value = 1\n"); + writeFileSync(join(removedParent, "deleted.py"), "removed = True\n"); git(repository, "add", "."); const originalLink = git(repository, "hash-object", "src/entry.py"); git( @@ -252,6 +255,7 @@ test("diff inventory and previews stay inside the selected repository", async () writeFileSync(join(repository, "src", "entry.py"), "nested/linked.py"); writeFileSync(join(nested, "linked.py"), "value = 2\n"); rmSync(join(repository, "src", "deleted.py")); + rmSync(removedParent, { recursive: true }); git(repository, "add", "."); const updatedLink = git(repository, "hash-object", "src/entry.py"); git( @@ -281,14 +285,39 @@ test("diff inventory and previews stay inside the selected repository", async () "--out", output, ]; + const inventoryOutput = join(root, "in-scope-files.txt"); + const inventoryArgs = [ + "-B", + join(installedPluginRoot, "scripts", "generate_in_scope_files.py"), + "--repo", + repository, + "--scope", + ".", + "--out", + inventoryOutput, + "--diff-base", + base, + "--diff-head", + head, + "--diff-mode", + "local-patch", + ]; const result = spawnSync(python!, args, { encoding: "utf8" }); + const safeInventory = spawnSync(python!, inventoryArgs, { + encoding: "utf8", + }); expect(result.status, result.stderr).toBe(0); + expect(safeInventory.status, safeInventory.stderr).toBe(0); + expect(readFileSync(inventoryOutput, "utf8")).toContain( + "removed/deleted.py\n", + ); const rows = readFileSync(output, "utf8") .trim() .split("\n") .map((row) => JSON.parse(row) as { path: string; preview: string }); expect(rows.map((row) => row.path)).toEqual([ + "removed/deleted.py", "src/deleted.py", "src/entry.py", "src/handler.py", @@ -303,31 +332,29 @@ test("diff inventory and previews stay inside the selected repository", async () const externalFixture = join(root, "synthetic-fixture"); mkdirSync(externalFixture); + writeFileSync(join(externalFixture, "deleted.py"), "external = True\n"); writeFileSync(join(externalFixture, "linked.py"), "synthetic = True\n"); + symlinkSync(externalFixture, removedParent, "junction"); + + const escapedDeletion = spawnSync(python!, args, { encoding: "utf8" }); + const deletionInventory = spawnSync(python!, inventoryArgs, { + encoding: "utf8", + }); + expect([escapedDeletion.status, deletionInventory.status]).toEqual([1, 2]); + expect(escapedDeletion.stderr).toContain( + "Changed Git working-tree paths must stay inside the selected target.", + ); + expect(deletionInventory.stderr).toContain( + "changed Git working-tree paths must stay inside the selected target", + ); + + rmSync(removedParent); + git(repository, "update-index", "--skip-worktree", "src/nested/linked.py"); rmSync(nested, { recursive: true }); symlinkSync(externalFixture, nested, "junction"); const escaped = spawnSync(python!, args, { encoding: "utf8" }); - const inventory = spawnSync( - python!, - [ - "-B", - join(installedPluginRoot, "scripts", "generate_in_scope_files.py"), - "--repo", - repository, - "--scope", - ".", - "--out", - join(root, "in-scope-files.txt"), - "--diff-base", - base, - "--diff-head", - head, - "--diff-mode", - "local-patch", - ], - { encoding: "utf8" }, - ); + const inventory = spawnSync(python!, inventoryArgs, { encoding: "utf8" }); expect([escaped.status, inventory.status]).toEqual([1, 2]); expect(escaped.stderr).toContain( "Changed Git working-tree paths must stay inside the selected target.", From 7c30deb8d9eb6464666ed1b2e57cbb5cd36c4ac7 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:55:20 +0000 Subject: [PATCH 3/8] test(scan): remove synthetic junction portably --- sdk/typescript/tests-ts/diff-rank-input.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index 8a9e8adc9..beb6050ad 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -7,6 +7,7 @@ import { realpathSync, rmSync, symlinkSync, + unlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -348,7 +349,7 @@ test("diff inventory and previews stay inside the selected repository", async () "changed Git working-tree paths must stay inside the selected target", ); - rmSync(removedParent); + unlinkSync(removedParent); git(repository, "update-index", "--skip-worktree", "src/nested/linked.py"); rmSync(nested, { recursive: true }); symlinkSync(externalFixture, nested, "junction"); From f3431c796dddf4e7de51f5a5adf259f639ad8a91 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 24 Aug 2026 14:59:16 -0700 Subject: [PATCH 4/8] fix(scan): bind local diff previews to repository handles --- .../_bundled_plugin/.codex-plugin/plugin.json | 2 +- .../scripts/generate_in_scope_files.py | 19 ++- .../scripts/generate_rank_input.py | 64 +++++++++- sdk/typescript/src/version.ts | 2 +- .../tests-ts/diff-rank-input.test.ts | 120 ++++++++++++++++-- 5 files changed, 187 insertions(+), 20 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json b/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json index 2530914a2..1d75f1627 100644 --- a/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json +++ b/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex-security", - "version": "0.1.25", + "version": "0.1.45", "description": "Codex Security workflows for security scans, analysis, and investigation.", "author": { "name": "OpenAI" diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index 09bff54a9..d209e90a9 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -171,12 +171,12 @@ def generate_diff_in_scope_files( changed_path_parent_is_within_target, git_changed_paths, path_is_excluded, + preview_for_changed_path, ) from rank_preview import ( DEFAULT_PREVIEW_BYTES, TEXT_CODE_EXTENSIONS, is_binary_sample, - preview_for, ) from workbench_target import git_blob_bytes @@ -232,12 +232,19 @@ def generate_diff_in_scope_files( ) if is_binary_sample(contents): continue - elif ( - path.is_symlink() - or not path.is_file() - or preview_for(path, DEFAULT_PREVIEW_BYTES)[1] - ): + elif path.is_symlink() or not path.is_file(): continue + else: + try: + _, is_binary = preview_for_changed_path( + path, repository, DEFAULT_PREVIEW_BYTES + ) + except (OSError, RuntimeError, ValueError) as error: + raise InventoryError( + "changed Git working-tree paths must stay inside the selected target" + ) from error + if is_binary: + continue relative_path = relative.as_posix() if "\n" in relative_path or "\r" in relative_path: raise InventoryError( diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 006d77a44..adb2f4670 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -29,6 +29,7 @@ import json import os import re +import stat import subprocess import sys from collections import Counter @@ -37,9 +38,16 @@ # Some plugin hosts launch Python with safe-path isolation enabled. sys.path.insert(0, str(Path(__file__).resolve().parent)) +from finalize_scan_contract import ( + _descriptor_relative_reads_available, + _open_scan_local_directory, + _open_verified_scan_directory, + _windows_scan_local_files, +) from rank_preview import ( DEFAULT_PREVIEW_BYTES, TEXT_CODE_EXTENSIONS, + is_binary_sample, preview_for, preview_for_bytes, ) @@ -320,6 +328,53 @@ def changed_path_parent_is_within_target(path: Path, target: Path) -> bool: return False +def preview_for_changed_path( + path: Path, target: Path, preview_bytes: int +) -> tuple[str, bool]: + """Bind working-tree reads to the checked repository and parent identities.""" + + relative_parent = path.parent.resolve(strict=True).relative_to(target) + descriptor: int | None = None + try: + if os.name == "nt": + relative_path = (relative_parent / path.name).as_posix() + descriptor = _windows_scan_local_files().open_read_fd( + target, relative_path, "changed Git working-tree path" + ) + elif _descriptor_relative_reads_available(): + root_descriptor = _open_verified_scan_directory(target) + try: + parent_descriptor = _open_scan_local_directory( + root_descriptor, relative_parent.parts, create=False + ) + try: + descriptor = os.open( + path.name, + os.O_RDONLY | os.O_NOFOLLOW | getattr(os, "O_NONBLOCK", 0), + dir_fd=parent_descriptor, + ) + finally: + os.close(parent_descriptor) + finally: + os.close(root_descriptor) + + if not stat.S_ISREG(os.fstat(descriptor).st_mode): + raise OSError("changed Git working-tree path is not a regular file") + else: + raise OSError("changed Git working-tree input requires secure file operations") + + with os.fdopen(descriptor, "rb") as source: + descriptor = None + sample = source.read(4096) + if is_binary_sample(sample): + return "", True + data = sample + source.read() + return preview_for_bytes(path, data, preview_bytes) + finally: + if descriptor is not None: + os.close(descriptor) + + def windows_stream_component(path: Path) -> str | None: """Return the first NTFS alternate-data-stream component.""" @@ -761,7 +816,14 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: elif path.is_symlink(): preview = "" elif path.is_file(): - preview, is_binary = preview_for(path, args.preview_bytes) + try: + preview, is_binary = preview_for_changed_path( + path, repo, args.preview_bytes + ) + except (OSError, RuntimeError, ValueError) as error: + raise SystemExit( + "Changed Git working-tree paths must stay inside the selected target." + ) from error if is_binary: continue else: diff --git a/sdk/typescript/src/version.ts b/sdk/typescript/src/version.ts index 4f12a51fa..31ad25ba1 100644 --- a/sdk/typescript/src/version.ts +++ b/sdk/typescript/src/version.ts @@ -9,7 +9,7 @@ const PACKAGE_VERSIONS = packageVersions( export const VERSION = PACKAGE_VERSIONS.package; export const CODEX_SDK_VERSION = PACKAGE_VERSIONS.sdk; export const CODEX_EXECUTABLE_VERSION = PACKAGE_VERSIONS.executable; -export const BUNDLED_PLUGIN_VERSION = "0.1.25" as const; +export const BUNDLED_PLUGIN_VERSION = "0.1.45" as const; const PACKAGE_NAME = "@openai/codex-security"; diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index beb6050ad..401d07703 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -5,6 +5,7 @@ import { mkdtempSync, readFileSync, realpathSync, + renameSync, rmSync, symlinkSync, unlinkSync, @@ -68,14 +69,14 @@ function git(repository: string, ...args: string[]): string { } async function upgradeBundledPlugin(root: string): Promise { - expect(BUNDLED_PLUGIN_VERSION).toBe("0.1.25"); + expect(BUNDLED_PLUGIN_VERSION).toBe("0.1.45"); const previous = join(root, "previous-plugin"); cpSync(PLUGIN_ROOT, previous, { recursive: true }); const previousManifestPath = join(previous, ".codex-plugin", "plugin.json"); const previousManifest = JSON.parse( readFileSync(previousManifestPath, "utf8"), ) as { version: string }; - previousManifest.version = "0.1.24"; + previousManifest.version = "0.1.44"; writeFileSync(previousManifestPath, JSON.stringify(previousManifest)); const home = join(root, "codex-home"); @@ -109,17 +110,9 @@ async function upgradeBundledPlugin(root: string): Promise { const predecessor = await bootstrapPlugin(home, previous, options); const upgraded = await bootstrapPlugin(home, PLUGIN_ROOT, options); - expect(predecessor.version).toBe("0.1.24"); + expect(predecessor.version).toBe("0.1.44"); expect(upgraded.version).toBe(BUNDLED_PLUGIN_VERSION); expect(upgraded.installedRoot).not.toBe(predecessor.installedRoot); - const installedMcp = JSON.parse( - readFileSync(join(upgraded.installedRoot, ".mcp.json"), "utf8"), - ) as { mcpServers: Record }; - expect( - installedMcp.mcpServers["codex-security"]?.env_vars?.find( - (name) => name === "CODEX_SAFETY_IDENTIFIER", - ), - ).toBe("CODEX_SAFETY_IDENTIFIER"); expect( readFileSync( join(upgraded.installedRoot, "scripts", "generate_rank_input.py"), @@ -224,6 +217,111 @@ test.skipIf(!fileSymlinksAvailable)( }, ); +test("rejects parents replaced after local-diff confinement is checked", () => { + const root = realpathSync( + mkdtempSync(join(tmpdir(), "codex-security-diff-parent-race-")), + ); + temporaryRoots.push(root); + const repository = join(root, "repository"); + const parent = join(repository, "src"); + const filename = + process.platform === "win32" ? "handler.py" : "handler:local.py"; + mkdirSync(parent, { recursive: true }); + git(repository, "init", "-q"); + writeFileSync(join(parent, filename), "inside = 1\n"); + git(repository, "add", "."); + git(repository, "commit", "-qm", "base"); + const base = git(repository, "rev-parse", "HEAD"); + writeFileSync(join(parent, filename), "inside = 2\n"); + + const python = pythonExecutable(); + expect(python).not.toBeNull(); + const scripts = join(PLUGIN_ROOT, "scripts"); + const probe = [ + "import os, sys", + "scripts, repository, base, external, parked, output, generator = sys.argv[1:]", + "sys.path.insert(0, scripts)", + "import generate_rank_input as ranking", + "checked_parent = ranking.changed_path_parent_is_within_target", + "def swap_parent(path, target):", + " accepted = checked_parent(path, target)", + " os.replace(path.parent, parked)", + " if os.name == 'nt':", + " import _winapi", + " _winapi.CreateJunction(external, str(path.parent))", + " else:", + " os.symlink(external, path.parent, target_is_directory=True)", + " return accepted", + "if generator.startswith('safe-'):", + " generator = generator.removeprefix('safe-')", + "else:", + " ranking.changed_path_parent_is_within_target = swap_parent", + "if generator == 'ranking':", + " sys.argv = ['ranking', 'make-diff-rank-input', '--repo', repository, '--base', base, '--mode', 'local-patch', '--out', output]", + " ranking.main()", + "else:", + " import generate_in_scope_files as inventory", + " sys.argv = ['inventory', '--repo', repository, '--scope', '.', '--out', output, '--diff-base', base, '--diff-mode', 'local-patch']", + " inventory.main()", + ].join("\n"); + + for (const generator of ["ranking", "inventory"]) { + const external = join(root, `external-${generator}`); + const parked = join(root, `parked-${generator}`); + const output = join(root, `${generator}.output`); + mkdirSync(external); + writeFileSync( + join(external, filename), + generator === "ranking" + ? "outside = 'SYNTHETIC_EXTERNAL_MARKER'\n" + : Buffer.from("\0SYNTHETIC_EXTERNAL_MARKER"), + ); + + const runGenerator = (selected: string, destination: string) => + spawnSync( + python!, + [ + "-I", + "-B", + "-c", + probe, + scripts, + repository, + base, + external, + parked, + destination, + selected, + ], + { encoding: "utf8" }, + ); + const safeOutput = `${output}.safe`; + const safe = runGenerator(`safe-${generator}`, safeOutput); + expect(safe.status, `${generator}: ${safe.stderr}`).toBe(0); + if (generator === "ranking") { + expect(JSON.parse(readFileSync(safeOutput, "utf8"))).toMatchObject({ + path: `src/${filename}`, + preview: "inside = 2", + }); + } else { + expect(readFileSync(safeOutput, "utf8")).toBe(`src/${filename}\n`); + } + + const result = runGenerator(generator, output); + + expect( + result.status, + `${generator}: ${result.stdout}\n${result.stderr}`, + ).toBe(generator === "ranking" ? 1 : 2); + expect(result.stderr.toLowerCase()).toContain("inside the selected target"); + + if (generator === "ranking") { + unlinkSync(parent); + renameSync(parked, parent); + } + } +}); + test("diff inventory and previews stay inside the selected repository", async () => { const root = realpathSync( mkdtempSync(join(tmpdir(), "codex-security-diff-rank-")), From 35329b6df999d07d439c39734d283b26da557691 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 24 Aug 2026 18:17:26 -0700 Subject: [PATCH 5/8] test(scan): retain installed plugin safety forwarding coverage --- sdk/typescript/tests-ts/diff-rank-input.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index 401d07703..5926c4578 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -113,6 +113,14 @@ async function upgradeBundledPlugin(root: string): Promise { expect(predecessor.version).toBe("0.1.44"); expect(upgraded.version).toBe(BUNDLED_PLUGIN_VERSION); expect(upgraded.installedRoot).not.toBe(predecessor.installedRoot); + const installedMcp = JSON.parse( + readFileSync(join(upgraded.installedRoot, ".mcp.json"), "utf8"), + ) as { mcpServers: Record }; + expect( + installedMcp.mcpServers["codex-security"]?.env_vars?.find( + (name) => name === "CODEX_SAFETY_IDENTIFIER", + ), + ).toBe("CODEX_SAFETY_IDENTIFIER"); expect( readFileSync( join(upgraded.installedRoot, "scripts", "generate_rank_input.py"), From 564150fd300a7dff62588a6d41406882d7366582 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 25 Aug 2026 00:39:08 -0400 Subject: [PATCH 6/8] fix(scan): preserve benign working-tree file churn --- .../scripts/generate_in_scope_files.py | 2 + .../scripts/generate_rank_input.py | 18 ++++- .../tests-ts/diff-rank-input.test.ts | 81 +++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index d209e90a9..74b67cad6 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -239,6 +239,8 @@ def generate_diff_in_scope_files( _, is_binary = preview_for_changed_path( path, repository, DEFAULT_PREVIEW_BYTES ) + except (FileNotFoundError, PermissionError): + continue except (OSError, RuntimeError, ValueError) as error: raise InventoryError( "changed Git working-tree paths must stay inside the selected target" diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index adb2f4670..bec09fb31 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -333,7 +333,10 @@ def preview_for_changed_path( ) -> tuple[str, bool]: """Bind working-tree reads to the checked repository and parent identities.""" - relative_parent = path.parent.resolve(strict=True).relative_to(target) + try: + relative_parent = path.parent.resolve(strict=True).relative_to(target) + except (FileNotFoundError, PermissionError) as error: + raise ValueError("changed Git working-tree parent became unavailable") from error descriptor: int | None = None try: if os.name == "nt": @@ -344,9 +347,14 @@ def preview_for_changed_path( elif _descriptor_relative_reads_available(): root_descriptor = _open_verified_scan_directory(target) try: - parent_descriptor = _open_scan_local_directory( - root_descriptor, relative_parent.parts, create=False - ) + try: + parent_descriptor = _open_scan_local_directory( + root_descriptor, relative_parent.parts, create=False + ) + except (FileNotFoundError, PermissionError) as error: + raise ValueError( + "changed Git working-tree parent became unavailable" + ) from error try: descriptor = os.open( path.name, @@ -820,6 +828,8 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: preview, is_binary = preview_for_changed_path( path, repo, args.preview_bytes ) + except (FileNotFoundError, PermissionError): + continue except (OSError, RuntimeError, ValueError) as error: raise SystemExit( "Changed Git working-tree paths must stay inside the selected target." diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index 5926c4578..1bff7384c 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -330,6 +330,87 @@ test("rejects parents replaced after local-diff confinement is checked", () => { } }); +test("skips working-tree files that disappear or become unreadable during preview", () => { + const root = realpathSync( + mkdtempSync(join(tmpdir(), "codex-security-diff-file-churn-")), + ); + temporaryRoots.push(root); + const repository = join(root, "repository"); + const source = join(repository, "src", "handler.py"); + mkdirSync(join(repository, "src"), { recursive: true }); + git(repository, "init", "-q"); + writeFileSync(source, "inside = 1\n"); + git(repository, "add", "."); + git(repository, "commit", "-qm", "base"); + const base = git(repository, "rev-parse", "HEAD"); + + const python = pythonExecutable(); + expect(python).not.toBeNull(); + const scripts = join(PLUGIN_ROOT, "scripts"); + const probe = [ + "import sys", + "scripts, repository, base, output, generator, failure = sys.argv[1:]", + "sys.path.insert(0, scripts)", + "import generate_rank_input as ranking", + "read_changed_path = ranking.preview_for_changed_path", + "def unavailable_leaf(path, target, preview_bytes):", + " if failure == 'unreadable':", + " raise PermissionError('synthetic in-target file became unreadable')", + " path.unlink()", + " if failure == 'parent-missing':", + " path.parent.rmdir()", + " return read_changed_path(path, target, preview_bytes)", + "ranking.preview_for_changed_path = unavailable_leaf", + "if generator == 'ranking':", + " sys.argv = ['ranking', 'make-diff-rank-input', '--repo', repository, '--base', base, '--mode', 'local-patch', '--out', output]", + " ranking.main()", + "else:", + " import generate_in_scope_files as inventory", + " sys.argv = ['inventory', '--repo', repository, '--scope', '.', '--out', output, '--diff-base', base, '--diff-mode', 'local-patch']", + " inventory.main()", + ].join("\n"); + + for (const generator of ["ranking", "inventory"]) { + for (const failure of ["missing", "unreadable", "parent-missing"]) { + mkdirSync(join(repository, "src"), { recursive: true }); + writeFileSync(source, "inside = 2\n"); + const output = join(root, `${generator}-${failure}.jsonl`); + const result = spawnSync( + python!, + [ + "-I", + "-B", + "-c", + probe, + scripts, + repository, + base, + output, + generator, + failure, + ], + { encoding: "utf8" }, + ); + + if (failure === "parent-missing") { + expect( + result.status, + `${generator}/${failure}: ${result.stdout}\n${result.stderr}`, + ).toBe(generator === "ranking" ? 1 : 2); + expect(result.stderr.toLowerCase()).toContain( + "inside the selected target", + ); + } else { + expect( + result.status, + `${generator}/${failure}: ${result.stdout}\n${result.stderr}`, + ).toBe(0); + expect(readFileSync(output, "utf8")).toBe(""); + } + } + } +}); + test("diff inventory and previews stay inside the selected repository", async () => { const root = realpathSync( mkdtempSync(join(tmpdir(), "codex-security-diff-rank-")), From 4d2f5e53c5294359f3375e55cef42f8b6085f504 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 25 Aug 2026 00:54:20 -0400 Subject: [PATCH 7/8] fix(scan): preserve Windows working-tree file churn --- .../scripts/generate_rank_input.py | 28 +++++++++++++++-- .../tests-ts/diff-rank-input.test.ts | 31 +++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index bec09fb31..5800c3316 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -25,6 +25,7 @@ from __future__ import annotations import argparse +import errno import hashlib import json import os @@ -328,6 +329,28 @@ def changed_path_parent_is_within_target(path: Path, target: Path) -> bool: return False +def _open_windows_changed_path_descriptor(target: Path, relative_path: Path) -> int: + """Preserve ordinary missing-leaf behavior without relaxing parent checks.""" + + expected_path = target / relative_path + try: + return _windows_scan_local_files().open_read_fd( + target, relative_path.as_posix(), "changed Git working-tree path" + ) + except OSError as error: + if error.filename is None or os.path.normcase( + os.path.normpath(os.fspath(error.filename)) + ) != os.path.normcase(os.path.normpath(os.fspath(expected_path))): + raise + if error.errno in {errno.ENOENT, 3}: + raise FileNotFoundError( + error.errno, error.strerror, error.filename + ) from error + if error.errno in {errno.EACCES, 5}: + raise PermissionError(error.errno, error.strerror, error.filename) from error + raise + + def preview_for_changed_path( path: Path, target: Path, preview_bytes: int ) -> tuple[str, bool]: @@ -340,9 +363,8 @@ def preview_for_changed_path( descriptor: int | None = None try: if os.name == "nt": - relative_path = (relative_parent / path.name).as_posix() - descriptor = _windows_scan_local_files().open_read_fd( - target, relative_path, "changed Git working-tree path" + descriptor = _open_windows_changed_path_descriptor( + target, relative_parent / path.name ) elif _descriptor_relative_reads_available(): root_descriptor = _open_verified_scan_directory(target) diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index 1bff7384c..76d47f811 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -348,12 +348,26 @@ test("skips working-tree files that disappear or become unreadable during previe expect(python).not.toBeNull(); const scripts = join(PLUGIN_ROOT, "scripts"); const probe = [ - "import sys", + "import sys, types", "scripts, repository, base, output, generator, failure = sys.argv[1:]", "sys.path.insert(0, scripts)", "import generate_rank_input as ranking", + "from windows_scan_local_files import WindowsScanLocalFileError", "read_changed_path = ranking.preview_for_changed_path", "def unavailable_leaf(path, target, preview_bytes):", + " if failure.startswith('windows-'):", + " denied = 'denied' in failure", + " code = 22 if failure == 'windows-reparse' else (13 if failure.endswith('-13') else (5 if denied else (3 if failure.endswith('-3') else 2)))", + " failed_path = path.parent if 'parent' in failure else path", + " def unavailable_windows_file(*args):", + " raise WindowsScanLocalFileError(code, 'synthetic Windows file error', str(failed_path))", + " original_backend, original_platform = ranking._windows_scan_local_files, ranking.os.name", + " ranking._windows_scan_local_files = lambda: types.SimpleNamespace(open_read_fd=unavailable_windows_file)", + " ranking.os.name = 'nt'", + " try:", + " return read_changed_path(path, target, preview_bytes)", + " finally:", + " ranking._windows_scan_local_files, ranking.os.name = original_backend, original_platform", " if failure == 'unreadable':", " raise PermissionError('synthetic in-target file became unreadable')", " path.unlink()", @@ -371,7 +385,18 @@ test("skips working-tree files that disappear or become unreadable during previe ].join("\n"); for (const generator of ["ranking", "inventory"]) { - for (const failure of ["missing", "unreadable", "parent-missing"]) { + for (const failure of [ + "missing", + "unreadable", + "parent-missing", + "windows-missing-2", + "windows-missing-3", + "windows-denied-5", + "windows-denied-13", + "windows-parent-missing", + "windows-parent-denied", + "windows-reparse", + ]) { mkdirSync(join(repository, "src"), { recursive: true }); writeFileSync(source, "inside = 2\n"); const output = join(root, `${generator}-${failure}.jsonl`); @@ -392,7 +417,7 @@ test("skips working-tree files that disappear or become unreadable during previe { encoding: "utf8" }, ); - if (failure === "parent-missing") { + if (failure.includes("parent") || failure === "windows-reparse") { expect( result.status, `${generator}/${failure}: ${result.stdout}\n${result.stderr}`, From 446ff8fdbe08bdcb9a341a25fe79c867f1ed9e03 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 25 Aug 2026 01:07:43 -0400 Subject: [PATCH 8/8] fix(scan): tolerate Windows leaf sharing violations --- .../_bundled_plugin/scripts/generate_rank_input.py | 2 +- sdk/typescript/tests-ts/diff-rank-input.test.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 5800c3316..47c558b06 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -346,7 +346,7 @@ def _open_windows_changed_path_descriptor(target: Path, relative_path: Path) -> raise FileNotFoundError( error.errno, error.strerror, error.filename ) from error - if error.errno in {errno.EACCES, 5}: + if error.errno in {errno.EACCES, 5, 32, 33}: raise PermissionError(error.errno, error.strerror, error.filename) from error raise diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index 76d47f811..837489eb5 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -356,8 +356,7 @@ test("skips working-tree files that disappear or become unreadable during previe "read_changed_path = ranking.preview_for_changed_path", "def unavailable_leaf(path, target, preview_bytes):", " if failure.startswith('windows-'):", - " denied = 'denied' in failure", - " code = 22 if failure == 'windows-reparse' else (13 if failure.endswith('-13') else (5 if denied else (3 if failure.endswith('-3') else 2)))", + " code = 22 if failure == 'windows-reparse' else int(failure.rsplit('-', 1)[-1])", " failed_path = path.parent if 'parent' in failure else path", " def unavailable_windows_file(*args):", " raise WindowsScanLocalFileError(code, 'synthetic Windows file error', str(failed_path))", @@ -393,8 +392,12 @@ test("skips working-tree files that disappear or become unreadable during previe "windows-missing-3", "windows-denied-5", "windows-denied-13", - "windows-parent-missing", - "windows-parent-denied", + "windows-sharing-32", + "windows-lock-33", + "windows-parent-missing-2", + "windows-parent-denied-5", + "windows-parent-sharing-32", + "windows-parent-lock-33", "windows-reparse", ]) { mkdirSync(join(repository, "src"), { recursive: true });