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/agents/codex-rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Forwarding rules:
- If the user asks for `spark`, map that to `--model gpt-5.3-codex-spark`.
- If the user asks for a concrete model name such as `gpt-5.4-mini`, pass it through with `--model`.
- Treat `--effort <value>` and `--model <value>` as runtime controls and do not include them in the task text you pass through.
- Default to a write-capable Codex run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits.
- Default to a write-capable Codex run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits; in those cases, add `--read-only` instead.
- Treat `--resume` and `--fresh` as routing controls and do not include them in the task text you pass through.
- `--resume` means add `--resume-last`.
- `--fresh` means do not add `--resume-last`.
Expand Down
4 changes: 2 additions & 2 deletions plugins/codex/commands/rescue.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Delegate investigation, an explicit fix request, or follow-up rescue work to the Codex rescue subagent
argument-hint: "[--background|--wait] [--cwd <dir>|-C <dir>] [--resume|--fresh] [--model <model|spark|sol|terra|luna>] [--effort <none|minimal|low|medium|high|xhigh|max|ultra>] [what Codex should investigate, solve, or continue]"
argument-hint: "[--background|--wait] [--read-only] [--cwd <dir>|-C <dir>] [--resume|--fresh] [--model <model|spark|sol|terra|luna>] [--effort <none|minimal|low|medium|high|xhigh|max|ultra>] [what Codex should investigate, solve, or continue]"
allowed-tools: Bash(node:*), AskUserQuestion, Agent
---

Expand All @@ -17,7 +17,7 @@ Execution mode:
- If the request includes `--wait`, run the `codex:codex-rescue` subagent in the foreground.
- If neither flag is present, default to `--background`. Rescue tasks are open-ended and routinely exceed Claude's Bash-tool timeout; starting in background avoids the auto-background trap (see Operating rules).
- `--background` and `--wait` are execution flags for Claude Code. Do not forward them to `task`, and do not treat them as part of the natural-language task text.
- `--model` and `--effort` are runtime-selection flags. Preserve them for the forwarded `task` call, but do not treat them as part of the natural-language task text.
- `--model` and `--effort` are runtime-selection flags. Preserve them and `--read-only` for the forwarded `task` call, but do not treat them as part of the natural-language task text.
- `--cwd <dir>` and `-C <dir>` are workspace-routing flags. Preserve the directory for the resume preflight and the forwarded `task` call, but do not treat either form as part of the natural-language task text.
- If the request includes `--resume`, do not ask whether to continue. The user already chose.
- If the request includes `--fresh`, do not ask whether to continue. The user already chose.
Expand Down
15 changes: 11 additions & 4 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function printUsage() {
" node scripts/codex-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]",
" node scripts/codex-companion.mjs review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [--model <model|spark|sol|terra|luna>] [--effort <none|minimal|low|medium|high|xhigh|max|ultra>]",
" node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [--model <model|spark|sol|terra|luna>] [--effort <none|minimal|low|medium|high|xhigh|max|ultra>] [focus text]",
" node scripts/codex-companion.mjs task [--background] [--write] [--cwd <dir>] [--resume-last|--resume|--fresh] [--model <model|spark|sol|terra|luna>] [--effort <none|minimal|low|medium|high|xhigh|max|ultra>] [prompt]",
" node scripts/codex-companion.mjs task [--background] [--write] [--read-only] [--cwd <dir>] [--resume-last|--resume|--fresh] [--model <model|spark|sol|terra|luna>] [--effort <none|minimal|low|medium|high|xhigh|max|ultra>] [prompt]",
" node scripts/codex-companion.mjs transfer [--source <claude-jsonl>] [--json]",
" node scripts/codex-companion.mjs status [job-id] [--all] [--json]",
" node scripts/codex-companion.mjs result [job-id] [--json]",
Expand Down Expand Up @@ -544,7 +544,7 @@ async function executeTaskRun(request) {
defaultPrompt: resumeThreadId ? DEFAULT_CONTINUE_PROMPT : "",
model: request.model,
effort: request.effort,
sandbox: request.write ? "workspace-write" : "read-only",
sandbox: request.write ? "workspace-write" : request.readOnly ? "read-only" : null,
onProgress: request.onProgress,
persistThread: true,
turnTimeoutMs: request.turnTimeoutMs,
Expand Down Expand Up @@ -659,13 +659,14 @@ function buildTaskJob(workspaceRoot, taskMetadata, write) {
});
}

function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, jobId, turnTimeoutMs }) {
function buildTaskRequest({ cwd, model, effort, prompt, write, readOnly, resumeLast, jobId, turnTimeoutMs }) {
return {
cwd,
model,
effort,
prompt,
write,
readOnly,
resumeLast,
jobId,
turnTimeoutMs
Expand Down Expand Up @@ -843,7 +844,7 @@ async function handleReview(argv) {
async function handleTask(argv) {
const { options, positionals } = parseCommandInput(argv, {
valueOptions: ["model", "effort", "cwd", "prompt-file", "turn-timeout-ms"],
booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"],
booleanOptions: ["json", "write", "read-only", "resume-last", "resume", "fresh", "background"],
aliasMap: {
m: "model"
}
Expand All @@ -862,6 +863,10 @@ async function handleTask(argv) {
throw new Error("Choose either --resume/--resume-last or --fresh.");
}
const write = Boolean(options.write);
const readOnly = Boolean(options["read-only"]);
if (write && readOnly) {
throw new Error("Choose either --write or --read-only.");
}
const taskMetadata = buildTaskRunMetadata({
prompt,
resumeLast
Expand All @@ -878,6 +883,7 @@ async function handleTask(argv) {
effort,
prompt,
write,
readOnly,
resumeLast,
jobId: job.id,
turnTimeoutMs: resolveTurnTimeoutMsFromOptions(options)
Expand All @@ -897,6 +903,7 @@ async function handleTask(argv) {
effort,
prompt,
write,
readOnly,
resumeLast,
jobId: job.id,
turnTimeoutMs: resolveTurnTimeoutMsFromOptions(options),
Expand Down
40 changes: 38 additions & 2 deletions plugins/codex/scripts/lib/codex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function buildThreadParams(cwd, options = {}) {
cwd,
model: options.model ?? null,
approvalPolicy: options.approvalPolicy ?? "never",
sandbox: options.sandbox ?? "read-only",
sandbox: options.sandbox ?? null,
config: options.effort ? { model_reasoning_effort: options.effort } : null,
serviceName: SERVICE_NAME,
ephemeral: options.ephemeral ?? true
Expand All @@ -102,7 +102,7 @@ function buildResumeParams(threadId, cwd, options = {}) {
cwd,
model: options.model ?? null,
approvalPolicy: options.approvalPolicy ?? "never",
sandbox: options.sandbox ?? "read-only"
sandbox: options.sandbox ?? null
};
}

Expand All @@ -111,6 +111,37 @@ function buildTurnInput(prompt) {
return [{ type: "text", text: prompt, text_elements: [] }];
}

// Map a requested sandbox shorthand ("read-only"/"workspace-write") to the
// app-server's sandbox-type vocabulary ("readOnly"/"workspaceWrite").
function requestedSandboxType(requested) {
if (requested === "read-only") {
return "readOnly";
}
if (requested === "workspace-write") {
return "workspaceWrite";
}
return null;
}

// Fail closed when an explicit read-only pin was requested but the app-server
// resolved a write-capable sandbox. Codex 0.144.x keeps a loaded thread's
// sandbox and ignores thread/resume overrides, so without this check
// --read-only --resume-last could silently run with write access. (The write
// pin is not enforced here: a write request on a read-only resolved sandbox is
// a downgrade, not a safety leak.)
function assertExplicitSandboxHonored(requested, resolved) {
if (requestedSandboxType(requested) !== "readOnly") {
return;
}
const resolvedType = resolved?.type ?? null;
if (resolvedType === "readOnly") {
return;
}
throw new Error(
`Requested a read-only sandbox, but the app-server resolved ${resolvedType ?? "an unknown sandbox"} — refusing to run with a sandbox that does not match the explicit read-only pin. Start a fresh thread or drop --read-only.`
);
}

function shorten(text, limit = 72) {
const normalized = String(text ?? "").trim().replace(/\s+/g, " ");
if (!normalized) {
Expand Down Expand Up @@ -1229,6 +1260,11 @@ export async function runAppServerTurn(cwd, options = {}) {
reasoningEffort: response.reasoningEffort,
sandbox: response.sandbox
};
// Fail closed when an explicit sandbox pin is not honored. Codex app-server
// (0.144.x) keeps a loaded thread's sandbox and ignores thread/resume
// overrides, so --read-only --resume-last on a write-capable thread would
// otherwise run with write access — violating the --read-only contract.
assertExplicitSandboxHonored(options.sandbox, resolved.sandbox);
await validateReasoningSelection(client, {
model: options.model ?? threadSelection.model,
effort: options.effort ?? threadSelection.reasoningEffort,
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/scripts/stop-review-gate-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function runStopReview(cwd, input = {}) {
...process.env,
...(input.session_id ? { [SESSION_ID_ENV]: input.session_id } : {})
};
const result = spawnSync(process.execPath, [scriptPath, "task", "--json", prompt], {
const result = spawnSync(process.execPath, [scriptPath, "task", "--json", "--read-only", prompt], {
cwd,
env: childEnv,
encoding: "utf8",
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/skills/codex-cli-runtime/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Execution rules:
- Leave `--effort` unset unless the user explicitly requests a specific effort.
- Leave model unset by default. Add `--model` only when the user explicitly asks for one.
- Map `spark` to `--model gpt-5.3-codex-spark`.
- Default to a write-capable Codex run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits.
- Default to a write-capable Codex run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits; in those cases, add `--read-only` instead.

Command selection:
- Use exactly one `task` invocation per rescue handoff.
Expand Down
8 changes: 7 additions & 1 deletion tests/fake-codex-fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,13 @@ rl.on("line", (line) => {
sandbox: message.params.sandbox ?? null
};
saveState(state);
send({ id: message.id, result: { thread: buildThread(thread), model: selectedModel, modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: selectedEffort } });
// resume-ignores-sandbox: simulate a real Codex app-server that keeps a
// loaded thread's sandbox and ignores the thread/resume override — so an
// explicit --read-only pin on a write-capable thread is silently lost.
const resumeSandbox = BEHAVIOR === "resume-ignores-sandbox"
? { type: "workspaceWrite", writableRoots: [], networkAccess: false }
: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false };
send({ id: message.id, result: { thread: buildThread(thread), model: selectedModel, modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: resumeSandbox, reasoningEffort: selectedEffort } });
break;
}

Expand Down
Loading