Skip to content
Open
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: 7 additions & 1 deletion plugins/codex-security/mcp-app/helpers-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { decodePosixBytes } from "./src/helpers/posix-path";
import { windowsBinding } from "./src/native";
import { normalizeCandidatesCommand } from "./src/helpers/normalize-candidates";
import { validatePatchRiskAssessmentCommand } from "./src/helpers/validate-patch-risk-assessment";
import { deepReviewInputCommand } from "./src/helpers/deep-review-input";

let commandLine = process.argv.slice(2);
if (process.platform === "win32") {
Expand Down Expand Up @@ -35,9 +36,14 @@ if (command === "resolve-security-md") {
process.exitCode = normalizeCandidatesCommand(args, posixHome);
} else if (command === "validate-patch-risk-assessment") {
process.exitCode = validatePatchRiskAssessmentCommand(args);
} else if (
command === "copy-deep-review-input" ||
command === "select-deep-review-input"
) {
process.exitCode = deepReviewInputCommand(command, args, posixHome);
} else {
console.error(
"Usage: launch_codex_security_mcp[.cmd] --helper <resolve-security-md | normalize-candidates | validate-patch-risk-assessment> [options]",
"Usage: launch_codex_security_mcp[.cmd] --helper <resolve-security-md | normalize-candidates | validate-patch-risk-assessment | copy-deep-review-input | select-deep-review-input> [options]",
);
process.exitCode = 2;
}
276 changes: 276 additions & 0 deletions plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { decodeUtf8 } from "./utf8";
import { dirname } from "node:path";
import { mkdir, readFile, writeFile } from "./helper-files";
import { encodePosixPath } from "./posix-path";
import { JsonSyntaxError, object, parseJson, pythonRepr } from "./python-json";
import { expandHome, parsedPath } from "./resolve-security-md";

type Command = "copy-deep-review-input" | "select-deep-review-input";
interface RankRow {
path: string;
area: string;
score?: bigint;
include?: boolean;
}
const trim = (value: string) =>
value.replace(
/^[\p{White_Space}\u001c-\u001f]+|[\p{White_Space}\u001c-\u001f]+$/gu,
"",
);

function compare(left: string, right: string): number {
const a = Array.from(left, (character) => character.codePointAt(0)!);
const b = Array.from(right, (character) => character.codePointAt(0)!);
for (let index = 0; index < Math.min(a.length, b.length); index++) {
if (a[index] !== b[index]) return a[index]! - b[index]!;
}
return a.length - b.length;
}

function loadRows(path: string, selection: boolean): RankRow[] {
const label = selection ? "Rank output" : "Rank input";
const contents = decodeUtf8(readFile(path));
const lines = contents === "" ? [] : contents.split(/\r\n|[\r\n]/u);
if (lines.at(-1) === "") lines.pop();
const fields = selection
? ["path", "area", "score", "include", "reason"]
: ["path", "area", "preview"];
const rows = lines.map((line, index) => {
const fail = (message: string): never => {
throw new Error(`${path}:${index + 1}: ${message}`);
};
if (trim(line) === "") fail("blank JSONL rows are not allowed");
let row: unknown;
try {
row = parseJson(line);
} catch (error) {
if (error instanceof JsonSyntaxError)
fail(
`invalid JSON: ${error.message.replace(/: line \d+ column \d+ \(char \d+\)$/u, "")}`,
);
throw error;
}
if (!object(row)) return fail("expected a JSON object");
const missing = fields
.filter((field) => !Object.hasOwn(row, field))
.sort(compare);
const unexpected = Object.keys(row)
.filter((field) => !fields.includes(field))
.sort(compare);
const details: string[] = [];
if (missing.length) details.push(`missing fields ${pythonRepr(missing)}`);
if (unexpected.length)
details.push(`unexpected fields ${pythonRepr(unexpected)}`);
if (details.length) fail(details.join("; "));
for (const field of selection
? ["path", "area"]
: ["path", "area", "preview"]) {
if (
typeof row[field] !== "string" ||
(field === "path" && trim(row[field]) === "")
)
fail(
`${field} must be ${field === "path" ? "a non-empty string" : "a string"}`,
);
}
if (selection) {
if (typeof row.score !== "bigint")
fail("score must be an integer from 1 through 10");
if ((row.score as bigint) < 1n || (row.score as bigint) > 10n)
fail("score must be from 1 through 10");
if (typeof row.include !== "boolean") fail("include must be a boolean");
if (typeof row.reason !== "string" || trim(row.reason) === "")
fail("reason must be a non-empty string");
}
return row as unknown as RankRow;
});
const seen = new Set<string>(),
duplicates = new Set<string>();
for (const row of rows) {
if (seen.has(row.path)) duplicates.add(row.path);
seen.add(row.path);
}
if (duplicates.size)
throw new Error(
`${label} contains duplicate paths: ${pythonRepr([...duplicates].sort(compare))}`,
);
return rows;
}

function writeRows(output: string, rows: RankRow[]): void {
mkdir(dirname(output));
function* contents(): Iterable<Buffer> {
for (const row of rows) {
const json = JSON.stringify({ path: row.path, area: row.area }).replace(
/[\u007f-\uffff]/g,
(character) =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
);
yield Buffer.from(json + (process.platform === "win32" ? "\r\n" : "\n"));
}
}
writeFile(output, contents());
}

class ArgumentError extends Error {}
function integer(value: string): bigint {
const text = value.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "");
if (!/^[+-]?\p{Decimal_Number}+(?:_\p{Decimal_Number}+)*$/u.test(text))
throw new ArgumentError(
`argument --top-percent: invalid int value: ${pythonRepr(value)}`,
);
return BigInt(
Array.from(text.replaceAll("_", ""), (character) => {
if (!/\p{Decimal_Number}/u.test(character)) return character;
const point = character.codePointAt(0)!;
let start = point;
while (/\p{Decimal_Number}/u.test(String.fromCodePoint(start - 1)))
start--;
return String((point - start) % 10);
}).join(""),
);
}

function argumentsFor(
args: string[],
selection: boolean,
): Record<string, string | bigint | true> {
Comment thread
kmbroai marked this conversation as resolved.
const input = selection ? "rank-output" : "rank-input";
const names = [input, "out", "help", ...(selection ? ["top-percent"] : [])];
const values: Record<string, string | bigint | true> = {};
const extra: string[] = [];
const looksOptional = (arg: string) =>
arg.startsWith("-") &&
arg !== "-" &&
!arg.includes(" ") &&
!/^-(?:\p{Decimal_Number}+|\p{Decimal_Number}*\.\p{Decimal_Number}+)\n?$/u.test(
arg,
);
for (let index = 0; index < args.length; index++) {
const arg = args[index]!;
if (arg === "--") {
extra.push(...args.slice(index));
break;
}
const equals = arg.indexOf("=");
const option = equals === -1 ? arg : arg.slice(0, equals);
const matches = option.startsWith("--")
? names.filter((name) => `--${name}`.startsWith(option))
: [];
const name =
names.find((name) => option === `--${name}`) ??
(arg.startsWith("-h")
? "help"
: matches.length === 1
? matches[0]
: undefined);
if (matches.length > 1 && !name)
throw new ArgumentError(
`ambiguous option: ${arg} could match ${matches.map((name) => `--${name}`).join(", ")}`,
);
if (!name) {
extra.push(arg);
continue;
}
if (name === "help") {
if (equals !== -1)
throw new ArgumentError(
`argument -h/--help: ignored explicit argument ${pythonRepr(arg.slice(equals + 1))}`,
);
return { help: true };
}
let value: string;
if (equals !== -1) value = arg.slice(equals + 1);
else {
if (index + 1 === args.length || looksOptional(args[index + 1]!))
throw new ArgumentError(`argument --${name}: expected one argument`);
value = args[++index]!;
}
values[name] = name === "top-percent" ? integer(value) : value;
}
const missing = [input, "out"].filter((name) => values[name] === undefined);
if (missing.length)
throw new ArgumentError(
`the following arguments are required: ${missing.map((name) => `--${name}`).join(", ")}`,
);
if (extra.length)
throw new ArgumentError(`unrecognized arguments: ${extra.join(" ")}`);
return values;
}

function print(message: string, stderr = false): void {
let text = message + "\n";
if (stderr)
text = text.replace(
/[\ud800-\udfff]/gu,
(character) =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
);
(stderr ? process.stderr : process.stdout).write(
process.platform === "win32"
? text.replaceAll("\n", "\r\n")
: stderr
? text
: encodePosixPath(text),
);
}

export function deepReviewInputCommand(
command: Command,
args: string[],
posixHome = process.env.HOME,
): number {
const selection = command === "select-deep-review-input";
const input = selection ? "rank-output" : "rank-input";
const usage = `usage: launch_codex_security_mcp[.cmd] --helper ${command} [-h] --${input} PATH --out PATH${selection ? " [--top-percent INT]" : ""}`;
try {
const values = argumentsFor(args, selection);
if (values.help) {
print(
`${usage}\n\nCreate deep_review_input.jsonl from ${selection ? "worker-produced rank_output.jsonl" : "rank_input.jsonl"}.\n\noptions:\n -h, --help show this help message and exit\n --${input} PATH ${selection ? "Worker ranking output" : "Deterministic rank input"} JSONL.\n --out PATH Output deep_review_input.jsonl path.${selection ? "\n --top-percent INT Percent of included files to keep for deep review. Defaults to 100." : ""}`,
);
return 0;
}
const path = (name: string) =>
parsedPath(expandHome(parsedPath(values[name] as string), posixHome));
const rows = loadRows(path(input), selection);
let selected = rows,
total = rows.length;
if (selection) {
const included = rows.filter((row) => row.include);
const base = included.length ? included : rows;
base.sort(
(left, right) =>
Number(right.score! - left.score!) || compare(left.path, right.path),
);
total = base.length;
let keep = 0;
if (total) {
const percent = Number(values["top-percent"] ?? 100n);
if (!Number.isFinite(percent))
throw new Error("int too large to convert to float");
const count = total * (percent / 100);
if (!Number.isFinite(count))
throw new Error("cannot convert float infinity to integer");
keep = Math.max(1, Math.trunc(count));
}
selected = base.slice(0, keep);
}
const output = path("out");
writeRows(output, selected);
const message = selection
? `Selected ${selected.length} of ${total} rows into ${output}`
: `Copied ${selected.length} rows into ${output}`;
print(message);
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
print(
error instanceof ArgumentError
? `${usage}\n${command}: error: ${message}`
: message,
true,
);
return error instanceof ArgumentError ? 2 : 1;
}
}
27 changes: 26 additions & 1 deletion plugins/codex-security/mcp-app/src/helpers/helper-files.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { readFileSync } from "node:fs";
import {
closeSync,
mkdirSync,
openSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { windowsBinding } from "../native";
import { widePath, windowsFileSystem } from "../../../native/windows-files.mjs";
import { encodePosixPath } from "./posix-path";
Expand All @@ -9,3 +15,22 @@ export function readFile(path: string | number): Buffer {
? windowsFileSystem(windowsBinding()).readFile(widePath(path))
: readFileSync(encodePosixPath(path));
}

export function mkdir(path: string): void {
if (process.platform === "win32")
windowsFileSystem(windowsBinding()).mkdir(widePath(path));
else mkdirSync(encodePosixPath(path), { recursive: true });
}

export function writeFile(path: string, chunks: Iterable<Buffer>): void {
if (process.platform === "win32") {
windowsFileSystem(windowsBinding()).writeFile(widePath(path), chunks);
return;
}
const descriptor = openSync(encodePosixPath(path), "w");
try {
for (const chunk of chunks) writeFileSync(descriptor, chunk);
} finally {
closeSync(descriptor);
}
}
2 changes: 1 addition & 1 deletion plugins/codex-security/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Windows uses `windows-binding.mts` and the same Rust crate. `WindowsHandle` owns

The binding exposes synchronous file and directory creation, attributes and reparse tags, identity and final/opened names, read/write/seek/size/EOF/flush, exact-handle rename and deletion, and exclusive whole-file locking. Rust's `File` supplies ordinary I/O, cursor-preserving truncation, `sync_all` for flush, and locks. Calls return numeric Windows errors, including 6 for closed handles and 33 for nonblocking lock contention. Buffer ranges, path encoding, and 64-bit seek arguments are checked before use. Overlapped handles are unsupported because pending operations could retain native buffers beyond the call. Path authorization, ancestor traversal, and reparse-point policy remain the caller's responsibility.

Five additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. `windowsReadLink` returns a UTF-16LE link target or its numeric Windows error; candidate normalization uses it to resolve missing paths without losing raw filenames. Assessment validation shares the typed wide-path reader.
Five additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. `windowsReadLink` returns a UTF-16LE link target or its numeric Windows error; candidate normalization uses it to resolve missing paths without losing raw filenames. Assessment validation and deep-review worklists share the typed wide-path reader and writer.

`windows-files.mts` leaves ordinary absolute-path resolution and canonicalization to `GetFullPathNameW` and `GetFinalPathNameByHandleW`, trimming trailing separators below the root. Its small verbatim-path normalizer preserves drive and UNC share roots when resolving dot segments, including literal trailing dots and spaces. Non-strict `realpath` can retain unresolved components; callers must check containment independently. It also supports missing output paths. `stat(path, false)` retains exact symbolic-link and reparse-point metadata so callers can reject junction traversal independently of the enumerator's link label. The SDK's public runtime floor remains Node 22.13.0. Node 20.0.0 is an additional native-foundation compatibility proof; it does not change the SDK engine requirement.

Expand Down
Loading
Loading