Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/project-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,11 @@ and applies its severity policy to recovered findings.
Workflow identity records explicitly requested deep settings, not ambient values
or shipped defaults, so changing those defaults does not prevent resumption.
Changing the explicit request still requires a different workflow ID.

### Native executable selection

Native plugin sessions use `CODEX_CLI_PATH` when supplied, or a real `codex`
executable on `PATH` (`codex.exe` on Windows). The selected executable must be
outside the scan target. Windows npm shims, managed-package directories and
desktop cache directories are no longer searched; set `CODEX_CLI_PATH` to the
installed executable when it is not directly on `PATH`.
3 changes: 1 addition & 2 deletions plugins/codex-security/mcp-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@openai/codex-sdk": "0.158.0",
"smol-toml": "1.8.0",
"zod": "^4.6.5"
},
"devDependencies": {
"@types/node": "^26.6.2",
"esbuild": "^0.28.2",
"prettier": "3.9.8",
"smol-toml": "1.8.0",
"typescript": "^7.0.2"
}
}
85 changes: 3 additions & 82 deletions plugins/codex-security/mcp-app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 2 additions & 11 deletions plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url";
import { brotliCompressSync, constants as zlibConstants } from "node:zlib";
import { execFileSync } from "node:child_process";
import { build } from "esbuild";
import { mcpBundleOptions } from "./bundle_options.mjs";

const root = resolve(import.meta.dirname, "..");
const sdkRequire = createRequire(
Expand Down Expand Up @@ -70,24 +71,14 @@ export async function buildMcpApp({ output, native = "universal" }) {
const bundle = join(mcpDir, name + ".bundle.cjs");
try {
await build({
bundle: true,
banner: {
js: "const __codexSecurityModuleUrl = require('node:url').pathToFileURL(__filename).href;",
},
define: { "import.meta.url": "__codexSecurityModuleUrl" },
...mcpBundleOptions,
entryPoints: [join(root, entryPoint)],
inject:
name === "server"
? [sdkRequire.resolve("pdfjs-dist/legacy/build/pdf.worker.mjs")]
: [],
external: ["fsevents"],
format: "cjs",
loader: { ".md": "text" },
logLevel: "info",
logOverride: { "empty-import-meta": "silent" },
outfile: bundle,
platform: "node",
target: "node20",
});
const runtime = brotliCompressSync(await readFile(bundle), {
params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 10 },
Expand Down
20 changes: 20 additions & 0 deletions plugins/codex-security/mcp-app/scripts/bundle_options.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createRequire } from "node:module";
import { dirname } from "node:path";

// Source tests and shipped runtimes use the same CommonJS and dependency resolution.
export const mcpBundleOptions = {
bundle: true,
alias: {
zod: dirname(createRequire(import.meta.url).resolve("zod/package.json")),
},
banner: {
js: "const __codexSecurityModuleUrl = require('node:url').pathToFileURL(__filename).href;",
},
define: { "import.meta.url": "__codexSecurityModuleUrl" },
external: ["fsevents"],
format: "cjs",
loader: { ".md": "text" },
logOverride: { "empty-import-meta": "silent" },
platform: "node",
target: "node20",
};
36 changes: 11 additions & 25 deletions plugins/codex-security/mcp-app/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as z from "zod/v4";
import {
missingPythonHelperMessage,
resolvePythonCommand,
workbenchCommandTimeout,
} from "./src/python_command.js";
import type { ScanResults } from "./src/types.js";
import { MCP_APP_VERSION } from "./src/version.js";
Expand Down Expand Up @@ -1194,7 +1195,7 @@ export function createCodexSecurityServer(): McpServer {
},
abortSignalFromExtra(extra),
);
return nativeScanCompletedResult(scan);
return nativeScanCompletedResult(scan, "get-scan");
} catch (error: unknown) {
if (error instanceof ScanPermissionError)
return toolErrorResult(deepScanInvocationFailureMessage(error));
Expand Down Expand Up @@ -2379,14 +2380,19 @@ function boundedErrorData(error: unknown): { message: string; name: string } {
};
}

async function nativeScanCompletedResult(scan: ScanResults) {
async function nativeScanCompletedResult(
scan: ScanResults,
command: "complete-scan" | "get-scan" = "complete-scan",
) {
let completed: JsonObject;
try {
completed = await runWorkbench([
"complete-scan",
command,
"--scan-id",
scan.scanId,
...optionalArg("--claim-token", scan.handoffClaimToken),
...(command === "complete-scan"
? optionalArg("--claim-token", scan.handoffClaimToken)
: []),
]);
} catch (error) {
return toolErrorResult(completionFailureMessage(error));
Expand Down Expand Up @@ -2600,27 +2606,7 @@ async function executeWorkbench(
encoding: "utf8" as const,
// Artifact bytes are base64-encoded here; retain the existing file-size behavior.
maxBuffer: args[0] === "read-artifact" ? Infinity : 4 * 1024 * 1024,
timeout: [
"begin-deep-scan",
"complete-scan",
"export-findings",
"get-scan",
"get-workspace",
"inspect-setup",
"list-findings",
"preserve-scan-results",
"recover-scan-results",
"request-finding-remediation",
"request-finding-remediation-action",
"save-workspace",
"set-finding-triage",
"set-finding-remediation",
"start-headless-standard-scan",
"start-prompt-only-scan",
"start-scan",
].includes(args[0] ?? "")
? 300_000
: 30_000,
timeout: workbenchCommandTimeout(args[0]),
},
);
if (workbenchInput !== undefined) {
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex-security/mcp-app/src/artifact-attack-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export async function recordCodexSecurityCandidateAttackPaths(
operation: "replace";
rowsWritten: number;
}> {
if (context.layout !== "scan") {
if (!context.scanId) {
throw new Error(
"Candidate attack-path analysis requires a scan-bound artifact context.",
);
Expand Down
1 change: 0 additions & 1 deletion plugins/codex-security/mcp-app/src/artifact-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export async function createScanArtifactContext(
rawRepoRoot,
"Codex Security scan target root",
),
layout: "scan",
scanId,
...defined("scope", optionalString(scan.scope)),
...defined("pluginRoot", options.pluginRoot),
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex-security/mcp-app/src/artifact-inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const reviewItemSchema = loadArtifactZodSchema(
export async function prepareCodexSecurityReviewItems(
context: ArtifactContext,
): Promise<PreparedReviewItems> {
if (context.layout !== "scan") {
if (!context.scanId) {
throw new Error(
`${label}: only a parent scan can prepare its shared inventory.`,
);
Expand Down
1 change: 0 additions & 1 deletion plugins/codex-security/mcp-app/src/artifact-io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
export interface ArtifactContext {
root: string;
repoRoot: string;
layout: "scan";
scanId?: string;
scope?: string;
pluginRoot?: string;
Expand Down
Loading
Loading