From 3b5103f35a4e31158b336d3d5bde41ca9728d89c Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:42:32 -0700 Subject: [PATCH 1/4] fix(plugin): complete bundled 0.1.20 parity --- sdk/typescript/_bundled_plugin/.mcp.json | 1 + .../skills/fix-finding/SKILL.md | 64 +++-------- .../tests-ts/deep-scan-executable.test.ts | 106 ++++++++++++++++++ 3 files changed, 123 insertions(+), 48 deletions(-) create mode 100644 sdk/typescript/tests-ts/deep-scan-executable.test.ts diff --git a/sdk/typescript/_bundled_plugin/.mcp.json b/sdk/typescript/_bundled_plugin/.mcp.json index 9a4fc836e..006e8ec18 100644 --- a/sdk/typescript/_bundled_plugin/.mcp.json +++ b/sdk/typescript/_bundled_plugin/.mcp.json @@ -9,6 +9,7 @@ "CODEX_SQLITE_HOME", "CODEX_API_KEY", "CODEX_CLI_PATH", + "CODEX_MANAGED_PACKAGE_ROOT", "OPENROUTER_API_KEY", "FIREWORKS_API_KEY", "AWS_BEARER_TOKEN_BEDROCK", diff --git a/sdk/typescript/_bundled_plugin/skills/fix-finding/SKILL.md b/sdk/typescript/_bundled_plugin/skills/fix-finding/SKILL.md index b07466617..859378096 100644 --- a/sdk/typescript/_bundled_plugin/skills/fix-finding/SKILL.md +++ b/sdk/typescript/_bundled_plugin/skills/fix-finding/SKILL.md @@ -22,59 +22,27 @@ Never trade an earlier property for a later one. Minimal means the smallest repo ## Patch Contract -Before editing, establish from repository evidence: +Before editing, inspect the affected implementation, its direct callers, nearby helpers, and relevant existing tests. Establish from repository evidence: -- affected component and current source-to-sink path or broken control -- attacker-controlled input and required preconditions -- security invariant and narrowest plausible enforcement boundary -- legitimate behavior, APIs, error semantics, and compatibility constraints to preserve or intentionally change with supporting product evidence -- available PoC, reproducer, tests, evidence, and affected locations -- nearest relevant helpers and implementation, error-handling, and test precedents +- the attacker-controlled input and concrete source-to-sink path or broken control +- the security invariant and narrowest shared enforcement boundary +- legitimate behavior, APIs, error semantics, and compatibility constraints that must remain +- the closest existing implementation, validation, and error-handling precedents -Inspect the repository to fill gaps. Ask the user only when a material product, security, or compatibility decision remains. - -## Runtime Validation - -Use this guidance whenever reproducing the finding, running tests, or validating the fix: - -- Complete the patch contract before broad setup; start with the smallest high-signal check through the real vulnerable boundary. -- Use repository-supported setup commands. Keep repair effort bounded so it does not displace path analysis, patching, or focused verification. -- Do not stop a progressing command merely because it is slow. Inspect process state, logs, artifacts, or resource use first. -- If runtime validation remains unavailable, use the strongest targeted static or harness-based artifact that preserves the real integration boundary. Do not substitute a simplified harness that removes the behavior being protected. Record every unrun check as unknown. +Treat the finding as a data-flow and boundary problem, not merely the named input example. Check equivalent encodings, parser forms, aliases, callers, sinks, and every representation or copy of security-sensitive state that could bypass the proposed change. Handle unsafe state explicitly; do not silently accept, truncate, or reinterpret it into another reachable form. ## Workflow -1. Revalidate and scope the finding. - - Inspect repository instructions, affected code, direct callers, and only the context needed to prove the vulnerable path. - - Establish concrete reachability in the current checkout; generic weakness labels, file anchors, and suspicious-looking code are not proof. - - If the same broken security boundary cannot be shown after a bounded investigation, do not patch an adjacent weakness or add speculative defense in depth. Return `no_change` when evidence shows the path is already safe; otherwise return `blocked` with the missing proof. - - Complete the patch contract and inspect relevant helpers, controls, and implementation and test precedents. -2. Reproduce or encode the issue before fixing when feasible. - - Prefer a failing regression test, unit test, integration test, property test, or realistic-interface reproduction. - - Capture the malicious condition and at least one legitimate control through the same boundary before implementation. - - Keep an unsafe-behavior test only when it is safe, deterministic, and appropriate for the repository. Otherwise use the strongest repeatable validation artifact available and record the gap. - - If the issue no longer reproduces before any code changes, investigate whether it was already fixed and preserve the validation evidence. -3. Choose the patch strategy. - - Determine whether a narrow tactical change can close the boundary while preserving the patch contract. - - Consider broader remediation only when the narrow option cannot close the boundary without breaking supported behavior. Remove or disable functionality only when repository or product evidence supports that mitigation. - - If the only complete fix requires an unresolved decision about product policy, public-API compatibility, or cross-subsystem ownership, return `blocked` with the options, security tradeoff, and likely owner or codeowner when available. - - Use nearby variants to test the chosen boundary. Report unrelated sibling findings or longer-term architectural work separately instead of expanding this patch. -4. Implement the fix and its proof. - - Make the smallest repository-native change that fully enforces the invariant. - - Prefer existing helpers and abstractions. Preserve APIs, legitimate inputs, and error semantics unless changing them is required by the security contract. - - Handle unsafe state explicitly; do not silently accept, truncate, or reinterpret it. - - Avoid unrelated refactors and preserve user changes outside the candidate patch. - - Add focused regression coverage that fails on the vulnerable behavior and passes after the fix. - - Include positive coverage for the legitimate control. Test at the lowest level that proves the invariant and through the realistic interface when feasible. -5. Verify in order. - - **Applicability and buildability**: inspect the final diff for unrelated changes, then run the narrowest relevant syntax, import, build, type, or focused test check. - - **Security closure**: rerun the original PoC, trigger, or strongest exploit check. Re-trace the source-to-sink or broken-control path in the patched code. - - **Change-aware bypass review**: reread the finding and final diff without relying on the original rationale. Trace changed branches from direct callers, check equivalent sinks, and exercise an alternate malicious input class when practical. - - **Preserved behavior**: rerun the legitimate control and confirm the recorded APIs, error semantics, and compatibility constraints remain intact. - - **Repository checks**: run the focused regression tests, the owning package's relevant tests, and applicable formatter, linter, type checker, dependency, and integration checks. - - Confirm the regression check would fail if the security change were removed, when practical. - - Treat a failed earlier gate as disqualifying. Revise only the candidate changes or return `blocked`; never compensate for failed security closure or behavior preservation with style, smaller scope, or additional reporting. -6. Report the outcome with exact commands, results, changed files, and remaining risk. +1. Trace the reported path and inspect only the context needed to identify the real shared boundary. Return `no_change` when repository evidence shows that the reported path is already safe; do not make a speculative change. +2. When feasible, run the smallest high-signal reproduction through that boundary and one legitimate control through the same path. +3. Implement the smallest repository-native fix at the shared boundary. Prefer nearby helpers and established APIs. Do not broaden into unrelated redesign, cleanup, or sibling findings. +4. Before verification, challenge the patch rather than defending it: inspect every direct caller of each changed helper and both outcomes of each changed condition. Look for one sibling path, representation, or copy that still reaches the vulnerable sink and one ordinary or default input that the patch newly rejects or reinterprets; revise the implementation if either exists. +5. Verify in order: + - inspect the final diff and run the narrowest syntax, import, build, or type check relevant to it + - rerun the security trigger or strongest focused substitute and review one alternate malicious input class + - rerun the legitimate control, nearest existing tests, and the owning package's applicable required checks + +Return `blocked` if the vulnerability may be real, but essential evidence, tooling, access, or a product or compatibility decision is missing, so a safe fix cannot be responsibly completed or verified. ## Workbench Remediation Stages diff --git a/sdk/typescript/tests-ts/deep-scan-executable.test.ts b/sdk/typescript/tests-ts/deep-scan-executable.test.ts new file mode 100644 index 000000000..fef7e184d --- /dev/null +++ b/sdk/typescript/tests-ts/deep-scan-executable.test.ts @@ -0,0 +1,106 @@ +import { existsSync } from "node:fs"; +import { + mkdir, + mkdtemp, + readFile, + realpath, + rm, + writeFile, +} from "node:fs/promises"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { delimiter, dirname, join } from "node:path"; +import { expect, test } from "bun:test"; +import { loadBundledRuntime, PLUGIN_ROOT } from "./plugin-root.js"; + +async function bundledCodexPathResolver() { + const runtime = await loadBundledRuntime(); + const source = ["resolveCodexPath", "resolveWindowsPackageBinary"] + .map((name) => { + const definition = new RegExp( + `function ${name}\\([^\\n]*\\) \\{[\\s\\S]*?\\n\\}`, + "u", + ).exec(runtime)?.[0]; + if (!definition) throw new Error(`Missing bundled function: ${name}.`); + return definition; + }) + .join("\n"); + const fsImport = /\b(import_node_fs\d*)\.existsSync/u.exec(source)?.[1]; + const pathImport = /\b(import_node_path\d*)\.join/u.exec(source)?.[1]; + const moduleImport = /\b(import_node_module\d*)\.createRequire/u.exec( + source, + )?.[1]; + if (!fsImport || !pathImport || !moduleImport) { + throw new Error("Bundled Codex resolver imports were not found."); + } + return new Function( + fsImport, + pathImport, + moduleImport, + `${source}\nreturn resolveCodexPath;`, + )({ existsSync }, { delimiter, dirname, join }, { createRequire }) as ( + environment: NodeJS.ProcessEnv, + platform: NodeJS.Platform, + architecture: NodeJS.Architecture, + ) => string; +} + +test.each([ + ["x64", "x86_64-pc-windows-msvc"], + ["arm64", "aarch64-pc-windows-msvc"], +] as const)( + "resolves a managed Windows %s worker through the packaged MCP environment", + async (architecture, targetTriple) => { + const root = await realpath( + await mkdtemp(join(tmpdir(), "codex-security-managed-cli-")), + ); + try { + const packages = join(root, "managed CLI", "node_modules", "@openai"); + const packageRoot = join(packages, "codex"); + const platformPackage = join(packages, `codex-win32-${architecture}`); + const executable = join( + platformPackage, + "vendor", + targetTriple, + "bin", + "codex.exe", + ); + await mkdir(packageRoot, { recursive: true }); + await mkdir(dirname(executable), { recursive: true }); + await writeFile(join(packageRoot, "package.json"), "{}\n"); + await writeFile(join(platformPackage, "package.json"), "{}\n"); + await writeFile(executable, "synthetic executable\n"); + + const configuration = JSON.parse( + await readFile(join(PLUGIN_ROOT, ".mcp.json"), "utf8"), + ) as { mcpServers: Record }; + const allowed = new Set( + configuration.mcpServers["codex-security"]!.env_vars, + ); + const environment = Object.fromEntries( + Object.entries({ + PATH: "", + CODEX_MANAGED_PACKAGE_ROOT: ` ${packageRoot} `, + }).filter(([name]) => name === "PATH" || allowed.has(name)), + ); + const resolveCodexPath = await bundledCodexPathResolver(); + + expect(resolveCodexPath(environment, "win32", architecture)).toBe( + executable, + ); + const configured = join(root, "custom CLI", "codex.exe"); + expect( + resolveCodexPath( + { ...environment, CODEX_CLI_PATH: configured }, + "win32", + architecture, + ), + ).toBe(configured); + expect(resolveCodexPath(environment, "linux", architecture)).toBe( + "codex", + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); From 67e947f30a2fde2c2996b328b4028dfff48caac4 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:09:45 -0700 Subject: [PATCH 2/4] fix(runtime): refresh same-version plugin installs --- sdk/typescript/src/runtime.ts | 20 +++-- sdk/typescript/tests-ts/runtime.test.ts | 113 +++++++++++++++++++++++- 2 files changed, 122 insertions(+), 11 deletions(-) diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index 13c41f2c2..212e5ee66 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -2054,17 +2054,21 @@ export async function bootstrapPlugin( ); } - const staged = - existing === null - ? null - : await pluginMetadata(join(marketplace, "plugins", PLUGIN_NAME)).catch( - () => null, - ); - if (staged?.version !== version) { + // A bundled plugin can change without changing its upstream version. + // Finish the replacement before removing the currently staged plugin. + await mkdir(codexHome, { recursive: true, mode: 0o700 }); + const stagingHome = await mkdtemp(join(codexHome, ".sdk-marketplace-")); + try { + const staged = await createMarketplace(stagingHome, root, options.signal); + throwIfSignalAborted(options.signal); if (existing !== null) { await rm(marketplace, { recursive: true, force: true }); } - await createMarketplace(codexHome, root, options.signal); + await rename(staged, marketplace); + } finally { + await rm(stagingHome, { recursive: true, force: true }).catch( + () => undefined, + ); } const config = await readFile(join(codexHome, "config.toml"), "utf8").catch( (error: unknown) => { diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index 16280e70e..445a08e51 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -1427,6 +1427,7 @@ describe("plugin runtime preparation", () => { test("uses the installed plugin path returned by Codex", async () => { const root = await temporaryDirectory(); const selected = await plugin(root); + await writeFile(join(selected, "scripts", "obsolete.py"), "obsolete\n"); const home = join(root, "home"); await mkdir(home); await writeFile(join(home, "config.toml"), "[features]\nplugins = true\n"); @@ -1483,10 +1484,22 @@ describe("plugin runtime preparation", () => { join(selected, "scripts", "helper.py"), "print('updated')\n", ); + await rm(join(selected, "scripts", "obsolete.py")); const reused = await bootstrapPlugin(home, selected, { codexCommand: { command: "/codex" }, runCodex: async (_command, args) => { calls.push([...args]); + const scripts = join( + home, + "sdk-marketplace", + "plugins", + "codex-security", + "scripts", + ); + expect(await readFile(join(scripts, "helper.py"), "utf8")).toBe( + "print('updated')\n", + ); + expect(existsSync(join(scripts, "obsolete.py"))).toBe(false); return JSON.stringify({ installedPath: installed, version: "1.2.3" }); }, }); @@ -1647,6 +1660,78 @@ describe("plugin runtime preparation", () => { ]); }); + test("keeps the existing marketplace when its replacement cannot be staged", async () => { + const root = await temporaryDirectory(); + const previous = await plugin(join(root, "previous"), "1.2.3"); + const next = await plugin(join(root, "next"), "1.2.4"); + const outside = join(root, "outside"); + await mkdir(outside); + await writeFile(join(outside, "untouched"), "preserved\n"); + await symlink( + outside, + join(next, "linked"), + process.platform === "win32" ? "junction" : "dir", + ); + const home = join(root, "home"); + const marketplace = await createMarketplace(home, previous); + const config = `[marketplaces.codex-security-sdk]\nsource_type = "local"\nsource = ${JSON.stringify(marketplace)}\n`; + await writeFile(join(home, "config.toml"), config); + await writeFile(join(home, "auth.json"), '{"token":"preserved"}\n'); + let registrationCalls = 0; + + await expect( + bootstrapPlugin(home, next, { + codexCommand: { command: "/codex" }, + runCodex: async () => { + registrationCalls += 1; + return ""; + }, + }), + ).rejects.toThrow(PluginBootstrapError); + + expect(registrationCalls).toBe(0); + expect( + await readFile( + join(marketplace, "plugins", "codex-security", "scripts", "helper.py"), + "utf8", + ), + ).toBe("print('ok')\n"); + expect(await readFile(join(home, "config.toml"), "utf8")).toBe(config); + expect(await readFile(join(home, "auth.json"), "utf8")).toBe( + '{"token":"preserved"}\n', + ); + expect(await readFile(join(outside, "untouched"), "utf8")).toBe( + "preserved\n", + ); + expect( + (await readdir(home)).filter((path) => + path.startsWith(".sdk-marketplace-"), + ), + ).toEqual([]); + }); + + test("can restage a plugin selected from its current marketplace", async () => { + const root = await temporaryDirectory(); + const selected = await plugin(root); + const home = join(root, "home"); + const marketplace = await createMarketplace(home, selected); + const staged = join(marketplace, "plugins", "codex-security"); + + const installed = await bootstrapPlugin(home, staged, { + codexCommand: { command: "/codex" }, + runCodex: async (_command, args) => { + if (args[1] === "marketplace") return ""; + expect( + await readFile(join(staged, "scripts", "helper.py"), "utf8"), + ).toBe("print('ok')\n"); + return JSON.stringify({ installedPath: staged, version: "1.2.3" }); + }, + }); + + expect(installed.pluginRoot).toBe(staged); + expect(installed.installedRoot).toBe(staged); + }); + test("upgrades a cached plugin without deleting persistent credentials", async () => { const root = await temporaryDirectory(); const previous = await plugin(join(root, "previous"), "1.2.3"); @@ -1741,10 +1826,11 @@ describe("plugin runtime preparation", () => { ]); }); - test("upgrades a plugin with the real bundled Codex executable", async () => { + test("refreshes and upgrades a plugin with the real bundled Codex executable", async () => { const root = await temporaryDirectory(); const previous = await plugin(join(root, "previous"), "1.2.3"); const next = await plugin(join(root, "next"), "1.2.4"); + await writeFile(join(previous, "scripts", "obsolete.py"), "obsolete\n"); const home = join(root, "home"); await mkdir(home, { mode: 0o700 }); await writeFile( @@ -1769,9 +1855,30 @@ describe("plugin runtime preparation", () => { const credentials = await readFile(join(home, "auth.json"), "utf8"); const options = { codexCommand: command, environment }; - expect((await bootstrapPlugin(home, previous, options)).version).toBe( - "1.2.3", + const first = await bootstrapPlugin(home, previous, options); + expect(first.version).toBe("1.2.3"); + expect( + await readFile(join(first.installedRoot, "scripts", "helper.py"), "utf8"), + ).toBe("print('ok')\n"); + + await writeFile( + join(previous, "scripts", "helper.py"), + "print('updated')\n", ); + await rm(join(previous, "scripts", "obsolete.py")); + const refreshed = await bootstrapPlugin(home, previous, options); + expect(refreshed.version).toBe("1.2.3"); + expect(refreshed.installedRoot).toBe(first.installedRoot); + expect( + await readFile( + join(refreshed.installedRoot, "scripts", "helper.py"), + "utf8", + ), + ).toBe("print('updated')\n"); + expect( + existsSync(join(refreshed.installedRoot, "scripts", "obsolete.py")), + ).toBe(false); + const upgraded = await bootstrapPlugin(home, next, options); expect(upgraded.version).toBe("1.2.4"); From f0989c4760785cdae522c8edd23dca0bae873e8a Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:30:24 -0700 Subject: [PATCH 3/4] fix(runtime): restore marketplace after activation failure --- sdk/typescript/src/runtime.ts | 34 +++++++-- sdk/typescript/tests-ts/runtime.test.ts | 99 +++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index 212e5ee66..80fb74272 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -2058,17 +2058,41 @@ export async function bootstrapPlugin( // Finish the replacement before removing the currently staged plugin. await mkdir(codexHome, { recursive: true, mode: 0o700 }); const stagingHome = await mkdtemp(join(codexHome, ".sdk-marketplace-")); + const previous = join(stagingHome, "previous-marketplace"); + let preserveStaging = false; try { const staged = await createMarketplace(stagingHome, root, options.signal); throwIfSignalAborted(options.signal); if (existing !== null) { - await rm(marketplace, { recursive: true, force: true }); + await rename(marketplace, previous); + } + try { + await rename(staged, marketplace); + } catch (error) { + if (existing !== null) { + try { + await rename(previous, marketplace); + } catch (restorationError) { + preserveStaging = true; + throw new PluginBootstrapError( + `Unable to activate the Codex Security plugin marketplace. The previous marketplace remains at ${previous}.`, + { + cause: new AggregateError( + [error, restorationError], + "Marketplace activation and rollback failed.", + ), + }, + ); + } + } + throw error; } - await rename(staged, marketplace); } finally { - await rm(stagingHome, { recursive: true, force: true }).catch( - () => undefined, - ); + if (!preserveStaging) { + await rm(stagingHome, { recursive: true, force: true }).catch( + () => undefined, + ); + } } const config = await readFile(join(codexHome, "config.toml"), "utf8").catch( (error: unknown) => { diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index 445a08e51..352ba843d 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -19,6 +19,7 @@ import { import * as fsPromises from "node:fs/promises"; import { tmpdir } from "node:os"; import { + basename, delimiter, dirname, isAbsolute, @@ -1732,6 +1733,104 @@ describe("plugin runtime preparation", () => { expect(installed.installedRoot).toBe(staged); }); + test("restores or retains the previous marketplace when activation fails", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "restores or retains the previous marketplace when activation fails", + ) + ) { + return; + } + const originalRename = fsPromises.rename; + for (const failRestoration of [false, true]) { + const root = await temporaryDirectory(); + const previous = await plugin(join(root, "previous")); + const next = await plugin(join(root, "next")); + await writeFile(join(next, "scripts", "helper.py"), "print('updated')\n"); + const home = join(root, "home"); + const marketplace = await createMarketplace(home, previous); + const config = `[marketplaces.codex-security-sdk]\nsource_type = "local"\nsource = ${JSON.stringify(marketplace)}\n`; + await writeFile(join(home, "config.toml"), config); + await writeFile(join(home, "auth.json"), '{"token":"preserved"}\n'); + const activationError = Object.assign(new Error("activation failed"), { + code: "EPERM", + }); + const restorationError = Object.assign(new Error("restoration failed"), { + code: "EACCES", + }); + let backup: string | undefined; + let registrationCalls = 0; + mock.module("node:fs/promises", () => ({ + ...fsPromises, + rename: async (...args: Parameters) => { + const source = String(args[0]); + const destination = String(args[1]); + if (source === marketplace) backup = destination; + if (destination === marketplace) { + if (basename(source) === "sdk-marketplace") { + throw activationError; + } + if (failRestoration) throw restorationError; + } + return await originalRename(...args); + }, + })); + let failure: unknown; + try { + failure = await bootstrapPlugin(home, next, { + codexCommand: { command: "/codex" }, + runCodex: async () => { + registrationCalls += 1; + return ""; + }, + }).then( + () => null, + (error: unknown) => error, + ); + } finally { + mock.module("node:fs/promises", () => ({ + ...fsPromises, + rename: originalRename, + })); + } + + expect(registrationCalls).toBe(0); + expect(await readFile(join(home, "config.toml"), "utf8")).toBe(config); + expect(await readFile(join(home, "auth.json"), "utf8")).toBe( + '{"token":"preserved"}\n', + ); + if (failRestoration) { + expect(failure).toBeInstanceOf(PluginBootstrapError); + expect((failure as Error).message).toContain(backup!); + expect(((failure as Error).cause as AggregateError).errors).toEqual([ + activationError, + restorationError, + ]); + expect(existsSync(marketplace)).toBe(false); + } else { + expect(failure).toBe(activationError); + expect( + (await readdir(home)).filter((path) => + path.startsWith(".sdk-marketplace-"), + ), + ).toEqual([]); + } + expect( + await readFile( + join( + failRestoration ? backup! : marketplace, + "plugins", + "codex-security", + "scripts", + "helper.py", + ), + "utf8", + ), + ).toBe("print('ok')\n"); + } + }); + test("upgrades a cached plugin without deleting persistent credentials", async () => { const root = await temporaryDirectory(); const previous = await plugin(join(root, "previous"), "1.2.3"); From ffaa35bdc79277f3736e49b24204f6d3d74ccd5e Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:53:29 -0700 Subject: [PATCH 4/4] fix(plugin): version the bundled SDK revision --- .../_bundled_plugin/.codex-plugin/plugin.json | 2 +- sdk/typescript/src/runtime.ts | 46 +-- sdk/typescript/src/version.ts | 2 +- sdk/typescript/tests-ts/runtime.test.ts | 348 ++++-------------- 4 files changed, 90 insertions(+), 308 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json b/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json index 04c30b9c6..48eea6ba9 100644 --- a/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json +++ b/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex-security", - "version": "0.1.20", + "version": "0.1.20+sdk.1", "description": "Codex Security workflows for security scans, analysis, and investigation.", "author": { "name": "OpenAI" diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index 80fb74272..13c41f2c2 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -2054,45 +2054,17 @@ export async function bootstrapPlugin( ); } - // A bundled plugin can change without changing its upstream version. - // Finish the replacement before removing the currently staged plugin. - await mkdir(codexHome, { recursive: true, mode: 0o700 }); - const stagingHome = await mkdtemp(join(codexHome, ".sdk-marketplace-")); - const previous = join(stagingHome, "previous-marketplace"); - let preserveStaging = false; - try { - const staged = await createMarketplace(stagingHome, root, options.signal); - throwIfSignalAborted(options.signal); + const staged = + existing === null + ? null + : await pluginMetadata(join(marketplace, "plugins", PLUGIN_NAME)).catch( + () => null, + ); + if (staged?.version !== version) { if (existing !== null) { - await rename(marketplace, previous); - } - try { - await rename(staged, marketplace); - } catch (error) { - if (existing !== null) { - try { - await rename(previous, marketplace); - } catch (restorationError) { - preserveStaging = true; - throw new PluginBootstrapError( - `Unable to activate the Codex Security plugin marketplace. The previous marketplace remains at ${previous}.`, - { - cause: new AggregateError( - [error, restorationError], - "Marketplace activation and rollback failed.", - ), - }, - ); - } - } - throw error; - } - } finally { - if (!preserveStaging) { - await rm(stagingHome, { recursive: true, force: true }).catch( - () => undefined, - ); + await rm(marketplace, { recursive: true, force: true }); } + await createMarketplace(codexHome, root, options.signal); } const config = await readFile(join(codexHome, "config.toml"), "utf8").catch( (error: unknown) => { diff --git a/sdk/typescript/src/version.ts b/sdk/typescript/src/version.ts index 01dd50067..13c3815eb 100644 --- a/sdk/typescript/src/version.ts +++ b/sdk/typescript/src/version.ts @@ -8,7 +8,7 @@ const PACKAGE_VERSIONS = packageVersions( export const VERSION = PACKAGE_VERSIONS.package; export const CODEX_SDK_VERSION = PACKAGE_VERSIONS.sdk; export const CODEX_EXECUTABLE_VERSION = PACKAGE_VERSIONS.executable; -export const BUNDLED_PLUGIN_VERSION = "0.1.20" as const; +export const BUNDLED_PLUGIN_VERSION = "0.1.20+sdk.1" as const; const PACKAGE_NAME = "@openai/codex-security"; const VERSION_PATTERN = diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index 352ba843d..446b7ea66 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -19,7 +19,6 @@ import { import * as fsPromises from "node:fs/promises"; import { tmpdir } from "node:os"; import { - basename, delimiter, dirname, isAbsolute, @@ -1428,7 +1427,6 @@ describe("plugin runtime preparation", () => { test("uses the installed plugin path returned by Codex", async () => { const root = await temporaryDirectory(); const selected = await plugin(root); - await writeFile(join(selected, "scripts", "obsolete.py"), "obsolete\n"); const home = join(root, "home"); await mkdir(home); await writeFile(join(home, "config.toml"), "[features]\nplugins = true\n"); @@ -1485,22 +1483,10 @@ describe("plugin runtime preparation", () => { join(selected, "scripts", "helper.py"), "print('updated')\n", ); - await rm(join(selected, "scripts", "obsolete.py")); const reused = await bootstrapPlugin(home, selected, { codexCommand: { command: "/codex" }, runCodex: async (_command, args) => { calls.push([...args]); - const scripts = join( - home, - "sdk-marketplace", - "plugins", - "codex-security", - "scripts", - ); - expect(await readFile(join(scripts, "helper.py"), "utf8")).toBe( - "print('updated')\n", - ); - expect(existsSync(join(scripts, "obsolete.py"))).toBe(false); return JSON.stringify({ installedPath: installed, version: "1.2.3" }); }, }); @@ -1513,67 +1499,73 @@ describe("plugin runtime preparation", () => { ]); }); - test("refreshes cached plugins before forwarding delegated scan attribution", async () => { - const root = await temporaryDirectory(); - const previous = await plugin(join(root, "previous"), "0.1.19"); - await writeFile( - join(previous, ".mcp.json"), - JSON.stringify({ - mcpServers: { "codex-security": { env_vars: [] } }, - }), - ); - const home = join(root, "home"); - const marketplace = join(home, "sdk-marketplace"); - await mkdir(home); - const runCodex: NonNullable< - NonNullable[2]>["runCodex"] - > = async (_command, args) => { - if (args[1] === "marketplace") { - await writeFile( - join(home, "config.toml"), - `[marketplaces.codex-security-sdk]\nsource_type = "local"\nsource = ${JSON.stringify(marketplace)}\n`, - ); - return ""; - } - const manifest = JSON.parse( - await readFile( - join( - marketplace, - "plugins", - "codex-security", - ".codex-plugin", - "plugin.json", + test.each(["0.1.19", "0.1.20"])( + "refreshes cached %s plugins before forwarding worker configuration", + async (previousVersion) => { + const root = await temporaryDirectory(); + const previous = await plugin(join(root, "previous"), previousVersion); + await writeFile( + join(previous, ".mcp.json"), + JSON.stringify({ + mcpServers: { "codex-security": { env_vars: [] } }, + }), + ); + const home = join(root, "home"); + const marketplace = join(home, "sdk-marketplace"); + await mkdir(home); + const runCodex: NonNullable< + NonNullable[2]>["runCodex"] + > = async (_command, args) => { + if (args[1] === "marketplace") { + await writeFile( + join(home, "config.toml"), + `[marketplaces.codex-security-sdk]\nsource_type = "local"\nsource = ${JSON.stringify(marketplace)}\n`, + ); + return ""; + } + const manifest = JSON.parse( + await readFile( + join( + marketplace, + "plugins", + "codex-security", + ".codex-plugin", + "plugin.json", + ), + "utf8", ), + ) as { version: string }; + return JSON.stringify({ + installedPath: join(home, "installed", manifest.version), + version: manifest.version, + }); + }; + const options = { + codexCommand: { command: "/codex", prefixArgs: [] }, + runCodex, + }; + + expect((await bootstrapPlugin(home, previous, options)).version).toBe( + previousVersion, + ); + const upgraded = await bootstrapPlugin(home, PLUGIN_ROOT, options); + const configuration = JSON.parse( + await readFile( + join(marketplace, "plugins", "codex-security", ".mcp.json"), "utf8", ), - ) as { version: string }; - return JSON.stringify({ - installedPath: join(home, "installed", manifest.version), - version: manifest.version, - }); - }; - const options = { - codexCommand: { command: "/codex", prefixArgs: [] }, - runCodex, - }; - - expect((await bootstrapPlugin(home, previous, options)).version).toBe( - "0.1.19", - ); - const upgraded = await bootstrapPlugin(home, PLUGIN_ROOT, options); - const configuration = JSON.parse( - await readFile( - join(marketplace, "plugins", "codex-security", ".mcp.json"), - "utf8", - ), - ) as { mcpServers: Record }; + ) as { mcpServers: Record }; - expect(upgraded.version).toBe(BUNDLED_PLUGIN_VERSION); - expect(upgraded.version).not.toBe("0.1.19"); - expect(configuration.mcpServers["codex-security"]?.env_vars).toContain( - "CODEX_SECURITY_SURFACE", - ); - }); + expect(upgraded.version).toBe(BUNDLED_PLUGIN_VERSION); + expect(upgraded.version).not.toBe(previousVersion); + expect(configuration.mcpServers["codex-security"]?.env_vars).toContain( + "CODEX_SECURITY_SURFACE", + ); + expect(configuration.mcpServers["codex-security"]?.env_vars).toContain( + "CODEX_MANAGED_PACKAGE_ROOT", + ); + }, + ); test("rejects plugin installs without the selected path and version", async () => { for (const output of [ @@ -1661,176 +1653,6 @@ describe("plugin runtime preparation", () => { ]); }); - test("keeps the existing marketplace when its replacement cannot be staged", async () => { - const root = await temporaryDirectory(); - const previous = await plugin(join(root, "previous"), "1.2.3"); - const next = await plugin(join(root, "next"), "1.2.4"); - const outside = join(root, "outside"); - await mkdir(outside); - await writeFile(join(outside, "untouched"), "preserved\n"); - await symlink( - outside, - join(next, "linked"), - process.platform === "win32" ? "junction" : "dir", - ); - const home = join(root, "home"); - const marketplace = await createMarketplace(home, previous); - const config = `[marketplaces.codex-security-sdk]\nsource_type = "local"\nsource = ${JSON.stringify(marketplace)}\n`; - await writeFile(join(home, "config.toml"), config); - await writeFile(join(home, "auth.json"), '{"token":"preserved"}\n'); - let registrationCalls = 0; - - await expect( - bootstrapPlugin(home, next, { - codexCommand: { command: "/codex" }, - runCodex: async () => { - registrationCalls += 1; - return ""; - }, - }), - ).rejects.toThrow(PluginBootstrapError); - - expect(registrationCalls).toBe(0); - expect( - await readFile( - join(marketplace, "plugins", "codex-security", "scripts", "helper.py"), - "utf8", - ), - ).toBe("print('ok')\n"); - expect(await readFile(join(home, "config.toml"), "utf8")).toBe(config); - expect(await readFile(join(home, "auth.json"), "utf8")).toBe( - '{"token":"preserved"}\n', - ); - expect(await readFile(join(outside, "untouched"), "utf8")).toBe( - "preserved\n", - ); - expect( - (await readdir(home)).filter((path) => - path.startsWith(".sdk-marketplace-"), - ), - ).toEqual([]); - }); - - test("can restage a plugin selected from its current marketplace", async () => { - const root = await temporaryDirectory(); - const selected = await plugin(root); - const home = join(root, "home"); - const marketplace = await createMarketplace(home, selected); - const staged = join(marketplace, "plugins", "codex-security"); - - const installed = await bootstrapPlugin(home, staged, { - codexCommand: { command: "/codex" }, - runCodex: async (_command, args) => { - if (args[1] === "marketplace") return ""; - expect( - await readFile(join(staged, "scripts", "helper.py"), "utf8"), - ).toBe("print('ok')\n"); - return JSON.stringify({ installedPath: staged, version: "1.2.3" }); - }, - }); - - expect(installed.pluginRoot).toBe(staged); - expect(installed.installedRoot).toBe(staged); - }); - - test("restores or retains the previous marketplace when activation fails", async () => { - if ( - runMockInSubprocess( - import.meta.path, - "restores or retains the previous marketplace when activation fails", - ) - ) { - return; - } - const originalRename = fsPromises.rename; - for (const failRestoration of [false, true]) { - const root = await temporaryDirectory(); - const previous = await plugin(join(root, "previous")); - const next = await plugin(join(root, "next")); - await writeFile(join(next, "scripts", "helper.py"), "print('updated')\n"); - const home = join(root, "home"); - const marketplace = await createMarketplace(home, previous); - const config = `[marketplaces.codex-security-sdk]\nsource_type = "local"\nsource = ${JSON.stringify(marketplace)}\n`; - await writeFile(join(home, "config.toml"), config); - await writeFile(join(home, "auth.json"), '{"token":"preserved"}\n'); - const activationError = Object.assign(new Error("activation failed"), { - code: "EPERM", - }); - const restorationError = Object.assign(new Error("restoration failed"), { - code: "EACCES", - }); - let backup: string | undefined; - let registrationCalls = 0; - mock.module("node:fs/promises", () => ({ - ...fsPromises, - rename: async (...args: Parameters) => { - const source = String(args[0]); - const destination = String(args[1]); - if (source === marketplace) backup = destination; - if (destination === marketplace) { - if (basename(source) === "sdk-marketplace") { - throw activationError; - } - if (failRestoration) throw restorationError; - } - return await originalRename(...args); - }, - })); - let failure: unknown; - try { - failure = await bootstrapPlugin(home, next, { - codexCommand: { command: "/codex" }, - runCodex: async () => { - registrationCalls += 1; - return ""; - }, - }).then( - () => null, - (error: unknown) => error, - ); - } finally { - mock.module("node:fs/promises", () => ({ - ...fsPromises, - rename: originalRename, - })); - } - - expect(registrationCalls).toBe(0); - expect(await readFile(join(home, "config.toml"), "utf8")).toBe(config); - expect(await readFile(join(home, "auth.json"), "utf8")).toBe( - '{"token":"preserved"}\n', - ); - if (failRestoration) { - expect(failure).toBeInstanceOf(PluginBootstrapError); - expect((failure as Error).message).toContain(backup!); - expect(((failure as Error).cause as AggregateError).errors).toEqual([ - activationError, - restorationError, - ]); - expect(existsSync(marketplace)).toBe(false); - } else { - expect(failure).toBe(activationError); - expect( - (await readdir(home)).filter((path) => - path.startsWith(".sdk-marketplace-"), - ), - ).toEqual([]); - } - expect( - await readFile( - join( - failRestoration ? backup! : marketplace, - "plugins", - "codex-security", - "scripts", - "helper.py", - ), - "utf8", - ), - ).toBe("print('ok')\n"); - } - }); - test("upgrades a cached plugin without deleting persistent credentials", async () => { const root = await temporaryDirectory(); const previous = await plugin(join(root, "previous"), "1.2.3"); @@ -1925,11 +1747,13 @@ describe("plugin runtime preparation", () => { ]); }); - test("refreshes and upgrades a plugin with the real bundled Codex executable", async () => { + test("upgrades a cached 0.1.20 plugin with the real bundled Codex executable", async () => { const root = await temporaryDirectory(); - const previous = await plugin(join(root, "previous"), "1.2.3"); - const next = await plugin(join(root, "next"), "1.2.4"); - await writeFile(join(previous, "scripts", "obsolete.py"), "obsolete\n"); + const previous = await plugin(join(root, "previous"), "0.1.20"); + await writeFile( + join(previous, ".mcp.json"), + JSON.stringify({ mcpServers: { "codex-security": { env_vars: [] } } }), + ); const home = join(root, "home"); await mkdir(home, { mode: 0o700 }); await writeFile( @@ -1955,32 +1779,18 @@ describe("plugin runtime preparation", () => { const options = { codexCommand: command, environment }; const first = await bootstrapPlugin(home, previous, options); - expect(first.version).toBe("1.2.3"); - expect( - await readFile(join(first.installedRoot, "scripts", "helper.py"), "utf8"), - ).toBe("print('ok')\n"); + expect(first.version).toBe("0.1.20"); + const upgraded = await bootstrapPlugin(home, PLUGIN_ROOT, options); + const configuration = JSON.parse( + await readFile(join(upgraded.installedRoot, ".mcp.json"), "utf8"), + ) as { mcpServers: Record }; - await writeFile( - join(previous, "scripts", "helper.py"), - "print('updated')\n", + expect(upgraded.version).toBe(BUNDLED_PLUGIN_VERSION); + expect(upgraded.version).not.toBe(first.version); + expect(upgraded.installedRoot).not.toBe(first.installedRoot); + expect(configuration.mcpServers["codex-security"]?.env_vars).toContain( + "CODEX_MANAGED_PACKAGE_ROOT", ); - await rm(join(previous, "scripts", "obsolete.py")); - const refreshed = await bootstrapPlugin(home, previous, options); - expect(refreshed.version).toBe("1.2.3"); - expect(refreshed.installedRoot).toBe(first.installedRoot); - expect( - await readFile( - join(refreshed.installedRoot, "scripts", "helper.py"), - "utf8", - ), - ).toBe("print('updated')\n"); - expect( - existsSync(join(refreshed.installedRoot, "scripts", "obsolete.py")), - ).toBe(false); - - const upgraded = await bootstrapPlugin(home, next, options); - - expect(upgraded.version).toBe("1.2.4"); expect(await readFile(join(home, "auth.json"), "utf8")).toBe(credentials); expect( spawnSync(command.command, ["login", "status"], {