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
1 change: 1 addition & 0 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ async function executeTaskRun(request) {
defaultPrompt: resumeThreadId ? DEFAULT_CONTINUE_PROMPT : "",
model: request.model,
effort: request.effort,
approvalPolicy: request.write ? "on-request" : "never",
sandbox: request.write ? "workspace-write" : "read-only",
onProgress: request.onProgress,
persistThread: true,
Expand Down
2 changes: 2 additions & 0 deletions plugins/codex/scripts/lib/codex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,7 @@ export async function runAppServerTurn(cwd, options = {}) {
emitProgress(options.onProgress, `Resuming thread ${options.resumeThreadId}.`, "starting");
const response = await resumeThread(client, options.resumeThreadId, cwd, {
model: options.model,
approvalPolicy: options.approvalPolicy,
sandbox: options.sandbox,
ephemeral: false
});
Expand All @@ -1113,6 +1114,7 @@ export async function runAppServerTurn(cwd, options = {}) {
emitProgress(options.onProgress, "Starting Codex task thread.", "starting");
const response = await startThread(client, cwd, {
model: options.model,
approvalPolicy: options.approvalPolicy,
sandbox: options.sandbox,
ephemeral: options.persistThread ? false : true,
threadName: options.persistThread ? options.threadName : options.threadName ?? null
Expand Down
20 changes: 18 additions & 2 deletions tests/fake-codex-fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,16 @@ rl.on("line", (line) => {
throw new Error("thread/start.persistFullHistory requires experimentalApi capability");
}
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 } });
state.lastThreadStart = {
threadId: thread.id,
cwd: message.params.cwd ?? null,
model: message.params.model ?? null,
approvalPolicy: message.params.approvalPolicy ?? null,
sandbox: message.params.sandbox ?? null,
ephemeral: message.params.ephemeral ?? null
};
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: message.params.approvalPolicy || "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: null } });
send({ method: "thread/started", params: { thread: { id: thread.id } } });
break;
}
Expand Down Expand Up @@ -346,8 +355,15 @@ rl.on("line", (line) => {
}
const thread = ensureThread(state, message.params.threadId);
thread.updatedAt = now();
state.lastThreadResume = {
threadId: message.params.threadId,
cwd: message.params.cwd ?? null,
model: message.params.model ?? null,
approvalPolicy: message.params.approvalPolicy ?? null,
sandbox: message.params.sandbox ?? null
};
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 } });
send({ id: message.id, result: { thread: buildThread(thread), model: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: message.params.approvalPolicy || "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: null } });
break;
}

Expand Down
53 changes: 53 additions & 0 deletions tests/runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ test("session start hook exports the Claude session id, transcript path, and plu
test("write task output focuses on the Codex result without generic follow-up hints", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
const statePath = path.join(binDir, "fake-codex-state.json");
installFakeCodex(binDir);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
Expand All @@ -714,6 +715,58 @@ test("write task output focuses on the Codex result without generic follow-up hi

assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n");
const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8"));
assert.equal(fakeState.lastThreadStart.approvalPolicy, "on-request");
assert.equal(fakeState.lastThreadStart.sandbox, "workspace-write");
});

test("read-only task keeps never approval policy on app-server thread/start", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
const statePath = path.join(binDir, "fake-codex-state.json");
installFakeCodex(binDir);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
run("git", ["add", "README.md"], { cwd: repo });
run("git", ["commit", "-m", "init"], { cwd: repo });

const result = run("node", [SCRIPT, "task", "inspect the failing test"], {
cwd: repo,
env: buildEnv(binDir)
});

assert.equal(result.status, 0, result.stderr);
const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8"));
assert.equal(fakeState.lastThreadStart.approvalPolicy, "never");
assert.equal(fakeState.lastThreadStart.sandbox, "read-only");
});

test("task --resume-last --write forwards write approval policy to app-server thread/resume", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
const statePath = path.join(binDir, "fake-codex-state.json");
installFakeCodex(binDir);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
run("git", ["add", "README.md"], { cwd: repo });
run("git", ["commit", "-m", "init"], { cwd: repo });

const firstRun = run("node", [SCRIPT, "task", "initial task"], {
cwd: repo,
env: buildEnv(binDir)
});
assert.equal(firstRun.status, 0, firstRun.stderr);

const result = run("node", [SCRIPT, "task", "--resume-last", "--write", "follow up"], {
cwd: repo,
env: buildEnv(binDir)
});

assert.equal(result.status, 0, result.stderr);
const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8"));
assert.equal(fakeState.lastThreadResume.threadId, "thr_1");
assert.equal(fakeState.lastThreadResume.approvalPolicy, "on-request");
assert.equal(fakeState.lastThreadResume.sandbox, "workspace-write");
});

test("task --resume acts like --resume-last without leaking the flag into the prompt", () => {
Expand Down