fix(perms): route ALL managed launch paths through one launch-posture decision (guard finding)

Guard NOT-CLEAR on 7f6b28f6: the "every seat" contract missed three managed paths the fresh-only
YOLO wiring did not exercise. Centralize the decision in yolo-mode.ts (claudePostureFlag /
codexPostureArg / piTrust) and route every fresh/resume/fork path through it:

- Claude restore (claude-resume.ts): was `claude --resume <token>` — now carries the posture flag
  (the unconditional acceptEdits floor when OFF; --dangerously-skip-permissions when YOLO ON).
- Codex native fork (codex-runtime-adapter.ts fork branch): now codexPostureArg(profileArg) — OFF
  keeps the profile/floor, ON forces --dangerously-bypass-approvals-and-sandbox. Fresh + resume
  also routed through the same helper (behavior unchanged when OFF).
- Pi restore (pi-resume.ts) + launch: piTrust(configured) — ON forces `approve`, OFF the configured
  posture. Pi's --approve/--no-approve govern RESOURCE TRUST, NOT a permission policy — wording
  corrected in source comments/helper doc/test descriptions accordingly (PM/Leg-B framing). The
  zero-permission-config-write property is scoped to Claude/Codex permission policy.

Test-first: yolo-mode.test.ts adds ON/OFF pins for Claude restore, Codex fork, and the
codexPostureArg / piTrust decisions; flipped the 3 claude-resume assertions (floor now present).
Daemon bounded suite 303/303, cli setup 24/24; daemon+cli tsc 0. VM/source-only; never pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Opus 4.8 committed 2026-08-03 21:04:42 +00:00
1 parent 7f6b28f664
commit 6581dbaf34
9 files changed
+129 -41

No files matched your search

@@ -2,7 +2,7 @@ import nodePath from "node:path";
import fs from "node:fs";
import { createHash, randomUUID } from "node:crypto";
import type { TmuxAdapter } from "./tmux.js";
import { yoloEnabled } from "./yolo-mode.js";
import { claudePostureFlag } from "./yolo-mode.js";
import type {
RuntimeAdapter, NodeBinding, ResolvedStartupFile,
InstalledResource, ProjectionResult, StartupDeliveryResult, ReadinessResult,
@@ -170,9 +170,9 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
return { ok: false, error: "resumeToken and forkSource are mutually exclusive — pick one" };
}
// OPR.0.4.8.2: the acceptEdits floor by default; YOLO mode (opt-in) swaps in the full-bypass
// launch flag. Launch-flag surface only — no config-file writes on either path.
const permissionMode = yoloEnabled() ? "--dangerously-skip-permissions" : "--permission-mode acceptEdits";
// OPR.0.4.8.2: the acceptEdits floor by default; YOLO (opt-in) swaps in the full-bypass flag.
// The SAME decision (claudePostureFlag) is used on the restore path (claude-resume.ts).
const permissionMode = claudePostureFlag();
// Fork branch: build `claude --resume <parent> --fork-session --name <seat>`
// and capture the NEW post-fork session id. The parent token is NEVER
@@ -1,6 +1,7 @@
import { setTimeout as sleep } from "node:timers/promises";
import type { TmuxAdapter } from "./tmux.js";
import { shellQuote } from "./shell-quote.js";
import { claudePostureFlag } from "./yolo-mode.js";
import { assessNativeResumeProbe } from "../domain/native-resume-probe.js";
export type ResumeResult =
@@ -42,7 +43,9 @@ export class ClaudeResumeAdapter {
return { ok: false, code: "no_resume", message: "Claude resume not available" };
}
const cmd = `claude --resume ${shellQuote(resumeToken!)}`;
// OPR.0.4.8.2: the RESTORE path uses the SAME launch-posture decision as fresh launch (the
// unconditional acceptEdits floor when OFF; the full bypass when YOLO is ON) — every seat.
const cmd = `claude ${claudePostureFlag()} --resume ${shellQuote(resumeToken!)}`;
const textResult = await this.tmux.sendText(tmuxSessionName, cmd);
if (!textResult.ok) {
@@ -5,7 +5,7 @@ import os from "node:os";
import { execFileSync } from "node:child_process";
import Database from "better-sqlite3";
import type { TmuxAdapter } from "./tmux.js";
import { yoloEnabled } from "./yolo-mode.js";
import { codexPostureArg } from "./yolo-mode.js";
import type {
RuntimeAdapter, NodeBinding, ResolvedStartupFile,
InstalledResource, ProjectionResult, StartupDeliveryResult, ReadinessResult,
@@ -337,7 +337,9 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
if (!parentId) {
return { ok: false, error: "codex fork: forkSource.value is required (parent native_id)" };
}
const cmd = `codex${profileArg} fork${queueStateDirArg} ${shellQuote(parentId)}`;
// 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).
const cmd = `codex${codexPostureArg(profileArg)} fork${queueStateDirArg} ${shellQuote(parentId)}`;
const textResult = await this.tmux.sendText(binding.tmuxSession, cmd);
if (!textResult.ok) {
return { ok: false, error: `Failed to send launch command: ${textResult.message}` };
@@ -357,16 +359,13 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
return { ok: true, resumeToken: threadId, resumeType: "codex_id" };
}
// 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`).
const cmd = opts.resumeToken
? buildCodexResumeCore(opts.resumeToken, profile, false, queueStateDirArg.trim() || undefined)
: yoloEnabled()
? // OPR.0.4.8.2 YOLO (opt-in): full-bypass on every seat (overrides even a named profile).
`codex -C ${shellQuote(binding.cwd)}${gitDirArg}${queueStateDirArg}${modelArg} --dangerously-bypass-approvals-and-sandbox`
: profile
? `codex${profileArg} -C ${shellQuote(binding.cwd)}${gitDirArg}${queueStateDirArg}${modelArg}`
: // OPR.0.4.8.2 agnostic rip-out (Codex floor): no forced -a/-s. Codex's no-flags default
// is restricted-fs + OnRequest (the workspace-only floor, confirmed via `codex doctor`).
`codex -C ${shellQuote(binding.cwd)}${gitDirArg}${queueStateDirArg}${modelArg}`;
: `codex${codexPostureArg(profileArg)} -C ${shellQuote(binding.cwd)}${gitDirArg}${queueStateDirArg}${modelArg}`;
const textResult = await this.tmux.sendText(binding.tmuxSession, cmd);
if (!textResult.ok) {
+4 -1
View File
@@ -12,6 +12,7 @@ import { setTimeout as sleep } from "node:timers/promises";
import { randomUUID } from "node:crypto";
import type { TmuxAdapter } from "./tmux.js";
import type { ResumeResult } from "./claude-resume.js";
import { piTrust } from "./yolo-mode.js";
import {
piSeatPaths, parsePiRunnerState, buildPiRunnerCommand, buildPendingRunnerState,
} from "./pi-runner-protocol.js";
@@ -87,7 +88,9 @@ export class PiResumeAdapter {
// The model declaration must survive resume: the runner's provider-key
// allowlist keys off the declared provider (VM leg finding).
model: model ?? undefined,
trust: this.options.trustPosture ?? "no-approve",
// OPR.0.4.8.2: Pi RESOURCE TRUST (not a permission policy). Same decision as the launch path:
// YOLO forces `approve` on every restored seat; otherwise the configured posture.
trust: piTrust(this.options.trustPosture),
sessionFile,
launchId,
});
@@ -12,7 +12,7 @@
import nodePath from "node:path";
import { randomUUID } from "node:crypto";
import type { TmuxAdapter } from "./tmux.js";
import { yoloEnabled } from "./yolo-mode.js";
import { piTrust } from "./yolo-mode.js";
import type {
RuntimeAdapter, NodeBinding, ResolvedStartupFile,
InstalledResource, ProjectionResult, StartupDeliveryResult, ReadinessResult,
@@ -239,8 +239,9 @@ export class PiRuntimeAdapter implements RuntimeAdapter {
stateRoot: this.stateRoot,
cwd: binding.cwd,
model: binding.model,
// OPR.0.4.8.2 YOLO (opt-in): forces --approve on every seat; otherwise the conservative floor.
trust: yoloEnabled() ? "approve" : this.trustPosture,
// OPR.0.4.8.2: Pi RESOURCE TRUST (not a permission policy). YOLO forces `approve` on every
// seat; otherwise the configured posture. Same decision used on the restore path (pi-resume).
trust: piTrust(this.trustPosture),
sessionFile: opts.resumeToken,
forkRef,
launchId,
+33 -5
View File
@@ -2,14 +2,42 @@
//
// 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 with its harness full-bypass LAUNCH FLAG:
// - Claude: --dangerously-skip-permissions
// - Codex: --dangerously-bypass-approvals-and-sandbox
// - Pi: --approve
// 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
// --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.
// config files — it only selects a launch flag. Opt-in via the OPENRIG_YOLO env setting. (The
// zero-permission-config-write property concerns Claude/Codex permission policy; Pi is resource trust.)
export function yoloEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
const v = env.OPENRIG_YOLO;
return v === "1" || v === "true";
}
// ── The single launch-posture decision per harness — used on EVERY managed launch path (fresh,
// resume, fork) so the floor (OFF) and the full-bypass (ON) are uniform, never path-dependent. ──
/** Claude launch posture flag: floor `--permission-mode acceptEdits`, or the YOLO full bypass. */
export function claudePostureFlag(env: NodeJS.ProcessEnv = process.env): string {
return yoloEnabled(env) ? "--dangerously-skip-permissions" : "--permission-mode acceptEdits";
}
/**
* 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 "".
*/
export function codexPostureArg(profileArg: string, env: NodeJS.ProcessEnv = process.env): string {
return yoloEnabled(env) ? " --dangerously-bypass-approvals-and-sandbox" : profileArg;
}
/** Pi RESOURCE TRUST (Pi's --approve/--no-approve govern resource trust, NOT a permission policy):
* YOLO forces `approve`; otherwise the configured posture (default `no-approve`). */
export function piTrust(
configured: "approve" | "no-approve" | undefined,
env: NodeJS.ProcessEnv = process.env,
): "approve" | "no-approve" {
return yoloEnabled(env) ? "approve" : configured ?? "no-approve";
}
@@ -1,5 +1,5 @@
import { shellQuote } from "../adapters/shell-quote.js";
import { yoloEnabled } from "../adapters/yolo-mode.js";
import { codexPostureArg } from "../adapters/yolo-mode.js";
// L3 adds `attention_required` for the Claude resume-selection prompt proxy.
// Distinct from `inconclusive` (we don't know yet) and `failed` (terminal
@@ -48,18 +48,12 @@ export function buildCodexResumeCore(
useLast?: boolean,
extraArgs?: string,
): string {
// OPR.0.4.8.2 agnostic rip-out (Codex floor): the no-profile branch NO LONGER forces
// `-s danger-full-access` / `-a on-request`. Codex's own no-flags default is restricted-fs +
// OnRequest approval (the workspace-only floor the founder selected — confirmed via
// `codex doctor`), so "setting nothing" IS the floor. A named config profile still governs itself.
// OPR.0.4.8.2: YOLO (opt-in) forces the full-bypass launch flag on EVERY seat (overriding even a
// named profile); otherwise a named profile governs itself and the no-profile case is the harness
// default (no forced flags). Launch-flag surface only.
const profileOrPosture = yoloEnabled()
? " --dangerously-bypass-approvals-and-sandbox"
: codexConfigProfile
? ` -p ${shellQuote(codexConfigProfile)}`
: "";
// 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`).
const profileArg = codexConfigProfile ? ` -p ${shellQuote(codexConfigProfile)}` : "";
const profileOrPosture = codexPostureArg(profileArg);
const middle = extraArgs ? `${extraArgs} ` : "";
const tokenArg = useLast ? "--last" : shellQuote(resumeToken);
return `codex${profileOrPosture} resume ${middle}${tokenArg}`;
+4 -3
View File
@@ -61,7 +61,8 @@ describe("ClaudeResumeAdapter", () => {
expect(sendText).toHaveBeenCalledOnce();
expect(sendText.mock.calls[0]![0]).toBe("r99-demo1-lead");
expect(sendText.mock.calls[0]![1]).toBe("claude --resume 'my-session'");
// OPR.0.4.8.2: restore now carries the launch-posture floor (acceptEdits), same as fresh.
expect(sendText.mock.calls[0]![1]).toBe("claude --permission-mode acceptEdits --resume 'my-session'");
expect(sendKeys).toHaveBeenCalledOnce();
expect(sendKeys.mock.calls[0]![0]).toBe("r99-demo1-lead");
expect(sendKeys.mock.calls[0]![1]).toEqual(["Enter"]);
@@ -104,7 +105,7 @@ describe("ClaudeResumeAdapter", () => {
await adapter.resume("r99-demo1-lead", "claude_name", "tok; rm -rf /", "/repo");
expect(sendText.mock.calls[0]![1]).toBe("claude --resume 'tok; rm -rf /'");
expect(sendText.mock.calls[0]![1]).toBe("claude --permission-mode acceptEdits --resume 'tok; rm -rf /'");
});
it("sendKeys(Enter) fails after sendText -> C-c sent to clear buffer", async () => {
@@ -237,7 +238,7 @@ describe("ClaudeResumeAdapter", () => {
}
// Explicitly: only the launch command + Enter were sent.
expect(sendText).toHaveBeenCalledTimes(1);
expect(sendText.mock.calls[0]![1]).toContain("claude --resume");
expect(sendText.mock.calls[0]![1]).toContain("claude --permission-mode acceptEdits --resume");
expect(sendKeys).toHaveBeenCalledTimes(1);
expect(sendKeys.mock.calls[0]![1]).toEqual(["Enter"]);
});
+60 -1
View File
@@ -1,7 +1,9 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { yoloEnabled } from "../src/adapters/yolo-mode.js";
import { yoloEnabled, codexPostureArg, piTrust } from "../src/adapters/yolo-mode.js";
import { buildCodexResumeCore } from "../src/domain/native-resume-probe.js";
import { ClaudeCodeAdapter, type ClaudeAdapterFsOps } from "../src/adapters/claude-code-adapter.js";
import { ClaudeResumeAdapter } from "../src/adapters/claude-resume.js";
import { CodexRuntimeAdapter } from "../src/adapters/codex-runtime-adapter.js";
import type { NodeBinding } from "../src/domain/runtime-adapter.js";
import type { TmuxAdapter } from "../src/adapters/tmux.js";
@@ -89,4 +91,61 @@ describe("OPR.0.4.8.2 YOLO mode — opt-in, default OFF, launch-flag surface onl
expect(on).toContain("--dangerously-bypass-approvals-and-sandbox");
expect(on).not.toContain("-p 'my-profile'");
});
// ── The three managed launch paths the fresh-only wiring missed (guard finding) ──
it("Claude RESTORE (ClaudeResumeAdapter) carries the posture flag: OFF floor / ON bypass", async () => {
delete process.env.OPENRIG_YOLO;
const tmuxOff = mockTmux();
await new ClaudeResumeAdapter(tmuxOff).resume("r01-impl", "claude_name", "my-session", "/repo");
const off = (tmuxOff.sendText as ReturnType<typeof vi.fn>).mock.calls[0]?.[1] as string;
expect(off).toContain("--permission-mode acceptEdits");
expect(off).toContain("--resume 'my-session'");
process.env.OPENRIG_YOLO = "1";
const tmuxOn = mockTmux();
await new ClaudeResumeAdapter(tmuxOn).resume("r01-impl", "claude_name", "my-session", "/repo");
const on = (tmuxOn.sendText as ReturnType<typeof vi.fn>).mock.calls[0]?.[1] as string;
expect(on).toContain("--dangerously-skip-permissions");
expect(on).not.toContain("--permission-mode acceptEdits");
});
it("Codex native FORK carries the posture: OFF no bypass / ON --dangerously-bypass-approvals-and-sandbox", async () => {
const codexFs = {
readFile: () => { throw new Error("nf"); },
writeFile: () => {},
exists: () => false,
mkdirp: () => {},
copyFile: () => {},
listFiles: () => [],
} as unknown as ConstructorParameters<typeof CodexRuntimeAdapter>[0]["fsOps"];
const forkOpts = { name: "dev-impl@test-rig", forkSource: { kind: "native_id" as const, value: "parent-123" } };
delete process.env.OPENRIG_YOLO;
const tmuxOff = mockTmux();
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");
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("fork");
});
it("codexPostureArg: OFF passes the profile arg through; ON forces the bypass flag", () => {
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");
});
it("piTrust (RESOURCE TRUST, not permission policy): OFF keeps configured/no-approve; ON forces approve", () => {
expect(piTrust("no-approve", {} as NodeJS.ProcessEnv)).toBe("no-approve");
expect(piTrust(undefined, {} as NodeJS.ProcessEnv)).toBe("no-approve");
expect(piTrust("approve", {} as NodeJS.ProcessEnv)).toBe("approve");
expect(piTrust("no-approve", { OPENRIG_YOLO: "1" } as NodeJS.ProcessEnv)).toBe("approve");
});
});