@workerdeck/core 0.18.0 → 0.19.0

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/build/index.d.mts CHANGED
@@ -1645,7 +1645,15 @@ declare const codexAdapter: EngineAdapter;
1645
1645
  * const c=JSON.parse(d.slice(s,i));
1646
1646
  * for(const m of c.models) console.log(m.slug, m.display_name,
1647
1647
  * m.visibility, m.supported_reasoning_levels.map(l=>l.effort).join(","))'\
1648
- * "$(node -p 'require.resolve("@openai/codex-darwin-arm64/package.json").replace("package.json","vendor/aarch64-apple-darwin/bin/codex")')"
1648
+ * "$(node -p 'const{createRequire}=require("module");
1649
+ * const w=require.resolve("@openai/codex/package.json");
1650
+ * createRequire(w).resolve("@openai/codex-darwin-arm64/package.json")
1651
+ * .replace("package.json","vendor/aarch64-apple-darwin/bin/codex")')"
1652
+ *
1653
+ * The two-hop resolve is NOT optional: under pnpm's strict layout the platform
1654
+ * package is a dependency of `@openai/codex`, so it resolves only from that
1655
+ * wrapper's location, never from the repo root. Resolving it directly throws
1656
+ * MODULE_NOT_FOUND — the same two hops `resolveBundledCodexExecutable` makes.
1649
1657
  *
1650
1658
  * Mapping decisions:
1651
1659
  * - the internal `codex-auto-review` row is dropped (the codex analogue of
package/build/index.mjs CHANGED
@@ -4,12 +4,12 @@ import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthro
4
4
  import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, contextReading, imagePartRef, replayCoalesceKey, replayRetains, snapshotRetains, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
5
5
  import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
6
6
  import { execFile, spawn } from "node:child_process";
7
- import { appendFileSync, existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createVfs, runScript } from "@workerdeck/sandbox";
9
9
  import { z } from "zod";
10
10
  import { lookup } from "node:dns/promises";
11
- import { tmpdir } from "node:os";
12
- import { join } from "node:path";
11
+ import { homedir, tmpdir } from "node:os";
12
+ import { dirname, join, resolve, sep } from "node:path";
13
13
  //#region src/lib/attachments.ts
14
14
  /** The four the Anthropic API accepts. Notably absent: image/heic — an iPhone's
15
15
  * native photo format, which clients must transcode before upload. */
@@ -4117,6 +4117,330 @@ var CodexAgentTracker = class {
4117
4117
  }
4118
4118
  };
4119
4119
  //#endregion
4120
+ //#region src/engines/codex/trust.ts
4121
+ /**
4122
+ * Codex project trust: will this session's cwd get its `.codex/config.toml`?
4123
+ *
4124
+ * Codex only layers a project's `.codex/config.toml` onto the operator's base
4125
+ * config when the project is *trusted* (a `[projects."<path>"]
4126
+ * trust_level = "trusted"` entry in `$CODEX_HOME/config.toml`), and the
4127
+ * app-server surface has no trust prompt — that lives in the TUI. So under
4128
+ * WorkerDeck an untrusted project's config, MCP servers included, is silently
4129
+ * ignored: no error, no notice, servers just missing. The runner asks this
4130
+ * module at session start whether that is about to happen, so the transcript
4131
+ * can say so.
4132
+ *
4133
+ * Semantics, all measured against both the bundled 0.146.0 and 0.149.0
4134
+ * (2026-08-22, via `codex mcp list` from probe cwds and via `thread/start` +
4135
+ * `mcpServerStatus/list` on the app-server surface — identical answers):
4136
+ *
4137
+ * - **Discovery**: config layers come from the cwd and its ancestors up to and
4138
+ * including the nearest directory containing `.git` (dir or file). With no
4139
+ * git anywhere above, the cwd alone is consulted. Directories above the
4140
+ * nearest git root never contribute, trusted or not.
4141
+ * - **Trust per layer**: an exact entry for the layer's own canonical path
4142
+ * decides (an explicit `"untrusted"` beats inherited trust); without one the
4143
+ * layer inherits from the chain's git root — trusted iff the git root has a
4144
+ * trusted entry, where a linked worktree's root also counts its main
4145
+ * repository's entry (the `.git` file's gitdir names it). A trusted
4146
+ * mid-chain directory does NOT trust its children, and plain path
4147
+ * containment without git confers nothing.
4148
+ * - **Canonical paths**: codex matches entries against the canonicalized cwd —
4149
+ * a macOS `/tmp/...` entry never matches the `/private/tmp/...` it points
4150
+ * at, while the reverse spelling works (and the app-server canonicalizes its
4151
+ * `cwd` param too). Both sides here are realpath'd, which can only err
4152
+ * toward silence.
4153
+ * - **The gate is sandbox-scoped**: `thread/start` under `workspace-write` or
4154
+ * `danger-full-access` (permission modes `acceptEdits`/`bypassPermissions`)
4155
+ * WRITES the trust entry itself and loads the config — only `read-only`
4156
+ * (mode `default`) leaves the project untrusted and the config ignored. A
4157
+ * later `turn/start` with a wider sandboxPolicy does not heal the thread
4158
+ * (measured): the caller probes `default`-mode sessions only, and the notice
4159
+ * stays true for the session it opens.
4160
+ * - `trust_level`'s vocabulary is exactly `trusted`/`untrusted`; any other
4161
+ * value fails codex's bootstrap outright ("unknown variant"), so a config
4162
+ * carrying one probes silent — that session announces its own failure.
4163
+ *
4164
+ * The correctness bar for every degrade path: a FALSE notice — warning about a
4165
+ * project codex actually trusts — is worse than a missed one. The narrow TOML
4166
+ * reader below refuses (→ silence) anything it cannot interpret with
4167
+ * certainty, rather than guessing.
4168
+ */
4169
+ const BARE_KEY = /[A-Za-z0-9_-]/;
4170
+ function skipWs(text, pos) {
4171
+ let i = pos;
4172
+ while (i < text.length && (text[i] === " " || text[i] === " ")) i++;
4173
+ return i;
4174
+ }
4175
+ /** One-line TOML basic string starting at `pos` (which must be `"`). Undefined
4176
+ * on an escape TOML doesn't define or a close quote that never comes — the
4177
+ * caller refuses the file rather than guessing what codex would read. */
4178
+ function parseBasicString(text, pos) {
4179
+ let out = "";
4180
+ let i = pos + 1;
4181
+ while (i < text.length) {
4182
+ const ch = text[i];
4183
+ if (ch === "\"") return {
4184
+ value: out,
4185
+ end: i + 1
4186
+ };
4187
+ if (ch === "\\") {
4188
+ const esc = text[i + 1];
4189
+ if (esc === "b") out += "\b";
4190
+ else if (esc === "t") out += " ";
4191
+ else if (esc === "n") out += "\n";
4192
+ else if (esc === "f") out += "\f";
4193
+ else if (esc === "r") out += "\r";
4194
+ else if (esc === "\"") out += "\"";
4195
+ else if (esc === "\\") out += "\\";
4196
+ else if (esc === "u" || esc === "U") {
4197
+ const width = esc === "u" ? 4 : 8;
4198
+ const hex = text.slice(i + 2, i + 2 + width);
4199
+ if (hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex)) return void 0;
4200
+ const code = Number.parseInt(hex, 16);
4201
+ if (code > 1114111) return void 0;
4202
+ out += String.fromCodePoint(code);
4203
+ i += width;
4204
+ } else return void 0;
4205
+ i += 2;
4206
+ continue;
4207
+ }
4208
+ out += ch;
4209
+ i++;
4210
+ }
4211
+ }
4212
+ /** One-line TOML literal string starting at `pos` (which must be `'`). */
4213
+ function parseLiteralString(text, pos) {
4214
+ const close = text.indexOf("'", pos + 1);
4215
+ if (close === -1) return void 0;
4216
+ return {
4217
+ value: text.slice(pos + 1, close),
4218
+ end: close + 1
4219
+ };
4220
+ }
4221
+ /** A dotted key path — bare, `"basic"` and `'literal'` keys, whitespace around
4222
+ * the dots — as found in table headers and on the left of assignments. */
4223
+ function parseKeyPath(text, pos) {
4224
+ const keys = [];
4225
+ let i = pos;
4226
+ for (;;) {
4227
+ i = skipWs(text, i);
4228
+ const ch = text[i];
4229
+ if (ch === "\"" || ch === "'") {
4230
+ const str = ch === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
4231
+ if (!str) return void 0;
4232
+ keys.push(str.value);
4233
+ i = str.end;
4234
+ } else if (ch !== void 0 && BARE_KEY.test(ch)) {
4235
+ let end = i;
4236
+ while (end < text.length && BARE_KEY.test(text[end])) end++;
4237
+ keys.push(text.slice(i, end));
4238
+ i = end;
4239
+ } else return;
4240
+ i = skipWs(text, i);
4241
+ if (text[i] !== ".") return {
4242
+ value: keys,
4243
+ end: i
4244
+ };
4245
+ i++;
4246
+ }
4247
+ }
4248
+ /**
4249
+ * Scan an assignment's value (or the continuation line of a multi-line array),
4250
+ * confirming where it ends. Returns the bracket depth carried onto the next
4251
+ * line (0 = the value is complete) plus the string itself when the whole value
4252
+ * was one plain one-line string. Undefined refuses the file: multi-line
4253
+ * strings are where a line reader starts misreading string *content* as
4254
+ * sections and entries — the exact mistake that could flip a real trust entry
4255
+ * — so they are not parsed around, they end the attempt.
4256
+ */
4257
+ function scanValueLine(text, pos, depth) {
4258
+ let i = skipWs(text, pos);
4259
+ if (depth === 0 && (text[i] === "\"" || text[i] === "'")) {
4260
+ if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return void 0;
4261
+ const str = text[i] === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
4262
+ if (!str) return void 0;
4263
+ const rest = skipWs(text, str.end);
4264
+ if (rest < text.length && text[rest] !== "#") return void 0;
4265
+ return {
4266
+ depth: 0,
4267
+ value: str.value
4268
+ };
4269
+ }
4270
+ while (i < text.length) {
4271
+ const ch = text[i];
4272
+ if (ch === "#") break;
4273
+ if (ch === "\"" || ch === "'") {
4274
+ if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return void 0;
4275
+ const str = ch === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
4276
+ if (!str) return void 0;
4277
+ i = str.end;
4278
+ continue;
4279
+ }
4280
+ if (ch === "[" || ch === "{") depth++;
4281
+ else if (ch === "]" || ch === "}") {
4282
+ depth--;
4283
+ if (depth < 0) return void 0;
4284
+ }
4285
+ i++;
4286
+ }
4287
+ return { depth };
4288
+ }
4289
+ /**
4290
+ * The `[projects."<path>"] trust_level = "..."` entries of a codex
4291
+ * `config.toml`, by a deliberately narrow reader (core takes no TOML
4292
+ * dependency for this). Handles what codex itself writes plus the reasonable
4293
+ * hand-edits — comments, CRLF, whitespace, quoted keys with escapes, literal
4294
+ * and bare keys, `[projects]`-with-dotted-keys and top-level dotted forms,
4295
+ * single-line inline tables, multi-line arrays — and returns **undefined for
4296
+ * anything else it meets anywhere in the file** (multi-line strings,
4297
+ * `projects` as an inline table, array-of-tables, junk): the caller treats
4298
+ * undefined as "cannot know" and stays silent. Conflicting duplicate entries
4299
+ * also refuse — invalid for TOML, and guessing wrong is a false notice.
4300
+ */
4301
+ function parseProjectTrustEntries(source) {
4302
+ const entries = /* @__PURE__ */ new Map();
4303
+ let section = [];
4304
+ let carryDepth = 0;
4305
+ for (const rawLine of source.split("\n")) {
4306
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
4307
+ if (carryDepth > 0) {
4308
+ const scanned = scanValueLine(line, 0, carryDepth);
4309
+ if (!scanned) return void 0;
4310
+ carryDepth = scanned.depth;
4311
+ continue;
4312
+ }
4313
+ const start = skipWs(line, 0);
4314
+ if (start >= line.length || line[start] === "#") continue;
4315
+ if (line[start] === "[") {
4316
+ const array = line.startsWith("[[", start);
4317
+ const path = parseKeyPath(line, start + (array ? 2 : 1));
4318
+ if (!path) return void 0;
4319
+ const close = array ? "]]" : "]";
4320
+ if (!line.startsWith(close, path.end)) return void 0;
4321
+ const rest = skipWs(line, path.end + close.length);
4322
+ if (rest < line.length && line[rest] !== "#") return void 0;
4323
+ if (array && path.value[0] === "projects") return void 0;
4324
+ section = path.value;
4325
+ continue;
4326
+ }
4327
+ const key = parseKeyPath(line, start);
4328
+ if (!key) return void 0;
4329
+ if (line[key.end] !== "=") return void 0;
4330
+ const scanned = scanValueLine(line, key.end + 1, 0);
4331
+ if (!scanned) return void 0;
4332
+ carryDepth = scanned.depth;
4333
+ const full = [...section, ...key.value];
4334
+ if (full[0] !== "projects") continue;
4335
+ if (full.length < 3) return void 0;
4336
+ if (full.length === 3 && full[2] === "trust_level") {
4337
+ if (carryDepth !== 0 || scanned.value === void 0) return void 0;
4338
+ const project = full[1];
4339
+ const existing = entries.get(project);
4340
+ if (existing !== void 0 && existing !== scanned.value) return void 0;
4341
+ entries.set(project, scanned.value);
4342
+ }
4343
+ }
4344
+ if (carryDepth > 0) return void 0;
4345
+ return entries;
4346
+ }
4347
+ /**
4348
+ * A linked worktree inherits trust from its main repository's entry (measured:
4349
+ * trusting the main repo path loads the worktree's project config). The
4350
+ * worktree's `.git` is a FILE whose `gitdir:` line names
4351
+ * `<main>/.git/worktrees/<name>`; the directory owning that `.git` is the
4352
+ * anchor to look up. Anything unreadable or shaped differently resolves false
4353
+ * — this route can only ADD trust, i.e. silence, never a false notice.
4354
+ */
4355
+ function mainRepositoryTrusted(gitRootDir, canonical) {
4356
+ const gitPath = join(gitRootDir, ".git");
4357
+ try {
4358
+ if (!statSync(gitPath).isFile()) return false;
4359
+ const match = /^gitdir:[ \t]*(.+?)[ \t]*$/m.exec(readFileSync(gitPath, "utf8"));
4360
+ if (!match) return false;
4361
+ const gitdir = resolve(gitRootDir, match[1]);
4362
+ const at = gitdir.lastIndexOf(`${sep}.git${sep}`);
4363
+ if (at <= 0) return false;
4364
+ let main = gitdir.slice(0, at);
4365
+ try {
4366
+ main = realpathSync(main);
4367
+ } catch {}
4368
+ return canonical.get(main) === "trusted";
4369
+ } catch {
4370
+ return false;
4371
+ }
4372
+ }
4373
+ /**
4374
+ * The notice for a codex session about to run on a cwd whose
4375
+ * `.codex/config.toml` codex will ignore, or undefined when there is nothing
4376
+ * to say — no project config anywhere codex would look, the project is
4377
+ * trusted, or the situation cannot be established with certainty. Read-only
4378
+ * throughout: WorkerDeck never writes trust entries (adjacent to the auth red
4379
+ * lines — trusting a directory is the operator's decision, made in codex's
4380
+ * own prompt or by their own hand).
4381
+ */
4382
+ function untrustedProjectNotice(options) {
4383
+ let cwd;
4384
+ try {
4385
+ cwd = realpathSync(options.cwd);
4386
+ } catch {
4387
+ return;
4388
+ }
4389
+ let home = resolve(options.codexHome);
4390
+ try {
4391
+ home = realpathSync(options.codexHome);
4392
+ } catch {}
4393
+ const chain = [];
4394
+ let dir = cwd;
4395
+ for (;;) {
4396
+ chain.push(dir);
4397
+ if (existsSync(join(dir, ".git"))) break;
4398
+ const parent = dirname(dir);
4399
+ if (parent === dir) break;
4400
+ dir = parent;
4401
+ }
4402
+ const anchor = chain[chain.length - 1];
4403
+ const gitRoot = existsSync(join(anchor, ".git")) ? anchor : void 0;
4404
+ const layers = (gitRoot ? chain : [cwd]).filter((layer) => {
4405
+ if (!existsSync(join(layer, ".codex", "config.toml"))) return false;
4406
+ try {
4407
+ return realpathSync(join(layer, ".codex")) !== home;
4408
+ } catch {
4409
+ return false;
4410
+ }
4411
+ });
4412
+ if (layers.length === 0) return void 0;
4413
+ const homeConfigPath = join(options.codexHome, "config.toml");
4414
+ let source = "";
4415
+ try {
4416
+ source = readFileSync(homeConfigPath, "utf8");
4417
+ } catch (error) {
4418
+ if (error.code !== "ENOENT") return void 0;
4419
+ }
4420
+ const entries = parseProjectTrustEntries(source);
4421
+ if (!entries) return void 0;
4422
+ for (const value of entries.values()) if (value !== "trusted" && value !== "untrusted") return void 0;
4423
+ const canonical = /* @__PURE__ */ new Map();
4424
+ for (const [key, value] of entries) {
4425
+ let path = key;
4426
+ try {
4427
+ path = realpathSync(key);
4428
+ } catch {}
4429
+ if (canonical.get(path) === "trusted") continue;
4430
+ canonical.set(path, value);
4431
+ }
4432
+ const rootTrusted = gitRoot !== void 0 && (canonical.get(gitRoot) === "trusted" || mainRepositoryTrusted(gitRoot, canonical));
4433
+ const ignored = layers.filter((layer) => {
4434
+ const entry = canonical.get(layer);
4435
+ if (entry !== void 0) return entry !== "trusted";
4436
+ return !rootTrusted;
4437
+ });
4438
+ if (ignored.length === 0) return void 0;
4439
+ const trustDir = gitRoot ?? cwd;
4440
+ const configs = ignored.map((layer) => join(layer, ".codex", "config.toml"));
4441
+ return `codex does not trust this directory, so ${configs.length === 1 ? `its project config (${configs[0]}) is` : `its project configs (${configs.join(", ")}) are`} being ignored — MCP servers and settings declared there will be missing from this session. To trust it, run codex once in ${trustDir} and accept the trust prompt, or add [projects."${trustDir}"] with trust_level = "trusted" to ${homeConfigPath}.`;
4442
+ }
4443
+ //#endregion
4120
4444
  //#region src/engines/codex/runner.ts
4121
4445
  /**
4122
4446
  * thread/start's sandbox axis (string form) — our permission modes as codex
@@ -4124,16 +4448,30 @@ var CodexAgentTracker = class {
4124
4448
  * the OS sandbox and — with the ask policy below — escalates to a real
4125
4449
  * question), `acceptEdits` → workspace-write (in-workspace writes sail
4126
4450
  * through, the acceptEdits grant), `bypassPermissions` → danger-full-access.
4451
+ * `auto` rides the SAME sandbox as acceptEdits — it is not a wider grant, it
4452
+ * only moves *who answers* the approvals (see {@link APPROVALS_REVIEWER_BY_MODE}).
4127
4453
  */
4128
4454
  const THREAD_SANDBOX_BY_MODE = {
4129
4455
  default: "read-only",
4130
4456
  acceptEdits: "workspace-write",
4457
+ auto: "workspace-write",
4131
4458
  bypassPermissions: "danger-full-access"
4132
4459
  };
4133
- /** turn/start's sandboxPolicy axis (object form — same policy, second shape). */
4460
+ /**
4461
+ * turn/start's sandboxPolicy axis (object form — same policy, second shape).
4462
+ *
4463
+ * The `workspaceWrite` entries here are a SHAPE, not the whole policy: every
4464
+ * unstated field of that variant is serde-defaulted by the app-server, so
4465
+ * sending it bare silently overrides the operator's `[sandbox_workspace_write]`
4466
+ * — `network_access` back to false, `writable_roots` back to empty — on every
4467
+ * turn. {@link CodexRunner.#turnSandboxPolicy} restates those fields from
4468
+ * `config/read`; nothing else may send this map's `workspaceWrite` entries
4469
+ * directly.
4470
+ */
4134
4471
  const TURN_SANDBOX_BY_MODE = {
4135
4472
  default: { type: "readOnly" },
4136
4473
  acceptEdits: { type: "workspaceWrite" },
4474
+ auto: { type: "workspaceWrite" },
4137
4475
  bypassPermissions: { type: "dangerFullAccess" }
4138
4476
  };
4139
4477
  /**
@@ -4177,8 +4515,33 @@ const THREAD_SCOPED_NOTIFICATIONS = new Set([
4177
4515
  const APPROVAL_POLICY_BY_MODE = {
4178
4516
  default: GRANULAR_ASK,
4179
4517
  acceptEdits: GRANULAR_ASK,
4518
+ auto: GRANULAR_ASK,
4180
4519
  bypassPermissions: GRANULAR_NEVER
4181
4520
  };
4521
+ /**
4522
+ * The THIRD approval axis — *who reviews*, independent of the sandbox axis and
4523
+ * the ask axis above. Codex's `approvalsReviewer` (thread/start and turn/start,
4524
+ * present since 0.146.0) routes every approval request either to the user
4525
+ * (`'user'`, codex's own default) or to `'auto_review'`: a prompted subagent
4526
+ * that gathers context and applies a risk framework before allowing or denying.
4527
+ * That is codex's "Approve for me" preset, and our `auto` mode is exactly it.
4528
+ *
4529
+ * Sent explicitly for EVERY mode rather than omitted for the default — a thread
4530
+ * inherits `approvalsReviewer` across turns ("this turn and subsequent turns"),
4531
+ * so leaving it unset would let a stale reviewer from an earlier turn survive a
4532
+ * mode switch back to a user-reviewed mode. Stating it every time makes the
4533
+ * mode the single source of truth.
4534
+ *
4535
+ * NOTE the asymmetry with the Claude engine's `auto`: that classifier is
4536
+ * operator-configurable (`autoMode.environment`, allow/soft_deny/hard_deny);
4537
+ * this reviewer has no configuration surface at all.
4538
+ */
4539
+ const APPROVALS_REVIEWER_BY_MODE = {
4540
+ default: "user",
4541
+ acceptEdits: "user",
4542
+ auto: "auto_review",
4543
+ bypassPermissions: "user"
4544
+ };
4182
4545
  /** Fallback timeout for a pending approval nobody answers — the SessionRunner
4183
4546
  * default, so unattended codex sessions land the same way Claude ones do. */
4184
4547
  const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
@@ -4596,6 +4959,8 @@ var CodexRunner = class {
4596
4959
  #turnChain = Promise.resolve();
4597
4960
  #activeTurn;
4598
4961
  #connection;
4962
+ /** Per-child, from `config/read`; undefined = read failed, send the bare shape. */
4963
+ #workspaceWrite;
4599
4964
  #threadLoaded = false;
4600
4965
  #numTurns = 0;
4601
4966
  #totalCostUsd;
@@ -4723,6 +5088,7 @@ var CodexRunner = class {
4723
5088
  start() {
4724
5089
  if (this.#started) return this.#turnChain;
4725
5090
  this.#started = true;
5091
+ this.#warnUntrustedProject();
4726
5092
  if (this.#config.resume && this.#config.backfillHistory !== false) {
4727
5093
  this.#backfillPending = true;
4728
5094
  this.#turnChain = this.#turnChain.then(() => this.#backfillHistory());
@@ -4732,6 +5098,37 @@ var CodexRunner = class {
4732
5098
  return this.#turnChain;
4733
5099
  }
4734
5100
  /**
5101
+ * One-time transcript notice for the codex trust gap: a `default`-mode
5102
+ * session (read-only sandbox) on an untrusted cwd has its
5103
+ * `.codex/config.toml` — MCP servers included — silently ignored, and the
5104
+ * app-server surface has no trust prompt to say so (the TUI's prompt is
5105
+ * where the entry normally gets written). `acceptEdits`/`bypassPermissions`
5106
+ * sessions are exempt because their `thread/start` (workspace-write /
5107
+ * danger-full-access sandbox) writes the trust entry itself and loads the
5108
+ * config — measured against 0.146.0 and 0.149.0; a notice there would be
5109
+ * false. Emitted as `session_error`, which both clients render as an inline
5110
+ * notice while the session keeps running (the backfill-history precedent),
5111
+ * so nothing new rides the wire. Every degrade path is silence: a false
5112
+ * warning on a trusted project is worse than a missed one.
5113
+ */
5114
+ #warnUntrustedProject() {
5115
+ if (this.#permissionMode !== "default") return;
5116
+ try {
5117
+ const env = this.#childEnv();
5118
+ const pin = env.CODEX_HOME;
5119
+ if (pin !== void 0 && pin.length === 0) return;
5120
+ const codexHome = pin ?? join(env.HOME ?? homedir(), ".codex");
5121
+ const message = untrustedProjectNotice({
5122
+ cwd: this.#cwd,
5123
+ codexHome
5124
+ });
5125
+ if (message) this.#emit({
5126
+ type: "session_error",
5127
+ message
5128
+ });
5129
+ } catch {}
5130
+ }
5131
+ /**
4735
5132
  * List skills over a **throwaway** connection, for a session with nothing else
4736
5133
  * to do yet.
4737
5134
  *
@@ -4940,6 +5337,48 @@ var CodexRunner = class {
4940
5337
  this.#turnChain = this.#turnChain.then(() => this.#runTurn());
4941
5338
  }
4942
5339
  /**
5340
+ * Read `[sandbox_workspace_write]` as codex resolves it for this session's
5341
+ * cwd, once per child, so {@link CodexRunner.#turnSandboxPolicy} can restate
5342
+ * it verbatim.
5343
+ *
5344
+ * Why this exists at all: `turn/start`'s object-form sandbox policy is
5345
+ * serde-defaulted field by field, so `{type: 'workspaceWrite'}` bare means
5346
+ * `networkAccess: false, writableRoots: []` NO MATTER what the operator
5347
+ * configured — and we must keep sending the object every turn, because
5348
+ * restating it is what makes a between-turns permission-mode switch take
5349
+ * effect. Measured against 0.149.0 with `network_access = true` set: the
5350
+ * bare object produced `curl: (6) Could not resolve host`, the fully-stated
5351
+ * object and an omitted policy both produced `200`. `read-only` is not
5352
+ * affected — the setting is scoped to workspace-write, as its name says, and
5353
+ * a read-only sandbox has no network either way.
5354
+ *
5355
+ * A failure here is not fatal: `#workspaceWrite` stays undefined and we send
5356
+ * the bare shape, which is exactly the behaviour that shipped before.
5357
+ */
5358
+ async #readWorkspaceWrite(connection) {
5359
+ this.#workspaceWrite = void 0;
5360
+ try {
5361
+ const block = (await connection.request("config/read", { cwd: this.#cwd }))?.config?.sandbox_workspace_write;
5362
+ if (!block) return;
5363
+ const roots = block.writable_roots;
5364
+ this.#workspaceWrite = {
5365
+ writableRoots: Array.isArray(roots) ? roots.filter((r) => typeof r === "string") : [],
5366
+ networkAccess: block.network_access === true,
5367
+ excludeTmpdirEnvVar: block.exclude_tmpdir_env_var === true,
5368
+ excludeSlashTmp: block.exclude_slash_tmp === true
5369
+ };
5370
+ } catch {}
5371
+ }
5372
+ /** The mode's turn-level sandbox policy, with the operator's workspace-write settings intact. */
5373
+ #turnSandboxPolicy() {
5374
+ const policy = TURN_SANDBOX_BY_MODE[this.#permissionMode];
5375
+ if (policy?.type !== "workspaceWrite" || !this.#workspaceWrite) return policy;
5376
+ return {
5377
+ type: "workspaceWrite",
5378
+ ...this.#workspaceWrite
5379
+ };
5380
+ }
5381
+ /**
4943
5382
  * The session's live connection with its thread loaded, (re)building both as
4944
5383
  * needed: spawn + `initialize`/`initialized` on a fresh child, then
4945
5384
  * `thread/start` (new) or `thread/resume` (a create-request `resume`, or a
@@ -4983,12 +5422,14 @@ var CodexRunner = class {
4983
5422
  throw error;
4984
5423
  }
4985
5424
  connection.notify("initialized");
5425
+ await this.#readWorkspaceWrite(connection);
4986
5426
  }
4987
5427
  if (!this.#threadLoaded) {
4988
5428
  const options = {
4989
5429
  cwd: this.#cwd,
4990
5430
  approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
4991
- sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode]
5431
+ sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode],
5432
+ approvalsReviewer: APPROVALS_REVIEWER_BY_MODE[this.#permissionMode]
4992
5433
  };
4993
5434
  if (this.#model) options.model = this.#model;
4994
5435
  const resuming = this.#sdkSessionId !== void 0;
@@ -5231,7 +5672,8 @@ var CodexRunner = class {
5231
5672
  input: turn.input,
5232
5673
  cwd: this.#cwd,
5233
5674
  approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
5234
- sandboxPolicy: TURN_SANDBOX_BY_MODE[this.#permissionMode]
5675
+ sandboxPolicy: this.#turnSandboxPolicy(),
5676
+ approvalsReviewer: APPROVALS_REVIEWER_BY_MODE[this.#permissionMode]
5235
5677
  };
5236
5678
  const model = this.#model ?? this.#resolvedModel;
5237
5679
  if (model) params.model = model;
@@ -5965,7 +6407,15 @@ var CodexRunner = class {
5965
6407
  * const c=JSON.parse(d.slice(s,i));
5966
6408
  * for(const m of c.models) console.log(m.slug, m.display_name,
5967
6409
  * m.visibility, m.supported_reasoning_levels.map(l=>l.effort).join(","))'\
5968
- * "$(node -p 'require.resolve("@openai/codex-darwin-arm64/package.json").replace("package.json","vendor/aarch64-apple-darwin/bin/codex")')"
6410
+ * "$(node -p 'const{createRequire}=require("module");
6411
+ * const w=require.resolve("@openai/codex/package.json");
6412
+ * createRequire(w).resolve("@openai/codex-darwin-arm64/package.json")
6413
+ * .replace("package.json","vendor/aarch64-apple-darwin/bin/codex")')"
6414
+ *
6415
+ * The two-hop resolve is NOT optional: under pnpm's strict layout the platform
6416
+ * package is a dependency of `@openai/codex`, so it resolves only from that
6417
+ * wrapper's location, never from the repo root. Resolving it directly throws
6418
+ * MODULE_NOT_FOUND — the same two hops `resolveBundledCodexExecutable` makes.
5969
6419
  *
5970
6420
  * Mapping decisions:
5971
6421
  * - the internal `codex-auto-review` row is dropped (the codex analogue of
@@ -5977,7 +6427,7 @@ var CodexRunner = class {
5977
6427
  * `max`/`ultra` beyond the SDK union; trust the binary, keep strings open.
5978
6428
  */
5979
6429
  const CODEX_CATALOG = {
5980
- provenance: "embedded model presets of @openai/codex@0.146.0 (darwin-arm64 binary), extracted 2026-08-05",
6430
+ provenance: "embedded model presets of @openai/codex@0.149.0 (darwin-arm64 binary), extracted 2026-08-22",
5981
6431
  models: [
5982
6432
  {
5983
6433
  value: "gpt-5.6-sol",