From e47c1802a49beedc7ece61ecada46e12a21340a7 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 11 Aug 2026 13:47:35 -0700 Subject: [PATCH 1/2] feat(sdk): attribute Responses traffic to SDK and CLI --- sdk/typescript/src/api.ts | 31 +++++ sdk/typescript/src/cli.ts | 12 +- sdk/typescript/src/index.ts | 2 +- sdk/typescript/src/scan-comparison.ts | 12 ++ sdk/typescript/tests-ts/api-surface.test.ts | 122 ++++++++++++++++++++ sdk/typescript/tests-ts/cli-skills.test.ts | 2 + 6 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 sdk/typescript/tests-ts/api-surface.test.ts diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index 0df4d1a4..0a4929e7 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -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; @@ -298,6 +304,7 @@ export class CodexSecurity { }; readonly #dependencies: ClientDependencies; + readonly #surface: CodexSecuritySurface; readonly #loginHandles = new Set(); readonly #abortController = new AbortController(); #activeOperation: Promise | null = null; @@ -308,12 +315,20 @@ export class CodexSecurity { #closePromise: Promise | 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( @@ -953,6 +968,9 @@ export class CodexSecurity { ...(sdkCodexConfig as NonNullable), default_permissions: SCAN_PERMISSION_PROFILE, allow_login_shell: false, + responses_api_metadata: { + codex_security_surface: this.#surface, + }, }, }); const thread = codex.startThread({ @@ -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, diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 00f1024b..fce97c7c 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -32,6 +32,7 @@ import { parse as parseToml } from "smol-toml"; import { classifyConnectionFailure, CodexSecurity, + createSecurityInternal, scanAuthentication, type DeepScanOptions, type ScanAuthMode, @@ -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"; @@ -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 }), @@ -484,7 +487,8 @@ const DEFAULT_DEPENDENCIES: CliDependencies = { args, ); }, - matchFindings: matchScanFindings, + matchFindings: (input, options) => + matchScanFindingsInternal(input, options, { surface: "cli" }), }; export async function runCodexSkillCommand( @@ -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", diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index f1ef3a1e..154867eb 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -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"; diff --git a/sdk/typescript/src/scan-comparison.ts b/sdk/typescript/src/scan-comparison.ts index bcea7ed9..80eb97ea 100644 --- a/sdk/typescript/src/scan-comparison.ts +++ b/sdk/typescript/src/scan-comparison.ts @@ -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 { @@ -75,6 +76,14 @@ export type ScanComparisonResult = z.infer; export async function matchScanFindings( input: ScanComparisonInput, options: ScanComparisonOptions = {}, +): Promise { + return await matchScanFindingsInternal(input, options, { surface: "sdk" }); +} + +export async function matchScanFindingsInternal( + input: ScanComparisonInput, + options: ScanComparisonOptions = {}, + runtimeOptions: { surface: CodexSecuritySurface }, ): Promise { const codex = options.codex ?? @@ -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, diff --git a/sdk/typescript/tests-ts/api-surface.test.ts b/sdk/typescript/tests-ts/api-surface.test.ts new file mode 100644 index 00000000..d3fbeb94 --- /dev/null +++ b/sdk/typescript/tests-ts/api-surface.test.ts @@ -0,0 +1,122 @@ +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, createSecurity } 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, + dependencies: Record, + runtimeOptions?: { surface: "cli" | "sdk" }, +) => CodexSecurity; + +afterEach(async () => { + await fixtures.cleanup(); +}); + +function preparedRuntime(codexHome: string): Record { + 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 { + 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 | undefined + )?.["codex_security_surface"]; +} + +describe("CodexSecurity Responses metadata", () => { + test("the public factory creates an SDK client", async () => { + const client = createSecurity(); + expect(client).toBeInstanceOf(CodexSecurity); + await client.close(); + }); + + 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"); + }); +}); diff --git a/sdk/typescript/tests-ts/cli-skills.test.ts b/sdk/typescript/tests-ts/cli-skills.test.ts index 08ed7ff4..f705898a 100644 --- a/sdk/typescript/tests-ts/cli-skills.test.ts +++ b/sdk/typescript/tests-ts/cli-skills.test.ts @@ -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", From 3a152c3f02b0e7b0b1a5f13bc3ed6bfb6c7b97f5 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 11 Aug 2026 13:54:31 -0700 Subject: [PATCH 2/2] test(sdk): match approved attribution coverage --- .../tests-ts/api-environment.test.ts | 20 +++++++++++++++++++ sdk/typescript/tests-ts/api-surface.test.ts | 8 +------- sdk/typescript/tests-ts/api.test.ts | 17 ---------------- 3 files changed, 21 insertions(+), 24 deletions(-) create mode 100644 sdk/typescript/tests-ts/api-environment.test.ts diff --git a/sdk/typescript/tests-ts/api-environment.test.ts b/sdk/typescript/tests-ts/api-environment.test.ts new file mode 100644 index 00000000..cb6ece73 --- /dev/null +++ b/sdk/typescript/tests-ts/api-environment.test.ts @@ -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", + ); + }); +}); diff --git a/sdk/typescript/tests-ts/api-surface.test.ts b/sdk/typescript/tests-ts/api-surface.test.ts index d3fbeb94..c8710bbd 100644 --- a/sdk/typescript/tests-ts/api-surface.test.ts +++ b/sdk/typescript/tests-ts/api-surface.test.ts @@ -2,7 +2,7 @@ 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, createSecurity } from "../src/index.js"; +import { CodexSecurity } from "../src/index.js"; import { PLUGIN_ROOT } from "./plugin-root.js"; import { completedEvents, @@ -104,12 +104,6 @@ async function scanResponseSurface(runtimeOptions?: { } describe("CodexSecurity Responses metadata", () => { - test("the public factory creates an SDK client", async () => { - const client = createSecurity(); - expect(client).toBeInstanceOf(CodexSecurity); - await client.close(); - }); - test("SDK runtime scans use sdk metadata", async () => { expect(await scanResponseSurface()).toBe("sdk"); }); diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 9df4a129..b0069a83 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -37,7 +37,6 @@ import { } from "../src/index.js"; import { classifyConnectionFailure, - environmentValue, initialCredentialsAvailable, } from "../src/api.js"; import { @@ -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)),