-
Notifications
You must be signed in to change notification settings - Fork 0
[WRONG BRANCH] security: serialize codex prompt probe, honor request cancellation, and bound probe output #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,7 +100,7 @@ function resolveCodexBinary(): string | null { | |
| /** 8 MiB is far above any real prompt and far below anything that hurts the server. */ | ||
| const MAX_PROBE_OUTPUT_BYTES = 8 * 1024 * 1024; | ||
|
|
||
| function runProbe(binary: string, cwd: string, timeoutMs: number): Promise<string | null> { | ||
| function runProbe(binary: string, cwd: string, timeoutMs: number, signal?: AbortSignal): Promise<string | null> { | ||
| return new Promise(resolve => { | ||
| // A probe must never hang OR balloon the management API: it is bounded in | ||
| // time AND in bytes, and every failure degrades to "unavailable" rather than | ||
|
|
@@ -118,11 +118,15 @@ function runProbe(binary: string, cwd: string, timeoutMs: number): Promise<strin | |
| if (settled) return; | ||
| settled = true; | ||
| clearTimeout(timer); | ||
| signal?.removeEventListener("abort", abort); | ||
| child.stdout?.destroy(); | ||
| if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); | ||
| resolve(value); | ||
| }; | ||
| const abort = () => settle(null); | ||
| const timer = setTimeout(() => settle(null), timeoutMs); | ||
| signal?.addEventListener("abort", abort, { once: true }); | ||
| if (signal?.aborted) abort(); | ||
|
Comment on lines
+126
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: printf '%s\n' '--- applicable repository guidance ---'
head -5 /tmp/coderabbit-repo-knowledge/luvs01-opencodex-c5353b3c/*/*.md 2>/dev/null
printf '%s\n' '--- target file outline ---'
ast-grep outline src/codex/prompt-text-probe.ts
printf '%s\n' '--- target implementation ---'
sed -n '80,145p' src/codex/prompt-text-probe.tsRepository: luvs01/opencodex Length of output: 5140 Avoid spawning after request cancellation. When 🤖 Prompt for AI Agents |
||
| child.stdout?.on("data", (chunk: Buffer) => { | ||
| size += chunk.length; | ||
| if (size > MAX_PROBE_OUTPUT_BYTES) { settle(null); return; } | ||
|
|
@@ -182,7 +186,9 @@ export const extractSectionsForTests = extractSections; | |
| * `cwd` matters: AGENTS.md and environment context are directory-dependent, so a | ||
| * probe from the wrong place would describe a prompt the user never sees. | ||
| */ | ||
| export async function probePromptText(timeoutMs = 15_000): Promise<PromptTextProbe> { | ||
| let probeActive = false; | ||
|
|
||
| export async function probePromptText(timeoutMs = 15_000, signal?: AbortSignal): Promise<PromptTextProbe> { | ||
| // The probe runs in CODEX_HOME, never in a caller-supplied directory. A `cwd` | ||
| // parameter let an authenticated request read any readable folder's AGENTS.md, | ||
| // and it also described a prompt that depends on where Codex happened to run. | ||
|
|
@@ -192,7 +198,21 @@ export async function probePromptText(timeoutMs = 15_000): Promise<PromptTextPro | |
| if (!binary) { | ||
| return { ok: false, codexHome, layers: {}, detail: "codex binary not found" }; | ||
| } | ||
| const raw = await runProbe(binary, codexHome, timeoutMs); | ||
| // The management endpoint is authenticated but may still be called in a | ||
| // burst. Admit only one child process for the whole server instead of letting | ||
| // parallel requests multiply Codex startups and their buffered output. | ||
| if (probeActive) { | ||
| return { ok: false, codexHome, layers: {}, detail: "prompt probe already in progress" }; | ||
|
Comment on lines
+204
to
+205
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the dashboard runs under its checked-in AGENTS.md reference: AGENTS.md:L284-L286 Useful? React with 👍 / 👎. |
||
| } | ||
| probeActive = true; | ||
| let raw: string | null; | ||
| try { | ||
| raw = await runProbe(binary, codexHome, timeoutMs, signal); | ||
| } catch { | ||
| raw = null; | ||
| } finally { | ||
| probeActive = false; | ||
| } | ||
| if (raw === null) { | ||
| return { ok: false, codexHome, layers: {}, detail: "codex debug prompt-input failed" }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -799,6 +799,18 @@ describe("020 coverage completions", () => { | |
| // Decoding per chunk corrupts UTF-8 that straddles a chunk boundary. | ||
| expect(probe).toContain("Buffer.concat(chunks).toString(\"utf8\")"); | ||
| }); | ||
|
|
||
| test("27. the probe bounds aggregate subprocesses and follows request cancellation", async () => { | ||
| const probe = await Bun.file(new URL("../src/codex/prompt-text-probe.ts", import.meta.url)).text(); | ||
| expect(probe).toContain("if (probeActive)"); | ||
| expect(probe).toContain("probeActive = true"); | ||
| expect(probe).toContain("probeActive = false"); | ||
| expect(probe).toContain('signal?.addEventListener("abort", abort'); | ||
|
|
||
| const routes = await Bun.file(new URL("../src/server/management/codex-prompt-routes.ts", import.meta.url)).text(); | ||
| const textRoute = routes.slice(routes.indexOf('/api/codex-prompt/text')); | ||
| expect(textRoute.slice(0, 1_200)).toContain("req.signal"); | ||
| }); | ||
|
Comment on lines
+803
to
+813
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift Test cancellation and serialization at runtime. This test only searches source text. It can pass when the required strings appear in comments or unrelated code. It does not prove that a concurrent probe is rejected, an aborted child is terminated, stdout is closed, or the route passes the signal to the actual probe call. Add a focused Bun test with a controllable child-process seam and an As per path instructions, “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.” 🤖 Prompt for AI AgentsSource: Path instructions |
||
| test("24. every ownership state is named, not collapsed into a boolean", async () => { | ||
| // developerInstructionsOwned:false covers an ABSENT key and an EXTERNAL one, and | ||
| // a GUI that cannot tell them apart hides its own create affordance from every | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: luvs01/opencodex
Length of output: 196
🏁 Script executed:
Repository: luvs01/opencodex
Length of output: 14692
Hold
probeActiveuntil the child closes.runProberesolves immediately aftersettlesendsSIGKILLatsrc/codex/prompt-text-probe.ts:123-124. Thefinallyblock then clearsprobeActiveat lines 213-215, while theclosehandler may still be pending. A timeout, abort, output overflow, or child error can therefore admit another probe while the previous child is still terminating. Resolve only afterclose, or keep the admission gate held through a termination-cleanup promise.🤖 Prompt for AI Agents