Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
149d50a
fix(workbench): keep repository inventory and snapshots within the ta…
mldangelo-oai Aug 15, 2026
6d14f75
fix(workbench): preserve valid scoped and deleted Git paths
mldangelo-oai Aug 15, 2026
50be60a
fix(workbench): preserve safe Git history and refresh cached plugins
mldangelo-oai Aug 15, 2026
523ca7f
fix(workbench): cache canonical Unicode-aware snapshot parents
mldangelo-oai Aug 15, 2026
9db2878
fix(workbench): confine deep review paths using filesystem identity
mldangelo-oai Aug 15, 2026
7d07d9a
fix(workbench): preserve safe path aliases and changed-file coverage
mldangelo-oai Aug 15, 2026
526cdba
fix(workbench): preserve vanished staged additions during inventory
mldangelo-oai Aug 15, 2026
43319dc
fix(workbench): keep optional snapshot progress nonblocking
mldangelo-oai Aug 15, 2026
37bd54b
fix(workbench): preserve nested progress and replaced staged paths
mldangelo-oai Aug 15, 2026
6bb77d8
fix(workbench): preserve enclosing roots and Windows-safe junction fi…
mldangelo-oai Aug 15, 2026
23963af
fix(workbench): preserve selected roots during nested checkout digests
mldangelo-oai Aug 15, 2026
995da43
fix(workbench): anchor nested snapshots to selected checkout roots
mldangelo-oai Aug 15, 2026
a4dcb3a
Merge main into repository target confinement
mldangelo-oai Aug 16, 2026
90843f1
refactor(workbench): share canonical target containment checks
mldangelo-oai Aug 16, 2026
648c152
fix(workbench): preserve canonical leaf containment in previews
mldangelo-oai Aug 16, 2026
c9c1214
Merge current main into repository confinement fix
mldangelo-oai Aug 17, 2026
9249639
Merge main and confine repository paths
mldangelo-oai Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-security",
"version": "0.1.22",
"version": "0.1.38",
"description": "Codex Security workflows for security scans, analysis, and investigation.",
"author": {
"name": "OpenAI"
Expand Down
28 changes: 21 additions & 7 deletions sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def generate_diff_in_scope_files(
is_binary_sample,
preview_for,
)
from workbench_target import git_blob_bytes
from workbench_target import existing_ancestor_is_within_target, git_blob_bytes

rows: list[bytes] = []
try:
Expand Down Expand Up @@ -206,6 +206,19 @@ def generate_diff_in_scope_files(

for path, status in eligible:
relative = path.relative_to(repository)
if mode != "revisions":
try:
within_target = existing_ancestor_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]
Expand All @@ -215,12 +228,13 @@ 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]
):
continue
else:
if (
path.is_symlink()
or not path.is_file()
or preview_for(path, DEFAULT_PREVIEW_BYTES)[1]
):
continue
relative_path = relative.as_posix()
if "\n" in relative_path or "\r" in relative_path:
raise InventoryError(
Expand Down
30 changes: 20 additions & 10 deletions sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@
preview_for,
preview_for_bytes,
)
from workbench_target import git_blob_bytes, git_directory_snapshot_paths
from workbench_target import (
existing_ancestor_is_within_target,
git_blob_bytes,
git_directory_snapshot_paths,
)

EXCLUDED_DIRS = {
".cache",
Expand Down Expand Up @@ -708,6 +712,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 = existing_ancestor_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 = ""
Expand All @@ -723,16 +738,11 @@ 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 = ""
continue
rows.append({"path": rel.as_posix(), "area": args.area, "preview": preview})

rows.sort(key=lambda row: str(row["path"]))
Expand Down
79 changes: 72 additions & 7 deletions sdk/typescript/_bundled_plugin/scripts/workbench_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,10 @@ def worktree_content_digest_for_context(
update_digest_field(
digest,
b"untracked-content",
directory_content_digest(path.resolve()).encode(),
directory_content_digest(
path.resolve(),
_selected_target=((work_tree or repository) / pathspec).resolve(),
).encode(),
)
elif stat.S_ISREG(metadata.st_mode):
content_digest = hashlib.sha256()
Expand Down Expand Up @@ -369,7 +372,21 @@ def clean_worktree_content_digest() -> str:
return f"codex-security-snapshot/v1:sha256:{digest.hexdigest()}"


def git_directory_snapshot_paths(target: Path) -> list[Path] | None:
def git_directory_snapshot_paths(
target: Path, *, skip_unsafe_paths: bool = False, _selected_target: Path | None = None
) -> list[Path] | None:
try:
resolved_target = target.resolve()
canonical_target = (
_selected_target if _selected_target is not None else resolved_target
)
within_target = directory_is_within_target(resolved_target, canonical_target)
except (OSError, RuntimeError):
within_target = False
if not within_target:
if skip_unsafe_paths:
return []
raise SystemExit("Git working-tree paths must stay inside the selected target.")
repository_root = git_output(target, "rev-parse", "--show-toplevel")
if repository_root is None:
return None
Expand All @@ -387,22 +404,43 @@ def git_directory_snapshot_paths(target: Path) -> list[Path] | None:
if listed is None:
raise SystemExit("Could not inspect files in the selected Git working tree.")
paths: list[Path] = []
resolved_parents: dict[Path, Path] = {target: resolved_target}
for raw_path in (raw_path for raw_path in listed.split(b"\0") if raw_path):
path = repository / os.fsdecode(raw_path)
try:
metadata = path.lstat()
except FileNotFoundError:
# The index can retain a path that was staged and then deleted.
continue
parent = path.parent
try:
if parent not in resolved_parents:
resolved_parents[parent] = parent.resolve()
directory = path.resolve() if stat.S_ISDIR(metadata.st_mode) else None
within_target = directory_is_within_target(
resolved_parents[parent], canonical_target
)
if directory is not None:
within_target = (
within_target or directory.samefile(canonical_target)
) and directory_is_within_target(directory, canonical_target)
except (OSError, RuntimeError):
within_target = False
if not within_target:
if skip_unsafe_paths:
continue
raise SystemExit("Git working-tree paths must stay inside the selected target.")
paths.append(path)
if not stat.S_ISDIR(metadata.st_mode):
continue
nested_repository_root = git_output(path, "rev-parse", "--show-toplevel")
if (
nested_repository_root is not None
and Path(nested_repository_root).resolve() == path.resolve()
and Path(nested_repository_root).resolve() == directory
):
nested_paths = git_directory_snapshot_paths(path)
nested_paths = git_directory_snapshot_paths(
path, skip_unsafe_paths=skip_unsafe_paths, _selected_target=canonical_target
)
if nested_paths is not None:
paths.extend(nested_paths)
continue
Expand All @@ -414,14 +452,41 @@ def git_directory_snapshot_paths(target: Path) -> list[Path] | None:
return sorted(set(paths))


def directory_content_digest(target: Path, *, excluded: tuple[Path, ...] = ()) -> str:
def directory_is_within_target(directory: Path, target: Path) -> bool:
"""Compare resolved directories, including equivalent filesystem spellings."""
if directory.is_relative_to(target):
return True
try:
return any(candidate.samefile(target) for candidate in (directory, *directory.parents))
except OSError:
return False


def existing_ancestor_is_within_target(path: Path, target: Path) -> bool:
"""Resolve the nearest existing path without losing missing Git entries."""
candidate = path
while True:
try:
candidate.lstat()
except (FileNotFoundError, NotADirectoryError):
parent = candidate.parent
if parent == candidate:
return False
candidate = parent
continue
return directory_is_within_target(candidate.resolve(strict=True), target)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle broken in-repository symlinks in local diffs

When a local patch contains a staged or untracked broken symlink such as broken.py -> missing.py, lstat() succeeds but resolve(strict=True) raises FileNotFoundError; both diff inventory and rank-input generation convert that into a fatal “Could not inspect” error, preventing the entire scan even though their later branches already handle symlinks without reading their targets. Validate a broken link's unresolved destination or containing directory so an in-target link can proceed while destinations outside the selected target remain rejected.

AGENTS.md reference: sdk/typescript/AGENTS.md:L22-L23

Useful? React with 👍 / 👎.



def directory_content_digest(
target: Path, *, excluded: tuple[Path, ...] = (), _selected_target: Path | None = None
) -> str:
excluded_relative = []
for path in excluded:
try:
excluded_relative.append(path.relative_to(target))
except ValueError:
continue
paths = git_directory_snapshot_paths(target)
paths = git_directory_snapshot_paths(target, _selected_target=_selected_target)
if paths is None:
paths = sorted(target.rglob("*"))
digest = hashlib.sha256()
Expand Down Expand Up @@ -464,7 +529,7 @@ def directory_content_digest(target: Path, *, excluded: tuple[Path, ...] = ()) -


def directory_snapshot_regular_file_count(target: Path) -> int:
paths = git_directory_snapshot_paths(target)
paths = git_directory_snapshot_paths(target, skip_unsafe_paths=True)
if paths is None:
paths = sorted(target.rglob("*"))
count = 0
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.22" as const;
export const BUNDLED_PLUGIN_VERSION = "0.1.38" as const;

const PACKAGE_NAME = "@openai/codex-security";

Expand Down
67 changes: 38 additions & 29 deletions sdk/typescript/tests-ts/diff-rank-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ test("diff previews stay inside the selected repository", () => {
const repository = join(root, "repository");
const nested = join(repository, "src", "nested");
mkdirSync(nested, { recursive: true });
mkdirSync(join(repository, "removed"));
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, "removed", "deleted.py"), "removed = True\n");
writeFileSync(join(repository, "src", "entry.py"), "handler.py");
writeFileSync(join(nested, "linked.py"), "value = 1\n");
git(repository, "add", ".");
Expand All @@ -71,7 +72,7 @@ test("diff previews stay inside the selected repository", () => {
writeFileSync(join(repository, "src", "handler.py"), "value = 2\n");
writeFileSync(join(repository, "src", "entry.py"), "nested/linked.py");
writeFileSync(join(nested, "linked.py"), "value = 2\n");
rmSync(join(repository, "src", "deleted.py"));
rmSync(join(repository, "removed"), { recursive: true });
git(repository, "add", ".");
const updatedLink = git(repository, "hash-object", "src/entry.py");
git(
Expand All @@ -82,43 +83,39 @@ 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 vanished = join(repository, "vanished");
mkdirSync(vanished);
writeFileSync(join(vanished, "added.py"), "vanished = True\n");
git(repository, "add", "vanished/added.py");
rmSync(vanished, { recursive: true });

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(PLUGIN_ROOT, "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")
.trim()
.split("\n")
.map((row) => JSON.parse(row) as { path: string; preview: string });
expect(rows.map((row) => row.path)).toEqual([
"src/deleted.py",
"removed/deleted.py",
"src/entry.py",
"src/handler.py",
"src/nested/linked.py",
Expand All @@ -127,7 +124,19 @@ 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" });
expect(escaped.status).not.toBe(0);
expect(escaped.stderr).toContain(
"Changed Git working-tree paths must stay inside the selected target.",
);
});

Expand Down
Loading
Loading