Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5dc0f1c
fix(git): pin trusted executables across scan hosts
mldangelo-oai Aug 15, 2026
9be47a6
fix(git): preserve sanitized and inherited executable paths
mldangelo-oai Aug 15, 2026
49000ab
fix(git): anchor trusted tools to the scanned repository
mldangelo-oai Aug 15, 2026
78123a7
fix(git): reject repository aliases by filesystem identity
mldangelo-oai Aug 15, 2026
71ba0ab
Merge commit '5d1afcd312933121e36dc892dcb12e8d2e3e1de3' into mdangelo…
mldangelo-oai Aug 16, 2026
15a97ef
fix(git): reuse trusted lookup and preserve platform defaults
mldangelo-oai Aug 16, 2026
357b437
fix(git): bind optional tools with platform-aware environments
mldangelo-oai Aug 16, 2026
b701818
fix(git): tolerate unavailable historical targets
mldangelo-oai Aug 16, 2026
743e32f
fix(runtime): stage packaged ripgrep for local installs
mldangelo-oai Aug 16, 2026
c0142c5
fix(api): preserve explicit Git disable bindings
mldangelo-oai Aug 16, 2026
62bfcd1
fix(runtime): reject canonical Windows batch targets
mldangelo-oai Aug 16, 2026
b83d18e
test: preserve Python shim startup environment
mldangelo-oai Aug 16, 2026
632d3ea
Merge current main into trusted executable binding
mldangelo-oai Aug 17, 2026
ab4e036
fix(sdk): preserve explicit tool selections
mldangelo-oai Aug 17, 2026
38d9956
Merge main session setup into trusted tool bindings
mldangelo-oai Aug 17, 2026
b2fa38c
fix(sdk): carry explicit Git bindings through target validation
mldangelo-oai Aug 17, 2026
2a365a0
fix(sdk): exclude internal helpers from public declarations
mldangelo-oai Aug 17, 2026
1fa7620
test(sdk): preserve output boundaries when Git is disabled
mldangelo-oai Aug 17, 2026
72ff479
Merge Windows executable discovery into trusted Git selection
mldangelo-oai Aug 17, 2026
e9afbd9
fix(git): align trusted tool bindings across scan hosts
mldangelo-oai Aug 18, 2026
516111a
Merge main into trusted Git executable binding
mldangelo-oai Aug 18, 2026
30a94f4
fix(git): honor selected executable for bulk checkouts
mldangelo-oai Aug 18, 2026
633f0e8
fix(multiscan): exclude local source roots from Git selection
mldangelo-oai Aug 18, 2026
cff2531
fix(multiscan): protect all campaign source roots
mldangelo-oai Aug 18, 2026
5896809
fix(git): protect repositories supplying scan inputs
mldangelo-oai Aug 18, 2026
bd9ca6c
test: check shared credential lock after both scans start
mldangelo-oai Aug 18, 2026
fa12ba4
fix(git): retain input roots behind dangling links
mldangelo-oai Aug 18, 2026
6fe9d9d
fix(git): protect scan outputs and isolate cyclic inputs
mldangelo-oai Aug 18, 2026
24189bc
fix: preserve deliberate local Git selections
mldangelo-oai Aug 18, 2026
83ab753
fix(multiscan): isolate unavailable input roots
mldangelo-oai Aug 18, 2026
76b9e90
fix(git): handle host path compatibility
mldangelo-oai Aug 18, 2026
0552695
fix: align tool locations with scan inputs
mldangelo-oai Aug 18, 2026
8f4f32a
Merge current main and bind workbench Git
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
7 changes: 4 additions & 3 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,10 @@ These container settings are distinct from standalone CLI flags and
interactive discovery's `GH_HOST`.

Variables such as `CODEX_SECURITY_SCAN_ID`, `CODEX_SECURITY_SCAN_DIR`,
`CODEX_SECURITY_PLUGIN_ROOT`, `CODEX_SECURITY_CONFIG_PATH`, and
`CODEX_SECURITY_TARGET_PATHS_FILE` are generated by an active scan. They are
internal runtime data, not supported user configuration.
`CODEX_SECURITY_PLUGIN_ROOT`, `CODEX_SECURITY_CONFIG_PATH`,
`CODEX_SECURITY_GIT`, and `CODEX_SECURITY_TARGET_PATHS_FILE` are generated by
an active scan. They are internal runtime data, not supported user
configuration.

Use `--provider openrouter` to send inference through OpenRouter. Set
`OPENROUTER_API_KEY` and specify a supported model with `--model`.
Expand Down
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.29",
"description": "Codex Security workflows for security scans, analysis, and investigation.",
"author": {
"name": "OpenAI"
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/_bundled_plugin/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
"PYTHON",
"PYTHONUTF8",
"CODEX_SECURITY_GIT",
"CODEX_SECURITY_KNOWLEDGE_BASE",
"CODEX_SECURITY_DEEP_SCAN_CONFIG_PATH",
"CODEX_SECURITY_SCAN_ROOT",
Expand Down
31 changes: 15 additions & 16 deletions sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import tempfile
from pathlib import Path

# Some plugin hosts launch Python with safe-path isolation enabled.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from workbench_target import git_blob_bytes, git_command


class InventoryError(ValueError):
"""Raised when the repository, scope, or inventory cannot be used safely."""
Expand Down Expand Up @@ -127,20 +131,18 @@ def generate_in_scope_files(repository: Path, scope: str, output: Path) -> int:


def committed_changed_paths(repository: Path, base: str, head: str) -> list[tuple[Path, str]]:
result = subprocess.run(
[
"git",
"-C",
str(repository),
"diff",
"--raw",
"-z",
"--diff-filter=ACMRD",
f"{base}..{head}",
],
capture_output=True,
check=True,
result = git_command(
repository,
"diff",
"--no-ext-diff",
"--no-textconv",
"--raw",
"-z",
"--diff-filter=ACMRD",
f"{base}..{head}",
text=False,
)
result.check_returncode()
fields = result.stdout.split(b"\0")
changed: list[tuple[Path, str]] = []
index = 0
Expand All @@ -166,16 +168,13 @@ def generate_diff_in_scope_files(
output: Path,
) -> 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 rank_preview import (
DEFAULT_PREVIEW_BYTES,
TEXT_CODE_EXTENSIONS,
is_binary_sample,
preview_for,
)
from workbench_target import git_blob_bytes

rows: list[bytes] = []
try:
changed = (
Expand Down
38 changes: 20 additions & 18 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,7 @@
preview_for,
preview_for_bytes,
)
from workbench_target import git_blob_bytes, git_directory_snapshot_paths
from workbench_target import git_blob_bytes, git_command, git_directory_snapshot_paths

EXCLUDED_DIRS = {
".cache",
Expand Down Expand Up @@ -626,20 +626,18 @@ def bind_repo_scopes(args: argparse.Namespace) -> None:


def run_git_changed_paths(repo: Path, diff_args: list[str]) -> list[tuple[Path, str]]:
result = subprocess.run(
[
"git",
"-C",
str(repo),
"diff",
"--name-status",
"-z",
"--diff-filter=ACMRD",
*diff_args,
],
check=True,
capture_output=True,
result = git_command(
repo,
"diff",
"--no-ext-diff",
"--no-textconv",
"--name-status",
"-z",
"--diff-filter=ACMRD",
*diff_args,
text=False,
)
result.check_returncode()
fields = result.stdout.split(b"\0")
if fields and not fields[-1]:
fields.pop()
Expand All @@ -663,11 +661,15 @@ def git_changed_paths(repo: Path, base: str, head: str, mode: str) -> list[tuple
if mode == "local-patch":
unstaged = run_git_changed_paths(repo, [base])
staged = run_git_changed_paths(repo, ["--cached", base])
untracked = subprocess.run(
["git", "-C", str(repo), "ls-files", "--others", "--exclude-standard", "-z"],
capture_output=True,
check=True,
untracked = git_command(
repo,
"ls-files",
"--others",
"--exclude-standard",
"-z",
text=False,
)
untracked.check_returncode()
combined = dict(staged)
combined.update(unstaged)
combined.update(
Expand Down
56 changes: 56 additions & 0 deletions sdk/typescript/_bundled_plugin/scripts/workbench_constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Shared constants for the Codex Security workbench."""

import argparse
import os
import sys
from pathlib import Path

MODES = ("diff", "standard", "deep")
DIFF_TARGET_KINDS = ("working_tree", "commit", "range")
Expand Down Expand Up @@ -70,6 +73,59 @@
EMPTY_GIT_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"


def _protected_repository_root(target: Path) -> Path:
root = target.resolve()
if root.is_file():
root = root.parent
protected = root
for ancestor in (root, *root.parents):
try:
(ancestor / ".git").lstat()
except FileNotFoundError:
continue
protected = ancestor
return protected


def trusted_git_executable(protected_root: Path) -> str | None:
"""Return the host-selected Git executable without searching ``PATH``."""
configured = os.environ.get("CODEX_SECURITY_GIT")
if not configured:
return None

candidate = Path(configured)
if not candidate.is_absolute():
raise SystemExit("CODEX_SECURITY_GIT must name an absolute trusted executable.")

try:
invocation = Path(os.path.abspath(candidate))
canonical = candidate.resolve(strict=True)
repository = _protected_repository_root(protected_root)
except (OSError, RuntimeError):
return None

windows = sys.platform == "win32"
native_windows_suffixes = {".exe", ".com"}
if (
not canonical.is_file()
or not os.access(canonical, os.F_OK if windows else os.X_OK)
or (
windows
and (
candidate.suffix.lower() not in native_windows_suffixes
or canonical.suffix.lower() in {".bat", ".cmd"}
)
)
):
return None
if any(
path == repository or repository in path.parents
for path in (invocation, canonical)
):
raise SystemExit("CODEX_SECURITY_GIT must stay outside the protected repository.")
return str(invocation)


def main() -> None:
argparse.ArgumentParser(description=__doc__).parse_args()

Expand Down
10 changes: 8 additions & 2 deletions sdk/typescript/_bundled_plugin/scripts/workbench_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# Some plugin hosts launch Python with safe-path isolation enabled.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from filesystem_identity import stored_filesystem_identity_matches
from workbench_constants import GIT_REPOSITORY_ENVIRONMENT
from workbench_constants import GIT_REPOSITORY_ENVIRONMENT, trusted_git_executable


def git_output(
Expand Down Expand Up @@ -134,19 +134,25 @@ def git_command(
for name in GIT_REPOSITORY_ENVIRONMENT:
environment.pop(name, None)
environment["GIT_LITERAL_PATHSPECS"] = "1"
executable = trusted_git_executable(target)
# Repository-local config is untrusted; fsmonitor may name an executable hook.
command = [
"git",
executable or "git",
"-c",
"core.fsmonitor=false",
"-c",
f"core.hooksPath={os.devnull}",
"-c",
"i18n.logOutputEncoding=UTF-8",
"-C",
str(target),
]
if git_dir is not None and work_tree is not None:
command.extend(["--git-dir", str(git_dir), "--work-tree", str(work_tree)])
full_command = [*command, *args]
if executable is None:
empty_output = "" if text else b""
return subprocess.CompletedProcess(full_command, 127, empty_output, empty_output)
try:
return subprocess.run(
full_command,
Expand Down
41 changes: 34 additions & 7 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ import {
importAmbientAuth,
prepareCodexSecurityCredentialHome,
preserveCodexSecurityPluginRegistration,
pluginExecutionEnvironment,
pluginExecutionEnvironmentWithGit,
planOutputArchive,
prepareOutputDir,
preparePersistentOutputRoot,
Expand All @@ -134,6 +134,7 @@ import {
enclosingGitWorktreeRoot,
normalizeRepository,
normalizeTarget,
outermostGitMarkerRoot,
repositoryRevision,
resolveRepositoryPath,
type NormalizedTarget,
Expand All @@ -143,6 +144,10 @@ import {
validateCommittedDiffCheckout,
validateMode,
} from "./targets.js";
import {
inspectTrustedExecutable,
type InspectedExecutable,
} from "./trusted-executable.js";

interface CodexThreadLike {
readonly id: string | null;
Expand Down Expand Up @@ -547,6 +552,28 @@ export class CodexSecurity {
python,
} = session;
releaseCredentialHome = session.releaseCredentialHome;
const pluginEnvironment = selectedScanEnvironment(
runtime.environment,
options.auth,
modelProvider,
);
const gitProtectedRoot = await outermostGitMarkerRoot(repo, signal);
let git = await inspectTrustedExecutable(
"git",
pluginEnvironment,
gitProtectedRoot,
);
for (const source of knowledgeBase?.sources ?? []) {
const sourceDirectory = (await lstat(source)).isDirectory()
? source
: dirname(source);
git = await inspectTrustedExecutable(
"git",
git.environment,
await outermostGitMarkerRoot(sourceDirectory, signal),
);
}
checkOpen();
const deepScanConfigPath =
mode === "deep"
? runtime.deepScanConfigPath ??
Expand Down Expand Up @@ -765,13 +792,10 @@ export class CodexSecurity {
python,
pluginRoot: runtime.plugin.pluginRoot,
environment: {
...selectedScanEnvironment(
runtime.environment,
options.auth,
modelProvider,
),
...pluginEnvironment,
CODEX_SECURITY_STATE_DIR: stateDirectory,
},
git,
signal,
failureMessage: "Could not save the Codex Security scan",
};
Expand Down Expand Up @@ -975,6 +999,7 @@ export class CodexSecurity {
session,
runtimePaths,
options.auth,
git,
);
const thread = codex.startThread({
workingDirectory: scanDir,
Expand Down Expand Up @@ -1646,6 +1671,7 @@ export class CodexSecurity {
session: PreparedSession,
runtimePaths: Record<string, string>,
auth: ScanAuthMode = "auto",
git: InspectedExecutable,
): { codex: CodexClientLike; environment: ProcessEnvironment } {
const {
runtime,
Expand All @@ -1656,11 +1682,12 @@ export class CodexSecurity {
sessionConfig,
} = session;
const environment = {
...pluginExecutionEnvironment(
...pluginExecutionEnvironmentWithGit(
python,
withoutCodexHome(
selectedScanEnvironment(runtime.environment, auth, modelProvider),
),
git,
),
...(externalProvider === null
? {}
Expand Down
Loading
Loading