Skip to content
2 changes: 1 addition & 1 deletion sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-security",
"version": "0.1.22",
"version": "0.1.23",
"description": "Codex Security workflows for security scans, analysis, and investigation.",
"author": {
"name": "OpenAI"
Expand Down
64 changes: 16 additions & 48 deletions sdk/typescript/_bundled_plugin/skills/fix-finding/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,14 @@ 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.

## Pre-Patch Investigation

Expand All @@ -52,37 +41,16 @@ The investigation requires repository-relative evidence and a clear separation b

## Implementation 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.

## Patch Candidate Review

Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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.22" as const;
export const BUNDLED_PLUGIN_VERSION = "0.1.23" as const;

const PACKAGE_NAME = "@openai/codex-security";

Expand Down
106 changes: 106 additions & 0 deletions sdk/typescript/tests-ts/deep-scan-executable.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, { env_vars: string[] }> };
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 });
}
},
);
29 changes: 21 additions & 8 deletions sdk/typescript/tests-ts/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1896,10 +1896,13 @@ describe("plugin runtime preparation", () => {
]);
});

test("upgrades a plugin with the real bundled Codex executable", async () => {
test("upgrades the released 0.1.22 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");
const previous = await plugin(join(root, "previous"), "0.1.22");
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(
Expand All @@ -1924,12 +1927,22 @@ 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 upgraded = await bootstrapPlugin(home, next, options);
const first = await bootstrapPlugin(home, previous, options);
expect(first.version).toBe("0.1.22");
const upgraded = await bootstrapPlugin(home, PLUGIN_ROOT, options);
const configuration = JSON.parse(
await readFile(join(upgraded.installedRoot, ".mcp.json"), "utf8"),
) as {
mcpServers: Record<string, { command: string; env_vars: string[] }>;
};
const server = configuration.mcpServers["codex-security"];

expect(upgraded.version).toBe("1.2.4");
expect(upgraded.version).toBe(BUNDLED_PLUGIN_VERSION);
expect(upgraded.version).not.toBe(first.version);
expect(upgraded.installedRoot).not.toBe(first.installedRoot);
expect(server?.command).toBe("./scripts/launch_codex_security_mcp");
expect(server?.env_vars).toContain("CODEX_MANAGED_PACKAGE_ROOT");
expect(server?.env_vars).toContain("CODEX_MCP_NODE_PATH");
expect(await readFile(join(home, "auth.json"), "utf8")).toBe(credentials);
expect(
spawnSync(command.command, ["login", "status"], {
Expand Down
Loading