fix(perms): OPR.0.4.8.2 Codex OFF floor = explicit -s workspace-write (QA blocker qitem-0e536cbc)

The no-flags OFF/no-profile floor is broken on the installed managed runtime: an
interactive Codex launch with required --add-dir writable roots and no -s flag is
rejected ("effective permissions do not allow additional writable roots") and falls
to shell before readiness (dev-qa firsthand, Codex 0.120.0). The clean-home codex
doctor default never exercised the --add-dir writable-roots interaction.

Centralize the fix in codexPostureArg (yolo-mode.ts), applied on ALL managed paths
(fresh / resume / native-fork):
  - OFF, no profile -> EXPLICIT " -s workspace-write" floor flag (was empty)
  - ON (YOLO)       -> " -s danger-full-access" (was --dangerously-bypass-...-sandbox)
  - OFF, named profile -> " -p <profile>" unchanged
Remove all "harness default" floor wording per PM amendment 1c; the floor is the
explicit workspace-write flag. RED-first: codex OFF/ON pins flipped across yolo-mode,
codex-runtime-adapter, codex-resume, native-resume-probe, node-inventory, and
resume-metadata suites. Source-only; no live host config touched.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Opus 4.8 committed 2026-08-03 21:36:05 +00:00
1 parent 0a96a8d5c1
commit dd61b1372b
10 files changed
+62 -51

No files matched your search

@@ -338,7 +338,9 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
return { ok: false, error: "codex fork: forkSource.value is required (parent native_id)" };
}
// OPR.0.4.8.2: the FORK path uses the SAME posture decision (codexPostureArg) — YOLO forces
// full-bypass on every seat; otherwise the named profile (or the harness-default floor).
// -s danger-full-access on every seat; otherwise the named profile, or the explicit
// -s workspace-write floor flag (see codexPostureArg: the no-flags default does NOT grant the
// --add-dir writable roots managed seats pass, so the floor must be the explicit flag).
const cmd = `codex${codexPostureArg(profileArg)} fork${queueStateDirArg} ${shellQuote(parentId)}`;
const textResult = await this.tmux.sendText(binding.tmuxSession, cmd);
if (!textResult.ok) {
@@ -360,9 +362,9 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
}
// OPR.0.4.8.2: one posture decision (codexPostureArg) for the fresh launch too — YOLO forces
// full-bypass (overrides even a named profile); otherwise the named profile, or the harness-
// default floor (no forced -a/-s; Codex's no-flags default is restricted-fs + OnRequest, the
// workspace-only floor confirmed via `codex doctor`).
// -s danger-full-access (overrides even a named profile); otherwise the named profile, or the
// explicit -s workspace-write floor flag (the no-flags default does NOT grant the --add-dir
// writable roots this launch passes, so the floor must be the explicit workspace-write flag).
const cmd = opts.resumeToken
? buildCodexResumeCore(opts.resumeToken, profile, false, queueStateDirArg.trim() || undefined)
: `codex${codexPostureArg(profileArg)} -C ${shellQuote(binding.cwd)}${gitDirArg}${queueStateDirArg}${modelArg}`;
+15 -7
View File
@@ -3,9 +3,10 @@
// A simple deterministic setting that rides the STABLE launch-flag surface only (per the founder's
// two-surface rule: launch flags may be deterministic code; config-file policy may NOT). When ON,
// every managed seat boots at its harness's maximally-permissive LAUNCH FLAG:
// - Claude: --dangerously-skip-permissions (permission bypass)
// - Codex: --dangerously-bypass-approvals-and-sandbox (permission + sandbox bypass)
// - Pi: --approve (full RESOURCE TRUST — Pi's
// - Claude: --dangerously-skip-permissions (permission bypass)
// - Codex: -s danger-full-access (maximally-permissive sandbox; OFF floor = the
// explicit -s workspace-write flag, NOT a harness default)
// - Pi: --approve (full RESOURCE TRUST — Pi's
// --approve/--no-approve govern RESOURCE TRUST, not a permission policy)
// When OFF (the default), seats boot with the usability floor, unchanged. The YOLO path writes ZERO
// config files — it only selects a launch flag. Opt-in via the OPENRIG_YOLO env setting. (The
@@ -27,12 +28,19 @@ export function claudePostureFlag(env: NodeJS.ProcessEnv = process.env): string
}
/**
* Codex launch posture segment (leading space included). YOLO forces the full-bypass flag on EVERY
* seat, overriding even a named `-p` profile; otherwise pass the caller's profile arg (or "" for the
* harness-default floor). `profileArg` is the already-formatted ` -p <profile>` string or "".
* Codex launch posture segment (leading space included), applied on EVERY managed Codex path
* (fresh / resume / native-fork). `profileArg` is the already-formatted ` -p <profile>` string or "".
* - YOLO ON → ` -s danger-full-access` (maximally-permissive sandbox), overriding even a named profile.
* - OFF + named profile → the profile (it governs its own sandbox).
* - OFF + no profile → the EXPLICIT workspace-only floor ` -s workspace-write`. NOTE: emitting NO
* sandbox flag is NOT equivalent — on the installed managed runtime the no-flags default does not
* grant the `--add-dir` writable roots managed seats pass, so the seat fails before readiness (QA
* firsthand). The floor must therefore be the explicit workspace-write flag (the arch-ruled value).
* No forced approval flag on any path — the approval flow is left to Codex, unforced.
*/
export function codexPostureArg(profileArg: string, env: NodeJS.ProcessEnv = process.env): string {
return yoloEnabled(env) ? " --dangerously-bypass-approvals-and-sandbox" : profileArg;
if (yoloEnabled(env)) return " -s danger-full-access";
return profileArg ? profileArg : " -s workspace-write";
}
/** Pi RESOURCE TRUST (Pi's --approve/--no-approve govern resource trust, NOT a permission policy):
@@ -49,9 +49,9 @@ export function buildCodexResumeCore(
extraArgs?: string,
): string {
// OPR.0.4.8.2: the RESUME path uses the SAME posture decision (codexPostureArg) as fresh/fork.
// YOLO forces full-bypass (overriding even a named profile); otherwise a named profile governs
// itself and the no-profile case is the harness-default floor (no forced -a/-s; Codex's no-flags
// default is restricted-fs + OnRequest, the workspace-only floor confirmed via `codex doctor`).
// YOLO forces -s danger-full-access (overriding even a named profile); otherwise a named profile
// governs itself and the no-profile case is the explicit -s workspace-write floor flag (the
// no-flags default does NOT grant the --add-dir writable roots managed seats pass).
const profileArg = codexConfigProfile ? ` -p ${shellQuote(codexConfigProfile)}` : "";
const profileOrPosture = codexPostureArg(profileArg);
const middle = extraArgs ? `${extraArgs} ` : "";
+2 -2
View File
@@ -147,8 +147,8 @@ function computeRecoveryGuidance(input: {
return {
summary: resumeToken
? "Try native Codex resume first; Codex uses its harness default (restricted fs + on-request) unless a named config profile is present."
: "No stored Codex resume token. Try codex resume --last; Codex uses its harness default unless a named config profile is present.",
? "Try native Codex resume first; the managed launch sets the explicit -s workspace-write floor flag unless a named config profile is present."
: "No stored Codex resume token. Try codex -s workspace-write resume --last; the managed launch sets the explicit -s workspace-write floor flag unless a named config profile is present.",
commands,
notes,
};
+5 -5
View File
@@ -58,13 +58,13 @@ describe("CodexResumeAdapter", () => {
expect(sendText).toHaveBeenCalledOnce();
expect(sendText.mock.calls[0]![0]).toBe("r99-demo1-impl");
expect(sendText.mock.calls[0]![1]).toBe("codex resume 'uuid-123'");
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write resume 'uuid-123'");
expect(sendKeys).toHaveBeenCalledOnce();
expect(sendKeys.mock.calls[0]![1]).toEqual(["Enter"]);
expect(sendText.mock.invocationCallOrder[0]).toBeLessThan(sendKeys.mock.invocationCallOrder[0]!);
});
it("codex_last: sendText posture-preserving codex resume --last", async () => {
it("codex_last: sendText posture-preserving codex -s workspace-write resume --last", async () => {
const sendText = vi.fn(async () => ({ ok: true as const }));
const sendKeys = vi.fn(async () => ({ ok: true as const }));
const tmux = mockTmux({ sendText, sendKeys });
@@ -73,7 +73,7 @@ describe("CodexResumeAdapter", () => {
await adapter.resume("r99-demo1-impl", "codex_last", null, "/repo");
expect(sendText).toHaveBeenCalledOnce();
expect(sendText.mock.calls[0]![1]).toBe("codex resume --last");
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write resume --last");
expect(sendKeys).toHaveBeenCalledOnce();
});
@@ -112,7 +112,7 @@ describe("CodexResumeAdapter", () => {
await adapter.resume("r99-demo1-impl", "codex_id", "uuid; rm -rf /", "/repo");
expect(sendText.mock.calls[0]![1]).toBe("codex resume 'uuid; rm -rf /'");
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write resume 'uuid; rm -rf /'");
});
it("profile-bearing resume uses -p flag after preflight passes", async () => {
@@ -281,7 +281,7 @@ describe("CodexResumeAdapter", () => {
// Closes the lifecycle scenario matrix slice's documented Codex deferral.
it("probe says attention_required (codex auth-refusal) -> { ok: false, code: 'attention_required', evidence }", async () => {
const refusalPane = [
"$ codex resume 019d-token",
"$ codex -s workspace-write resume 019d-token",
"Error: Your access token could not be refreshed because you have since",
"logged out or signed in to another account. Please sign in again.",
].join("\n");
@@ -65,12 +65,12 @@ function expectedFreshLaunchCommand(options: { cwd?: string; model?: string; que
const gitDirArg = ` --add-dir ${quote(nodePath.join(cwd, ".git"))}`;
const queueDirArg = options.queueRoot === null ? "" : ` --add-dir ${quote(options.queueRoot ?? testQueueRoot())}`;
const modelArg = options.model ? ` -m ${quote(options.model)}` : "";
return `codex -C ${quote(cwd)}${gitDirArg}${queueDirArg}${modelArg}`;
return `codex -s workspace-write -C ${quote(cwd)}${gitDirArg}${queueDirArg}${modelArg}`;
}
function expectedResumeCommand(token = "sess-456", queueRoot: string | null = testQueueRoot()): string {
const queueDirArg = queueRoot === null ? "" : `--add-dir ${quote(queueRoot)} `;
return `codex resume ${queueDirArg}${quote(token)}`;
return `codex -s workspace-write resume ${queueDirArg}${quote(token)}`;
}
function expectedProfileFreshLaunchCommand(profile: string, options: { cwd?: string; model?: string; queueRoot?: string | null } = {}): string {
@@ -679,8 +679,8 @@ describe("Codex runtime adapter", () => {
fsOps: mockFs(),
listProcesses: () => [
{ pid: 900, ppid: 1, command: "-zsh" },
{ pid: 901, ppid: 900, command: "node /opt/homebrew/bin/codex -C /project" },
{ pid: 902, ppid: 901, command: "/opt/homebrew/lib/node_modules/@openai/codex/vendor/codex/codex -C /project" },
{ pid: 901, ppid: 900, command: "node /opt/homebrew/bin/codex -s workspace-write -C /project" },
{ pid: 902, ppid: 901, command: "/opt/homebrew/lib/node_modules/@openai/codex/vendor/codex/codex -s workspace-write -C /project" },
],
readThreadIdByPid: (pid) => pid === 902 ? "019d45bc-117d-78a3-a4ad-6fb186e5a86d" : undefined,
sleep: async () => {},
@@ -906,7 +906,7 @@ describe("Codex runtime adapter", () => {
// commit 63ee206 alongside the legacy CodexResumeAdapter path.
it("launchHarness returns attention_required when Codex post-logout token-refresh fails during resume", async () => {
const refusalPane = [
"$ codex resume sess-456",
"$ codex -s workspace-write resume sess-456",
"Error: Your access token could not be refreshed because you have since",
"logged out or signed in to another account. Please sign in again.",
].join("\n");
@@ -933,7 +933,7 @@ describe("Codex runtime adapter", () => {
const tmux = mockTmux({
getPaneCommand: vi.fn(async () => "zsh"),
capturePaneContent: vi.fn(async () => [
"$ codex resume sess-456",
"$ codex -s workspace-write resume sess-456",
"Your access token could not be refreshed.",
"Please log out and sign in again.",
].join("\n")),
@@ -1046,7 +1046,7 @@ describe("Codex runtime adapter", () => {
return callCount <= 10 ? "node" : "codex";
}),
capturePaneContent: vi.fn(async () => {
if (callCount <= 10) return "codex resume 019ecd3b-test-thread\nCodex v0.128.0\nloading...";
if (callCount <= 10) return "codex -s workspace-write resume 019ecd3b-test-thread\nCodex v0.128.0\nloading...";
return "OpenAI Codex (v0.128.0)\n gpt-5.5 · session 019ecd3b-test-thread\n› ";
}),
});
@@ -13,9 +13,9 @@ describe("native resume probe", () => {
).toBe("claude --resume 'abc-123' --name 'dev-impl@demo-rig'");
});
it("builds a Codex no-profile resume with NO forced flags (harness default)", () => {
it("builds a Codex no-profile resume with the explicit -s workspace-write floor flag", () => {
expect(buildNativeResumeCommand("codex", "019d-token")).toBe(
"codex resume '019d-token'"
"codex -s workspace-write resume '019d-token'"
);
});
@@ -31,9 +31,9 @@ describe("native resume probe", () => {
});
describe("buildCodexResumeCore (shared builder)", () => {
it("no-profile emits NO forced flags (harness default) matching fresh launch", () => {
it("no-profile emits the explicit -s workspace-write floor flag matching fresh launch", () => {
expect(buildCodexResumeCore("tok-123")).toBe(
"codex resume 'tok-123'"
"codex -s workspace-write resume 'tok-123'"
);
});
@@ -45,7 +45,7 @@ describe("native resume probe", () => {
it("useLast emits --last instead of token", () => {
expect(buildCodexResumeCore("", null, true)).toBe(
"codex resume --last"
"codex -s workspace-write resume --last"
);
});
@@ -524,7 +524,7 @@ describe("native resume probe", () => {
describe("Codex auth-refusal recognition", () => {
it("classifies Codex post-logout token-refresh failure as attention_required", () => {
const paneContent = [
"$ codex resume 019d-token",
"$ codex -s workspace-write resume 019d-token",
"Error: Your access token could not be refreshed because you have since",
"logged out or signed in to another account. Please sign in again.",
].join("\n");
@@ -542,7 +542,7 @@ describe("native resume probe", () => {
it("classifies Codex token-refresh failure with `log out and sign in` guidance as attention_required", () => {
const paneContent = [
"$ codex resume 019d-token",
"$ codex -s workspace-write resume 019d-token",
"Your access token could not be refreshed.",
"Please log out and sign in again.",
].join("\n");
+3 -3
View File
@@ -191,9 +191,9 @@ describe("Node Inventory Projection", () => {
expect(entry?.resumeCommand).toBeNull();
expect(entry?.recoveryGuidance).toBeNull(); // relocated to detail
const detail = getNodeDetail(db, "rig-2", "dev.qa");
expect(detail?.recoveryGuidance?.summary).toContain("codex resume --last");
expect(detail?.recoveryGuidance?.summary).toContain("harness default");
expect(detail?.recoveryGuidance?.commands).toEqual(["cd /workspace/app", "codex resume --last"]);
expect(detail?.recoveryGuidance?.summary).toContain("codex -s workspace-write resume --last");
expect(detail?.recoveryGuidance?.summary).toContain("explicit -s workspace-write floor flag");
expect(detail?.recoveryGuidance?.commands).toEqual(["cd /workspace/app", "codex -s workspace-write resume --last"]);
expect(detail?.recoveryGuidance?.notes).toContain("Use workspace and recent prompt text to identify the right conversation.");
expect(detail?.recoveryGuidance?.notes).toContain("If the identity anchor was captured, the picker may include: dev-qa@test-rig");
});
@@ -100,8 +100,8 @@ describe("ResumeMetadataRefresher", () => {
tmuxAdapter: tmux,
listProcesses: () => [
{ pid: 900, ppid: 1, command: "-zsh" },
{ pid: 901, ppid: 900, command: "node /opt/homebrew/bin/codex -C /project" },
{ pid: 902, ppid: 901, command: "/opt/homebrew/lib/node_modules/@openai/codex/vendor/codex/codex -C /project" },
{ pid: 901, ppid: 900, command: "node /opt/homebrew/bin/codex -s workspace-write -C /project" },
{ pid: 902, ppid: 901, command: "/opt/homebrew/lib/node_modules/@openai/codex/vendor/codex/codex -s workspace-write -C /project" },
],
readCodexThreadIdByPid: (pid) => pid === 902 ? "019d45c3-e909-7152-b52e-34edab4070ed" : undefined,
sleep: async () => {},
+12 -11
View File
@@ -75,20 +75,20 @@ describe("OPR.0.4.8.2 YOLO mode — opt-in, default OFF, launch-flag surface onl
expect(on).not.toContain("--permission-mode acceptEdits");
});
it("Codex resume: OFF -> harness-default floor (no bypass); ON -> --dangerously-bypass-approvals-and-sandbox", () => {
it("Codex resume: OFF -> explicit -s workspace-write floor flag; ON -> -s danger-full-access", () => {
delete process.env.OPENRIG_YOLO;
const off = buildCodexResumeCore("tok-1", null, false);
expect(off).toBe("codex resume 'tok-1'");
expect(off).toBe("codex -s workspace-write resume 'tok-1'");
process.env.OPENRIG_YOLO = "1";
const on = buildCodexResumeCore("tok-1", null, false);
expect(on).toContain("--dangerously-bypass-approvals-and-sandbox");
expect(on).toContain("-s danger-full-access");
});
it("Codex resume ON overrides even a named config profile (every seat full-bypass)", () => {
it("Codex resume ON overrides even a named config profile (every seat -s danger-full-access)", () => {
process.env.OPENRIG_YOLO = "1";
const on = buildCodexResumeCore("tok-1", "my-profile", false);
expect(on).toContain("--dangerously-bypass-approvals-and-sandbox");
expect(on).toContain("-s danger-full-access");
expect(on).not.toContain("-p 'my-profile'");
});
@@ -110,7 +110,7 @@ describe("OPR.0.4.8.2 YOLO mode — opt-in, default OFF, launch-flag surface onl
expect(on).not.toContain("--permission-mode acceptEdits");
});
it("Codex native FORK carries the posture: OFF no bypass / ON --dangerously-bypass-approvals-and-sandbox", async () => {
it("Codex native FORK carries the posture: OFF -s workspace-write / ON -s danger-full-access", async () => {
const codexFs = {
readFile: () => { throw new Error("nf"); },
writeFile: () => {},
@@ -126,20 +126,21 @@ describe("OPR.0.4.8.2 YOLO mode — opt-in, default OFF, launch-flag surface onl
await new CodexRuntimeAdapter({ tmux: tmuxOff, fsOps: codexFs }).launchHarness(makeBinding(), forkOpts);
const off = (tmuxOff.sendText as ReturnType<typeof vi.fn>).mock.calls[0]?.[1] as string;
expect(off).toContain("fork");
expect(off).not.toContain("--dangerously-bypass-approvals-and-sandbox");
expect(off).toContain("-s workspace-write");
expect(off).not.toContain("-s danger-full-access");
process.env.OPENRIG_YOLO = "1";
const tmuxOn = mockTmux();
await new CodexRuntimeAdapter({ tmux: tmuxOn, fsOps: codexFs }).launchHarness(makeBinding(), forkOpts);
const on = (tmuxOn.sendText as ReturnType<typeof vi.fn>).mock.calls[0]?.[1] as string;
expect(on).toContain("--dangerously-bypass-approvals-and-sandbox");
expect(on).toContain("-s danger-full-access");
expect(on).toContain("fork");
});
it("codexPostureArg: OFF passes the profile arg through; ON forces the bypass flag", () => {
it("codexPostureArg: OFF no-profile -> explicit -s workspace-write floor; OFF profile passes through; ON -> -s danger-full-access", () => {
expect(codexPostureArg(" -p 'x'", {} as NodeJS.ProcessEnv)).toBe(" -p 'x'");
expect(codexPostureArg("", {} as NodeJS.ProcessEnv)).toBe("");
expect(codexPostureArg(" -p 'x'", { OPENRIG_YOLO: "1" } as NodeJS.ProcessEnv)).toBe(" --dangerously-bypass-approvals-and-sandbox");
expect(codexPostureArg("", {} as NodeJS.ProcessEnv)).toBe(" -s workspace-write");
expect(codexPostureArg(" -p 'x'", { OPENRIG_YOLO: "1" } as NodeJS.ProcessEnv)).toBe(" -s danger-full-access");
});
it("piTrust (RESOURCE TRUST, not permission policy): OFF keeps configured/no-approve; ON forces approve", () => {