@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 +9 -1
- package/build/index.mjs +458 -8
- package/build/index.mjs.map +1 -1
- package/package.json +5 -5
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
|
|
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
|
-
/**
|
|
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:
|
|
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
|
|
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.
|
|
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",
|