tickmarkr 2.6.5 → 2.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -258,9 +258,13 @@ two-pane surface: the left rail lists views (**All models**, **Shapes**, **Steer
258
258
  installed agent CLI with its auth state and model count; the right pane is a searchable model list
259
259
  with tier, context, price, and probe-latency columns. `Space` allows/denies, `Enter` classifies an
260
260
  unclassified model (with a required benchmark-provenance note) or pins a classified one to a shape,
261
- `m` opens the routing-mode presets, and `w` renders the unified diff of your repo config overlay.
262
- Nothing is written until you confirm the diff with `y`; quitting leaves config unchanged. The
263
- Shapes view pins from a candidate picker ranked by the production router.
261
+ `m` opens the routing-mode presets, and `w` renders the unified diff of the one overlay the save
262
+ lands in, named in the review: an allow/deny membership edit goes to the layer that holds it (the
263
+ repo overlay when it declares that family, else your user overlay), everything else to the user
264
+ overlay; a batch needing both is refused — save each half in its own session. Nothing is written
265
+ until you confirm with `y`. A worker-only deny on a partially excluded adapter takes two rail
266
+ presses: out · all seats, then out · workers. The Shapes view pins from a candidate picker ranked by
267
+ the production router.
264
268
 
265
269
  Routing-mode semantics, pin/floor/prefer precedence, review and consult steering syntax,
266
270
  provenance rules, and `--quality` / `--mode` flags are documented in
@@ -1,25 +1,36 @@
1
- import type { AuthHealth, WorkerAdapter } from "./types.js";
2
- export type CodexCommitStatus = "protected" | "allowed" | "unknown";
1
+ import { type AuthHealth, type CodexCommitStatus, type WorkerAdapter } from "./types.js";
2
+ export type { CodexCommitStatus };
3
3
  export interface CodexCommitProbe {
4
4
  worktree: string;
5
5
  commonDir: string;
6
+ branch: string;
6
7
  control: string;
7
8
  lock: string;
9
+ hostileHook: string;
10
+ hostileRoot: string;
8
11
  token: string;
9
12
  signal?: AbortSignal;
10
13
  }
11
- /** Runs the write script inside the Codex sandbox and resolves its stdout. Injected in tests. */
14
+ /** Runs the probe script inside the Codex sandbox and resolves its stdout. Injected in tests. */
12
15
  export type CodexSandbox = (probe: CodexCommitProbe) => Promise<string>;
13
16
  export interface CodexCommitResult {
14
17
  status: CodexCommitStatus;
15
18
  detail: string;
16
19
  }
17
- export declare const codexSandboxArgs: (p: CodexCommitProbe) => string[];
20
+ /** The `sh` argv production runs at the fixture worktree: `codex sandbox` under the worker grant fragment. */
21
+ export declare const codexSandboxArgs: (p: CodexCommitProbe, grant?: string) => string[];
22
+ /** A sandbox executing `grant`; production is CODEX_GIT_GRANT. Tests pass a mutant grant to prove discrimination. */
23
+ export declare const codexSandboxFor: (grant: string) => CodexSandbox;
18
24
  export declare const codexSandbox: CodexSandbox;
19
25
  export declare function probeCodexCommit(repoRoot: string, sandbox?: CodexSandbox, signal?: AbortSignal): Promise<CodexCommitResult>;
26
+ /** A persisted verdict outside the closed table (an older or newer doctor.json) reads unknown, never allowed. */
27
+ export declare const codexCommitStatus: (health: Record<string, AuthHealth>) => CodexCommitStatus;
20
28
  /** Doctor's one call: gated on the REAL codex adapter object (a codex-id stub gets no probe and no output
21
29
  * unless a test injects a sandbox — the kimiTurnEnabled rule). Mutates only the health record doctor owns. */
22
30
  export declare function recordCodexCommit(adapters: WorkerAdapter[], health: Record<string, AuthHealth>, cwd: string, sandbox?: CodexSandbox): Promise<void>;
23
31
  /** Plan's headline beneath a Codex worker row; null for every other worker and for an allowed probe.
24
32
  * An absent field is unknown, but only the real adapter earns the line — stubs stay byte-identical. */
25
33
  export declare function codexCommitHeadline(worker: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): string | null;
34
+ /** Plan's one line for Codex's NON-worker roles (judge, review, consult): they launch with the same grant, so
35
+ * only an escape earns it — allowed, protected and unknown concern commits, which those roles never make. */
36
+ export declare function codexEscapeRoleLine(roles: string[], adapters: WorkerAdapter[], health: Record<string, AuthHealth>): string | null;
@@ -4,20 +4,26 @@ import { existsSync, mkdtempSync, readFileSync, rmdirSync, rmSync } from "node:f
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { promisify } from "node:util";
7
- import { codex } from "./codex.js";
7
+ import { CODEX_GIT_GRANT, codex } from "./codex.js";
8
+ import { shq } from "./types.js";
8
9
  const exec = promisify(execFile);
9
10
  const git = async (cwd, ...args) => (await exec("git", args, { cwd })).stdout.trim();
10
- // $1 control, $2 lock, $3 token. noclobber (set -C) creates the lock the way git does — never over an existing one.
11
- const SCRIPT = `printf %s "$3" > "$1" && echo control=ok || echo control=fail
12
- e=$( (set -C; printf %s "$3" > "$2") 2>&1 ) && echo lock=ok || echo "lock=fail $e"`;
13
- // The grant names the git common dir (the worker launch grant) AND the exact linked metadata directory that
14
- // holds index.lock, so a denial is protection despite an exact grant — never a grant this probe forgot.
15
- export const codexSandboxArgs = (p) => [
16
- "sandbox", "-c", 'sandbox_mode="workspace-write"',
17
- "-c", `sandbox_workspace_write.writable_roots=${JSON.stringify([p.commonDir, dirname(p.lock)])}`,
18
- "--", "sh", "-c", SCRIPT, "sh", p.control, p.lock, p.token,
11
+ const CONTROL = "tickmarkr-codex-commit-control";
12
+ // $1 control, $2 token, $3 hostile hooks member, $4 hostile common-root member. The commit disables hooks (the
13
+ // probe asks what the sandbox permits, never what a user hook decides) and forces the reflog so common/logs is
14
+ // always exercised. noclobber (set -C) never writes over a foreign occupant of a hostile path.
15
+ const SCRIPT = `printf %s "$2" > "$1" && echo control=ok || echo control=fail
16
+ git add -- "$1" && git -c core.hooksPath=/dev/null -c core.logAllRefUpdates=true -c commit.gpgsign=false -c user.name=tickmarkr -c user.email=probe@tickmarkr.invalid commit -q -m "tickmarkr-probe $2" && echo commit=ok || echo commit=fail
17
+ (set -C; printf %s "$2" > "$3") 2>/dev/null && echo hook=written || echo hook=denied
18
+ (set -C; printf %s "$2" > "$4") 2>/dev/null && echo root=written || echo root=denied`;
19
+ /** The `sh` argv production runs at the fixture worktree: `codex sandbox` under the worker grant fragment. */
20
+ export const codexSandboxArgs = (p, grant = CODEX_GIT_GRANT) => [
21
+ "-c",
22
+ `codex sandbox -c 'sandbox_mode="workspace-write"' ${grant} -- sh -c ${shq(SCRIPT)} sh ${[p.control, p.token, p.hostileHook, p.hostileRoot].map(shq).join(" ")}`,
19
23
  ];
20
- export const codexSandbox = async (p) => (await exec("codex", codexSandboxArgs(p), { cwd: p.worktree, timeout: 20_000, signal: p.signal })).stdout;
24
+ /** A sandbox executing `grant`; production is CODEX_GIT_GRANT. Tests pass a mutant grant to prove discrimination. */
25
+ export const codexSandboxFor = (grant) => async (p) => (await exec("sh", codexSandboxArgs(p, grant), { cwd: p.worktree, timeout: 20_000, signal: p.signal })).stdout;
26
+ export const codexSandbox = codexSandboxFor(CODEX_GIT_GRANT);
21
27
  const holds = (path, token) => {
22
28
  try {
23
29
  return readFileSync(path, "utf8") === token;
@@ -27,38 +33,98 @@ const holds = (path, token) => {
27
33
  }
28
34
  };
29
35
  const unknown = (detail) => ({ status: "unknown", detail });
36
+ // The bytes on disk decide an escape whatever the receipts say — and whether or not the sandbox exited cleanly.
37
+ const escaped = (hostile, token) => {
38
+ const written = hostile.filter((m) => holds(m, token));
39
+ return written.length ? { status: "escape", detail: `the sandbox wrote ${written.join(" and ")} outside the worker grant` } : undefined;
40
+ };
41
+ // One outcome per receipt field: a field reporting two outcomes (commit=ok AND commit=fail) is contradictory.
42
+ const contradictory = (out) => {
43
+ const seen = new Map();
44
+ for (const [, field, outcome] of out.matchAll(/^(control|commit|hook|root)=(\S+)/gm)) {
45
+ if ((seen.get(field) ?? outcome) !== outcome)
46
+ return true;
47
+ seen.set(field, outcome);
48
+ }
49
+ return false;
50
+ };
51
+ const tipOf = async (repo, ref) => git(repo, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`).catch(() => undefined);
52
+ // This probe's commit and nothing else: one parent (the fixture base), its token in the subject and the control.
53
+ const isProbeCommit = async (repo, commit, base, token) => {
54
+ try {
55
+ return await git(repo, "log", "-1", "--format=%P %s", commit) === `${base} tickmarkr-probe ${token}`
56
+ && await git(repo, "cat-file", "blob", `${commit}:${CONTROL}`) === token;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ };
30
62
  export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal) {
31
63
  const token = randomUUID();
32
64
  const parent = mkdtempSync(join(tmpdir(), "tickmarkr-codex-commit-"));
33
65
  // git names the fixture's metadata directory after the checkout's basename; a unique one makes that
34
66
  // directory — and so the lock path — known BEFORE `worktree add`, which can fail after creating it
35
- const worktree = join(parent, `tickmarkr-probe-${token}`);
67
+ const branch = `tickmarkr-probe-${token}`;
68
+ const ref = `refs/heads/${branch}`;
69
+ const worktree = join(parent, branch);
36
70
  let lock;
71
+ let base;
72
+ let hostile = [];
37
73
  try {
38
74
  const commonDir = await git(repoRoot, "rev-parse", "--path-format=absolute", "--git-common-dir");
39
75
  lock = join(commonDir, "worktrees", basename(worktree), "index.lock");
40
- await git(repoRoot, "worktree", "add", "--detach", worktree, "HEAD");
76
+ const hostileHook = join(commonDir, "hooks", branch), hostileRoot = join(commonDir, branch);
77
+ hostile = [hostileHook, hostileRoot];
78
+ if (hostile.some((m) => existsSync(m)))
79
+ return unknown("a foreign file already holds a hostile probe path");
80
+ base = await git(repoRoot, "rev-parse", "--verify", "HEAD^{commit}");
81
+ await git(repoRoot, "worktree", "add", "-b", branch, worktree, base);
41
82
  if (join(await git(worktree, "rev-parse", "--absolute-git-dir"), "index.lock") !== lock)
42
83
  return unknown("the fixture worktree metadata is not where git was expected to put it");
43
- const control = join(worktree, "tickmarkr-codex-commit-control");
84
+ const control = join(worktree, CONTROL);
44
85
  if (existsSync(lock))
45
86
  return unknown("a foreign index.lock already holds the fixture worktree");
46
- const out = await sandbox({ worktree, commonDir, control, lock, token, signal });
47
- // Every verdict needs the sandbox's own report AND the bytes on disk to agree; anything else is unknown.
87
+ const out = await sandbox({ worktree, commonDir, branch, control, lock, hostileHook, hostileRoot, token, signal });
88
+ // An escape on disk dominates; every other verdict needs consistent receipts AND disk.
89
+ const escape = escaped(hostile, token);
90
+ if (escape)
91
+ return escape;
92
+ if (contradictory(out))
93
+ return unknown("the sandbox receipts contradict each other");
48
94
  if (!/^control=ok$/m.test(out) || !holds(control, token))
49
95
  return unknown("the sandbox did not write the ordinary control file");
50
- if (/^lock=ok$/m.test(out) && holds(lock, token))
51
- return { status: "allowed", detail: "the sandbox wrote index.lock" };
52
- if (/^lock=fail .*(not permitted|permission denied)/mi.test(out) && !existsSync(lock)) {
53
- return { status: "protected", detail: "the sandbox wrote an ordinary worktree file but denied index.lock" };
96
+ if (!/^hook=denied$/m.test(out) || !/^root=denied$/m.test(out) || hostile.some((m) => existsSync(m))) {
97
+ return unknown("the sandbox result for the shared-metadata members was unreadable");
98
+ }
99
+ const tip = await tipOf(repoRoot, ref);
100
+ if (/^commit=ok$/m.test(out) && tip && tip !== base && await isProbeCommit(repoRoot, tip, base, token)) {
101
+ let reflog = "";
102
+ try {
103
+ reflog = readFileSync(join(commonDir, "logs", ref), "utf8");
104
+ }
105
+ catch { /* absent: unknown below */ }
106
+ if (reflog.trimEnd().split("\n").pop()?.startsWith(`${base} ${tip} `)) {
107
+ return { status: "allowed", detail: "the sandbox committed on its fixture branch and was denied both shared-metadata members" };
108
+ }
109
+ return unknown("the fixture branch moved but its reflog does not record the probe commit");
54
110
  }
55
- return unknown("the sandbox result for index.lock was unreadable");
111
+ if (/^commit=fail$/m.test(out) && tip === base) {
112
+ return { status: "protected", detail: "the sandbox wrote an ordinary worktree file but denied the linked-worktree commit" };
113
+ }
114
+ return unknown("the fixture branch does not match the sandbox commit report (a foreign change may have moved it)");
56
115
  }
57
116
  catch (e) {
117
+ // a sandbox that wrote a hostile member and then failed (or was cancelled) still escaped: inspect before cleanup
118
+ const escape = escaped(hostile, token);
119
+ if (escape)
120
+ return escape;
58
121
  return unknown(signal?.aborted ? "probe cancelled" : `probe did not run (${(e instanceof Error ? e.message : String(e)).split("\n")[0]})`);
59
122
  }
60
123
  finally {
61
- // only the lock this probe wrote (its token) is ours to remove; a foreign one is never touched
124
+ // only files holding this probe's token are ours to remove; a foreign occupant is never touched
125
+ for (const m of hostile)
126
+ if (holds(m, token))
127
+ rmSync(m, { force: true });
62
128
  if (lock && holds(lock, token))
63
129
  rmSync(lock, { force: true });
64
130
  // a foreign lock lives in the fixture's metadata directory, which `worktree remove` would delete with
@@ -74,14 +140,34 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
74
140
  }
75
141
  catch { /* other worktrees live there */ }
76
142
  }
143
+ // The fixture branch and its reflog go only while the tip is still this probe's (the base or its own
144
+ // commit), and update-ref's old-value check refuses if anyone moves it between this read and the delete.
145
+ // Metadata preserved for a foreign lock keeps its branch too: that checkout's HEAD still names it.
146
+ const tip = base === undefined || foreign ? undefined : await tipOf(repoRoot, ref);
147
+ if (tip && base && (tip === base || await isProbeCommit(repoRoot, tip, base, token))) {
148
+ await git(repoRoot, "update-ref", "-d", ref, tip).catch(() => undefined);
149
+ }
77
150
  rmSync(parent, { recursive: true, force: true });
78
151
  }
79
152
  }
80
153
  const NOTE = {
81
154
  protected: "linked-worktree git metadata protected",
82
- allowed: "linked-worktree commit probe: index.lock writable",
155
+ allowed: "linked-worktree commit probe: commit allowed, shared hooks/config denied",
156
+ escape: "BLOCKING security warning: codex sandbox escape into shared git metadata",
83
157
  unknown: "linked-worktree commit probe unknown",
84
158
  };
159
+ const CONSEQUENCE = {
160
+ protected: "; a Codex worker cannot commit in its worktree",
161
+ allowed: "",
162
+ escape: "; every Codex role (worker, judge, review, consult) launches with this grant — stop Codex seats and inspect .git/hooks and git config --list --show-origin (core.hooksPath, core.fsmonitor, filter.*, diff.external)",
163
+ unknown: "",
164
+ };
165
+ const STATUSES = new Set(Object.keys(NOTE));
166
+ /** A persisted verdict outside the closed table (an older or newer doctor.json) reads unknown, never allowed. */
167
+ export const codexCommitStatus = (health) => {
168
+ const s = health[codex.id]?.codexCommit;
169
+ return s !== undefined && STATUSES.has(s) ? s : "unknown";
170
+ };
85
171
  /** Doctor's one call: gated on the REAL codex adapter object (a codex-id stub gets no probe and no output
86
172
  * unless a test injects a sandbox — the kimiTurnEnabled rule). Mutates only the health record doctor owns. */
87
173
  export async function recordCodexCommit(adapters, health, cwd, sandbox) {
@@ -90,15 +176,19 @@ export async function recordCodexCommit(adapters, health, cwd, sandbox) {
90
176
  if (!adapter || (adapter !== codex && !sandbox) || !h?.installed)
91
177
  return;
92
178
  const r = await probeCodexCommit(cwd, sandbox);
93
- const say = `${NOTE[r.status]} — ${r.detail}${r.status === "protected" ? "; a Codex worker cannot commit in its worktree" : ""}`;
179
+ const say = `${NOTE[r.status]} — ${r.detail}${CONSEQUENCE[r.status]}`;
94
180
  health[codex.id] = { ...h, codexCommit: r.status, note: `${h.note ? `${h.note}; ` : ""}${say}` };
95
181
  }
182
+ const ESCAPE_ACTION = "stop Codex seats and inspect .git/hooks and git config --list --show-origin (core.hooksPath, core.fsmonitor, filter.*, diff.external) before dispatch";
96
183
  /** Plan's headline beneath a Codex worker row; null for every other worker and for an allowed probe.
97
184
  * An absent field is unknown, but only the real adapter earns the line — stubs stay byte-identical. */
98
185
  export function codexCommitHeadline(worker, adapters, health) {
99
186
  if (worker !== codex.id || !adapters.includes(codex))
100
187
  return null;
101
- const status = health[codex.id]?.codexCommit ?? "unknown";
188
+ const status = codexCommitStatus(health);
189
+ if (status === "escape") {
190
+ return ` BLOCKING security warning: codex sandbox escape (doctor probe) — this worker's launch grant let the sandbox write shared git metadata; ${ESCAPE_ACTION}`;
191
+ }
102
192
  if (status === "protected") {
103
193
  return " worker restriction: codex linked-worktree git metadata is protected (doctor probe) — this worker cannot commit its own work; routing consequence: steer this task to an eligible non-Codex worker";
104
194
  }
@@ -106,3 +196,10 @@ export function codexCommitHeadline(worker, adapters, health) {
106
196
  ? " worker restriction: unknown — the codex linked-worktree commit probe has no verdict; run tickmarkr doctor"
107
197
  : null;
108
198
  }
199
+ /** Plan's one line for Codex's NON-worker roles (judge, review, consult): they launch with the same grant, so
200
+ * only an escape earns it — allowed, protected and unknown concern commits, which those roles never make. */
201
+ export function codexEscapeRoleLine(roles, adapters, health) {
202
+ if (!roles.length || !adapters.includes(codex) || codexCommitStatus(health) !== "escape")
203
+ return null;
204
+ return `BLOCKING security warning: codex sandbox escape (doctor probe) — Codex ${roles.join("/")} seats launch with the worker grant that let the sandbox write shared git metadata; ${ESCAPE_ACTION}`;
205
+ }
@@ -4,6 +4,7 @@ export declare function readCodexModelsCache(path?: string): {
4
4
  fetchedAt?: string;
5
5
  retirements?: Record<string, ModelRetirement | null>;
6
6
  };
7
+ export declare const CODEX_GIT_GRANT = "-c \"sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir); tkr_gitdir=$(git rev-parse --absolute-git-dir); if [ \"$tkr_gitdir\" != \"$tkr_common\" ]; then printf '\"%s\",' \"$tkr_gitdir\"; fi; printf '\"%s/objects\",\"%s/refs\",\"%s/logs\"' \"$tkr_common\" \"$tkr_common\" \"$tkr_common\")]\"";
7
8
  export declare const CODEX_TRUST_DIALOG: TrustDialog;
8
9
  export declare const CODEX_INPUT_BOX: import("./types.js").InputBox;
9
10
  export declare function seedCodexTrust(repoRoot: string, configPath?: string): TrustVerdict;
@@ -96,9 +96,15 @@ function codexRetirement(upgrade) {
96
96
  }
97
97
  // tickmarkr worktrees keep their gitdir under the MAIN repo's .git/worktrees/<name> — outside the
98
98
  // workspace-write sandbox root — so git commit dies on index.lock (incident run-20260709-104447
99
- // P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the pane shell at the
100
- // worktree cwd; in a plain repo it resolves to ./.git (already writable, harmless).
101
- const GITDIR_WRITABLE = `-c "sandbox_workspace_write.writable_roots=[\\"$(git rev-parse --path-format=absolute --git-common-dir)\\"]"`;
99
+ // P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the launching shell at the
100
+ // worktree cwd, for EVERY Codex role (worker, judge, review, consult share these builders).
101
+ // v2.6.6 T9 (K, D-912): the grant is exactly what a linked-worktree branch commit writes — the worktree's
102
+ // own gitdir (only when it differs from the common dir), common/objects, common/refs, common/logs. The
103
+ // shipped [common] grant (and D-910's [common, gitdir]) let a sandboxed seat write <common>/hooks and
104
+ // <common>/config, which unsandboxed git later runs. No common-root fallback: an ordinary checkout gets
105
+ // no gitdir root and its commit fails closed. packed-refs is not granted (packed-ref ops fail closed).
106
+ // doctor's probe (codex-commit-check.ts) executes these exact bytes; it never rebuilds the grant.
107
+ export const CODEX_GIT_GRANT = `-c "sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir); tkr_gitdir=$(git rev-parse --absolute-git-dir); if [ "$tkr_gitdir" != "$tkr_common" ]; then printf '"%s",' "$tkr_gitdir"; fi; printf '"%s/objects","%s/refs","%s/logs"' "$tkr_common" "$tkr_common" "$tkr_common")]"`;
102
108
  // OBS-125: codex 0.144.x gates enabled hooks behind PER-PROJECT-PATH hook trust — a NEW gate distinct
103
109
  // from the directory trust seedCodexTrust seeds. Every worker runs in a fresh ephemeral worktree, an
104
110
  // untrusted project, so codex re-prompts "Hooks need review" and the interactive worker stalls forever
@@ -257,12 +263,12 @@ export const codex = {
257
263
  probe: async () => probeVersion("codex"),
258
264
  channels: (cfg) => channelsFromConfig("codex", cfg),
259
265
  // v1.65 T3: every flag the command builder below hardcodes (incl. codexMcpSuppressionFlags' -c/
260
- // --disable and GITDIR_WRITABLE's -c) — all listed by top-level `codex --help`, verified 2026-07-22.
266
+ // --disable and CODEX_GIT_GRANT's -c) — all listed by top-level `codex --help`, verified 2026-07-22.
261
267
  hardcodedFlags: { binary: "codex", flags: ["--sandbox", "-a", "-s", "--model", "-c", "--disable", "--dangerously-bypass-hook-trust"] },
262
268
  // --sandbox workspace-write is the autonomous sandbox mode (codex v0.144.1+)
263
269
  // MCP suppression built per dispatch (config can change between runs) — see codexMcpSuppressionFlags.
264
270
  // CODEX_HOOK_TRUST (OBS-125) clears the per-worktree "Hooks need review" gate while keeping the sandbox.
265
- headlessCommand: (promptFile, model, effort) => `codex exec --sandbox workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${GITDIR_WRITABLE}${codexEffortFlag(effort)} --model ${shq(model)} - < ${shq(promptFile)}`,
271
+ headlessCommand: (promptFile, model, effort) => `codex exec --sandbox workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${CODEX_GIT_GRANT}${codexEffortFlag(effort)} --model ${shq(model)} - < ${shq(promptFile)}`,
266
272
  // OBS-930: the visible pane runs the REAL TUI. Codex's TUI takes its prompt only as the [PROMPT]
267
273
  // positional (`codex --help`, 0.153.4 — no file/stdin form), so the launch inlines the file exactly
268
274
  // as the claude adapter does: the prompt is the LAST positional and every flag value is followed by
@@ -274,7 +280,7 @@ export const codex = {
274
280
  // OBS-930 (Linux): the inlined prompt is ONE argv string and Linux caps one at 131072 bytes, so a
275
281
  // prompt over promptArgvCeiling() returns null → worker-mode-fallback → the headless form (types.ts).
276
282
  interactiveCommand: (promptFile, model, effort) => promptFitsArgv(promptFile)
277
- ? `codex -a never -s workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${GITDIR_WRITABLE}${codexEffortFlag(effort)} --model ${shq(model)} "$(cat ${shq(promptFile)})"`
283
+ ? `codex -a never -s workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${CODEX_GIT_GRANT}${codexEffortFlag(effort)} --model ${shq(model)} "$(cat ${shq(promptFile)})"`
278
284
  : null,
279
285
  invoke(task, _cwd, a, ctx) {
280
286
  return { command: this.headlessCommand(ctx.promptFile, a.model, a.effort) };
@@ -51,7 +51,7 @@ export interface AuthHealth {
51
51
  modelAuth?: Record<string, ModelAuth>;
52
52
  modelIdentities?: Record<string, string>;
53
53
  modelRetirements?: Record<string, ModelRetirement | null>;
54
- codexCommit?: "protected" | "allowed" | "unknown";
54
+ codexCommit?: CodexCommitStatus;
55
55
  launchServices?: LaunchServicesRecord;
56
56
  }
57
57
  export interface LaunchServicesRecord {
@@ -59,6 +59,9 @@ export interface LaunchServicesRecord {
59
59
  start: string;
60
60
  advisory?: string;
61
61
  }
62
+ /** allowed: linked branch commit landed, shared hooks/common root denied · protected: commit denied, controls
63
+ * readable · escape: a unique hostile member was written · unknown: unreadable or contradictory receipts. */
64
+ export type CodexCommitStatus = "allowed" | "protected" | "escape" | "unknown";
62
65
  export declare function modelAuthed(health: AuthHealth | undefined, model: string, allowUnverifiedModels?: boolean): boolean;
63
66
  export interface Invocation {
64
67
  command: string;
@@ -15,6 +15,7 @@ import { HerdrDriver } from "../../drivers/herdr.js";
15
15
  import { ORCA_FIXTURE_VERSION, parseEnvelope, resolveOrcaCliBinary } from "../../drivers/orca.js";
16
16
  import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
17
17
  import { recordCodexCommit } from "../../adapters/codex-commit-check.js";
18
+ import { codex } from "../../adapters/codex.js";
18
19
  import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, observedSeat, preferRanks } from "../../route/preference.js";
19
20
  import { CATALOG_REFRESH_TIMEOUT_MS, LIVEBENCH_TABLE_DATE, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
20
21
  import { sh } from "../../run/git.js";
@@ -672,7 +673,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
672
673
  const h = health[a.id];
673
674
  // OBS-503: a crash-on-version verdict carries its reason in note — print it instead of the lie.
674
675
  const state = !h.installed ? (h.note ?? "not installed") : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
675
- const healthy = h.installed && (a.id !== kimi.id || h.authed);
676
+ // v2.6.6 T9 (K): a Codex sandbox escape is a BLOCKING security warning — never a passing row
677
+ const healthy = h.installed && (a.id !== kimi.id || h.authed) && !(a.id === codex.id && h.codexCommit === "escape");
676
678
  return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
677
679
  });
678
680
  const reviewDemotions = recentReviewDemotions(cwd);