From cafea9dc51b8031a50d8be567ceabf957f777dd1 Mon Sep 17 00:00:00 2001 From: Behruz Nassre Esfahani Date: Mon, 20 Jul 2026 18:14:44 -0700 Subject: [PATCH] fix: escalate Claude companion network calls Require targeted sandbox escalation when Codex launches the Claude companion so API DNS access is available without enabling network access globally. Add contract and end-to-end coverage for rescue, review, and adversarial review paths.\n\nRefs #81 --- internal-skills/cli-runtime/runtime.md | 1 + internal-skills/review-runtime/runtime.md | 2 ++ skills/adversarial-review/SKILL.md | 2 ++ skills/rescue/SKILL.md | 1 + skills/review/SKILL.md | 2 ++ tests/e2e/codex-skills-e2e.test.mjs | 15 +++++++++++++++ tests/skills-contracts.test.mjs | 15 +++++++++++++++ 7 files changed, 38 insertions(+) diff --git a/internal-skills/cli-runtime/runtime.md b/internal-skills/cli-runtime/runtime.md index ccd205a..d797090 100644 --- a/internal-skills/cli-runtime/runtime.md +++ b/internal-skills/cli-runtime/runtime.md @@ -49,6 +49,7 @@ Task defaults: - Run the companion command as one blocking foreground shell-tool call, not as a background terminal/session. - Do not request a shell session id, poll a shell session later, or return before the companion command exits. - If the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call. +- When using `exec_command`, pass `sandbox_permissions: "require_escalated"` and the justification `Allow the Claude Code companion to contact the Claude API for this requested task.` on that one call. Do not first try the companion command in the default network-disabled sandbox. - If the parent supplied a non-empty parent thread id for background completion, allow at most one success-only `send_input` notification before finishing. - Mention the tool name `send_input` literally when describing that notification path. - Use the exact tool shape `send_input({ target: , message: })`. diff --git a/internal-skills/review-runtime/runtime.md b/internal-skills/review-runtime/runtime.md index 368a284..7bed39c 100644 --- a/internal-skills/review-runtime/runtime.md +++ b/internal-skills/review-runtime/runtime.md @@ -19,6 +19,7 @@ Foreground contract: - Foreground command: - `review --view-state on-success ...` - `adversarial-review --view-state on-success ...` +- Run the companion command with `sandbox_permissions: "require_escalated"` and the justification `Allow the Claude Code companion to contact the Claude API for this requested review.` Do not first try the companion command in the default network-disabled sandbox. - Return companion stdout faithfully and do not add review execution commentary around it. Background contract: @@ -38,6 +39,7 @@ Background contract: - run the companion command as one blocking foreground shell-tool call, not as a background terminal/session - do not request a shell session id, poll a shell session later, or return before the companion command exits - if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call + - when using `exec_command`, pass `sandbox_permissions: "require_escalated"` and the justification `Allow the Claude Code companion to contact the Claude API for this requested review.` on that one call; do not first try the companion command in the default network-disabled sandbox - use at most one `send_input` completion notification on success - mention the tool name `send_input` literally in the child instructions - use the exact tool shape `send_input({ target: , message: })` diff --git a/skills/adversarial-review/SKILL.md b/skills/adversarial-review/SKILL.md index eb16dd0..618df79 100644 --- a/skills/adversarial-review/SKILL.md +++ b/skills/adversarial-review/SKILL.md @@ -57,6 +57,7 @@ Argument handling: Foreground flow: - Run: `node "/scripts/claude-companion.mjs" adversarial-review --view-state on-success ` +- Run that companion command with `sandbox_permissions: "require_escalated"` and the justification `Allow the Claude Code companion to contact the Claude API for this requested review.` Do not first try the companion command in the default network-disabled sandbox. - Foreground adversarial review belongs to the main Codex thread. Do not spawn a review subagent, do not invoke a generic review-runner role, and do not proxy this foreground path through any background worker abstraction. - Do not fall back to raw `claude`, `claude-code`, `claude review`, `bash -lc ...claude...`, or any other direct Claude CLI syntax when the companion path is available. The foreground syntax contract here is the resolved companion command above, not a hand-rolled Claude invocation. - If the resolved companion command fails, surface that failure. Do not silently retry foreground adversarial review through a different CLI shape, a generic review runner, or a custom shell wrapper. @@ -94,6 +95,7 @@ Background flow: - run that command as one blocking foreground shell-tool call, not as a background terminal/session - do not request a shell session id, poll a shell session later, or return before the companion command exits - if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call + - when using `exec_command`, pass `sandbox_permissions: "require_escalated"` and the justification `Allow the Claude Code companion to contact the Claude API for this requested review.` on that one call; do not first try the companion command in the default network-disabled sandbox - include `--owner-session-id ` only when the parent resolved a non-empty owner session id - include `--job-id ` when the parent reserved one - include the matching `--cwd ` whenever the command includes that reserved `--job-id` diff --git a/skills/rescue/SKILL.md b/skills/rescue/SKILL.md index b446f4f..1be434c 100644 --- a/skills/rescue/SKILL.md +++ b/skills/rescue/SKILL.md @@ -124,6 +124,7 @@ Subagent launch: - run that command as one blocking foreground shell-tool call, not as a background terminal/session - do not request a shell session id, poll a shell session later, or return before the companion command exits - if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call + - when using `exec_command`, pass `sandbox_permissions: "require_escalated"` and the justification `Allow the Claude Code companion to contact the Claude API for this requested task.` on that one call; do not first try the companion command in the default network-disabled sandbox - for foreground rescue only, tell the child to return that command's stdout text exactly, with no preamble, summary, code fence, trimming, normalization, or punctuation changes - tell the child to ignore stderr progress chatter such as `[cc] ...` lines and preserve only the stdout-equivalent final result text - if a parent thread id is provided for experimental background notification, allow one extra `send_input` call after a successful shell result and before finishing diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index fa0ee17..dea804f 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -55,6 +55,7 @@ Argument handling: Foreground flow: - Run: `node "/scripts/claude-companion.mjs" review --view-state on-success ` +- Run that companion command with `sandbox_permissions: "require_escalated"` and the justification `Allow the Claude Code companion to contact the Claude API for this requested review.` Do not first try the companion command in the default network-disabled sandbox. - Foreground review belongs to the main Codex thread. Do not spawn a review subagent, do not invoke a generic review-runner role, and do not proxy this foreground path through any background worker abstraction. - Do not fall back to raw `claude`, `claude-code`, `claude review`, `bash -lc ...claude...`, or any other direct Claude CLI syntax when the companion path is available. The foreground syntax contract here is the resolved companion command above, not a hand-rolled Claude invocation. - If the resolved companion command fails, surface that failure. Do not silently retry foreground review through a different CLI shape, a generic review runner, or a custom shell wrapper. @@ -92,6 +93,7 @@ Background flow: - run that command as one blocking foreground shell-tool call, not as a background terminal/session - do not request a shell session id, poll a shell session later, or return before the companion command exits - if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call + - when using `exec_command`, pass `sandbox_permissions: "require_escalated"` and the justification `Allow the Claude Code companion to contact the Claude API for this requested review.` on that one call; do not first try the companion command in the default network-disabled sandbox - include `--owner-session-id ` only when the parent resolved a non-empty owner session id - include `--job-id ` when the parent reserved one - include the matching `--cwd ` whenever the command includes that reserved `--job-id` diff --git a/tests/e2e/codex-skills-e2e.test.mjs b/tests/e2e/codex-skills-e2e.test.mjs index b2da566..b866e6e 100644 --- a/tests/e2e/codex-skills-e2e.test.mjs +++ b/tests/e2e/codex-skills-e2e.test.mjs @@ -934,6 +934,7 @@ function startMockProvider({ "Run that command as one blocking foreground shell-tool call, not as a background terminal or session.\n" + "Do not request a shell session id, poll a shell session later, or return before the command exits.\n" + "If the shell tool is exec_command, call it once in non-interactive mode and wait for exit in that same call.\n" + + "Use sandbox_permissions: \"require_escalated\" with a justification that allows the Claude Code companion to contact the Claude API. Do not first try the companion command in the default network-disabled sandbox.\n" + "Return only that command's stdout text exactly.\n" + "Ignore stderr progress chatter such as [cc] lines.\n" + "If the tool output includes both stderr progress and a final stdout-style result, preserve only the final stdout-equivalent result text.\n" + @@ -1009,6 +1010,18 @@ function startMockProvider({ "built-in child should be told to preserve the exact task text in the command" ); } + assert.ok( + bodyText.includes("sandbox_permissions") && bodyText.includes("require_escalated"), + "built-in child should request targeted escalation for Claude API access" + ); + assert.ok( + bodyText.includes("contact the Claude API"), + "built-in child should receive a scoped network justification" + ); + assert.ok( + bodyText.includes("Do not first try the companion command in the default network-disabled sandbox."), + "built-in child should avoid the known ENOTFOUND first attempt" + ); assert.doesNotMatch( bodyText, /claude-companion\.mjs"\s+task\s+--background|claude-companion\.mjs"\s+task\s+--wait|claude-companion\.mjs\s+task\s+--background|claude-companion\.mjs\s+task\s+--wait/, @@ -1858,6 +1871,7 @@ describe("Codex direct-skill E2E", () => { "Run that command as one blocking foreground shell-tool call, not as a background terminal or session.\n" + "Do not request a shell session id, poll a shell session later, or return before the command exits.\n" + "If the shell tool is exec_command, call it once in non-interactive mode and wait for exit in that same call.\n" + + "Use sandbox_permissions: \"require_escalated\" with the justification Allow the Claude Code companion to contact the Claude API for this requested review. Do not first try the companion command in the default network-disabled sandbox.\n" + "If the command succeeds and a parent thread id is available, send exactly this notification to the parent thread before finishing: " + JSON.stringify(notificationMessage) + "\n" + "Use that same sentence as your own final assistant message.\n" + @@ -2071,6 +2085,7 @@ describe("Codex direct-skill E2E", () => { "Run that command as one blocking foreground shell-tool call, not as a background terminal or session.\n" + "Do not request a shell session id, poll a shell session later, or return before the command exits.\n" + "If the shell tool is exec_command, call it once in non-interactive mode and wait for exit in that same call.\n" + + "Use sandbox_permissions: \"require_escalated\" with the justification Allow the Claude Code companion to contact the Claude API for this requested review. Do not first try the companion command in the default network-disabled sandbox.\n" + "If the command succeeds and a parent thread id is available, send exactly this notification to the parent thread before finishing: " + JSON.stringify(notificationMessage) + "\n" + "Use that same sentence as your own final assistant message.\n" + diff --git a/tests/skills-contracts.test.mjs b/tests/skills-contracts.test.mjs index 032b4ea..1d83f22 100644 --- a/tests/skills-contracts.test.mjs +++ b/tests/skills-contracts.test.mjs @@ -50,6 +50,9 @@ test("internal runtime references keep the active-root and notification invarian assert.match(reviewRuntime, /blocking foreground shell-tool call, not as a background terminal\/session/i); assert.match(reviewRuntime, /Do not request a shell session id, poll a shell session later, or return before the companion command exits/i); assert.match(reviewRuntime, /if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call/i); + assert.match(reviewRuntime, /`sandbox_permissions: "require_escalated"`/i); + assert.match(reviewRuntime, /contact the Claude API for this requested review/i); + assert.match(reviewRuntime, /Do not first try the companion command in the default network-disabled sandbox/i); assert.match(reviewRuntime, /mention the tool name `send_input` literally/i); assert.match(reviewRuntime, /exact tool shape `send_input\(\{ target: , message: \}\)`/i); assert.match(reviewRuntime, /do not silently drop the completion notification path when the parent provided a non-empty parent thread id/i); @@ -64,6 +67,9 @@ test("internal runtime references keep the active-root and notification invarian assert.match(rescueRuntime, /blocking foreground shell-tool call, not as a background terminal\/session/i); assert.match(rescueRuntime, /Do not request a shell session id, poll a shell session later, or return before the companion command exits/i); assert.match(rescueRuntime, /if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call/i); + assert.match(rescueRuntime, /`sandbox_permissions: "require_escalated"`/i); + assert.match(rescueRuntime, /contact the Claude API for this requested task/i); + assert.match(rescueRuntime, /Do not first try the companion command in the default network-disabled sandbox/i); assert.match(rescueRuntime, /allow at most one success-only `send_input` notification before finishing/i); assert.match(rescueRuntime, /Mention the tool name `send_input` literally/i); assert.match(rescueRuntime, /exact tool shape `send_input\(\{ target: , message: \}\)`/i); @@ -115,6 +121,9 @@ test("review skills keep background execution outside the companion command", () assert.match(review, /blocking foreground shell-tool call, not as a background terminal\/session/i); assert.match(review, /Do not request a shell session id, poll a shell session later, or return before the companion command exits/i); assert.match(review, /if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call/i); + assert.match(review, /`sandbox_permissions: "require_escalated"`/i); + assert.match(review, /contact the Claude API for this requested review/i); + assert.match(review, /do not first try the companion command in the default network-disabled sandbox/i); assert.match(review, /allow one extra `send_input` call after a successful shell result/i); assert.match(review, /must mention the tool name `send_input` literally/i); assert.match(review, /must target the provided parent thread id/i); @@ -169,6 +178,9 @@ test("review skills keep background execution outside the companion command", () assert.match(adversarial, /blocking foreground shell-tool call, not as a background terminal\/session/i); assert.match(adversarial, /Do not request a shell session id, poll a shell session later, or return before the companion command exits/i); assert.match(adversarial, /if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call/i); + assert.match(adversarial, /`sandbox_permissions: "require_escalated"`/i); + assert.match(adversarial, /contact the Claude API for this requested review/i); + assert.match(adversarial, /do not first try the companion command in the default network-disabled sandbox/i); assert.match(adversarial, /allow one extra `send_input` call after a successful shell result/i); assert.match(adversarial, /must mention the tool name `send_input` literally/i); assert.match(adversarial, /must target the provided parent thread id/i); @@ -257,6 +269,9 @@ test("rescue skill documents the experimental built-in-agent forwarding path", ( assert.match(rescue, /blocking foreground shell-tool call, not as a background terminal\/session/i); assert.match(rescue, /Do not request a shell session id, poll a shell session later, or return before the companion command exits/i); assert.match(rescue, /if the available shell tool is `exec_command`, call it once in non-interactive mode and wait for command exit in that same call/i); + assert.match(rescue, /`sandbox_permissions: "require_escalated"`/i); + assert.match(rescue, /contact the Claude API for this requested task/i); + assert.match(rescue, /do not first try the companion command in the default network-disabled sandbox/i); assert.match(rescue, /prefer these steering messages over embedding the raw result text/i); assert.match(rescue, /do not embed the raw Claude result inside the notification message/i); assert.match(rescue, /do not include any other prose in that notification message/i);