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
2 changes: 1 addition & 1 deletion plugins/codex/agents/codex-rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,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] [--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [what Codex should investigate, solve, or continue]"
argument-hint: "[--background|--wait] [--read-only] [--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [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 foreground.
- `--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.
- 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.
- Otherwise, before starting Codex, check for a resumable rescue thread from this Claude session by running:
Expand Down
20 changes: 14 additions & 6 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,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>]",
" node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base <ref>] [--scope <auto|working-tree|branch>] [focus text]",
" node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
" node scripts/codex-companion.mjs task [--background] [--write] [--read-only] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [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 @@ -488,12 +488,13 @@ 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,
Comment thread
cubicj marked this conversation as resolved.
onProgress: request.onProgress,
persistThread: true,
threadName: resumeThreadId ? null : buildPersistentTaskThreadName(request.prompt || DEFAULT_CONTINUE_PROMPT)
});

const write = Boolean(request.write) || result.sandbox?.type !== "readOnly";
const rawOutput = typeof result.finalMessage === "string" ? result.finalMessage : "";
const failureMessage = result.error?.message ?? result.stderr ?? "";
const rendered = renderTaskResult(
Expand All @@ -505,7 +506,7 @@ async function executeTaskRun(request) {
{
title: taskMetadata.title,
jobId: request.jobId ?? null,
write: Boolean(request.write)
write
}
);
const payload = {
Expand All @@ -525,7 +526,7 @@ async function executeTaskRun(request) {
summary: firstMeaningfulLine(rawOutput, firstMeaningfulLine(failureMessage, `${taskMetadata.title} finished.`)),
jobTitle: taskMetadata.title,
jobClass: "task",
write: Boolean(request.write)
write
};
}

Expand Down Expand Up @@ -601,13 +602,14 @@ function buildTaskJob(workspaceRoot, taskMetadata, write) {
});
}

function buildTaskRequest({ cwd, model, effort, prompt, write, resumeLast, jobId }) {
function buildTaskRequest({ cwd, model, effort, prompt, write, readOnly, resumeLast, jobId }) {
return {
cwd,
model,
effort,
prompt,
write,
readOnly,
resumeLast,
jobId
};
Expand Down Expand Up @@ -762,7 +764,7 @@ async function handleReview(argv) {
async function handleTask(argv) {
const { options, positionals } = parseCommandInput(argv, {
valueOptions: ["model", "effort", "cwd", "prompt-file"],
booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"],
booleanOptions: ["json", "write", "read-only", "resume-last", "resume", "fresh", "background"],
aliasMap: {
m: "model"
}
Expand All @@ -780,6 +782,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 @@ -796,6 +802,7 @@ async function handleTask(argv) {
effort,
prompt,
write,
readOnly,
resumeLast,
jobId: job.id
});
Expand All @@ -814,6 +821,7 @@ async function handleTask(argv) {
effort,
prompt,
write,
readOnly,
resumeLast,
jobId: job.id,
onProgress: progress
Expand Down
24 changes: 17 additions & 7 deletions plugins/codex/scripts/lib/codex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function buildThreadParams(cwd, options = {}) {
cwd,
model: options.model ?? null,
approvalPolicy: options.approvalPolicy ?? "never",
sandbox: options.sandbox ?? "read-only",
sandbox: options.sandbox ?? null,
serviceName: SERVICE_NAME,
ephemeral: options.ephemeral ?? true
};
Expand All @@ -78,10 +78,19 @@ function buildResumeParams(threadId, cwd, options = {}) {
cwd,
model: options.model ?? null,
approvalPolicy: options.approvalPolicy ?? "never",
sandbox: options.sandbox ?? "read-only"
sandbox: options.sandbox ?? null
};
}

function enforceReadOnlySandbox(requestedSandbox, resolvedSandbox) {
if (requestedSandbox !== "read-only" || resolvedSandbox?.type === "readOnly") {
return;
}
throw new Error(
"A read-only sandbox was requested, but the Codex app-server kept a write-capable sandbox for this thread. Refusing to start the turn. Rerun without --resume-last to start a fresh thread, or drop --read-only."
);
}

/** @returns {UserInput[]} */
function buildTurnInput(prompt) {
return [{ type: "text", text: prompt, text_elements: [] }];
Expand Down Expand Up @@ -1099,27 +1108,27 @@ export async function runAppServerTurn(cwd, options = {}) {
}

return withAppServer(cwd, async (client) => {
let threadId;
let response;

if (options.resumeThreadId) {
emitProgress(options.onProgress, `Resuming thread ${options.resumeThreadId}.`, "starting");
const response = await resumeThread(client, options.resumeThreadId, cwd, {
response = await resumeThread(client, options.resumeThreadId, cwd, {
model: options.model,
sandbox: options.sandbox,
ephemeral: false
});
threadId = response.thread.id;
} else {
emitProgress(options.onProgress, "Starting Codex task thread.", "starting");
const response = await startThread(client, cwd, {
response = await startThread(client, cwd, {
model: options.model,
sandbox: options.sandbox,
ephemeral: options.persistThread ? false : true,
threadName: options.persistThread ? options.threadName : options.threadName ?? null
});
threadId = response.thread.id;
}

enforceReadOnlySandbox(options.sandbox, response.sandbox);
const threadId = response.thread.id;
emitProgress(options.onProgress, `Thread ready (${threadId}).`, "starting", {
threadId
});
Expand All @@ -1146,6 +1155,7 @@ export async function runAppServerTurn(cwd, options = {}) {
return {
status: buildResultStatus(turnState),
threadId,
sandbox: response.sandbox ?? null,
turnId: turnState.turnId,
finalMessage: turnState.lastAgentMessage,
reasoningSummary: turnState.reasoningSummary,
Expand Down
2 changes: 2 additions & 0 deletions plugins/codex/scripts/lib/tracked-jobs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export async function runTrackedJob(job, runner, options = {}) {
status: completionStatus,
threadId: execution.threadId ?? null,
turnId: execution.turnId ?? null,
write: execution.write ?? runningRecord.write,
pid: null,
phase: completionStatus === "completed" ? "done" : "failed",
completedAt,
Expand All @@ -171,6 +172,7 @@ export async function runTrackedJob(job, runner, options = {}) {
status: completionStatus,
threadId: execution.threadId ?? null,
turnId: execution.turnId ?? null,
write: execution.write ?? runningRecord.write,
summary: execution.summary,
phase: completionStatus === "completed" ? "done" : "failed",
pid: null,
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
12 changes: 10 additions & 2 deletions tests/fake-codex-fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,12 @@ rl.on("line", (line) => {
if (requiresExperimental("persistExtendedHistory", message, state) || requiresExperimental("persistFullHistory", message, state)) {
throw new Error("thread/start.persistFullHistory requires experimentalApi capability");
}
state.lastThreadStart = { ...message.params };
const thread = nextThread(state, message.params.cwd, message.params.ephemeral);
send({ id: message.id, result: { thread: buildThread(thread), model: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: null } });
const sandbox = BEHAVIOR === "config-write-sandbox"
? { type: "workspaceWrite", writableRoots: [], networkAccess: false }
: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false };
send({ id: message.id, result: { thread: buildThread(thread), model: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox, reasoningEffort: null } });
send({ method: "thread/started", params: { thread: { id: thread.id } } });
break;
}
Expand Down Expand Up @@ -344,10 +348,14 @@ rl.on("line", (line) => {
if (requiresExperimental("persistExtendedHistory", message, state) || requiresExperimental("persistFullHistory", message, state)) {
throw new Error("thread/resume.persistFullHistory requires experimentalApi capability");
}
state.lastThreadResume = { ...message.params };
const thread = ensureThread(state, message.params.threadId);
thread.updatedAt = now();
saveState(state);
send({ id: message.id, result: { thread: buildThread(thread), model: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: null } });
const sandbox = 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: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox, reasoningEffort: null } });
break;
}

Expand Down
Loading