@workerdeck/core 0.18.0 → 0.20.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 +98 -3
- package/build/index.mjs +640 -16
- package/build/index.mjs.map +1 -1
- package/package.json +5 -5
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. */
|
|
@@ -607,8 +607,12 @@ function staleReplaySeqs(events, afterSeq) {
|
|
|
607
607
|
* 1. `afterSeq` — the caller already holds everything at or below it.
|
|
608
608
|
* 2. `resetSeq` — transcript *content* strictly below the latest
|
|
609
609
|
* `conversation_reset` is skipped, so a re-attach cannot resurrect a cleared
|
|
610
|
-
* conversation while state events still replay.
|
|
611
|
-
*
|
|
610
|
+
* conversation while state events still replay. **Every engine that can emit
|
|
611
|
+
* a reset must track it and pass it** — this was Claude's alone only for as
|
|
612
|
+
* long as Claude's was the only engine that could produce the event, and the
|
|
613
|
+
* failure when a runner forgets is quiet: the end state is right for a
|
|
614
|
+
* current reducer, so nothing looks broken while every attach re-sends the
|
|
615
|
+
* whole cleared conversation for the process's lifetime.
|
|
612
616
|
* 3. `coalesceReplay` — last-write-wins state readings superseded later in the
|
|
613
617
|
* same replay (`staleReplaySeqs`), plus everything `replayRetains` says the
|
|
614
618
|
* reducer reads and discards. Opt-in, and only sound for a consumer whose
|
|
@@ -1309,6 +1313,28 @@ var SessionRunner = class {
|
|
|
1309
1313
|
async interrupt() {
|
|
1310
1314
|
await this.#query?.interrupt();
|
|
1311
1315
|
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Reset the conversation by sending the `/clear` the CLI already honors.
|
|
1318
|
+
*
|
|
1319
|
+
* Deliberately not a second mechanism: this engine's reset arrives *from the
|
|
1320
|
+
* SDK*, and `normalizeSdkMessage` turns the CLI's report of it into the
|
|
1321
|
+
* `conversation_reset` event (adopting the new conversation id and re-polling
|
|
1322
|
+
* context usage on the way through). Reimplementing the clear here would give
|
|
1323
|
+
* one engine two ways to reach the same state, and only one of them would get
|
|
1324
|
+
* the id adoption right. So the command and the composer's `/clear` are one
|
|
1325
|
+
* behaviour, and this method is the thin end of it.
|
|
1326
|
+
*
|
|
1327
|
+
* The one place it differs from the other two engines: this resolves when the
|
|
1328
|
+
* `/clear` has been **handed to the CLI**, not when the reset has happened —
|
|
1329
|
+
* the CLI queues its own streamed input, so waiting is its job, and there is
|
|
1330
|
+
* no chain here to ride. The observable contract is the same (a clear sent
|
|
1331
|
+
* mid-turn queues rather than cutting the turn short); only the moment the
|
|
1332
|
+
* promise settles is weaker, and no caller depends on it.
|
|
1333
|
+
*/
|
|
1334
|
+
async clearContext() {
|
|
1335
|
+
if (this.#status === "closed" || this.#status === "failed") throw new Error("session is closed");
|
|
1336
|
+
this.sendMessage("/clear");
|
|
1337
|
+
}
|
|
1312
1338
|
async setPermissionMode(mode) {
|
|
1313
1339
|
await this.#query?.setPermissionMode(mode);
|
|
1314
1340
|
this.#permissionMode = mode;
|
|
@@ -1836,6 +1862,15 @@ var AiSdkRunner = class {
|
|
|
1836
1862
|
*/
|
|
1837
1863
|
#contextUsage;
|
|
1838
1864
|
#activityCount = 0;
|
|
1865
|
+
/**
|
|
1866
|
+
* Seq of the latest `conversation_reset` event, 0 when none. The log itself is
|
|
1867
|
+
* never truncated — it still carries the state-bearing events (`capabilities`,
|
|
1868
|
+
* `system_init`, …) a fresh attacher depends on and which are not re-emitted —
|
|
1869
|
+
* but `subscribe()` skips transcript *content* strictly below this mark, so a
|
|
1870
|
+
* replay does not resurrect a cleared conversation. A later reset supersedes
|
|
1871
|
+
* an earlier one by overwriting it.
|
|
1872
|
+
*/
|
|
1873
|
+
#resetSeq = 0;
|
|
1839
1874
|
#status = "starting";
|
|
1840
1875
|
#permissionMode;
|
|
1841
1876
|
#messages = [];
|
|
@@ -1887,8 +1922,10 @@ var AiSdkRunner = class {
|
|
|
1887
1922
|
this.#activityCount = 0;
|
|
1888
1923
|
for (const event of this.#events) {
|
|
1889
1924
|
this.#activityCount += transcriptActivity(event);
|
|
1890
|
-
if (event.type === "conversation_reset")
|
|
1891
|
-
|
|
1925
|
+
if (event.type === "conversation_reset") {
|
|
1926
|
+
this.#resetSeq = event.seq;
|
|
1927
|
+
this.#contextUsage = void 0;
|
|
1928
|
+
} else this.#contextUsage = contextReading(event) ?? this.#contextUsage;
|
|
1892
1929
|
}
|
|
1893
1930
|
this.#messages = [...state.messages];
|
|
1894
1931
|
for (const call of state.pendingToolCalls) this.#pendingToolCalls.set(call.toolCallId, call);
|
|
@@ -2158,6 +2195,30 @@ var AiSdkRunner = class {
|
|
|
2158
2195
|
}
|
|
2159
2196
|
return result.text;
|
|
2160
2197
|
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Reset the conversation: drop the message array the next turn would have
|
|
2200
|
+
* been built from. There is no engine round trip — this runner *is* where the
|
|
2201
|
+
* transcript lives, so clearing it is the whole operation.
|
|
2202
|
+
*
|
|
2203
|
+
* Two things ride along, both already written elsewhere and both load-bearing
|
|
2204
|
+
* here. `#emit`'s `conversation_reset` arm retires `#contextUsage` (the
|
|
2205
|
+
* reading described a conversation that no longer exists), and the same arm
|
|
2206
|
+
* in `restore` keeps a parked session that comes back after a clear from
|
|
2207
|
+
* resurrecting it. Pending tool calls are NOT swept: a parked call is work a
|
|
2208
|
+
* backend still owes an answer for, and a clear is not an interrupt — the
|
|
2209
|
+
* refusal below is what keeps the two apart.
|
|
2210
|
+
*/
|
|
2211
|
+
async clearContext() {
|
|
2212
|
+
if (this.#status === "closed" || this.#status === "failed") throw new Error("session is closed");
|
|
2213
|
+
const run = this.#turnChain.then(() => {
|
|
2214
|
+
if (this.#closed) throw new Error("session is closed");
|
|
2215
|
+
if (this.#pendingToolCalls.size > 0) throw new Error("cannot clear context while tool calls are outstanding");
|
|
2216
|
+
this.#messages = [];
|
|
2217
|
+
this.#emit({ type: "conversation_reset" });
|
|
2218
|
+
});
|
|
2219
|
+
this.#turnChain = run.then(() => void 0, () => void 0);
|
|
2220
|
+
await run;
|
|
2221
|
+
}
|
|
2161
2222
|
async interrupt() {
|
|
2162
2223
|
if (this.#abort) this.#abort.abort();
|
|
2163
2224
|
else if (this.#pendingToolCalls.size > 0) {
|
|
@@ -2238,7 +2299,7 @@ var AiSdkRunner = class {
|
|
|
2238
2299
|
return this.#events.find((event) => event.seq === seq);
|
|
2239
2300
|
}
|
|
2240
2301
|
subscribe(listener, afterSeq = 0, options) {
|
|
2241
|
-
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
|
|
2302
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
|
|
2242
2303
|
}
|
|
2243
2304
|
#scheduleTurn() {
|
|
2244
2305
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
@@ -2628,7 +2689,10 @@ var AiSdkRunner = class {
|
|
|
2628
2689
|
this.#lastActivityAt = event.ts;
|
|
2629
2690
|
this.#activityCount += transcriptActivity(body);
|
|
2630
2691
|
this.#contextUsage = contextReading(body) ?? this.#contextUsage;
|
|
2631
|
-
if (body.type === "conversation_reset")
|
|
2692
|
+
if (body.type === "conversation_reset") {
|
|
2693
|
+
this.#resetSeq = event.seq;
|
|
2694
|
+
this.#contextUsage = void 0;
|
|
2695
|
+
}
|
|
2632
2696
|
this.#events.push(event);
|
|
2633
2697
|
this.#subscribers.emit(event);
|
|
2634
2698
|
}
|
|
@@ -4100,6 +4164,25 @@ var CodexAgentTracker = class {
|
|
|
4100
4164
|
sweep() {
|
|
4101
4165
|
for (const record of this.#byThread.values()) if (record.status === "running") this.#settle(record, "failed");
|
|
4102
4166
|
}
|
|
4167
|
+
/**
|
|
4168
|
+
* The conversation these agents belonged to is gone (a `conversation_reset`).
|
|
4169
|
+
*
|
|
4170
|
+
* Deliberately NOT {@link CodexAgentTracker.sweep}: that settles the running
|
|
4171
|
+
* ones as failed and keeps the rows, which is right when the *process* dies —
|
|
4172
|
+
* the transcript still holds the anchor `tool_use` each row points at, and a
|
|
4173
|
+
* row that vanished would leave that card unexplained. A clear is the other
|
|
4174
|
+
* way round. The anchors go with the transcript, so a surviving row would
|
|
4175
|
+
* publish a `toolUseId` that resolves to nothing — and clients key a
|
|
4176
|
+
* pressable, enterable agent line off exactly that id.
|
|
4177
|
+
*/
|
|
4178
|
+
forget() {
|
|
4179
|
+
this.#byThread.clear();
|
|
4180
|
+
}
|
|
4181
|
+
/** The thread ids currently tracked — what a clear remembers so a still-running
|
|
4182
|
+
* agent's later traffic can be dropped rather than re-anchored. */
|
|
4183
|
+
threadIds() {
|
|
4184
|
+
return Array.from(this.#byThread.keys());
|
|
4185
|
+
}
|
|
4103
4186
|
/** The rollup as `SessionInfo.subagents` serves it — spawn order, fresh
|
|
4104
4187
|
* objects, and `undefined` when there is nothing to say (absent and empty
|
|
4105
4188
|
* mean the same thing to a client, and bytes on a polled list are paid for). */
|
|
@@ -4117,6 +4200,330 @@ var CodexAgentTracker = class {
|
|
|
4117
4200
|
}
|
|
4118
4201
|
};
|
|
4119
4202
|
//#endregion
|
|
4203
|
+
//#region src/engines/codex/trust.ts
|
|
4204
|
+
/**
|
|
4205
|
+
* Codex project trust: will this session's cwd get its `.codex/config.toml`?
|
|
4206
|
+
*
|
|
4207
|
+
* Codex only layers a project's `.codex/config.toml` onto the operator's base
|
|
4208
|
+
* config when the project is *trusted* (a `[projects."<path>"]
|
|
4209
|
+
* trust_level = "trusted"` entry in `$CODEX_HOME/config.toml`), and the
|
|
4210
|
+
* app-server surface has no trust prompt — that lives in the TUI. So under
|
|
4211
|
+
* WorkerDeck an untrusted project's config, MCP servers included, is silently
|
|
4212
|
+
* ignored: no error, no notice, servers just missing. The runner asks this
|
|
4213
|
+
* module at session start whether that is about to happen, so the transcript
|
|
4214
|
+
* can say so.
|
|
4215
|
+
*
|
|
4216
|
+
* Semantics, all measured against both the bundled 0.146.0 and 0.149.0
|
|
4217
|
+
* (2026-08-22, via `codex mcp list` from probe cwds and via `thread/start` +
|
|
4218
|
+
* `mcpServerStatus/list` on the app-server surface — identical answers):
|
|
4219
|
+
*
|
|
4220
|
+
* - **Discovery**: config layers come from the cwd and its ancestors up to and
|
|
4221
|
+
* including the nearest directory containing `.git` (dir or file). With no
|
|
4222
|
+
* git anywhere above, the cwd alone is consulted. Directories above the
|
|
4223
|
+
* nearest git root never contribute, trusted or not.
|
|
4224
|
+
* - **Trust per layer**: an exact entry for the layer's own canonical path
|
|
4225
|
+
* decides (an explicit `"untrusted"` beats inherited trust); without one the
|
|
4226
|
+
* layer inherits from the chain's git root — trusted iff the git root has a
|
|
4227
|
+
* trusted entry, where a linked worktree's root also counts its main
|
|
4228
|
+
* repository's entry (the `.git` file's gitdir names it). A trusted
|
|
4229
|
+
* mid-chain directory does NOT trust its children, and plain path
|
|
4230
|
+
* containment without git confers nothing.
|
|
4231
|
+
* - **Canonical paths**: codex matches entries against the canonicalized cwd —
|
|
4232
|
+
* a macOS `/tmp/...` entry never matches the `/private/tmp/...` it points
|
|
4233
|
+
* at, while the reverse spelling works (and the app-server canonicalizes its
|
|
4234
|
+
* `cwd` param too). Both sides here are realpath'd, which can only err
|
|
4235
|
+
* toward silence.
|
|
4236
|
+
* - **The gate is sandbox-scoped**: `thread/start` under `workspace-write` or
|
|
4237
|
+
* `danger-full-access` (permission modes `acceptEdits`/`bypassPermissions`)
|
|
4238
|
+
* WRITES the trust entry itself and loads the config — only `read-only`
|
|
4239
|
+
* (mode `default`) leaves the project untrusted and the config ignored. A
|
|
4240
|
+
* later `turn/start` with a wider sandboxPolicy does not heal the thread
|
|
4241
|
+
* (measured): the caller probes `default`-mode sessions only, and the notice
|
|
4242
|
+
* stays true for the session it opens.
|
|
4243
|
+
* - `trust_level`'s vocabulary is exactly `trusted`/`untrusted`; any other
|
|
4244
|
+
* value fails codex's bootstrap outright ("unknown variant"), so a config
|
|
4245
|
+
* carrying one probes silent — that session announces its own failure.
|
|
4246
|
+
*
|
|
4247
|
+
* The correctness bar for every degrade path: a FALSE notice — warning about a
|
|
4248
|
+
* project codex actually trusts — is worse than a missed one. The narrow TOML
|
|
4249
|
+
* reader below refuses (→ silence) anything it cannot interpret with
|
|
4250
|
+
* certainty, rather than guessing.
|
|
4251
|
+
*/
|
|
4252
|
+
const BARE_KEY = /[A-Za-z0-9_-]/;
|
|
4253
|
+
function skipWs(text, pos) {
|
|
4254
|
+
let i = pos;
|
|
4255
|
+
while (i < text.length && (text[i] === " " || text[i] === " ")) i++;
|
|
4256
|
+
return i;
|
|
4257
|
+
}
|
|
4258
|
+
/** One-line TOML basic string starting at `pos` (which must be `"`). Undefined
|
|
4259
|
+
* on an escape TOML doesn't define or a close quote that never comes — the
|
|
4260
|
+
* caller refuses the file rather than guessing what codex would read. */
|
|
4261
|
+
function parseBasicString(text, pos) {
|
|
4262
|
+
let out = "";
|
|
4263
|
+
let i = pos + 1;
|
|
4264
|
+
while (i < text.length) {
|
|
4265
|
+
const ch = text[i];
|
|
4266
|
+
if (ch === "\"") return {
|
|
4267
|
+
value: out,
|
|
4268
|
+
end: i + 1
|
|
4269
|
+
};
|
|
4270
|
+
if (ch === "\\") {
|
|
4271
|
+
const esc = text[i + 1];
|
|
4272
|
+
if (esc === "b") out += "\b";
|
|
4273
|
+
else if (esc === "t") out += " ";
|
|
4274
|
+
else if (esc === "n") out += "\n";
|
|
4275
|
+
else if (esc === "f") out += "\f";
|
|
4276
|
+
else if (esc === "r") out += "\r";
|
|
4277
|
+
else if (esc === "\"") out += "\"";
|
|
4278
|
+
else if (esc === "\\") out += "\\";
|
|
4279
|
+
else if (esc === "u" || esc === "U") {
|
|
4280
|
+
const width = esc === "u" ? 4 : 8;
|
|
4281
|
+
const hex = text.slice(i + 2, i + 2 + width);
|
|
4282
|
+
if (hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex)) return void 0;
|
|
4283
|
+
const code = Number.parseInt(hex, 16);
|
|
4284
|
+
if (code > 1114111) return void 0;
|
|
4285
|
+
out += String.fromCodePoint(code);
|
|
4286
|
+
i += width;
|
|
4287
|
+
} else return void 0;
|
|
4288
|
+
i += 2;
|
|
4289
|
+
continue;
|
|
4290
|
+
}
|
|
4291
|
+
out += ch;
|
|
4292
|
+
i++;
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
/** One-line TOML literal string starting at `pos` (which must be `'`). */
|
|
4296
|
+
function parseLiteralString(text, pos) {
|
|
4297
|
+
const close = text.indexOf("'", pos + 1);
|
|
4298
|
+
if (close === -1) return void 0;
|
|
4299
|
+
return {
|
|
4300
|
+
value: text.slice(pos + 1, close),
|
|
4301
|
+
end: close + 1
|
|
4302
|
+
};
|
|
4303
|
+
}
|
|
4304
|
+
/** A dotted key path — bare, `"basic"` and `'literal'` keys, whitespace around
|
|
4305
|
+
* the dots — as found in table headers and on the left of assignments. */
|
|
4306
|
+
function parseKeyPath(text, pos) {
|
|
4307
|
+
const keys = [];
|
|
4308
|
+
let i = pos;
|
|
4309
|
+
for (;;) {
|
|
4310
|
+
i = skipWs(text, i);
|
|
4311
|
+
const ch = text[i];
|
|
4312
|
+
if (ch === "\"" || ch === "'") {
|
|
4313
|
+
const str = ch === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
|
|
4314
|
+
if (!str) return void 0;
|
|
4315
|
+
keys.push(str.value);
|
|
4316
|
+
i = str.end;
|
|
4317
|
+
} else if (ch !== void 0 && BARE_KEY.test(ch)) {
|
|
4318
|
+
let end = i;
|
|
4319
|
+
while (end < text.length && BARE_KEY.test(text[end])) end++;
|
|
4320
|
+
keys.push(text.slice(i, end));
|
|
4321
|
+
i = end;
|
|
4322
|
+
} else return;
|
|
4323
|
+
i = skipWs(text, i);
|
|
4324
|
+
if (text[i] !== ".") return {
|
|
4325
|
+
value: keys,
|
|
4326
|
+
end: i
|
|
4327
|
+
};
|
|
4328
|
+
i++;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
/**
|
|
4332
|
+
* Scan an assignment's value (or the continuation line of a multi-line array),
|
|
4333
|
+
* confirming where it ends. Returns the bracket depth carried onto the next
|
|
4334
|
+
* line (0 = the value is complete) plus the string itself when the whole value
|
|
4335
|
+
* was one plain one-line string. Undefined refuses the file: multi-line
|
|
4336
|
+
* strings are where a line reader starts misreading string *content* as
|
|
4337
|
+
* sections and entries — the exact mistake that could flip a real trust entry
|
|
4338
|
+
* — so they are not parsed around, they end the attempt.
|
|
4339
|
+
*/
|
|
4340
|
+
function scanValueLine(text, pos, depth) {
|
|
4341
|
+
let i = skipWs(text, pos);
|
|
4342
|
+
if (depth === 0 && (text[i] === "\"" || text[i] === "'")) {
|
|
4343
|
+
if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return void 0;
|
|
4344
|
+
const str = text[i] === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
|
|
4345
|
+
if (!str) return void 0;
|
|
4346
|
+
const rest = skipWs(text, str.end);
|
|
4347
|
+
if (rest < text.length && text[rest] !== "#") return void 0;
|
|
4348
|
+
return {
|
|
4349
|
+
depth: 0,
|
|
4350
|
+
value: str.value
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
while (i < text.length) {
|
|
4354
|
+
const ch = text[i];
|
|
4355
|
+
if (ch === "#") break;
|
|
4356
|
+
if (ch === "\"" || ch === "'") {
|
|
4357
|
+
if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return void 0;
|
|
4358
|
+
const str = ch === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
|
|
4359
|
+
if (!str) return void 0;
|
|
4360
|
+
i = str.end;
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
if (ch === "[" || ch === "{") depth++;
|
|
4364
|
+
else if (ch === "]" || ch === "}") {
|
|
4365
|
+
depth--;
|
|
4366
|
+
if (depth < 0) return void 0;
|
|
4367
|
+
}
|
|
4368
|
+
i++;
|
|
4369
|
+
}
|
|
4370
|
+
return { depth };
|
|
4371
|
+
}
|
|
4372
|
+
/**
|
|
4373
|
+
* The `[projects."<path>"] trust_level = "..."` entries of a codex
|
|
4374
|
+
* `config.toml`, by a deliberately narrow reader (core takes no TOML
|
|
4375
|
+
* dependency for this). Handles what codex itself writes plus the reasonable
|
|
4376
|
+
* hand-edits — comments, CRLF, whitespace, quoted keys with escapes, literal
|
|
4377
|
+
* and bare keys, `[projects]`-with-dotted-keys and top-level dotted forms,
|
|
4378
|
+
* single-line inline tables, multi-line arrays — and returns **undefined for
|
|
4379
|
+
* anything else it meets anywhere in the file** (multi-line strings,
|
|
4380
|
+
* `projects` as an inline table, array-of-tables, junk): the caller treats
|
|
4381
|
+
* undefined as "cannot know" and stays silent. Conflicting duplicate entries
|
|
4382
|
+
* also refuse — invalid for TOML, and guessing wrong is a false notice.
|
|
4383
|
+
*/
|
|
4384
|
+
function parseProjectTrustEntries(source) {
|
|
4385
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4386
|
+
let section = [];
|
|
4387
|
+
let carryDepth = 0;
|
|
4388
|
+
for (const rawLine of source.split("\n")) {
|
|
4389
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
4390
|
+
if (carryDepth > 0) {
|
|
4391
|
+
const scanned = scanValueLine(line, 0, carryDepth);
|
|
4392
|
+
if (!scanned) return void 0;
|
|
4393
|
+
carryDepth = scanned.depth;
|
|
4394
|
+
continue;
|
|
4395
|
+
}
|
|
4396
|
+
const start = skipWs(line, 0);
|
|
4397
|
+
if (start >= line.length || line[start] === "#") continue;
|
|
4398
|
+
if (line[start] === "[") {
|
|
4399
|
+
const array = line.startsWith("[[", start);
|
|
4400
|
+
const path = parseKeyPath(line, start + (array ? 2 : 1));
|
|
4401
|
+
if (!path) return void 0;
|
|
4402
|
+
const close = array ? "]]" : "]";
|
|
4403
|
+
if (!line.startsWith(close, path.end)) return void 0;
|
|
4404
|
+
const rest = skipWs(line, path.end + close.length);
|
|
4405
|
+
if (rest < line.length && line[rest] !== "#") return void 0;
|
|
4406
|
+
if (array && path.value[0] === "projects") return void 0;
|
|
4407
|
+
section = path.value;
|
|
4408
|
+
continue;
|
|
4409
|
+
}
|
|
4410
|
+
const key = parseKeyPath(line, start);
|
|
4411
|
+
if (!key) return void 0;
|
|
4412
|
+
if (line[key.end] !== "=") return void 0;
|
|
4413
|
+
const scanned = scanValueLine(line, key.end + 1, 0);
|
|
4414
|
+
if (!scanned) return void 0;
|
|
4415
|
+
carryDepth = scanned.depth;
|
|
4416
|
+
const full = [...section, ...key.value];
|
|
4417
|
+
if (full[0] !== "projects") continue;
|
|
4418
|
+
if (full.length < 3) return void 0;
|
|
4419
|
+
if (full.length === 3 && full[2] === "trust_level") {
|
|
4420
|
+
if (carryDepth !== 0 || scanned.value === void 0) return void 0;
|
|
4421
|
+
const project = full[1];
|
|
4422
|
+
const existing = entries.get(project);
|
|
4423
|
+
if (existing !== void 0 && existing !== scanned.value) return void 0;
|
|
4424
|
+
entries.set(project, scanned.value);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
if (carryDepth > 0) return void 0;
|
|
4428
|
+
return entries;
|
|
4429
|
+
}
|
|
4430
|
+
/**
|
|
4431
|
+
* A linked worktree inherits trust from its main repository's entry (measured:
|
|
4432
|
+
* trusting the main repo path loads the worktree's project config). The
|
|
4433
|
+
* worktree's `.git` is a FILE whose `gitdir:` line names
|
|
4434
|
+
* `<main>/.git/worktrees/<name>`; the directory owning that `.git` is the
|
|
4435
|
+
* anchor to look up. Anything unreadable or shaped differently resolves false
|
|
4436
|
+
* — this route can only ADD trust, i.e. silence, never a false notice.
|
|
4437
|
+
*/
|
|
4438
|
+
function mainRepositoryTrusted(gitRootDir, canonical) {
|
|
4439
|
+
const gitPath = join(gitRootDir, ".git");
|
|
4440
|
+
try {
|
|
4441
|
+
if (!statSync(gitPath).isFile()) return false;
|
|
4442
|
+
const match = /^gitdir:[ \t]*(.+?)[ \t]*$/m.exec(readFileSync(gitPath, "utf8"));
|
|
4443
|
+
if (!match) return false;
|
|
4444
|
+
const gitdir = resolve(gitRootDir, match[1]);
|
|
4445
|
+
const at = gitdir.lastIndexOf(`${sep}.git${sep}`);
|
|
4446
|
+
if (at <= 0) return false;
|
|
4447
|
+
let main = gitdir.slice(0, at);
|
|
4448
|
+
try {
|
|
4449
|
+
main = realpathSync(main);
|
|
4450
|
+
} catch {}
|
|
4451
|
+
return canonical.get(main) === "trusted";
|
|
4452
|
+
} catch {
|
|
4453
|
+
return false;
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
/**
|
|
4457
|
+
* The notice for a codex session about to run on a cwd whose
|
|
4458
|
+
* `.codex/config.toml` codex will ignore, or undefined when there is nothing
|
|
4459
|
+
* to say — no project config anywhere codex would look, the project is
|
|
4460
|
+
* trusted, or the situation cannot be established with certainty. Read-only
|
|
4461
|
+
* throughout: WorkerDeck never writes trust entries (adjacent to the auth red
|
|
4462
|
+
* lines — trusting a directory is the operator's decision, made in codex's
|
|
4463
|
+
* own prompt or by their own hand).
|
|
4464
|
+
*/
|
|
4465
|
+
function untrustedProjectNotice(options) {
|
|
4466
|
+
let cwd;
|
|
4467
|
+
try {
|
|
4468
|
+
cwd = realpathSync(options.cwd);
|
|
4469
|
+
} catch {
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
let home = resolve(options.codexHome);
|
|
4473
|
+
try {
|
|
4474
|
+
home = realpathSync(options.codexHome);
|
|
4475
|
+
} catch {}
|
|
4476
|
+
const chain = [];
|
|
4477
|
+
let dir = cwd;
|
|
4478
|
+
for (;;) {
|
|
4479
|
+
chain.push(dir);
|
|
4480
|
+
if (existsSync(join(dir, ".git"))) break;
|
|
4481
|
+
const parent = dirname(dir);
|
|
4482
|
+
if (parent === dir) break;
|
|
4483
|
+
dir = parent;
|
|
4484
|
+
}
|
|
4485
|
+
const anchor = chain[chain.length - 1];
|
|
4486
|
+
const gitRoot = existsSync(join(anchor, ".git")) ? anchor : void 0;
|
|
4487
|
+
const layers = (gitRoot ? chain : [cwd]).filter((layer) => {
|
|
4488
|
+
if (!existsSync(join(layer, ".codex", "config.toml"))) return false;
|
|
4489
|
+
try {
|
|
4490
|
+
return realpathSync(join(layer, ".codex")) !== home;
|
|
4491
|
+
} catch {
|
|
4492
|
+
return false;
|
|
4493
|
+
}
|
|
4494
|
+
});
|
|
4495
|
+
if (layers.length === 0) return void 0;
|
|
4496
|
+
const homeConfigPath = join(options.codexHome, "config.toml");
|
|
4497
|
+
let source = "";
|
|
4498
|
+
try {
|
|
4499
|
+
source = readFileSync(homeConfigPath, "utf8");
|
|
4500
|
+
} catch (error) {
|
|
4501
|
+
if (error.code !== "ENOENT") return void 0;
|
|
4502
|
+
}
|
|
4503
|
+
const entries = parseProjectTrustEntries(source);
|
|
4504
|
+
if (!entries) return void 0;
|
|
4505
|
+
for (const value of entries.values()) if (value !== "trusted" && value !== "untrusted") return void 0;
|
|
4506
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
4507
|
+
for (const [key, value] of entries) {
|
|
4508
|
+
let path = key;
|
|
4509
|
+
try {
|
|
4510
|
+
path = realpathSync(key);
|
|
4511
|
+
} catch {}
|
|
4512
|
+
if (canonical.get(path) === "trusted") continue;
|
|
4513
|
+
canonical.set(path, value);
|
|
4514
|
+
}
|
|
4515
|
+
const rootTrusted = gitRoot !== void 0 && (canonical.get(gitRoot) === "trusted" || mainRepositoryTrusted(gitRoot, canonical));
|
|
4516
|
+
const ignored = layers.filter((layer) => {
|
|
4517
|
+
const entry = canonical.get(layer);
|
|
4518
|
+
if (entry !== void 0) return entry !== "trusted";
|
|
4519
|
+
return !rootTrusted;
|
|
4520
|
+
});
|
|
4521
|
+
if (ignored.length === 0) return void 0;
|
|
4522
|
+
const trustDir = gitRoot ?? cwd;
|
|
4523
|
+
const configs = ignored.map((layer) => join(layer, ".codex", "config.toml"));
|
|
4524
|
+
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}.`;
|
|
4525
|
+
}
|
|
4526
|
+
//#endregion
|
|
4120
4527
|
//#region src/engines/codex/runner.ts
|
|
4121
4528
|
/**
|
|
4122
4529
|
* thread/start's sandbox axis (string form) — our permission modes as codex
|
|
@@ -4124,16 +4531,30 @@ var CodexAgentTracker = class {
|
|
|
4124
4531
|
* the OS sandbox and — with the ask policy below — escalates to a real
|
|
4125
4532
|
* question), `acceptEdits` → workspace-write (in-workspace writes sail
|
|
4126
4533
|
* through, the acceptEdits grant), `bypassPermissions` → danger-full-access.
|
|
4534
|
+
* `auto` rides the SAME sandbox as acceptEdits — it is not a wider grant, it
|
|
4535
|
+
* only moves *who answers* the approvals (see {@link APPROVALS_REVIEWER_BY_MODE}).
|
|
4127
4536
|
*/
|
|
4128
4537
|
const THREAD_SANDBOX_BY_MODE = {
|
|
4129
4538
|
default: "read-only",
|
|
4130
4539
|
acceptEdits: "workspace-write",
|
|
4540
|
+
auto: "workspace-write",
|
|
4131
4541
|
bypassPermissions: "danger-full-access"
|
|
4132
4542
|
};
|
|
4133
|
-
/**
|
|
4543
|
+
/**
|
|
4544
|
+
* turn/start's sandboxPolicy axis (object form — same policy, second shape).
|
|
4545
|
+
*
|
|
4546
|
+
* The `workspaceWrite` entries here are a SHAPE, not the whole policy: every
|
|
4547
|
+
* unstated field of that variant is serde-defaulted by the app-server, so
|
|
4548
|
+
* sending it bare silently overrides the operator's `[sandbox_workspace_write]`
|
|
4549
|
+
* — `network_access` back to false, `writable_roots` back to empty — on every
|
|
4550
|
+
* turn. {@link CodexRunner.#turnSandboxPolicy} restates those fields from
|
|
4551
|
+
* `config/read`; nothing else may send this map's `workspaceWrite` entries
|
|
4552
|
+
* directly.
|
|
4553
|
+
*/
|
|
4134
4554
|
const TURN_SANDBOX_BY_MODE = {
|
|
4135
4555
|
default: { type: "readOnly" },
|
|
4136
4556
|
acceptEdits: { type: "workspaceWrite" },
|
|
4557
|
+
auto: { type: "workspaceWrite" },
|
|
4137
4558
|
bypassPermissions: { type: "dangerFullAccess" }
|
|
4138
4559
|
};
|
|
4139
4560
|
/**
|
|
@@ -4177,8 +4598,33 @@ const THREAD_SCOPED_NOTIFICATIONS = new Set([
|
|
|
4177
4598
|
const APPROVAL_POLICY_BY_MODE = {
|
|
4178
4599
|
default: GRANULAR_ASK,
|
|
4179
4600
|
acceptEdits: GRANULAR_ASK,
|
|
4601
|
+
auto: GRANULAR_ASK,
|
|
4180
4602
|
bypassPermissions: GRANULAR_NEVER
|
|
4181
4603
|
};
|
|
4604
|
+
/**
|
|
4605
|
+
* The THIRD approval axis — *who reviews*, independent of the sandbox axis and
|
|
4606
|
+
* the ask axis above. Codex's `approvalsReviewer` (thread/start and turn/start,
|
|
4607
|
+
* present since 0.146.0) routes every approval request either to the user
|
|
4608
|
+
* (`'user'`, codex's own default) or to `'auto_review'`: a prompted subagent
|
|
4609
|
+
* that gathers context and applies a risk framework before allowing or denying.
|
|
4610
|
+
* That is codex's "Approve for me" preset, and our `auto` mode is exactly it.
|
|
4611
|
+
*
|
|
4612
|
+
* Sent explicitly for EVERY mode rather than omitted for the default — a thread
|
|
4613
|
+
* inherits `approvalsReviewer` across turns ("this turn and subsequent turns"),
|
|
4614
|
+
* so leaving it unset would let a stale reviewer from an earlier turn survive a
|
|
4615
|
+
* mode switch back to a user-reviewed mode. Stating it every time makes the
|
|
4616
|
+
* mode the single source of truth.
|
|
4617
|
+
*
|
|
4618
|
+
* NOTE the asymmetry with the Claude engine's `auto`: that classifier is
|
|
4619
|
+
* operator-configurable (`autoMode.environment`, allow/soft_deny/hard_deny);
|
|
4620
|
+
* this reviewer has no configuration surface at all.
|
|
4621
|
+
*/
|
|
4622
|
+
const APPROVALS_REVIEWER_BY_MODE = {
|
|
4623
|
+
default: "user",
|
|
4624
|
+
acceptEdits: "user",
|
|
4625
|
+
auto: "auto_review",
|
|
4626
|
+
bypassPermissions: "user"
|
|
4627
|
+
};
|
|
4182
4628
|
/** Fallback timeout for a pending approval nobody answers — the SessionRunner
|
|
4183
4629
|
* default, so unattended codex sessions land the same way Claude ones do. */
|
|
4184
4630
|
const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
|
|
@@ -4580,6 +5026,15 @@ var CodexRunner = class {
|
|
|
4580
5026
|
*/
|
|
4581
5027
|
#contextUsage;
|
|
4582
5028
|
#activityCount = 0;
|
|
5029
|
+
/**
|
|
5030
|
+
* Seq of the latest `conversation_reset` event, 0 when none. The log itself is
|
|
5031
|
+
* never truncated — it still carries the state-bearing events (`capabilities`,
|
|
5032
|
+
* `system_init`, …) a fresh attacher depends on and which are not re-emitted —
|
|
5033
|
+
* but `subscribe()` skips transcript *content* strictly below this mark, so a
|
|
5034
|
+
* replay does not resurrect a cleared conversation. A later reset supersedes
|
|
5035
|
+
* an earlier one by overwriting it.
|
|
5036
|
+
*/
|
|
5037
|
+
#resetSeq = 0;
|
|
4583
5038
|
#status = "starting";
|
|
4584
5039
|
#sdkSessionId;
|
|
4585
5040
|
#model;
|
|
@@ -4596,6 +5051,8 @@ var CodexRunner = class {
|
|
|
4596
5051
|
#turnChain = Promise.resolve();
|
|
4597
5052
|
#activeTurn;
|
|
4598
5053
|
#connection;
|
|
5054
|
+
/** Per-child, from `config/read`; undefined = read failed, send the bare shape. */
|
|
5055
|
+
#workspaceWrite;
|
|
4599
5056
|
#threadLoaded = false;
|
|
4600
5057
|
#numTurns = 0;
|
|
4601
5058
|
#totalCostUsd;
|
|
@@ -4640,6 +5097,15 @@ var CodexRunner = class {
|
|
|
4640
5097
|
* it, and only the child process dying (or the session closing) ends them
|
|
4641
5098
|
* all — see the module doc in `subagents.ts`. */
|
|
4642
5099
|
#agents = new CodexAgentTracker();
|
|
5100
|
+
/** Threads that belonged to a conversation this session has cleared — the
|
|
5101
|
+
* agents that were still running when it happened. Their notifications keep
|
|
5102
|
+
* arriving on the same connection (a clear does not interrupt them and does
|
|
5103
|
+
* not drop the child), and without this {@link CodexRunner.#agentFor} would
|
|
5104
|
+
* mint them a fresh anchor and stream the cleared conversation's agent work
|
|
5105
|
+
* into the new one. Never pruned: it is a handful of uuids for the session's
|
|
5106
|
+
* life, and a late report from a long-dead agent is exactly what it exists to
|
|
5107
|
+
* catch. */
|
|
5108
|
+
#clearedThreads = /* @__PURE__ */ new Set();
|
|
4643
5109
|
constructor(config, id = randomUUID()) {
|
|
4644
5110
|
const mode = config.permissionMode ?? "default";
|
|
4645
5111
|
if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
|
|
@@ -4723,6 +5189,7 @@ var CodexRunner = class {
|
|
|
4723
5189
|
start() {
|
|
4724
5190
|
if (this.#started) return this.#turnChain;
|
|
4725
5191
|
this.#started = true;
|
|
5192
|
+
this.#warnUntrustedProject();
|
|
4726
5193
|
if (this.#config.resume && this.#config.backfillHistory !== false) {
|
|
4727
5194
|
this.#backfillPending = true;
|
|
4728
5195
|
this.#turnChain = this.#turnChain.then(() => this.#backfillHistory());
|
|
@@ -4732,6 +5199,37 @@ var CodexRunner = class {
|
|
|
4732
5199
|
return this.#turnChain;
|
|
4733
5200
|
}
|
|
4734
5201
|
/**
|
|
5202
|
+
* One-time transcript notice for the codex trust gap: a `default`-mode
|
|
5203
|
+
* session (read-only sandbox) on an untrusted cwd has its
|
|
5204
|
+
* `.codex/config.toml` — MCP servers included — silently ignored, and the
|
|
5205
|
+
* app-server surface has no trust prompt to say so (the TUI's prompt is
|
|
5206
|
+
* where the entry normally gets written). `acceptEdits`/`bypassPermissions`
|
|
5207
|
+
* sessions are exempt because their `thread/start` (workspace-write /
|
|
5208
|
+
* danger-full-access sandbox) writes the trust entry itself and loads the
|
|
5209
|
+
* config — measured against 0.146.0 and 0.149.0; a notice there would be
|
|
5210
|
+
* false. Emitted as `session_error`, which both clients render as an inline
|
|
5211
|
+
* notice while the session keeps running (the backfill-history precedent),
|
|
5212
|
+
* so nothing new rides the wire. Every degrade path is silence: a false
|
|
5213
|
+
* warning on a trusted project is worse than a missed one.
|
|
5214
|
+
*/
|
|
5215
|
+
#warnUntrustedProject() {
|
|
5216
|
+
if (this.#permissionMode !== "default") return;
|
|
5217
|
+
try {
|
|
5218
|
+
const env = this.#childEnv();
|
|
5219
|
+
const pin = env.CODEX_HOME;
|
|
5220
|
+
if (pin !== void 0 && pin.length === 0) return;
|
|
5221
|
+
const codexHome = pin ?? join(env.HOME ?? homedir(), ".codex");
|
|
5222
|
+
const message = untrustedProjectNotice({
|
|
5223
|
+
cwd: this.#cwd,
|
|
5224
|
+
codexHome
|
|
5225
|
+
});
|
|
5226
|
+
if (message) this.#emit({
|
|
5227
|
+
type: "session_error",
|
|
5228
|
+
message
|
|
5229
|
+
});
|
|
5230
|
+
} catch {}
|
|
5231
|
+
}
|
|
5232
|
+
/**
|
|
4735
5233
|
* List skills over a **throwaway** connection, for a session with nothing else
|
|
4736
5234
|
* to do yet.
|
|
4737
5235
|
*
|
|
@@ -4786,6 +5284,15 @@ var CodexRunner = class {
|
|
|
4786
5284
|
}
|
|
4787
5285
|
sendMessage(text, attachments) {
|
|
4788
5286
|
if (this.#closed) throw new Error("session is closed");
|
|
5287
|
+
if (text.trim() === "/clear" && !attachments?.length) {
|
|
5288
|
+
this.clearContext().catch((error) => {
|
|
5289
|
+
this.#emit({
|
|
5290
|
+
type: "session_error",
|
|
5291
|
+
message: `could not clear the conversation: ${error instanceof Error ? error.message : String(error)}`
|
|
5292
|
+
});
|
|
5293
|
+
});
|
|
5294
|
+
return;
|
|
5295
|
+
}
|
|
4789
5296
|
const input = this.#buildInput(text, attachments ?? []);
|
|
4790
5297
|
const echo = () => this.#emit({
|
|
4791
5298
|
type: "user_message",
|
|
@@ -4857,6 +5364,66 @@ var CodexRunner = class {
|
|
|
4857
5364
|
await this.#interruptTurn();
|
|
4858
5365
|
await this.#turnChain;
|
|
4859
5366
|
}
|
|
5367
|
+
/**
|
|
5368
|
+
* Reset the conversation: a **fresh thread on the same session**.
|
|
5369
|
+
*
|
|
5370
|
+
* Codex has no clear/reset RPC — `thread/compact/start` summarises and
|
|
5371
|
+
* continues, `thread/fork` makes a second thread, and neither is "same
|
|
5372
|
+
* session, empty context". So the analog is to stop resuming the old thread
|
|
5373
|
+
* and start a new one, which is the path a dead child already takes minus the
|
|
5374
|
+
* resume. The old thread is NOT deleted: it stays in CODEX_HOME and stays
|
|
5375
|
+
* resumable from `GET /sdk-sessions`.
|
|
5376
|
+
*
|
|
5377
|
+
* Two things it does on the way through, both mirroring the Claude engine's
|
|
5378
|
+
* SDK-driven reset (`engines/claude/runner.ts`):
|
|
5379
|
+
*
|
|
5380
|
+
* 1. **The new thread id is adopted before `conversation_reset` is emitted**,
|
|
5381
|
+
* whenever a child is already up — the eager `thread/start` costs no
|
|
5382
|
+
* tokens and no model call, and it is what keeps the dormant record from
|
|
5383
|
+
* ever naming the conversation that was just cleared. With no child there
|
|
5384
|
+
* is nothing to start against and the id is simply dropped; the parking
|
|
5385
|
+
* service treats a resumable session with no engine session id as one with
|
|
5386
|
+
* nothing to come back to, and forgets the stale record.
|
|
5387
|
+
* 2. **The context reading is retired**, in `#emit`'s `conversation_reset`
|
|
5388
|
+
* arm. Codex cannot re-poll it the way Claude does — the only source is
|
|
5389
|
+
* `thread/tokenUsage/updated`, which arrives *during* a turn — so there is
|
|
5390
|
+
* no reading at all until the next turn runs, and the protocol's rule
|
|
5391
|
+
* applies: render nothing rather than a stale ring or a 0%.
|
|
5392
|
+
*
|
|
5393
|
+
* The turn counter stays monotonic across this, on purpose (it is an unread
|
|
5394
|
+
* cursor, not an item count), and so does `activityCount` — `#emit` owns both.
|
|
5395
|
+
*/
|
|
5396
|
+
async clearContext() {
|
|
5397
|
+
if (this.#closed) throw new Error("session is closed");
|
|
5398
|
+
const run = this.#turnChain.then(() => this.#clearNow());
|
|
5399
|
+
this.#turnChain = run.then(() => void 0, () => void 0);
|
|
5400
|
+
await run;
|
|
5401
|
+
}
|
|
5402
|
+
/** The clear itself, only ever called as a turn-chain link. */
|
|
5403
|
+
async #clearNow() {
|
|
5404
|
+
if (this.#closed) throw new Error("session is closed");
|
|
5405
|
+
const previousThread = this.#sdkSessionId;
|
|
5406
|
+
this.#sdkSessionId = void 0;
|
|
5407
|
+
this.#threadLoaded = false;
|
|
5408
|
+
if (this.#connection) try {
|
|
5409
|
+
await this.#ensureThread();
|
|
5410
|
+
} catch (error) {
|
|
5411
|
+
this.#sdkSessionId = previousThread;
|
|
5412
|
+
this.#threadLoaded = false;
|
|
5413
|
+
throw error;
|
|
5414
|
+
}
|
|
5415
|
+
for (const agent of this.#agents.threadIds()) this.#clearedThreads.add(agent);
|
|
5416
|
+
this.#agents.forget();
|
|
5417
|
+
for (const [id, pending] of this.#approvals) this.#settleApproval(id, pending, {
|
|
5418
|
+
behavior: "deny",
|
|
5419
|
+
message: "the conversation was cleared"
|
|
5420
|
+
}, "policy");
|
|
5421
|
+
this.#resumedHistory = void 0;
|
|
5422
|
+
this.#emit({
|
|
5423
|
+
type: "conversation_reset",
|
|
5424
|
+
sdkSessionId: this.#sdkSessionId
|
|
5425
|
+
});
|
|
5426
|
+
}
|
|
4860
5427
|
/** Address the in-flight turn only (no approval sweep) — also the follow-up
|
|
4861
5428
|
* for a deny+interrupt whose wire decision couldn't carry the interrupt. */
|
|
4862
5429
|
async #interruptTurn() {
|
|
@@ -4934,12 +5501,54 @@ var CodexRunner = class {
|
|
|
4934
5501
|
return this.#events.find((event) => event.seq === seq);
|
|
4935
5502
|
}
|
|
4936
5503
|
subscribe(listener, afterSeq = 0, options) {
|
|
4937
|
-
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
|
|
5504
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
|
|
4938
5505
|
}
|
|
4939
5506
|
#scheduleTurn() {
|
|
4940
5507
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
4941
5508
|
}
|
|
4942
5509
|
/**
|
|
5510
|
+
* Read `[sandbox_workspace_write]` as codex resolves it for this session's
|
|
5511
|
+
* cwd, once per child, so {@link CodexRunner.#turnSandboxPolicy} can restate
|
|
5512
|
+
* it verbatim.
|
|
5513
|
+
*
|
|
5514
|
+
* Why this exists at all: `turn/start`'s object-form sandbox policy is
|
|
5515
|
+
* serde-defaulted field by field, so `{type: 'workspaceWrite'}` bare means
|
|
5516
|
+
* `networkAccess: false, writableRoots: []` NO MATTER what the operator
|
|
5517
|
+
* configured — and we must keep sending the object every turn, because
|
|
5518
|
+
* restating it is what makes a between-turns permission-mode switch take
|
|
5519
|
+
* effect. Measured against 0.149.0 with `network_access = true` set: the
|
|
5520
|
+
* bare object produced `curl: (6) Could not resolve host`, the fully-stated
|
|
5521
|
+
* object and an omitted policy both produced `200`. `read-only` is not
|
|
5522
|
+
* affected — the setting is scoped to workspace-write, as its name says, and
|
|
5523
|
+
* a read-only sandbox has no network either way.
|
|
5524
|
+
*
|
|
5525
|
+
* A failure here is not fatal: `#workspaceWrite` stays undefined and we send
|
|
5526
|
+
* the bare shape, which is exactly the behaviour that shipped before.
|
|
5527
|
+
*/
|
|
5528
|
+
async #readWorkspaceWrite(connection) {
|
|
5529
|
+
this.#workspaceWrite = void 0;
|
|
5530
|
+
try {
|
|
5531
|
+
const block = (await connection.request("config/read", { cwd: this.#cwd }))?.config?.sandbox_workspace_write;
|
|
5532
|
+
if (!block) return;
|
|
5533
|
+
const roots = block.writable_roots;
|
|
5534
|
+
this.#workspaceWrite = {
|
|
5535
|
+
writableRoots: Array.isArray(roots) ? roots.filter((r) => typeof r === "string") : [],
|
|
5536
|
+
networkAccess: block.network_access === true,
|
|
5537
|
+
excludeTmpdirEnvVar: block.exclude_tmpdir_env_var === true,
|
|
5538
|
+
excludeSlashTmp: block.exclude_slash_tmp === true
|
|
5539
|
+
};
|
|
5540
|
+
} catch {}
|
|
5541
|
+
}
|
|
5542
|
+
/** The mode's turn-level sandbox policy, with the operator's workspace-write settings intact. */
|
|
5543
|
+
#turnSandboxPolicy() {
|
|
5544
|
+
const policy = TURN_SANDBOX_BY_MODE[this.#permissionMode];
|
|
5545
|
+
if (policy?.type !== "workspaceWrite" || !this.#workspaceWrite) return policy;
|
|
5546
|
+
return {
|
|
5547
|
+
type: "workspaceWrite",
|
|
5548
|
+
...this.#workspaceWrite
|
|
5549
|
+
};
|
|
5550
|
+
}
|
|
5551
|
+
/**
|
|
4943
5552
|
* The session's live connection with its thread loaded, (re)building both as
|
|
4944
5553
|
* needed: spawn + `initialize`/`initialized` on a fresh child, then
|
|
4945
5554
|
* `thread/start` (new) or `thread/resume` (a create-request `resume`, or a
|
|
@@ -4983,12 +5592,14 @@ var CodexRunner = class {
|
|
|
4983
5592
|
throw error;
|
|
4984
5593
|
}
|
|
4985
5594
|
connection.notify("initialized");
|
|
5595
|
+
await this.#readWorkspaceWrite(connection);
|
|
4986
5596
|
}
|
|
4987
5597
|
if (!this.#threadLoaded) {
|
|
4988
5598
|
const options = {
|
|
4989
5599
|
cwd: this.#cwd,
|
|
4990
5600
|
approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
|
|
4991
|
-
sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode]
|
|
5601
|
+
sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode],
|
|
5602
|
+
approvalsReviewer: APPROVALS_REVIEWER_BY_MODE[this.#permissionMode]
|
|
4992
5603
|
};
|
|
4993
5604
|
if (this.#model) options.model = this.#model;
|
|
4994
5605
|
const resuming = this.#sdkSessionId !== void 0;
|
|
@@ -5231,7 +5842,8 @@ var CodexRunner = class {
|
|
|
5231
5842
|
input: turn.input,
|
|
5232
5843
|
cwd: this.#cwd,
|
|
5233
5844
|
approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
|
|
5234
|
-
sandboxPolicy:
|
|
5845
|
+
sandboxPolicy: this.#turnSandboxPolicy(),
|
|
5846
|
+
approvalsReviewer: APPROVALS_REVIEWER_BY_MODE[this.#permissionMode]
|
|
5235
5847
|
};
|
|
5236
5848
|
const model = this.#model ?? this.#resolvedModel;
|
|
5237
5849
|
if (model) params.model = model;
|
|
@@ -5305,6 +5917,7 @@ var CodexRunner = class {
|
|
|
5305
5917
|
if (threadId === void 0 || threadId === this.#sdkSessionId) return void 0;
|
|
5306
5918
|
const known = this.#agents.get(threadId);
|
|
5307
5919
|
if (known) return known;
|
|
5920
|
+
if (this.#clearedThreads.has(threadId)) return void 0;
|
|
5308
5921
|
const nonce = this.#activeTurn?.nonce ?? "codex";
|
|
5309
5922
|
const record = this.#agents.open(threadId, `${nonce}:agent:${threadId}`, void 0, Date.now());
|
|
5310
5923
|
record.anchored = true;
|
|
@@ -5943,7 +6556,10 @@ var CodexRunner = class {
|
|
|
5943
6556
|
this.#lastActivityAt = event.ts;
|
|
5944
6557
|
this.#activityCount += transcriptActivity(body);
|
|
5945
6558
|
this.#contextUsage = contextReading(body) ?? this.#contextUsage;
|
|
5946
|
-
if (body.type === "conversation_reset")
|
|
6559
|
+
if (body.type === "conversation_reset") {
|
|
6560
|
+
this.#resetSeq = event.seq;
|
|
6561
|
+
this.#contextUsage = void 0;
|
|
6562
|
+
}
|
|
5947
6563
|
this.#events.push(event);
|
|
5948
6564
|
this.#subscribers.emit(event);
|
|
5949
6565
|
}
|
|
@@ -5965,7 +6581,15 @@ var CodexRunner = class {
|
|
|
5965
6581
|
* const c=JSON.parse(d.slice(s,i));
|
|
5966
6582
|
* for(const m of c.models) console.log(m.slug, m.display_name,
|
|
5967
6583
|
* m.visibility, m.supported_reasoning_levels.map(l=>l.effort).join(","))'\
|
|
5968
|
-
* "$(node -p 'require
|
|
6584
|
+
* "$(node -p 'const{createRequire}=require("module");
|
|
6585
|
+
* const w=require.resolve("@openai/codex/package.json");
|
|
6586
|
+
* createRequire(w).resolve("@openai/codex-darwin-arm64/package.json")
|
|
6587
|
+
* .replace("package.json","vendor/aarch64-apple-darwin/bin/codex")')"
|
|
6588
|
+
*
|
|
6589
|
+
* The two-hop resolve is NOT optional: under pnpm's strict layout the platform
|
|
6590
|
+
* package is a dependency of `@openai/codex`, so it resolves only from that
|
|
6591
|
+
* wrapper's location, never from the repo root. Resolving it directly throws
|
|
6592
|
+
* MODULE_NOT_FOUND — the same two hops `resolveBundledCodexExecutable` makes.
|
|
5969
6593
|
*
|
|
5970
6594
|
* Mapping decisions:
|
|
5971
6595
|
* - the internal `codex-auto-review` row is dropped (the codex analogue of
|
|
@@ -5977,7 +6601,7 @@ var CodexRunner = class {
|
|
|
5977
6601
|
* `max`/`ultra` beyond the SDK union; trust the binary, keep strings open.
|
|
5978
6602
|
*/
|
|
5979
6603
|
const CODEX_CATALOG = {
|
|
5980
|
-
provenance: "embedded model presets of @openai/codex@0.
|
|
6604
|
+
provenance: "embedded model presets of @openai/codex@0.149.0 (darwin-arm64 binary), extracted 2026-08-22",
|
|
5981
6605
|
models: [
|
|
5982
6606
|
{
|
|
5983
6607
|
value: "gpt-5.6-sol",
|