Skip to content

Commit 5d8cd48

Browse files
mldangelo-oaicopyberry
authored andcommitted
chore(codex-security): sync public projection
- [codex-security] preserve literal candidate paths (#11942... GitOrigin-Timestamp=2026-07-27T23:32:44-07:00 GitOrigin-RevId: 20ef8daf4e17b59e456d66db793b1f35e4b0c1e9
1 parent faff6ea commit 5d8cd48

2 files changed

Lines changed: 248 additions & 19 deletions

File tree

sdk/typescript/tests-ts/auth.test.ts

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { ChildProcess } from "node:child_process";
2-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
2+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
5+
import { setTimeout as delay } from "node:timers/promises";
56
import { afterEach, describe, expect, spyOn, test } from "bun:test";
67
import {
78
accountStatus,
@@ -137,7 +138,7 @@ describe("Codex authentication process boundary", () => {
137138
);
138139

139140
const handle = new CodexLoginHandle(
140-
{ command: "node", prefixArgs: [script] },
141+
{ command: process.execPath, prefixArgs: [script] },
141142
["login"],
142143
process.env,
143144
() => {},
@@ -221,7 +222,7 @@ grandchild.once("error", (error) => {
221222
);
222223

223224
const handle = new CodexLoginHandle(
224-
{ command: "node", prefixArgs: [script] },
225+
{ command: process.execPath, prefixArgs: [script] },
225226
["login"],
226227
process.env,
227228
() => {},
@@ -240,38 +241,75 @@ grandchild.once("error", (error) => {
240241
async () => {
241242
const root = await mkdtemp(join(tmpdir(), "codex-security-auth-pipes-"));
242243
temporaryDirectories.push(root);
244+
const ready = join(root, "grandchild-ready");
245+
const release = join(root, "release-grandchild");
243246
const script = join(root, "login-pipes.mjs");
247+
const grandchildScript = `
248+
import { existsSync, writeFileSync } from "node:fs";
249+
250+
const ready = process.argv[1];
251+
const release = process.argv[2];
252+
const timeout = setTimeout(() => process.exit(1), 10_000);
253+
const watcher = setInterval(() => {
254+
if (!existsSync(release)) return;
255+
clearInterval(watcher);
256+
clearTimeout(timeout);
257+
process.exit(0);
258+
}, 25);
259+
writeFileSync(ready, String(process.pid));
260+
`;
244261
await writeFile(
245262
script,
246-
'process.stdout.write("ready\\n", () => process.exit(0));\n',
263+
`
264+
import { spawn } from "node:child_process";
265+
import { existsSync } from "node:fs";
266+
267+
const grandchild = spawn(
268+
process.execPath,
269+
["-e", ${JSON.stringify(grandchildScript)}, ${JSON.stringify(ready)}, ${JSON.stringify(release)}],
270+
{
271+
stdio: ["ignore", "inherit", "inherit"],
272+
windowsHide: true,
273+
detached: true,
274+
},
275+
);
276+
const readyTimeout = setTimeout(() => {
277+
clearInterval(readyWatcher);
278+
grandchild.kill();
279+
console.error("Timed out waiting for the Windows login grandchild.");
280+
process.exit(1);
281+
}, 10_000);
282+
const readyWatcher = setInterval(() => {
283+
if (!existsSync(${JSON.stringify(ready)})) return;
284+
clearInterval(readyWatcher);
285+
clearTimeout(readyTimeout);
286+
process.stdout.write("ready\\n", (error) => process.exit(error ? 1 : 0));
287+
}, 25);
288+
grandchild.once("error", (error) => {
289+
clearInterval(readyWatcher);
290+
clearTimeout(readyTimeout);
291+
console.error(error.message);
292+
process.exit(1);
293+
});
294+
`,
247295
);
248296

249297
const originalOnce = ChildProcess.prototype.once;
250298
let loginChild: ChildProcess | undefined;
251-
let releaseClose: (() => void) | undefined;
252299
const processObserver = spyOn(ChildProcess.prototype, "once");
253300
processObserver.mockImplementation(function (
254301
this: ChildProcess,
255302
event: string,
256303
listener: (...eventArguments: never[]) => void,
257304
) {
258305
if (event === "exit") loginChild = this;
259-
if (event === "close") {
260-
return Reflect.apply(originalOnce, this, [
261-
event,
262-
(...eventArguments: unknown[]) => {
263-
releaseClose = () =>
264-
Reflect.apply(listener, this, eventArguments);
265-
},
266-
]);
267-
}
268306
return Reflect.apply(originalOnce, this, [event, listener]);
269307
});
270308

271309
let handle: CodexLoginHandle;
272310
try {
273311
handle = new CodexLoginHandle(
274-
{ command: "node", prefixArgs: [script] },
312+
{ command: process.execPath, prefixArgs: [script] },
275313
["login"],
276314
process.env,
277315
() => {},
@@ -280,8 +318,32 @@ grandchild.once("error", (error) => {
280318
processObserver.mockRestore();
281319
}
282320

321+
const readMarker = async (path: string): Promise<string> => {
322+
const deadline = Date.now() + 5_000;
323+
while (true) {
324+
try {
325+
return await readFile(path, "utf8");
326+
} catch (error) {
327+
if (
328+
!(error instanceof Error) ||
329+
!("code" in error) ||
330+
error.code !== "ENOENT" ||
331+
Date.now() >= deadline
332+
) {
333+
throw error;
334+
}
335+
await delay(25);
336+
}
337+
}
338+
};
339+
340+
let grandchildPid: number | undefined;
283341
try {
284-
const startedAt = Date.now();
342+
const readyMarker = await readMarker(ready);
343+
expect(readyMarker).toMatch(/^\d+$/u);
344+
grandchildPid = Number(readyMarker);
345+
expect(Number.isSafeInteger(grandchildPid)).toBe(true);
346+
expect(grandchildPid).toBeGreaterThan(0);
285347
const timeout = AbortSignal.timeout(5_000);
286348
const completion = Promise.race([
287349
handle.wait(),
@@ -297,12 +359,33 @@ grandchild.once("error", (error) => {
297359
success: true,
298360
exitCode: 0,
299361
});
300-
expect(Date.now() - startedAt).toBeGreaterThanOrEqual(900);
301-
expect(releaseClose).toBeFunction();
302362
expect(loginChild?.stdout?.destroyed).toBe(true);
303363
expect(loginChild?.stderr?.destroyed).toBe(true);
304364
} finally {
305-
releaseClose?.();
365+
await writeFile(release, "released");
366+
if (grandchildPid !== undefined) {
367+
const deadline = Date.now() + 5_000;
368+
while (true) {
369+
try {
370+
process.kill(grandchildPid, 0);
371+
} catch (error) {
372+
if (
373+
error instanceof Error &&
374+
"code" in error &&
375+
error.code === "ESRCH"
376+
) {
377+
break;
378+
}
379+
throw error;
380+
}
381+
if (Date.now() >= deadline) {
382+
throw new Error(
383+
"The Windows login grandchild did not exit after pipe cleanup.",
384+
);
385+
}
386+
await delay(25);
387+
}
388+
}
306389
}
307390
},
308391
);

sdk/typescript/tests-ts/runtime.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { existsSync, renameSync, symlinkSync } from "node:fs";
22
import {
33
chmod,
4+
copyFile,
45
lstat,
56
mkdir,
67
mkdtemp,
@@ -169,6 +170,151 @@ describe("plugin runtime preparation", () => {
169170
).toBeDefined();
170171
});
171172

173+
testPosix(
174+
"preserves literal POSIX candidate paths in the bundled plugin",
175+
async () => {
176+
const root = await temporaryDirectory();
177+
await mkdir(join(root, "source"));
178+
const cases = [
179+
{ path: "source\\candidate.py", contents: "literal candidate\n" },
180+
{ path: " leading.py", contents: "leading whitespace\n" },
181+
{ path: "trailing.py ", contents: "trailing whitespace\n" },
182+
{ path: " ", contents: "single whitespace filename\n" },
183+
{ path: " ", contents: "multiple whitespace filename\n" },
184+
{ path: "C:candidate.py", contents: "literal colon\n" },
185+
{ path: "carriage\rreturn.py", contents: "literal carriage return\n" },
186+
{ path: "vertical\vtab.py", contents: "literal vertical tab\n" },
187+
{ path: "form\ffeed.py", contents: "literal form feed\n" },
188+
{ path: "next\u0085line.py", contents: "literal next line\n" },
189+
{
190+
path: "unicode\u2028separator.py",
191+
contents: "literal line separator\n",
192+
},
193+
{
194+
path: "paragraph\u2029separator.py",
195+
contents: "literal paragraph separator\n",
196+
},
197+
];
198+
await Promise.all([
199+
...cases.map((item) => writeFile(join(root, item.path), item.contents)),
200+
writeFile(join(root, "source", "candidate.py"), "wrong candidate\n"),
201+
writeFile(join(root, "leading.py"), "wrong leading candidate\n"),
202+
writeFile(join(root, "trailing.py"), "wrong trailing candidate\n"),
203+
]);
204+
const scopePath = join(root, "in-scope-files.txt");
205+
await writeFile(
206+
scopePath,
207+
`${cases.map((item) => item.path).join("\n")}\n`,
208+
);
209+
210+
const python = Bun.which("python3") ?? Bun.which("python");
211+
expect(python).not.toBeNull();
212+
const sourcePlugin = await bundledPluginRoot();
213+
const projector = new URL(
214+
"../scripts/project-plugin.mjs",
215+
import.meta.url,
216+
);
217+
const publicManifest = new URL(
218+
"../public-repo/sdk/typescript/plugin.public.json",
219+
import.meta.url,
220+
);
221+
let bundledPlugin = sourcePlugin;
222+
if (existsSync(projector) && existsSync(publicManifest)) {
223+
const packageRoot = join(root, "package");
224+
const isolatedProjector = join(
225+
packageRoot,
226+
"scripts",
227+
"project-plugin.mjs",
228+
);
229+
const isolatedManifest = join(
230+
packageRoot,
231+
"public-repo",
232+
"sdk",
233+
"typescript",
234+
"plugin.public.json",
235+
);
236+
await Promise.all([
237+
mkdir(dirname(isolatedProjector), { recursive: true }),
238+
mkdir(dirname(isolatedManifest), { recursive: true }),
239+
]);
240+
await Promise.all([
241+
copyFile(projector, isolatedProjector),
242+
copyFile(publicManifest, isolatedManifest),
243+
]);
244+
const projection = Bun.spawnSync(
245+
[process.execPath, isolatedProjector],
246+
{
247+
cwd: packageRoot,
248+
env: {
249+
...process.env,
250+
CODEX_SECURITY_PLUGIN_ROOT: sourcePlugin,
251+
},
252+
stdout: "pipe",
253+
stderr: "pipe",
254+
},
255+
);
256+
expect(new TextDecoder().decode(projection.stderr)).toBe("");
257+
expect(projection.exitCode).toBe(0);
258+
bundledPlugin = join(packageRoot, "_bundled_plugin");
259+
}
260+
const normalizer = join(
261+
bundledPlugin,
262+
"scripts",
263+
"normalize_candidates.py",
264+
);
265+
expect(await readFile(normalizer, "utf8")).toBe(
266+
await readFile(
267+
join(sourcePlugin, "scripts", "normalize_candidates.py"),
268+
"utf8",
269+
),
270+
);
271+
const result = Bun.spawnSync([
272+
python!,
273+
"-I",
274+
"-B",
275+
"-c",
276+
[
277+
"import json, pathlib, runpy, sys",
278+
"module = runpy.run_path(sys.argv[1])",
279+
"root = pathlib.Path(sys.argv[2])",
280+
"scope = module['read_scope'](pathlib.Path(sys.argv[3]), root)",
281+
"finalizer = runpy.run_path(sys.argv[5])",
282+
"results = []",
283+
"for value in json.loads(sys.argv[4]):",
284+
" path, source = module['relative_file'](value, root)",
285+
" candidate = {'cwe_ids': ['CWE-89'], 'locations': [{'path': value, 'start_line': 1, 'role': 'entrypoint'}], 'summary': 'Test finding', 'evidence': 'Test evidence'}",
286+
" try:",
287+
" normalized = module['normalize_candidate'](candidate, root, scope, {})",
288+
" location = normalized['locations'][0]",
289+
" finalizer['_validate_location']({'path': location['path'], 'startLine': location['start_line'], 'endLine': location['end_line'], 'role': location['role']}, 'candidate.locations[0]')",
290+
" except ValueError:",
291+
" contract_valid = False",
292+
" else:",
293+
" contract_valid = True",
294+
" results.append({'path': path, 'contents': source.read_text(encoding='utf-8'), 'inScope': path in scope, 'contractValid': contract_valid})",
295+
"print(json.dumps(results))",
296+
].join("\n"),
297+
normalizer,
298+
root,
299+
scopePath,
300+
JSON.stringify(cases.map((item) => item.path)),
301+
join(bundledPlugin, "scripts", "finalize_scan_contract.py"),
302+
]);
303+
304+
expect(result.exitCode).toBe(0);
305+
expect(JSON.parse(new TextDecoder().decode(result.stdout))).toEqual(
306+
cases.map((item) => ({
307+
...item,
308+
inScope: true,
309+
contractValid:
310+
item.path.trim().length > 0 &&
311+
!item.path.includes("\\") &&
312+
!item.path.includes(":"),
313+
})),
314+
);
315+
},
316+
);
317+
172318
test("uses a configured plugin directory directly", async () => {
173319
const root = await temporaryDirectory();
174320
const ambientHome = join(root, ".codex", "plugins", "cache");

0 commit comments

Comments
 (0)