Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ For standard and diff scans, a passed `delegated_workers` check means the runtim

When `CODEX_SECURITY_CONFIG_PATH` is set, add `--config "$CODEX_SECURITY_CONFIG_PATH"` in POSIX shells or `--config "$env:CODEX_SECURITY_CONFIG_PATH"` in PowerShell. The CLI provides this sanitized, shell-readable copy of the active worker configuration because the credential-bearing `CODEX_HOME` is intentionally inaccessible to repository-influenced commands. Do not substitute an ambient Codex home in that case.

Otherwise, the helper discovers Codex config paths itself from `--cwd`, which defaults to the current working directory. It reads `/etc/codex/config.toml`, then `$CODEX_HOME/config.toml`, resolves `project_root_markers`, checks the matching `[projects."<absolute-project-root>"].trust_level`, and loads trusted project `.codex/config.toml` layers from the project root down to `--cwd`. It does not load project layers unless the user config marks that project root as `trusted`.
Otherwise, the helper discovers Codex config paths itself from `--cwd`, which defaults to the current working directory. It reads `/etc/codex/config.toml` on Unix-like hosts or `%ProgramData%\OpenAI\Codex\config.toml` on Windows, then `$CODEX_HOME/config.toml`, resolves `project_root_markers`, checks the matching `[projects."<absolute-project-root>"].trust_level`, and loads trusted project `.codex/config.toml` layers from the project root down to `--cwd`. It does not load project layers unless the user config marks that project root as `trusted`.

When the current Codex CLI session selected `-p/--profile <name>`, pass `--codex-config-profile <name>`. Current Codex loads `$CODEX_HOME/<name>.config.toml` above the base user config and below trusted project config, so the helper uses that layer for project-root markers, trust, and capability values before it discovers project config. A missing profile file is an empty layer, matching the CLI. Embedded `[profiles.<name>]` lookup remains only for older Codex configs that select `profile` without the CLI flag. Project-local `profile` and `profiles` values are ignored. For session-only CLI overrides or other effective config values that cannot be recovered from config paths, pass `--effective-config <path>=<json-value>`.

Expand Down
21 changes: 20 additions & 1 deletion sdk/typescript/_bundled_plugin/scripts/config_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_REGISTRY = PLUGIN_ROOT / "preflight" / "capability-profiles.toml"
DEFAULT_CODEX_HOME = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
SYSTEM_CONFIG = Path("/etc/codex/config.toml")


def default_system_config() -> Path:
if os.name == "nt":
program_data = os.environ.get("ProgramData", r"C:\ProgramData")
return Path(program_data) / "OpenAI" / "Codex" / "config.toml"
return Path("/etc/codex/config.toml")


SYSTEM_CONFIG = default_system_config()
DEFAULT_CONFIG = DEFAULT_CODEX_HOME / "config.toml"
VALID_SEVERITIES = {"block", "warn", "suggest"}
VALID_MULTI_AGENT_OWNERS = {"native", "codex-bridge"}
Expand Down Expand Up @@ -244,6 +253,16 @@ def project_trust_level(
if not isinstance(projects, dict):
continue
project = projects.get(str(project_root))
if not isinstance(project, dict) and os.name == "nt":
project_key = os.path.normcase(os.path.realpath(project_root))
project = next(
(
value
for path, value in projects.items()
if os.path.normcase(os.path.realpath(path)) == project_key
),
None,
)
if not isinstance(project, dict):
continue
trust_level = project.get("trust_level")
Expand Down
21 changes: 17 additions & 4 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import {
codexSecurityHasStoredFileCredentials,
codexSecurityStateDirectory,
createIsolatedHome,
expandHome,
importAmbientAuth,
prepareCodexSecurityCredentialHome,
preserveCodexSecurityPluginRegistration,
Expand Down Expand Up @@ -2101,8 +2102,10 @@ async function prepareDeepScanConfig(
options: DeepScanOptions,
signal: AbortSignal,
): Promise<void> {
const ambientHome =
environmentValue(environment, "CODEX_HOME") ?? join(homedir(), ".codex");
const ambientHome = expandHome(
environmentValue(environment, "CODEX_HOME") ?? join(homedir(), ".codex"),
environment,
);
const source = join(ambientHome, "codex-security", "config.toml");
let configured: TomlTable = {};
try {
Expand All @@ -2128,14 +2131,15 @@ async function prepareDeepScanConfig(
const value = options[name];
if (value !== undefined) overrides[key] = value;
}
const sharedConfig = await sameExistingPath(source, destination);
const hasOverrides = Object.keys(overrides).length > 0;
if (existing === undefined && !hasOverrides) {
if (destination !== source) {
if (!sharedConfig) {
await rm(destination, { force: true });
}
return;
}
if (destination === source && !hasOverrides) return;
if (sharedConfig && !hasOverrides) return;
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
await writeFile(
destination,
Expand All @@ -2147,6 +2151,15 @@ async function prepareDeepScanConfig(
);
}

async function sameExistingPath(left: string, right: string): Promise<boolean> {
if (left === right) return true;
const [canonicalLeft, canonicalRight] = await Promise.all([
realpath(left).catch(() => null),
realpath(right).catch(() => null),
]);
return canonicalLeft !== null && canonicalLeft === canonicalRight;
}

export function createSecurity(
config: CodexSecurityConfig = {},
): CodexSecurity {
Expand Down
9 changes: 6 additions & 3 deletions sdk/typescript/src/bulk-scan-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { promisify } from "node:util";
import { confirm, input, search } from "@inquirer/prompts";
import { Octokit } from "@octokit/core";
import Papa from "papaparse";
import { expandHome } from "./runtime.js";
import { resolveTrustedExecutable } from "./trusted-executable.js";

const execFile = promisify(execFileCallback);
Expand Down Expand Up @@ -171,9 +172,11 @@ export async function runBulkScanWizard(

const outputDir = resolve(
dependencies.currentDirectory(),
await prompt.input(
"Where should scan results be saved?",
"./security-scans",
expandHome(
await prompt.input(
"Where should scan results be saved?",
"./security-scans",
),
),
);
const inputPath = join(outputDir, "repositories.csv");
Expand Down
51 changes: 31 additions & 20 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,11 +586,14 @@ async function readPromptFiles(
const [scanPrompt, postScanPrompt] = await Promise.all([
scanPromptFile === undefined
? undefined
: readRegularInputFile(resolve(directory, scanPromptFile), repository),
: readRegularInputFile(
resolveCliPath(directory, scanPromptFile),
repository,
),
postScanPromptFile === undefined
? undefined
: readRegularInputFile(
resolve(directory, postScanPromptFile),
resolveCliPath(directory, postScanPromptFile),
repository,
),
]);
Expand Down Expand Up @@ -646,6 +649,10 @@ async function readRegularInputFile(
}
}

export function resolveCliPath(directory: string, value: string): string {
return resolve(directory, expandHome(value));
}

interface ScanArguments extends DeepScanOptions {
auth?: ScanAuthMode;
verbose?: boolean;
Expand Down Expand Up @@ -901,7 +908,9 @@ export async function runCodexSkillCommand(
if (name.toUpperCase() === "CODEX_HOME") delete environment[name];
}
if (configuredHome?.trim()) {
environment["CODEX_HOME"] = resolve(expandHome(configuredHome));
environment["CODEX_HOME"] = resolve(
expandHome(configuredHome, processEnvironment),
);
}
const invocation = spawn(command.command, [...args], {
env: environment,
Expand Down Expand Up @@ -1281,7 +1290,7 @@ export async function main(
}),
output: z.record(z.string(), z.unknown()).optional(),
async run({ args, format }) {
const repository = resolve(
const repository = resolveCliPath(
dependencies.currentDirectory(),
args.repository ?? ".",
);
Expand Down Expand Up @@ -1330,26 +1339,25 @@ export async function main(
output: z.record(z.string(), z.unknown()).optional(),
async run({ args, format, options }) {
const directory = dependencies.currentDirectory();
const scanRoot =
options.scanRoot === undefined
? undefined
: resolveCliPath(directory, options.scanRoot);
const repository =
options.scanRoot !== undefined && args.repository === undefined
scanRoot !== undefined && args.repository === undefined
? undefined
: resolve(directory, args.repository ?? directory);
: resolveCliPath(directory, args.repository ?? directory);
return presentHistory(
await history([
"list-scans",
...(repository === undefined ? [] : ["--repository", repository]),
...(options.scanRoot === undefined
? []
: ["--scan-root", resolve(directory, options.scanRoot)]),
...(scanRoot === undefined ? [] : ["--scan-root", scanRoot]),
]),
"list",
format,
{
repository,
scanRoot:
options.scanRoot === undefined
? undefined
: resolve(directory, options.scanRoot),
scanRoot,
},
);
},
Expand Down Expand Up @@ -2168,7 +2176,10 @@ export async function main(
"git",
[
"-C",
resolve(dependencies.currentDirectory(), args.repository ?? "."),
resolveCliPath(
dependencies.currentDirectory(),
args.repository ?? ".",
),
"rev-parse",
"--path-format=absolute",
"--git-path",
Expand Down Expand Up @@ -2346,8 +2357,8 @@ export async function main(
"--output-dir is required with a repository CSV.",
);
}
inputPath = resolve(currentDirectory, args.input);
outputDir = resolve(currentDirectory, options.outputDir);
inputPath = resolveCliPath(currentDirectory, args.input);
outputDir = resolveCliPath(currentDirectory, options.outputDir);
}
const result = await runMultiscan({
inputPath,
Expand Down Expand Up @@ -2443,20 +2454,20 @@ export async function main(
if (scanDir === undefined) return;
exitCode = await runExport(
{
scanDir: resolve(currentDirectory, scanDir),
scanDir: resolveCliPath(currentDirectory, scanDir),
format: options.exportFormat,
output:
options.output === "-"
? "-"
: resolve(
: resolveCliPath(
currentDirectory,
options.output ??
EXPORT_DEFAULT_OUTPUTS[options.exportFormat],
),
sourceRoot:
options.sourceRoot === undefined
? undefined
: resolve(currentDirectory, options.sourceRoot),
: resolveCliPath(currentDirectory, options.sourceRoot),
pythonPath: options.python,
},
output,
Expand Down Expand Up @@ -3272,7 +3283,7 @@ async function runSkill(
!staysWithinWindowsDeviceRoot(input, rawDeviceRoot) ||
localDeviceRoot !== normalizedDeviceRoot);
if (!windowsNetworkPath) {
const path = resolve(directory, input);
const path = resolveCliPath(directory, input);
const metadata = await lstat(path, { bigint: true }).catch(
(error: unknown) => {
if (
Expand Down
41 changes: 31 additions & 10 deletions sdk/typescript/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,17 @@ export function codexSecurityStateDirectory(
environment: ProcessEnvironment = process.env,
): string {
const configured = environmentValue(environment, "CODEX_SECURITY_STATE_DIR");
if (configured !== undefined) return resolve(expandHome(configured));
if (configured !== undefined) {
return resolve(expandHome(configured, environment));
}
const codexHome =
environmentValue(environment, "CODEX_HOME") ?? join(homedir(), ".codex");
return resolve(expandHome(codexHome), "state", "plugins", "codex-security");
return resolve(
expandHome(codexHome, environment),
"state",
"plugins",
"codex-security",
);
}

export function codexSecurityCredentialHome(
Expand Down Expand Up @@ -2023,11 +2030,13 @@ export function resolveCodexCommand(
environment: ProcessEnvironment = process.env,
): CodexCommand {
const configured = environmentValue(environment, "CODEX_CLI_PATH");
const expanded =
configured === undefined ? undefined : expandHome(configured, environment);
if (
configured &&
(process.platform !== "win32" || /\.(?:exe|com)$/iu.test(configured))
expanded &&
(process.platform !== "win32" || /\.(?:exe|com)$/iu.test(expanded))
) {
return { command: resolve(configured) };
return { command: resolve(expanded) };
}

const platform = process.platform === "android" ? "linux" : process.platform;
Expand Down Expand Up @@ -2482,7 +2491,9 @@ async function usablePython(
signal?: AbortSignal,
): Promise<string | null> {
const command = await resolveTrustedExecutable(
isPythonPathCandidate(candidate) ? expandHome(candidate) : candidate,
isPythonPathCandidate(candidate)
? expandHome(candidate, environment)
: candidate,
environment,
protectedRoot,
);
Expand Down Expand Up @@ -2548,10 +2559,20 @@ async function sameFile(left: string, right: string): Promise<boolean> {
}
}

export function expandHome(value: string): string {
if (value === "~") return homedir();
if (value.startsWith("~/") || value.startsWith("~\\")) {
return join(homedir(), value.slice(2));
export function expandHome(
value: string,
environment: ProcessEnvironment = process.env,
): string {
const home =
(process.platform === "win32"
? environmentValue(environment, "USERPROFILE") ??
environmentValue(environment, "HOME")
: environmentValue(environment, "HOME") ??
environmentValue(environment, "USERPROFILE")) ?? homedir();
if (value === "~") return home;
if (value.startsWith("~/")) return join(home, value.slice(2));
if (value.startsWith("~\\")) {
return join(home, ...value.slice(2).split("\\"));
}
return value;
}
Expand Down
Loading
Loading