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
31 changes: 31 additions & 0 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ export interface CodexSecurityMetadata {
executableVersion: string;
}

export type CodexSecuritySurface = "cli" | "sdk";

interface CodexSecurityRuntimeOptions {
surface: CodexSecuritySurface;
}

interface ClientDependencies {
createCodex(options: CodexOptions): CodexClientLike;
environment: ProcessEnvironment;
Expand Down Expand Up @@ -298,6 +304,7 @@ export class CodexSecurity {
};

readonly #dependencies: ClientDependencies;
readonly #surface: CodexSecuritySurface;
readonly #loginHandles = new Set<CodexLoginHandle>();
readonly #abortController = new AbortController();
#activeOperation: Promise<unknown> | null = null;
Expand All @@ -308,12 +315,20 @@ export class CodexSecurity {
#closePromise: Promise<void> | null = null;

public constructor(config?: CodexSecurityConfig);
/** @internal */
public constructor(
config: CodexSecurityConfig,
dependencies: ClientDependencies,
runtimeOptions: CodexSecurityRuntimeOptions,
);
public constructor(
config: CodexSecurityConfig = {},
dependencies: ClientDependencies = DEFAULT_DEPENDENCIES,
runtimeOptions: CodexSecurityRuntimeOptions = { surface: "sdk" },
) {
this.config = structuredClone(config);
this.#dependencies = dependencies;
this.#surface = runtimeOptions.surface;
}

public async run(
Expand Down Expand Up @@ -953,6 +968,9 @@ export class CodexSecurity {
...(sdkCodexConfig as NonNullable<CodexOptions["config"]>),
default_permissions: SCAN_PERMISSION_PROFILE,
allow_login_shell: false,
responses_api_metadata: {
codex_security_surface: this.#surface,
},
},
});
const thread = codex.startThread({
Expand Down Expand Up @@ -1686,6 +1704,19 @@ async function prepareDeepScanConfig(
);
}

export function createSecurity(
config: CodexSecurityConfig = {},
): CodexSecurity {
return createSecurityInternal(config, { surface: "sdk" });
}

export function createSecurityInternal(
config: CodexSecurityConfig = {},
runtimeOptions: CodexSecurityRuntimeOptions,
): CodexSecurity {
return new CodexSecurity(config, DEFAULT_DEPENDENCIES, runtimeOptions);
}

export async function initialCredentialsAvailable(
environment: ProcessEnvironment,
ambientHome: string,
Expand Down
12 changes: 9 additions & 3 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { parse as parseToml } from "smol-toml";
import {
classifyConnectionFailure,
CodexSecurity,
createSecurityInternal,
scanAuthentication,
type DeepScanOptions,
type ScanAuthMode,
Expand Down Expand Up @@ -86,7 +87,8 @@ import {
type CodexCommand,
} from "./runtime.js";
import {
matchScanFindings,
matchScanFindingsInternal,
type matchScanFindings,
type ScanComparisonInput,
} from "./scan-comparison.js";
import { readScanLogs } from "./scan-logs.js";
Expand Down Expand Up @@ -359,7 +361,8 @@ interface CliDependencies {
}

const DEFAULT_DEPENDENCIES: CliDependencies = {
createSecurity: (config) => new CodexSecurity(config),
createSecurity: (config) =>
createSecurityInternal(config, { surface: "cli" }),
environment: process.env,
prepareAuthenticationHome: prepareCodexSecurityCredentialHome,
checkForUpdate: () => checkForUpdate({ environment: process.env }),
Expand Down Expand Up @@ -484,7 +487,8 @@ const DEFAULT_DEPENDENCIES: CliDependencies = {
args,
);
},
matchFindings: matchScanFindings,
matchFindings: (input, options) =>
matchScanFindingsInternal(input, options, { surface: "cli" }),
};

export async function runCodexSkillCommand(
Expand Down Expand Up @@ -2368,6 +2372,8 @@ async function runSkill(
`model_reasoning_effort=${JSON.stringify(reasoningEffort)}`,
"--config",
'approval_policy="never"',
"--config",
'responses_api_metadata.codex_security_surface="cli"',
"--sandbox",
"workspace-write",
"--skip-git-repo-check",
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { CodexSecurity } from "./api.js";
export { CodexSecurity, createSecurity } from "./api.js";
export { estimateScanCost } from "./cost.js";
export type { ScanCost } from "./cost.js";
export type { ScanActivity, ScanActivityStatus } from "./scan-activity.js";
Expand Down
12 changes: 12 additions & 0 deletions sdk/typescript/src/scan-comparison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type TurnOptions,
} from "@openai/codex-sdk";
import { z } from "incur";
import type { CodexSecuritySurface } from "./api.js";
import { accountStatus } from "./auth.js";
import { CodexSecurityError } from "./errors.js";
import {
Expand Down Expand Up @@ -75,6 +76,14 @@ export type ScanComparisonResult = z.infer<typeof comparisonSchema>;
export async function matchScanFindings(
input: ScanComparisonInput,
options: ScanComparisonOptions = {},
): Promise<ScanComparisonResult> {
return await matchScanFindingsInternal(input, options, { surface: "sdk" });
}

export async function matchScanFindingsInternal(
input: ScanComparisonInput,
options: ScanComparisonOptions = {},
runtimeOptions: { surface: CodexSecuritySurface },
): Promise<ScanComparisonResult> {
const codex =
options.codex ??
Expand All @@ -86,6 +95,9 @@ export async function matchScanFindings(
),
config: {
allow_login_shell: false,
responses_api_metadata: {
codex_security_surface: runtimeOptions.surface,
},
"features.apps": false,
"features.code_mode": false,
"features.code_mode_only": false,
Expand Down
20 changes: 20 additions & 0 deletions sdk/typescript/tests-ts/api-environment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, test } from "bun:test";
import { environmentValue } from "../src/api.js";

describe("environmentValue", () => {
test("treats empty values as unset and finds case variants", () => {
expect(environmentValue({ CODEX_HOME: "" }, "CODEX_HOME")).toBeUndefined();
expect(
environmentValue({ CODEX_HOME: " " }, "CODEX_HOME"),
).toBeUndefined();
expect(
environmentValue(
{ CODEX_HOME: "", Codex_Home: "/ambient" },
"CODEX_HOME",
),
).toBe("/ambient");
expect(environmentValue({ Home: "/shell-home" }, "HOME")).toBe(
"/shell-home",
);
});
});
116 changes: 116 additions & 0 deletions sdk/typescript/tests-ts/api-surface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import type { CodexOptions } from "@openai/codex-sdk";
import { afterEach, describe, expect, test } from "bun:test";
import { CodexSecurity } from "../src/index.js";
import { PLUGIN_ROOT } from "./plugin-root.js";
import {
completedEvents,
createApiTestFixtures,
} from "./support/api-events.js";

const fixtures = createApiTestFixtures();
const InternalCodexSecurity = CodexSecurity as unknown as new (
config: Record<string, unknown>,
dependencies: Record<string, unknown>,
runtimeOptions?: { surface: "cli" | "sdk" },
) => CodexSecurity;

afterEach(async () => {
await fixtures.cleanup();
});

function preparedRuntime(codexHome: string): Record<string, unknown> {
return {
codexHome,
plugin: {
pluginRoot: PLUGIN_ROOT,
marketplaceRoot: PLUGIN_ROOT,
installedRoot: PLUGIN_ROOT,
marketplaceName: "codex-security-sdk",
name: "codex-security",
version: "0.1.0",
},
environment: {},
credentialsAvailable: true,
};
}

function mockWorkbench(args: readonly string[]) {
if (args[0] === "register-cli-scan") {
return {
scanId: "scan_example_001",
targetId: "target_sha256_example",
targetRevision: "deadbeef",
scanDir: args[args.indexOf("--scan-dir") + 1],
contract: { target: { allowedKinds: ["git_revision"] } },
};
}
if (args[0] === "get-scan-feedback") {
return {
scanId: "scan_example_001",
targetId: "target_sha256_example",
falsePositives: [],
};
}
return {};
}

async function scanResponseSurface(runtimeOptions?: {
surface: "cli" | "sdk";
}): Promise<string | undefined> {
const root = await fixtures.temporaryDirectory();
const repository = join(root, "repository");
const codexHome = join(root, "codex-home");
const scanDir = join(root, "scan");
await mkdir(repository);
await mkdir(codexHome);
await mkdir(scanDir, { mode: 0o700 });
let codexOptions: CodexOptions | null = null;

const client = new InternalCodexSecurity(
{},
{
environment: {},
prepareRuntime: async () => preparedRuntime(codexHome),
resolvePluginPython: async () => "/managed/python",
prepareOutputDir: async () => scanDir,
repositoryRevision: async () => "deadbeef",
runWorkbench: async (_options: unknown, args: readonly string[]) =>
mockWorkbench(args),
createCodex: (options: CodexOptions) => {
codexOptions = options;
return {
startThread: () => ({
id: null,
async runStreamed() {
await fixtures.copyCompletedScan(root);
return { events: completedEvents() };
},
}),
};
},
},
runtimeOptions,
);

await client.run(repository);
await client.close();
return (
(codexOptions as CodexOptions | null)?.config?.[
"responses_api_metadata"
] as Record<string, string> | undefined
)?.["codex_security_surface"];
}

describe("CodexSecurity Responses metadata", () => {
test("SDK runtime scans use sdk metadata", async () => {
expect(await scanResponseSurface()).toBe("sdk");
});

test("CLI runtime scans use cli metadata instead of sdk metadata", async () => {
const surface = await scanResponseSurface({ surface: "cli" });
expect(surface).toBe("cli");
expect(surface).not.toBe("sdk");
});
});
17 changes: 0 additions & 17 deletions sdk/typescript/tests-ts/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
} from "../src/index.js";
import {
classifyConnectionFailure,
environmentValue,
initialCredentialsAvailable,
} from "../src/api.js";
import {
Expand Down Expand Up @@ -287,22 +286,6 @@ describe("CodexSecurity orchestration", () => {
);
});

test("treats empty environment variables as unset and finds case variants", () => {
expect(environmentValue({ CODEX_HOME: "" }, "CODEX_HOME")).toBeUndefined();
expect(
environmentValue({ CODEX_HOME: " " }, "CODEX_HOME"),
).toBeUndefined();
expect(
environmentValue(
{ CODEX_HOME: "", Codex_Home: "/ambient" },
"CODEX_HOME",
),
).toBe("/ambient");
expect(environmentValue({ Home: "/shell-home" }, "HOME")).toBe(
"/shell-home",
);
});

test("selects a real-scan target in the active repository layout", async () => {
await expect(
stat(join(REPOSITORY_ROOT, INTEGRATION_TARGET)),
Expand Down
2 changes: 2 additions & 0 deletions sdk/typescript/tests-ts/cli-skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ describe("CLI skill commands", () => {
'model_reasoning_effort="xhigh"',
"--config",
'approval_policy="never"',
"--config",
'responses_api_metadata.codex_security_surface="cli"',
"--sandbox",
"workspace-write",
"--skip-git-repo-check",
Expand Down
Loading