diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index d5905a377..3178c7db5 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -631,6 +631,9 @@ The CLI and SDK recognize the following user-configurable environment: | `CI` | Disable interactive update notices in automated environments. | | `NO_COLOR`, `TERM` | Disable colored scan-history output when `NO_COLOR` is defined or `TERM=dumb`. | +Custom Codex executables must support thread source attribution for both `exec` +and `app-server` requests (Codex 0.149.1 or later). + On Windows, `CODEX_CLI_PATH` must name a native `.exe` or `.com`. Command shims such as `codex.cmd` automatically use the bundled Codex executable instead. diff --git a/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-000 b/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-000 index 7ced89d9c..8a861abee 100644 Binary files a/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-000 and b/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-000 differ diff --git a/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-001 b/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-001 index 6d7d85274..774602a86 100644 Binary files a/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-001 and b/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-001 differ diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index b33d037e8..26ba4cc99 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -58,8 +58,8 @@ "@inquirer/prompts": "8.3.0", "@linear/sdk": "89.0.0", "@octokit/core": "7.0.6", - "@openai/codex": "0.148.0-alpha.8", - "@openai/codex-sdk": "0.148.0-alpha.8", + "@openai/codex": "0.149.1", + "@openai/codex-sdk": "0.149.1", "ajv": "8.20.0", "extract-zip": "2.0.1", "fast-uri": "3.1.5", diff --git a/sdk/typescript/pnpm-lock.yaml b/sdk/typescript/pnpm-lock.yaml index f3b3b0729..9683918ef 100644 --- a/sdk/typescript/pnpm-lock.yaml +++ b/sdk/typescript/pnpm-lock.yaml @@ -18,11 +18,11 @@ importers: specifier: 7.0.6 version: 7.0.6 '@openai/codex': - specifier: 0.148.0-alpha.8 - version: 0.148.0-alpha.8 + specifier: 0.149.1 + version: 0.149.1 '@openai/codex-sdk': - specifier: 0.148.0-alpha.8 - version: 0.148.0-alpha.8 + specifier: 0.149.1 + version: 0.149.1 ajv: specifier: 8.20.0 version: 8.20.0 @@ -532,47 +532,47 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} - '@openai/codex-sdk@0.148.0-alpha.8': - resolution: {integrity: sha512-3t54+WC9E/YANQIf2LomahmLXYe8pOuCe5//wZi4tE7o2sjJPCBA01g3ySlMUjXdQrVZC7J+KzOekTBGabwLnA==} + '@openai/codex-sdk@0.149.1': + resolution: {integrity: sha512-R00Rz5327LefZggAxl28r7vFQq1vxa91OxtjZJOsQfAM/MyH8InW5qwwRu6pzUmRGp1E29XrOzm7u1TeV5Yz2A==} engines: {node: '>=18'} - '@openai/codex@0.148.0-alpha.8': - resolution: {integrity: sha512-+fOazxjtlG8SMs/TJi96Ta3OcZd2fgTR2Qi25eOemr6ehLYHFag4mEU7Ko2KLNoLOk8xtbvVvduZa0Ebk2AGOg==} + '@openai/codex@0.149.1': + resolution: {integrity: sha512-6q5pbcpFbJbqOpkubSDBwXmktQ55aD8eUzGzBF1zASob2DjwhBKDSNGtdZKalfrNJUdTDTPDMmzCXEXs5tMBYA==} engines: {node: '>=16'} hasBin: true - '@openai/codex@0.148.0-alpha.8-darwin-arm64': - resolution: {integrity: sha512-9V4LsW8Sg5u8yyR1HwLdtXo7ywIfQnyRJbcxnUfT2k2Y7CPn+DFnK/zPtXJdpXC2BVh4NTjyLKDGj+iD2UgqSA==} + '@openai/codex@0.149.1-darwin-arm64': + resolution: {integrity: sha512-6X84kTCbnTgPIJ2EdcPsrvwS0Wxsqpa+bCswGmRf4BjhcQ5nPMnBC6yCAaCMj+vrbXQHj+L6sa9FaR4QkmA1qw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@openai/codex@0.148.0-alpha.8-darwin-x64': - resolution: {integrity: sha512-D3V9D9v/UanU+Qf2wm3c937Dfw+/WlvWgq03yPtOlrzPWiQMorEYwdHvHdH5b2qz9pCCp1LxQicqLPbME6PjoA==} + '@openai/codex@0.149.1-darwin-x64': + resolution: {integrity: sha512-MfLBQLfcElJL9tvj6y45qVHHMGSXCPnQOixuD3/Zq0g1BW/eFizkrGLdn48cFpc+l8cK+gt5nYG5pQYwVs6g4A==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@openai/codex@0.148.0-alpha.8-linux-arm64': - resolution: {integrity: sha512-bGv145obh67MDW28apTtIX8LSho3vz7mQhknfIfYHhwkUFLb7vInI68CQqST7ZxkBVje7mCjvqzP/Bz38rf2bg==} + '@openai/codex@0.149.1-linux-arm64': + resolution: {integrity: sha512-OqxUfZ1TVvHd18zHPKK/8ZRlpk8Vy11mg5CMHaLxNWldTbwVImDKtSLWT+m8m4NM5Sz4PbjtZMrVT/RfpBW/mQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@openai/codex@0.148.0-alpha.8-linux-x64': - resolution: {integrity: sha512-UbWizYL5piFXvG/O9ykqOd7G5dc9SmG/rFIWwWIKrWqhL/OVdO31WbRXa4OqhnRGmoOmmgPey1Smi2X0rGm9kw==} + '@openai/codex@0.149.1-linux-x64': + resolution: {integrity: sha512-Of5fGYgr7tAMsyj6vhXb4/RM/UoA3Zq8BLegUBDC09UNy1XTLGYP/2XD+UX8z3qh0NDwxYdCjFIWdDNijKZggQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@openai/codex@0.148.0-alpha.8-win32-arm64': - resolution: {integrity: sha512-YwKtu/g2hEN1AquplybyRuacoSJOw1blVEED+y3GsImnG5BhfOKDV4c5XOvgohbPXs1nUbuMP6y8s4P3KRC4uQ==} + '@openai/codex@0.149.1-win32-arm64': + resolution: {integrity: sha512-5K0DmOKGK9Bos627p8sK8ATHjovPK0sDyT6h9Cb+4v+5CW5SGw1HLgjGxoLfJ8g3cg6mtg/pRCXXo2L/j71UVA==} engines: {node: '>=16'} cpu: [arm64] os: [win32] - '@openai/codex@0.148.0-alpha.8-win32-x64': - resolution: {integrity: sha512-tBW2FpwZHHSYw13pKKwRJzUh5Xp8KnwsaOqoUXX4s2tq+ydq4TZqReVN0LR+7lhajDVEm2e5rfGNUCh+B1asmw==} + '@openai/codex@0.149.1-win32-x64': + resolution: {integrity: sha512-G3QXGAg7nyyhqOeooAMUekBCeHd8a1QByhKcVAFyzNBaI06t6Ft7nsF+1SzFS0spuIdU4YyMi5YD26ukADBQUQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -1820,35 +1820,35 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 - '@openai/codex-sdk@0.148.0-alpha.8': + '@openai/codex-sdk@0.149.1': dependencies: - '@openai/codex': 0.148.0-alpha.8 + '@openai/codex': 0.149.1 - '@openai/codex@0.148.0-alpha.8': + '@openai/codex@0.149.1': optionalDependencies: - '@openai/codex-darwin-arm64': '@openai/codex@0.148.0-alpha.8-darwin-arm64' - '@openai/codex-darwin-x64': '@openai/codex@0.148.0-alpha.8-darwin-x64' - '@openai/codex-linux-arm64': '@openai/codex@0.148.0-alpha.8-linux-arm64' - '@openai/codex-linux-x64': '@openai/codex@0.148.0-alpha.8-linux-x64' - '@openai/codex-win32-arm64': '@openai/codex@0.148.0-alpha.8-win32-arm64' - '@openai/codex-win32-x64': '@openai/codex@0.148.0-alpha.8-win32-x64' - - '@openai/codex@0.148.0-alpha.8-darwin-arm64': + '@openai/codex-darwin-arm64': '@openai/codex@0.149.1-darwin-arm64' + '@openai/codex-darwin-x64': '@openai/codex@0.149.1-darwin-x64' + '@openai/codex-linux-arm64': '@openai/codex@0.149.1-linux-arm64' + '@openai/codex-linux-x64': '@openai/codex@0.149.1-linux-x64' + '@openai/codex-win32-arm64': '@openai/codex@0.149.1-win32-arm64' + '@openai/codex-win32-x64': '@openai/codex@0.149.1-win32-x64' + + '@openai/codex@0.149.1-darwin-arm64': optional: true - '@openai/codex@0.148.0-alpha.8-darwin-x64': + '@openai/codex@0.149.1-darwin-x64': optional: true - '@openai/codex@0.148.0-alpha.8-linux-arm64': + '@openai/codex@0.149.1-linux-arm64': optional: true - '@openai/codex@0.148.0-alpha.8-linux-x64': + '@openai/codex@0.149.1-linux-x64': optional: true - '@openai/codex@0.148.0-alpha.8-win32-arm64': + '@openai/codex@0.149.1-win32-arm64': optional: true - '@openai/codex@0.148.0-alpha.8-win32-x64': + '@openai/codex@0.149.1-win32-x64': optional: true '@scalar/openapi-types@0.8.0': {} diff --git a/sdk/typescript/scripts/check-package.mjs b/sdk/typescript/scripts/check-package.mjs index ca7820224..c414e9ae2 100644 --- a/sdk/typescript/scripts/check-package.mjs +++ b/sdk/typescript/scripts/check-package.mjs @@ -195,6 +195,7 @@ const distFiles = new Set( "scan-logs", "scan-sessions", "targets", + "thread-source", "trusted-executable", "version", "windows-path", diff --git a/sdk/typescript/scripts/smoke-package.mjs b/sdk/typescript/scripts/smoke-package.mjs index eff872755..ed4c9f4fe 100644 --- a/sdk/typescript/scripts/smoke-package.mjs +++ b/sdk/typescript/scripts/smoke-package.mjs @@ -242,6 +242,7 @@ async function smokeNestedDeepScanWorker(installedRoot, consumer) { }); const { events } = await codex .startThread({ + threadSource: "security_scan", workingDirectory: workerHome, skipGitRepoCheck: true, sandboxMode: "read-only", diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index cae426dba..d0e2a3133 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -101,6 +101,7 @@ import { type ScanProgress, type ScanWorkerStatus, } from "./worker-progress.js"; +import { CODEX_SECURITY_THREAD_SOURCES } from "./thread-source.js"; import { CODEX_EXECUTABLE_VERSION, CODEX_SDK_VERSION } from "./version.js"; import { acquireCodexSecurityCredentialHomeLock, @@ -506,6 +507,7 @@ export class CodexSecurity { options.auth, ); const thread = codex.startThread({ + threadSource: CODEX_SECURITY_THREAD_SOURCES.validation, workingDirectory: outputDir, skipGitRepoCheck: true, approvalPolicy, @@ -1126,6 +1128,7 @@ export class CodexSecurity { options.auth, ); const thread = codex.startThread({ + threadSource: CODEX_SECURITY_THREAD_SOURCES.scan, workingDirectory: scanDir, skipGitRepoCheck: true, approvalPolicy, @@ -1200,6 +1203,7 @@ export class CodexSecurity { filesTotal: scopeFileCount, }); const validationThread = codex.startThread({ + threadSource: CODEX_SECURITY_THREAD_SOURCES.scan, workingDirectory: join(scanDir, "artifacts"), skipGitRepoCheck: true, approvalPolicy, diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 79bebe6fa..3838592c5 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -130,6 +130,10 @@ import { type ScanComparisonInput, } from "./scan-comparison.js"; import { scanActivitiesFromEvent } from "./scan-activity.js"; +import { + CODEX_SECURITY_THREAD_SOURCES, + type CodexSecurityThreadSource, +} from "./thread-source.js"; import { readScanLogs } from "./scan-logs.js"; import { renderScanHistory, @@ -1002,6 +1006,12 @@ type MatchingPlan = JsonObject & { batches: (JsonObject & MatchingBatch)[]; }; +type SkillThreadSource = Extract< + CodexSecurityThreadSource, + | typeof CODEX_SECURITY_THREAD_SOURCES.remediation + | typeof CODEX_SECURITY_THREAD_SOURCES.validation +>; + interface SkillCommandOutput { readonly command: "validate" | "patch" | "verify-fix"; readonly stdout: Writable; @@ -1009,6 +1019,7 @@ interface SkillCommandOutput { readonly appServer?: { readonly directory: string; readonly prompt: string; + readonly threadSource: SkillThreadSource; readonly sandbox?: "read-only" | "workspace-write"; readonly onEvent?: (event: Readonly>) => void; }; @@ -1349,6 +1360,7 @@ export async function runCodexSkillCommand( : { directory: output.appServer.directory, prompt: output.appServer.prompt, + threadSource: output.appServer.threadSource, input: invocation.stdin!, sandbox: output.appServer.sandbox, onEvent: output.appServer.onEvent, @@ -5100,9 +5112,14 @@ async function runSkill( ].join("\n"); const patch = skill === "fix-finding"; const appServer = patch || verify; + const threadSource = patch + ? CODEX_SECURITY_THREAD_SOURCES.remediation + : CODEX_SECURITY_THREAD_SOURCES.validation; return dependencies.runCodex( [ - ...(appServer ? ["app-server"] : ["exec", "--ignore-user-config"]), + ...(appServer + ? ["app-server"] + : ["exec", "--ignore-user-config", "--thread-source", threadSource]), "--disable", "plugins", ...(appServer ? [] : ["--ephemeral", "--color", "never", "--json"]), @@ -5150,6 +5167,7 @@ async function runSkill( appServer: { directory, prompt, + threadSource, ...(verify ? { sandbox: "read-only" as const } : {}), ...(options.onEvent === undefined ? {} @@ -5168,6 +5186,7 @@ export async function readSkillCommandOutput( appServer?: { readonly directory?: string; readonly prompt: string; + readonly threadSource: SkillThreadSource; readonly input: NodeJS.WritableStream; readonly sandbox?: "read-only" | "workspace-write"; readonly onEvent?: (event: Readonly>) => void; @@ -5188,12 +5207,14 @@ export async function readSkillCommandOutput( appServer?.input.write(`${JSON.stringify(request)}\n`); }; const startThread = (config?: JsonObject): void => { + if (appServer === undefined) return; send({ id: 2, method: "thread/start", // An explicit cwd makes Codex persist trust for a new project. // Inherit the child process cwd and preserve the user's decision. params: { + threadSource: appServer.threadSource, approvalPolicy: appServer?.sandbox === "read-only" ? "on-request" : "never", sandbox: appServer?.sandbox ?? "workspace-write", diff --git a/sdk/typescript/src/component-plan.ts b/sdk/typescript/src/component-plan.ts index 41c4a2be5..27c619475 100644 --- a/sdk/typescript/src/component-plan.ts +++ b/sdk/typescript/src/component-plan.ts @@ -9,6 +9,7 @@ import { runReadOnlyCodex, type ReadOnlyCodexOptions, } from "./scan-comparison.js"; +import { CODEX_SECURITY_THREAD_SOURCES } from "./thread-source.js"; import { enclosingGitWorktreeRoot, normalizeRepository, @@ -88,7 +89,10 @@ export async function planComponents( ].join("\n"), z.toJSONSchema(componentPlanSchema, { target: "openapi-3.0" }), { ...options, config: options.config ?? {}, workingDirectory: tmpdir() }, - { surface: "cli" }, + { + surface: "cli", + threadSource: CODEX_SECURITY_THREAD_SOURCES.scan, + }, ); const plan = await normalizeComponentPlan( repository, diff --git a/sdk/typescript/src/scan-comparison.ts b/sdk/typescript/src/scan-comparison.ts index 2040a1b23..6bad957d0 100644 --- a/sdk/typescript/src/scan-comparison.ts +++ b/sdk/typescript/src/scan-comparison.ts @@ -26,8 +26,17 @@ import { runCodexCommand, type CodexCommand, } from "./runtime.js"; +import { + CODEX_SECURITY_THREAD_SOURCES, + type CodexSecurityThreadSource, +} from "./thread-source.js"; type Finding = { occurrenceId: string } & Record; +type ReadOnlyCodexThreadSource = Extract< + CodexSecurityThreadSource, + | typeof CODEX_SECURITY_THREAD_SOURCES.scan + | typeof CODEX_SECURITY_THREAD_SOURCES.scanComparison +>; export interface ScanComparisonInput { before: readonly Finding[]; @@ -117,7 +126,10 @@ export async function matchScanFindingsInternal( comparisonPrompt(input), z.toJSONSchema(comparisonSchema, { target: "openapi-3.0" }), options, - runtimeOptions, + { + ...runtimeOptions, + threadSource: CODEX_SECURITY_THREAD_SOURCES.scanComparison, + }, ); let response: unknown; try { @@ -138,7 +150,10 @@ export async function runReadOnlyCodex( prompt: string, outputSchema: unknown, options: ReadOnlyCodexOptions, - runtimeOptions: { surface: CodexSecuritySurface }, + runtimeOptions: { + surface: CodexSecuritySurface; + threadSource: ReadOnlyCodexThreadSource; + }, ): Promise { const config = options.config === undefined @@ -197,6 +212,7 @@ export async function runReadOnlyCodex( } as NonNullable, }); const thread = codex.startThread({ + threadSource: runtimeOptions.threadSource, ...(model === undefined ? {} : { model }), modelReasoningEffort: reasoningEffort, sandboxMode: "read-only", diff --git a/sdk/typescript/src/thread-source.ts b/sdk/typescript/src/thread-source.ts new file mode 100644 index 000000000..9ba8413b7 --- /dev/null +++ b/sdk/typescript/src/thread-source.ts @@ -0,0 +1,9 @@ +export const CODEX_SECURITY_THREAD_SOURCES = { + scan: "security_scan", + validation: "security_validation", + remediation: "security_remediation", + scanComparison: "security_scan_comparison", +} as const; + +export type CodexSecurityThreadSource = + (typeof CODEX_SECURITY_THREAD_SOURCES)[keyof typeof CODEX_SECURITY_THREAD_SOURCES]; diff --git a/sdk/typescript/tests-ts/api-attribution-concurrency.test.ts b/sdk/typescript/tests-ts/api-attribution-concurrency.test.ts index 7869890ac..457f04286 100644 --- a/sdk/typescript/tests-ts/api-attribution-concurrency.test.ts +++ b/sdk/typescript/tests-ts/api-attribution-concurrency.test.ts @@ -1,6 +1,6 @@ import { mkdir, readFile } from "node:fs/promises"; import { join } from "node:path"; -import type { CodexOptions } from "@openai/codex-sdk"; +import type { CodexOptions, ThreadOptions } from "@openai/codex-sdk"; import { afterEach, describe, expect, test } from "bun:test"; import { parse as parseToml } from "smol-toml"; import { CodexSecurity } from "../src/index.js"; @@ -75,7 +75,7 @@ describe("delegated scan attribution", () => { return {}; }, createCodex: (options: CodexOptions) => ({ - startThread: () => ({ + startThread: (threadOptions: ThreadOptions) => ({ id: null, async runStreamed() { active += 1; @@ -91,6 +91,7 @@ describe("delegated scan attribution", () => { codex_security_surface: surface, }, }); + expect(threadOptions.threadSource).toBe("security_scan"); await concurrentScans; const sharedConfig = parseToml( await readFile( diff --git a/sdk/typescript/tests-ts/api-surface.test.ts b/sdk/typescript/tests-ts/api-surface.test.ts index dc4072d47..c00b3244b 100644 --- a/sdk/typescript/tests-ts/api-surface.test.ts +++ b/sdk/typescript/tests-ts/api-surface.test.ts @@ -1,6 +1,6 @@ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; -import type { CodexOptions } from "@openai/codex-sdk"; +import type { CodexOptions, ThreadOptions } from "@openai/codex-sdk"; import { afterEach, describe, expect, test } from "bun:test"; import { CodexSecurity } from "../src/index.js"; import { mockWorkbench } from "./support/api-client.js"; @@ -21,7 +21,10 @@ afterEach(fixtures.cleanup); async function scanResponseSurface(runtimeOptions?: { surface: "cli" | "sdk"; -}): Promise { +}): Promise<{ + surface: string | undefined; + threadSource: string | undefined; +}> { const root = await fixtures.temporaryDirectory(); const repository = join(root, "repository"); const codexHome = join(root, "codex-home"); @@ -30,6 +33,7 @@ async function scanResponseSurface(runtimeOptions?: { await mkdir(codexHome); await mkdir(scanDir, { mode: 0o700 }); let codexOptions: CodexOptions | null = null; + let threadOptions: ThreadOptions | null = null; const client = new InternalCodexSecurity( {}, @@ -47,13 +51,16 @@ async function scanResponseSurface(runtimeOptions?: { createCodex: (options: CodexOptions) => { codexOptions = options; return { - startThread: () => ({ - id: null, - async runStreamed() { - await fixtures.copyCompletedScan(root); - return { events: completedEvents() }; - }, - }), + startThread: (options: ThreadOptions) => { + threadOptions = options; + return { + id: null, + async runStreamed() { + await fixtures.copyCompletedScan(root); + return { events: completedEvents() }; + }, + }; + }, }; }, }, @@ -62,21 +69,28 @@ async function scanResponseSurface(runtimeOptions?: { await client.run(repository); await client.close(); - return ( - (codexOptions as CodexOptions | null)?.config?.[ - "responses_api_metadata" - ] as Record | undefined - )?.["codex_security_surface"]; + return { + surface: ( + (codexOptions as CodexOptions | null)?.config?.[ + "responses_api_metadata" + ] as Record | undefined + )?.["codex_security_surface"], + threadSource: (threadOptions as ThreadOptions | null)?.threadSource, + }; } describe("CodexSecurity Responses metadata", () => { test("SDK runtime scans use sdk metadata", async () => { - expect(await scanResponseSurface()).toBe("sdk"); + expect(await scanResponseSurface()).toEqual({ + surface: "sdk", + threadSource: "security_scan", + }); }); 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"); + expect(await scanResponseSurface({ surface: "cli" })).toEqual({ + surface: "cli", + threadSource: "security_scan", + }); }); }); diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 5dc11da09..74836e58d 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -314,6 +314,7 @@ describe("CodexSecurity finding validation", () => { expect(captured.prompt!.endsWith(JSON.stringify(finding))).toBe(true); expect(captured.prompt).not.toContain("Synthetic file contents"); expect(captured.thread).toMatchObject({ + threadSource: "security_validation", workingDirectory: options.outputDir, approvalPolicy: "never", }); @@ -2277,6 +2278,7 @@ describe("CodexSecurity orchestration", () => { allow_login_shell: false, }); expect(threadOptions as Record | null).toEqual({ + threadSource: "security_scan", workingDirectory: scanDir, skipGitRepoCheck: true, approvalPolicy: "on-request", diff --git a/sdk/typescript/tests-ts/cli-patch-trust.test.ts b/sdk/typescript/tests-ts/cli-patch-trust.test.ts index 241131011..7a40577ca 100644 --- a/sdk/typescript/tests-ts/cli-patch-trust.test.ts +++ b/sdk/typescript/tests-ts/cli-patch-trust.test.ts @@ -135,6 +135,8 @@ test.each([ const output = await readSkillCommandOutput(events(), { directory: repository, prompt: "Synthetic finding", + threadSource: + mode === "patch" ? "security_remediation" : "security_validation", input, ...(mode === "patch" ? {} : { sandbox: "read-only" }), }); diff --git a/sdk/typescript/tests-ts/cli-skills.test.ts b/sdk/typescript/tests-ts/cli-skills.test.ts index 6ef6de139..d50d151ae 100644 --- a/sdk/typescript/tests-ts/cli-skills.test.ts +++ b/sdk/typescript/tests-ts/cli-skills.test.ts @@ -68,6 +68,9 @@ describe("CLI skill commands", () => { invocation = args; prompt = output?.appServer?.prompt ?? input ?? ""; expect(input).toBe(command === "patch" ? undefined : prompt); + expect(output?.appServer?.threadSource).toBe( + command === "patch" ? "security_remediation" : undefined, + ); return status; }, }), @@ -76,7 +79,12 @@ describe("CLI skill commands", () => { expect(invocation).toEqual([ ...(command === "patch" ? ["app-server"] - : ["exec", "--ignore-user-config"]), + : [ + "exec", + "--ignore-user-config", + "--thread-source", + "security_validation", + ]), "--disable", "plugins", ...(command === "patch" @@ -1180,7 +1188,7 @@ lines.on("line", (line) => { send({ id: 1, result: {} }); } else if (request.method === "thread/start") { assert.equal(process.cwd(), ${JSON.stringify(process.cwd())}); - assert.deepEqual(request.params, { approvalPolicy: "never", sandbox: "workspace-write" }); + assert.deepEqual(request.params, { threadSource: "security_remediation", approvalPolicy: "never", sandbox: "workspace-write" }); send({ id: 2, result: { thread: { id: "parent", source: "vscode", ephemeral: false } } }); } else if (request.method === "turn/start") { assert.equal(request.params.threadId, "parent"); @@ -1213,6 +1221,7 @@ lines.on("line", (line) => { appServer: { directory: process.cwd(), prompt: "Fix the synthetic finding", + threadSource: "security_remediation", }, }, { command: process.execPath }, @@ -1242,6 +1251,7 @@ lines.on("line", (line) => { } if (request.method === "thread/start") { assert.deepEqual(request.params, { + threadSource: "security_validation", approvalPolicy: "on-request", sandbox: "read-only", config: { mcp_servers: { repository: { enabled: false } } }, @@ -1287,6 +1297,7 @@ lines.on("line", (line) => { directory: process.cwd(), prompt: "Verify the synthetic finding without editing the repository", + threadSource: "security_validation", sandbox: "read-only", onEvent: (event) => activity.push(event), }, @@ -1357,6 +1368,7 @@ lines.on("line", (line) => { appServer: { directory: process.cwd(), prompt: "Synthetic finding", + threadSource: "security_remediation", }, }, { command: process.execPath }, @@ -1389,6 +1401,7 @@ lines.on("line", (line) => { appServer: { directory: process.cwd(), prompt: "Fix the synthetic finding", + threadSource: "security_remediation", }, }, { command: process.execPath }, diff --git a/sdk/typescript/tests-ts/component-scan.test.ts b/sdk/typescript/tests-ts/component-scan.test.ts index de375bec5..7d7da29b0 100644 --- a/sdk/typescript/tests-ts/component-scan.test.ts +++ b/sdk/typescript/tests-ts/component-scan.test.ts @@ -707,6 +707,7 @@ test("plans from a Git inventory without tools or ignored files", async () => { codex: { startThread(options) { expect(options).toMatchObject({ + threadSource: "security_scan", sandboxMode: "read-only", approvalPolicy: "never", networkAccessEnabled: false, diff --git a/sdk/typescript/tests-ts/custom-validation.test.ts b/sdk/typescript/tests-ts/custom-validation.test.ts index 4829639d7..6da81a99e 100644 --- a/sdk/typescript/tests-ts/custom-validation.test.ts +++ b/sdk/typescript/tests-ts/custom-validation.test.ts @@ -399,6 +399,7 @@ describe("custom validation", () => { }); return { startThread: (threadOptions) => { + expect(threadOptions.threadSource).toBe("security_scan"); workingDirectories.push(threadOptions.workingDirectory); return { id: "thread-1", diff --git a/sdk/typescript/tests-ts/deep-scan-worker-shutdown.test.ts b/sdk/typescript/tests-ts/deep-scan-worker-shutdown.test.ts index e3fc9cb51..594ce173f 100644 --- a/sdk/typescript/tests-ts/deep-scan-worker-shutdown.test.ts +++ b/sdk/typescript/tests-ts/deep-scan-worker-shutdown.test.ts @@ -37,7 +37,8 @@ async function bundledWorkerExecutor( expect(fileSystemImport).toBeDefined(); class FakeCodex { - startThread() { + startThread(options: { threadSource: string }) { + expect(options.threadSource).toBe("security_scan"); return { id: "fixture-worker-thread", async runStreamed(_input: string, options: { signal: AbortSignal }) { diff --git a/sdk/typescript/tests-ts/scan-comparison.test.ts b/sdk/typescript/tests-ts/scan-comparison.test.ts index acb2c30d7..0ffb8e116 100644 --- a/sdk/typescript/tests-ts/scan-comparison.test.ts +++ b/sdk/typescript/tests-ts/scan-comparison.test.ts @@ -21,6 +21,7 @@ import { comparisonEnvironment, matchCompletedScan, matchScanFindings, + matchScanFindingsInternal, type ScanComparisonInput, type ScanComparisonOptions, type ScanComparisonResult, @@ -67,6 +68,16 @@ function fakeCodex(response: unknown) { } describe("semantic scan comparison", () => { + test("uses comparison attribution for CLI comparison turns", async () => { + const { codex, calls } = fakeCodex({ matches: [], uncertain: [] }); + await matchScanFindingsInternal( + { before: [], after: [] }, + { codex }, + { surface: "cli" }, + ); + expect(calls.threadOptions?.threadSource).toBe("security_scan_comparison"); + }); + test("disables explicit and inherited MCP servers for read-only helper turns", async () => { const home = await mkdtemp(join(tmpdir(), "codex-security-comparison-")); temporaryDirectories.push(home); @@ -409,6 +420,7 @@ describe("semantic scan comparison", () => { }), ).toEqual(result); expect(calls.threadOptions).toEqual({ + threadSource: "security_scan_comparison", model: "comparison-model", modelReasoningEffort: "high", sandboxMode: "read-only",