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
2 changes: 1 addition & 1 deletion plugins/codex-security/mcp-app/artifact-writer-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "./src/artifact-context.js";
import { CODEX_SANDBOX_STATE_META_CAPABILITY } from "./src/deep-scan/parent-sandbox.js";
import { registerCompactWorkerArtifactTools } from "./src/server/compact-artifact-tools.js";
import { MCP_APP_VERSION } from "./src/version.js";
import { version as MCP_APP_VERSION } from "./package.json";
import { isRecord } from "./src/record.js";

/** Build the narrow worker-only MCP from coordinator-inherited state. */
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex-security/mcp-app/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
missingPythonHelperMessage,
resolvePythonCommand,
} from "./src/python_command.js";
import { MCP_APP_VERSION } from "./src/version.js";
import { version as MCP_APP_VERSION } from "./package.json";
import {
handoffClaimTokenSchema,
recoveryHandoffClaimTokenSchema,
Expand Down
69 changes: 21 additions & 48 deletions plugins/codex-security/mcp-app/src/deep-scan/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,27 +162,27 @@ export class CodexSdkWorkerExecutor implements CodexWorkerExecutor {
try {
const { events } = await thread.runStreamed(input, {
signal: controller.signal,
...(runtimeSettings.cyberAccessProgram === undefined
? {}
: { cyberAccessProgram: runtimeSettings.cyberAccessProgram }),
cyberAccessProgram: runtimeSettings.cyberAccessProgram,
});
let threadId: string | undefined;
let turnCompleted = false;
let lastStreamError: string | undefined;
const diagnostics: CodexWorkerDiagnostic[] = [];
for await (const event of events) {
if (event.type === "thread.started") {
threadId = event.thread_id;
await request.onThreadStarted?.(threadId);
} else if (event.type === "item.completed") {
const fallbackError =
event.item.type === "error"
? deepScanPermissionProfileFallbackError(event.item.message)
: undefined;
const item = event.type === "item.completed" ? event.item : event;
if (item.type === "error") {
const fallbackError = deepScanPermissionProfileFallbackError(
item.message,
);
if (fallbackError) {
controller.abort(fallbackError);
throw fallbackError;
}
}
if (event.type === "thread.started") {
threadId = event.thread_id;
await request.onThreadStarted?.(threadId);
} else if (event.type === "item.completed") {
appendSafeItemDiagnostic(diagnostics, event.item);
} else if (event.type === "turn.completed") {
turnCompleted = true;
Expand All @@ -191,13 +191,6 @@ export class CodexSdkWorkerExecutor implements CodexWorkerExecutor {
} else if (event.type === "turn.failed") {
throw new Error(event.error.message);
} else if (event.type === "error") {
const fallbackError = deepScanPermissionProfileFallbackError(
event.message,
);
if (fallbackError) {
controller.abort(fallbackError);
throw fallbackError;
}
// Codex exec currently emits retry-in-progress notifications as error events.
lastStreamError = event.message;
appendCodeModeFrameDiagnostic(diagnostics, event.message);
Expand Down Expand Up @@ -323,24 +316,17 @@ type TomlValue = string | number | boolean | TomlObject;
type TomlObject = { [key: string]: TomlValue };

function workerPermissionProfile(sandbox: DeepWorkerParentSandbox): TomlObject {
const filesystemEntries: Array<[string, TomlValue]> = [[":root", "read"]];
const seenFilesystemKeys = new Set<string>();

for (const key of sandbox.filesystemDenies) {
if (seenFilesystemKeys.has(key)) continue;
seenFilesystemKeys.add(key);
filesystemEntries.push([key, "deny"]);
}

if (sandbox.globScanMaxDepth !== undefined) {
filesystemEntries.push(["glob_scan_max_depth", sandbox.globScanMaxDepth]);
}

return {
extends: ":read-only",
// Object.fromEntries preserves literal keys such as "__proto__" without
// letting a denied path mutate the serializer object prototype.
filesystem: Object.fromEntries(filesystemEntries) as TomlObject,
filesystem: Object.fromEntries([
[":root", "read"],
...Array.from(sandbox.filesystemDenies, (key) => [key, "deny"]),
...(sandbox.globScanMaxDepth === undefined
? []
: [["glob_scan_max_depth", sandbox.globScanMaxDepth]]),
]),
network: { enabled: false },
};
}
Expand Down Expand Up @@ -672,10 +658,7 @@ function resolveFromSearchPath(
originalCwd: string,
): string | undefined {
for (const directory of searchPath?.split(delimiter) ?? []) {
const candidate = join(
absoluteSearchDirectory(directory, originalCwd),
executableName,
);
const candidate = join(resolve(originalCwd, directory), executableName);
if (isExecutableFile(candidate)) return candidate;
}
return undefined;
Expand All @@ -687,10 +670,7 @@ function resolveWindowsDirectFromSearchPath(
originalCwd: string,
): string | undefined {
for (const directory of searchPath?.split(delimiter) ?? []) {
const candidate = join(
absoluteSearchDirectory(directory, originalCwd),
executableName,
);
const candidate = join(resolve(originalCwd, directory), executableName);
if (!isWindowsAppsPath(candidate) && existsSync(candidate))
return candidate;
}
Expand All @@ -703,7 +683,7 @@ function resolveWindowsCodexFromSearchPath(
originalCwd: string,
): string | undefined {
for (const directory of searchPath?.split(delimiter) ?? []) {
const absoluteDirectory = absoluteSearchDirectory(directory, originalCwd);
const absoluteDirectory = resolve(originalCwd, directory);
const directBinary = join(absoluteDirectory, "codex.exe");
if (!isWindowsAppsPath(directBinary) && existsSync(directBinary))
return directBinary;
Expand Down Expand Up @@ -773,13 +753,6 @@ function isExecutableFile(value: string): boolean {
}
}

function absoluteSearchDirectory(
directory: string,
originalCwd: string,
): string {
return resolve(originalCwd, directory || ".");
}

function absoluteCodexPath(
value: string,
platform: NodeJS.Platform,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { asRecord as record } from "../record.js";
import { asRecord as record, isNonEmptyString } from "../record.js";
import { isAbsolute } from "node:path";
import { fileURLToPath } from "node:url";
import { isDeepStrictEqual } from "node:util";
Expand Down Expand Up @@ -274,10 +274,6 @@ function isValidSpecialPath(value: Record<string, unknown>): boolean {
return false;
}

function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}

function hasGlobMetacharacters(value: string): boolean {
return value.includes("*") || value.includes("?") || value.includes("[");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { asRecord as record } from "../record.js";
import type { JsonObject as JsonRecord } from "../types.js";
import { asRecord as record, isNonEmptyString } from "../record.js";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { createInterface, type Interface } from "node:readline";
import { isDeepStrictEqual } from "node:util";
import { MCP_APP_VERSION } from "../version.js";
import { version as MCP_APP_VERSION } from "../../package.json";
import { DeepScanNonRetryableError } from "./errors.js";
import { executablePathForSpawn } from "./executable-path.js";

Expand Down Expand Up @@ -30,8 +31,6 @@ export interface DeepScanPermissionProfilePreflightOptions {
readonly signal: AbortSignal;
}

type JsonRecord = Record<string, unknown>;

type PendingRequest = {
readonly id: number;
readonly method: string;
Expand Down Expand Up @@ -140,12 +139,11 @@ class AppServerPreflightClient {
crlfDelay: Infinity,
});
this.stdoutLines.on("line", (line) => this.consumeStdoutLine(line));
this.stdoutLines.on("error", () => {
this.fail(codexExecutableStdioError(options.codexPath));
});
this.child.stdin.on("error", () => {
const onStdioError = () => {
this.fail(codexExecutableStdioError(options.codexPath));
});
};
this.stdoutLines.on("error", onStdioError);
this.child.stdin.on("error", onStdioError);
this.child.on("error", (error) => {
this.fail(codexExecutableStartError(options.codexPath, error));
});
Expand Down Expand Up @@ -427,10 +425,10 @@ function validateOptions(
options: DeepScanPermissionProfilePreflightOptions,
): void {
if (
!nonEmptyString(options.codexPath) ||
!nonEmptyString(options.cwd) ||
!isNonEmptyString(options.codexPath) ||
!isNonEmptyString(options.cwd) ||
!Array.isArray(options.configOverrides) ||
options.configOverrides.some((value) => !nonEmptyString(value)) ||
options.configOverrides.some((value) => !isNonEmptyString(value)) ||
!record(options.expectedProfile) ||
!options.signal ||
typeof options.signal.addEventListener !== "function"
Expand All @@ -440,10 +438,6 @@ function validateOptions(
comparableProfile(options.expectedProfile);
}

function nonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}

// Verified permission incompatibilities explicitly stop the scan. A failed
// transport attempt alone does not establish that the scan cannot proceed.
function disallowedProfileAllowlistError(): DeepScanNonRetryableError {
Expand Down Expand Up @@ -504,14 +498,13 @@ function jsonRpcPreflightError(
? error.code
: undefined;
if (code === -32601) return unsupportedCodexApiError(codexPath, method);
const codeDetail = code === undefined ? "" : " (JSON-RPC code " + code + ")";
return new Error(
"Deep Scan cannot safely verify its read-only worker permission profile because " +
"the selected Codex executable " +
JSON.stringify(codexPath) +
" returned an error for " +
JSON.stringify(method) +
codeDetail +
(code === undefined ? "" : " (JSON-RPC code " + code + ")") +
". Check the Codex configuration and retry. Deep Scan did not run.",
);
}
Expand All @@ -523,11 +516,10 @@ function codexExecutableStartError(codexPath: string, error: Error): Error {
? value
: undefined;
const codeDetail = code === undefined ? "" : " (" + code + ")";
const message = codexExecutableFailureMessage(
codexPath,
"could not start" + codeDetail,
return new Error(
codexExecutableFailureMessage(codexPath, "could not start" + codeDetail),
{ cause: error },
);
return new Error(message, { cause: error });
}

function codexExecutableExitError(
Expand Down
62 changes: 20 additions & 42 deletions plugins/codex-security/mcp-app/src/deep-scan/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,26 @@ export interface DiscoveryPromptInput {
subagents: number;
}

export interface DedupPromptInput {
reducerLabel: string;
claimedWorkerIds: string[];
}

// Every worker starts in a fresh Codex thread. A single typed JSON object
// makes its complete input explicit without duplicating raw and escaped values.

export function renderDiscoveryPrompt(
input: DiscoveryPromptInput,
falsePositiveFeedbackPath?: string,
): string {
const prompt = renderDeepScanTemplate(discoveryTemplate, {
DISCOVERY_CONTEXT_JSON: formattedJson({
scanId: input.scanId,
pluginRoot: input.pluginRoot,
targetPath: input.targetPath,
scope: input.scope,
userContext: input.userContext ?? null,
workerLabel: input.workerLabel,
subagents: input.subagents,
}),
const context = formattedJson({
scanId: input.scanId,
pluginRoot: input.pluginRoot,
targetPath: input.targetPath,
scope: input.scope,
userContext: input.userContext ?? null,
workerLabel: input.workerLabel,
subagents: input.subagents,
});
const prompt = discoveryTemplate.replaceAll(
"{{DISCOVERY_CONTEXT_JSON}}",
() => context,
);
if (!falsePositiveFeedbackPath) return prompt;
return (
`${prompt.trimEnd()}\n\nDuring validation, read existing reviewer false-positive feedback at ` +
Expand All @@ -42,34 +39,15 @@ export function renderDiscoveryPrompt(
);
}

export function renderDedupPrompt(input: DedupPromptInput): string {
return renderDeepScanTemplate(dedupTemplate, {
DEDUP_CONTEXT_JSON: formattedJson({
reducerLabel: input.reducerLabel,
claimedWorkerIds: input.claimedWorkerIds,
}),
});
}

function renderDeepScanTemplate(
template: string,
values: Record<string, string>,
export function renderDedupPrompt(
reducerLabel: string,
claimedWorkerIds: string[],
): string {
const placeholders = [...template.matchAll(/\{\{([A-Z0-9_]+)\}\}/g)];
const missing = placeholders
.map((match) => match[1])
.filter(
(key): key is string => key !== undefined && !Object.hasOwn(values, key),
);
if (missing.length > 0) {
throw new Error(
`Missing Deep Scan template values: ${[...new Set(missing)].join(", ")}`,
);
}
return template.replace(
/\{\{([A-Z0-9_]+)\}\}/g,
(_placeholder, key: string) => String(values[key]),
);
const context = formattedJson({
reducerLabel,
claimedWorkerIds,
});
return dedupTemplate.replaceAll("{{DEDUP_CONTEXT_JSON}}", () => context);
}

function formattedJson(value: unknown): string {
Expand Down
8 changes: 3 additions & 5 deletions plugins/codex-security/mcp-app/src/deep-scan/worker-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,15 +263,13 @@ export class DeepScanWorkerRunner {
const promptRoot = join(reducerRoot, "prompts");
const resultPath = join(artifactDir, "result.json");
await fs.mkdir(artifactDir, { recursive: true });
const basePrompt = renderDedupPrompt({
reducerLabel,
claimedWorkerIds: consumed.map((worker) => worker.id),
});
const workerIds = consumed.map((worker) => worker.id);
const basePrompt = renderDedupPrompt(reducerLabel, workerIds);
await writePrivateFile(promptPath, basePrompt);
await this.options.store.claimDedup({
id: reducerId,
scanId: run.scanId,
workerIds: consumed.map((worker) => worker.id),
workerIds,
promptPath,
artifactDir,
});
Expand Down
4 changes: 4 additions & 0 deletions plugins/codex-security/mcp-app/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
export function asRecord(value: unknown): Record<string, unknown> | undefined {
return isRecord(value) ? value : undefined;
}

export function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
3 changes: 0 additions & 3 deletions plugins/codex-security/mcp-app/src/version.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ export async function runReducerPagingEval({
const claimedWorkerIds = fixture.context.deepReducer.claimedWorkers.map(
(worker) => worker.id,
);
const prompt = renderDedupPrompt({
reducerLabel: "paging-eval",
claimedWorkerIds,
});
const prompt = renderDedupPrompt("paging-eval", claimedWorkerIds);
assert.deepEqual(JSON.parse(prompt.match(/```json\n([\s\S]*?)\n```/)[1]), {
reducerLabel: "paging-eval",
claimedWorkerIds,
Expand Down
Loading
Loading