Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
# Some plugin hosts launch Python with safe-path isolation enabled.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from rank_preview import DEFAULT_PREVIEW_BYTES, TEXT_CODE_EXTENSIONS, preview_for
from workbench_constants import trusted_git_executable

EXCLUDED_DIRS = {
".cache",
Expand Down Expand Up @@ -488,12 +489,21 @@ def bind_repo_scopes(args: argparse.Namespace) -> None:


def run_git_changed_paths(repo: Path, diff_args: list[str]) -> list[tuple[Path, str]]:
git = trusted_git_executable(repo)
if git is None:
raise SystemExit("Git is unavailable on the trusted executable path.")
result = subprocess.run(
[
"git",
git,
"-c",
"core.fsmonitor=false",
"-c",
"core.hooksPath=/dev/null",
"-C",
str(repo),
"diff",
"--no-ext-diff",
"--no-textconv",
"--name-status",
"-z",
"--diff-filter=ACMRD",
Expand Down
22 changes: 22 additions & 0 deletions sdk/typescript/_bundled_plugin/scripts/workbench_constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Shared constants for the Codex Security workbench."""

import argparse
import os
from pathlib import Path

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


def trusted_git_executable(protected_root: Path | None = None) -> str | None:
executable = os.environ.get("CODEX_SECURITY_GIT")
if not executable:
return None
candidate = Path(executable)
if not candidate.is_absolute():
raise SystemExit("CODEX_SECURITY_GIT must name an absolute trusted executable.")
try:
canonical = candidate.resolve(strict=True)
except OSError as exc:
raise SystemExit("CODEX_SECURITY_GIT does not name an available executable.") from exc
if not canonical.is_file() or not os.access(canonical, os.X_OK):
raise SystemExit("CODEX_SECURITY_GIT does not name an available executable.")
if protected_root is not None:
root = protected_root.resolve()
if canonical == root or root in canonical.parents:
raise SystemExit("CODEX_SECURITY_GIT must stay outside the protected repository.")
return str(canonical)


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

Expand Down
16 changes: 14 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 @@ -54,10 +54,22 @@ def git_command(
environment.pop(name, None)
environment["GIT_LITERAL_PATHSPECS"] = "1"
# Repository-local config is untrusted; fsmonitor may name an executable hook.
command = ["git", "-c", "core.fsmonitor=false", "-C", str(target)]
executable = trusted_git_executable(target)
command = [
executable or "git",
"-c",
"core.fsmonitor=false",
"-c",
"core.hooksPath=/dev/null",
"-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
19 changes: 15 additions & 4 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
validatedGitEnvironment,
validateMode,
} from "./targets.js";
import { resolveTrustedExecutable } from "./trusted-executable.js";

interface CodexThreadLike {
readonly id: string | null;
Expand Down Expand Up @@ -465,6 +466,15 @@ export class CodexSecurity {
protectedRoot,
signal,
});
const pluginEnvironment = selectedScanEnvironment(
runtime.environment,
options.auth,
);
const git = await resolveTrustedExecutable(
"git",
pluginEnvironment,
protectedRoot,
);
checkOpen();
const scanOutputRoot =
requestedOutput === null &&
Expand Down Expand Up @@ -575,9 +585,11 @@ export class CodexSecurity {
python,
pluginRoot: runtime.plugin.pluginRoot,
environment: {
...selectedScanEnvironment(runtime.environment, options.auth),
...pluginEnvironment,
CODEX_SECURITY_STATE_DIR: stateDirectory,
},
git,
protectedRoot,
signal,
failureMessage: "Could not save the Codex Security scan",
};
Expand Down Expand Up @@ -729,9 +741,8 @@ export class CodexSecurity {
const environment = {
...pluginExecutionEnvironment(
python,
withoutCodexHome(
selectedScanEnvironment(runtime.environment, options.auth),
),
withoutCodexHome(pluginEnvironment),
git,
),
CODEX_HOME: runtime.codexHome,
...runtimePaths,
Expand Down
33 changes: 31 additions & 2 deletions sdk/typescript/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
} from "./errors.js";
import type { JsonObject } from "./config.js";
import { resolveTrustedExecutable } from "./trusted-executable.js";
import type { TrustedExecutable } from "./trusted-executable.js";

const execFile = promisify(execFileCallback);

Expand Down Expand Up @@ -92,6 +93,8 @@ export interface WorkbenchCommandOptions {
python: string;
pluginRoot: string;
environment: ProcessEnvironment;
git?: TrustedExecutable | null;
protectedRoot?: string;
signal?: AbortSignal;
failureMessage?: string;
}
Expand Down Expand Up @@ -456,6 +459,19 @@ export async function runWorkbench(
options: WorkbenchCommandOptions,
args: readonly string[],
): Promise<JsonObject> {
const git =
options.git === undefined
? await resolveTrustedExecutable(
"git",
options.environment,
options.protectedRoot ?? process.cwd(),
)
: options.git;
const environment = pluginExecutionEnvironment(
options.python,
options.environment,
git,
);
let stdout: string;
try {
({ stdout } = await execFile(
Expand All @@ -468,7 +484,7 @@ export async function runWorkbench(
],
{
env: Object.fromEntries(
Object.entries(options.environment).filter(
Object.entries(environment).filter(
([name]) =>
name.toUpperCase() !== "OPENAI_API_KEY" &&
name.toUpperCase() !== "CODEX_API_KEY",
Expand Down Expand Up @@ -1532,8 +1548,21 @@ export async function resolvePluginPython(
export function pluginExecutionEnvironment(
python: string,
environment: ProcessEnvironment = process.env,
git?: TrustedExecutable | null,
): ProcessEnvironment {
return { ...environment, PYTHON: python };
const result = { ...environment };
if (git !== undefined) {
for (const name of Object.keys(result)) {
const normalized = name.toUpperCase();
if (normalized === "PATH" || normalized.startsWith("GIT_")) {
delete result[name];
}
}
result["PATH"] = git?.environment["PATH"] ?? "";
result["CODEX_SECURITY_GIT"] = git?.executable ?? "";
}
result["PYTHON"] = python;
return result;
}

export async function cleanupSdkDirectory(path: string): Promise<void> {
Expand Down
2 changes: 2 additions & 0 deletions sdk/typescript/tests-ts/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2670,6 +2670,7 @@ describe("CodexSecurity orchestration", () => {
const runtime = preparedRuntime(codexHome);
return {
...runtime,
environment: { PATH: process.env["PATH"] },
plugin: {
...(runtime["plugin"] as Record<string, unknown>),
installedRoot: join(
Expand Down Expand Up @@ -2723,6 +2724,7 @@ describe("CodexSecurity orchestration", () => {
CODEX_SECURITY_SCAN_DIR: scanDir,
CODEX_SECURITY_PLUGIN_ROOT: PLUGIN_ROOT,
CODEX_SECURITY_TARGET_DISPLAY_NAME: basename(repository),
CODEX_SECURITY_GIT: expect.stringMatching(/git(?:\.exe)?$/iu),
});
expect(environment).not.toHaveProperty("CODEX_SECURITY_TARGET_PATHS_JSON");
const targetPathsFile = environment?.["CODEX_SECURITY_TARGET_PATHS_FILE"];
Expand Down
134 changes: 134 additions & 0 deletions sdk/typescript/tests-ts/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,113 @@ describe("runtime directories and plugin Python boundary", () => {
expect(result).toEqual({ ok: true });
});

testPosix(
"uses trusted Git for workbench commands instead of a repository-local shim",
async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const shimDirectory = join(repository, "node_modules", ".bin");
const pluginRoot = join(root, "plugin");
const marker = join(root, "repository-git-executed");
await mkdir(shimDirectory, { recursive: true });
await mkdir(join(pluginRoot, "scripts"), { recursive: true });
await writeFile(
join(shimDirectory, "git"),
`#!/bin/sh\nprintf executed > ${JSON.stringify(marker)}\nexit 1\n`,
{ mode: 0o700 },
);
await writeFile(
join(pluginRoot, "scripts", "workbench_db.py"),
[
"import json, os, subprocess",
"assert os.environ.get('GIT_CONFIG_COUNT') is None",
"git = os.environ['CODEX_SECURITY_GIT']",
"completed = subprocess.run([git, '--version'], check=True, capture_output=True, text=True)",
"print(json.dumps({'git': git, 'path': os.environ.get('PATH'), 'version': completed.stdout.strip()}))",
].join("\n"),
);
const python = Bun.which("python3") ?? Bun.which("python");
const git = Bun.which("git");
expect(python).not.toBeNull();
expect(git).not.toBeNull();

const result = await runWorkbench(
{
python: python!,
pluginRoot,
protectedRoot: repository,
environment: {
PATH: `${shimDirectory}${delimiter}${dirname(git!)}`,
GIT_CONFIG_COUNT: "1",
GIT_CONFIG_KEY_0: "core.fsmonitor",
GIT_CONFIG_VALUE_0: join(repository, "fsmonitor"),
},
},
["test-command"],
);

expect(result).toMatchObject({
git,
version: expect.stringMatching(/^git version /u),
});
expect(String(result["path"]).split(delimiter)).not.toContain(
shimDirectory,
);
expect(existsSync(marker)).toBe(false);
},
);

testPosix(
"does not let diff ranking fall back to a repository-local Git shim",
async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const shimDirectory = join(repository, "node_modules", ".bin");
const marker = join(root, "rank-git-executed");
const output = join(root, "rank-input.jsonl");
await mkdir(shimDirectory, { recursive: true });
await writeFile(
join(shimDirectory, "git"),
`#!/bin/sh\nprintf executed > ${JSON.stringify(marker)}\nexit 1\n`,
{ mode: 0o700 },
);
const python = Bun.which("python3") ?? Bun.which("python");
expect(python).not.toBeNull();

const result = spawnSync(
python!,
[
"-I",
"-B",
join(PLUGIN_ROOT, "scripts", "generate_rank_input.py"),
"make-diff-rank-input",
"--repo",
repository,
"--base",
"HEAD",
"--mode",
"local-patch",
"--out",
output,
],
{
encoding: "utf8",
env: {
PATH: shimDirectory,
CODEX_SECURITY_GIT: join(shimDirectory, "git"),
},
},
);

expect(result.status).toBe(1);
expect(result.stderr).toContain(
"CODEX_SECURITY_GIT must stay outside the protected repository.",
);
expect(existsSync(marker)).toBe(false);
expect(existsSync(output)).toBe(false);
},
);

test("preserves recorded artifact paths when archiving a completed scan", async () => {
const root = await temporaryDirectory();
const scanDir = join(root, "scan");
Expand Down Expand Up @@ -1863,6 +1970,33 @@ describe("runtime directories and plugin Python boundary", () => {
TEST: "1",
PYTHON: managed,
});
expect(
pluginExecutionEnvironment(
managed,
{ PATH: "/repository/bin", GIT_CONFIG_COUNT: "1", TEST: "1" },
{
executable: "/trusted/bin/git",
environment: { PATH: "/trusted/bin" },
},
),
).toEqual({
PATH: "/trusted/bin",
TEST: "1",
CODEX_SECURITY_GIT: "/trusted/bin/git",
PYTHON: managed,
});
expect(
pluginExecutionEnvironment(
managed,
{ Path: "/repository/bin", TEST: "1" },
null,
),
).toEqual({
PATH: "",
TEST: "1",
CODEX_SECURITY_GIT: "",
PYTHON: managed,
});
await expect(
resolvePluginPython({
configuredPath: "/bin/true",
Expand Down
8 changes: 7 additions & 1 deletion sdk/typescript/tests-ts/scan-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,13 @@ describe("malformed scan artifact recovery", () => {
fixture.repository,
join(fixture.stateDir, "checkout"),
],
{ encoding: "utf8" },
{
encoding: "utf8",
env: {
...process.env,
CODEX_SECURITY_GIT: Bun.which("git")!,
},
},
);
expect(copied.status, copied.stderr).toBe(0);
}
Expand Down