@bridge4dev/runner 0.66.0 → 0.67.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.
@@ -1,6 +1,24 @@
1
1
  import { type CodexHome } from './codex-home.js';
2
2
  import { AppServerClient, type ServerRequest } from './codex-protocol.js';
3
3
  import { type AgentAdapter, type AgentQuestion, type AgentSession, type EffortOption, type SessionSpec } from './types.js';
4
+ /** One entry of `/`, as `writableRootsFor` needs it. */
5
+ export interface RootEntry {
6
+ path: string;
7
+ /** Where a link leads; null when it leads nowhere. */
8
+ realPath: string | null;
9
+ isDirectory: boolean;
10
+ }
11
+ /** Lists `/`; throws when it cannot. Replaced only by tests. */
12
+ export type RootLister = () => RootEntry[];
13
+ /**
14
+ * The top-level directories a widened sandbox may write to, or null when `/`
15
+ * could not be read.
16
+ *
17
+ * Links are followed to where they lead and counted once (`/bin` → `/usr/bin`
18
+ * adds nothing `/usr` does not); a directory inside another root is dropped for
19
+ * the same reason; files (a swap file) and dangling links are skipped.
20
+ */
21
+ export declare function topLevelWritableRoots(listRoot?: RootLister): string[] | null;
4
22
  export interface CodexAdapterDeps {
5
23
  /** Injected in tests to drive a scripted app-server. */
6
24
  spawnClient?: (options: {
@@ -28,11 +46,15 @@ export interface CodexAdapterDeps {
28
46
  repairHome?: () => CodexHome;
29
47
  /**
30
48
  * How this runner's CODEX_HOME is authenticated (`[codex] auth` in
31
- * config.toml). Carried here because every repair has to honour it — a
32
- * repair that defaults to `link` would undo an owner's decision to keep the
33
- * runner's credential separate.
49
+ * config.toml) – ONLY when the owner wrote it. Every repair honours it, and a
50
+ * repair that defaulted to `link` would undo both an owner's decision to keep
51
+ * the runner's credential separate and a switch to a saved login (#422 S4 item
52
+ * 2). Production leaves it unset: the daemon records the configured mode once
53
+ * (`configureCodexAuth`), and the home reads it from there.
34
54
  */
35
55
  authMode?: 'link' | 'own';
56
+ /** What `/` holds, for the widened sandbox roots – a test seam (`codex-auto-dev-null`). */
57
+ listRoot?: RootLister;
36
58
  /** #382: how often live helpers are re-checked against Codex – a test seam. */
37
59
  subagentReconcileMs?: number;
38
60
  /** #382: how long a just-started helper's `idle` is disbelieved – a test seam. */
@@ -1,9 +1,12 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { AsyncQueue } from '../async-queue.js';
2
4
  import { log } from '../log.js';
3
5
  import { evaluateToolUse, maskSecrets, maskString, } from '../policy.js';
4
6
  import { memoryDeathSentence, sessionMemoryEnv, sessionMemoryPromptLine } from '../session-cage.js';
5
7
  import { RUNNER_VERSION } from '../version.js';
6
8
  import { repairCodexAuth } from './codex-home.js';
9
+ import { codexSessionAccount, noteCodexSessionAccount, noteCodexUsage, releaseCodexSessionAccount, } from '../codex-accounts.js';
7
10
  import { AppServerClient, asRecord, num, RpcError, RpcTimeoutError, str, } from './codex-protocol.js';
8
11
  import { truncate } from './claude.js';
9
12
  import { AgentTaskTray } from './agent-tasks.js';
@@ -87,11 +90,79 @@ const MODE_POLICY = {
87
90
  *
88
91
  * Read on every turn rather than captured at launch, because the setting can be
89
92
  * switched while the session runs.
93
+ *
94
+ * **Never `/` itself** (gotcha 541, plan `codex-auto-dev-null`). Codex mounts a
95
+ * writable root without devices, and `/` laid over its own `/dev` left
96
+ * `/dev/null` visible but unopenable: every `bash -lc` printed «Permission
97
+ * denied» and `git status` exited 128. The widened roots are the machine's real
98
+ * top-level directories instead, minus the kernel's own (`/dev`, `/proc`,
99
+ * `/sys`) – checked on codex-cli 0.154.0 through `command/exec`: `/dev/null`
100
+ * opens, `git status` passes, a write outside the project is allowed, and, unlike
101
+ * `/`, a list of directories leaves no empty `.git`/`.codex`/`.agents` behind
102
+ * (17.09.2026, on throwaway folders – never on this production host's `/`).
90
103
  */
91
- function writableRootsFor(spec) {
104
+ function writableRootsFor(spec, listRoot = listRootEntries) {
92
105
  if (spec.trustMode === 'STRICT')
93
106
  return [spec.cwd];
94
- return spec.gitPolicy?.agentAllowOutsideFolder === true ? ['/'] : [spec.cwd];
107
+ if (spec.gitPolicy?.agentAllowOutsideFolder !== true)
108
+ return [spec.cwd];
109
+ const roots = topLevelWritableRoots(listRoot);
110
+ if (roots === null || roots.length === 0) {
111
+ // Unknown means confined (ADR 0007, «polarity»): a step outside asks, as it
112
+ // did before the permission existed.
113
+ return [spec.cwd];
114
+ }
115
+ return roots;
116
+ }
117
+ /** The kernel's own file systems – never a place a sandbox should bind over. */
118
+ const NEVER_WRITABLE_ROOTS = ['/dev', '/proc', '/sys'];
119
+ function listRootEntries() {
120
+ // `withFileTypes`, so a top-level entry is judged by what `readdir` already
121
+ // knows. A `stat` per entry would reach INTO every mount point, and a dead
122
+ // network mount at the top level would then block the daemon's only thread on
123
+ // every turn (found by the independent check of S4). Only a symbolic link is
124
+ // resolved, and those are few (`/bin`, `/lib`, …).
125
+ return fs.readdirSync('/', { withFileTypes: true }).map((entry) => {
126
+ const full = path.join('/', entry.name);
127
+ if (!entry.isSymbolicLink()) {
128
+ return { path: full, realPath: full, isDirectory: entry.isDirectory() };
129
+ }
130
+ try {
131
+ const realPath = fs.realpathSync(full);
132
+ return { path: full, realPath, isDirectory: fs.statSync(realPath).isDirectory() };
133
+ }
134
+ catch {
135
+ return { path: full, realPath: null, isDirectory: false };
136
+ }
137
+ });
138
+ }
139
+ /**
140
+ * The top-level directories a widened sandbox may write to, or null when `/`
141
+ * could not be read.
142
+ *
143
+ * Links are followed to where they lead and counted once (`/bin` → `/usr/bin`
144
+ * adds nothing `/usr` does not); a directory inside another root is dropped for
145
+ * the same reason; files (a swap file) and dangling links are skipped.
146
+ */
147
+ export function topLevelWritableRoots(listRoot = listRootEntries) {
148
+ let entries;
149
+ try {
150
+ entries = listRoot();
151
+ }
152
+ catch (error) {
153
+ log.warn('codex: could not read the top-level folders – the sandbox stays on the project folder', {
154
+ error: String(error).slice(0, 200),
155
+ });
156
+ return null;
157
+ }
158
+ const kernel = (dir) => NEVER_WRITABLE_ROOTS.some((root) => dir === root || dir.startsWith(`${root}/`));
159
+ const unique = [
160
+ ...new Set(entries
161
+ .filter((entry) => entry.isDirectory && entry.realPath !== null)
162
+ .map((entry) => path.resolve(entry.realPath))
163
+ .filter((dir) => dir !== '/' && !kernel(dir))),
164
+ ].sort();
165
+ return unique.filter((dir) => !unique.some((other) => other !== dir && dir.startsWith(`${other}/`)));
95
166
  }
96
167
  /**
97
168
  * DevBridge's own rules — composed per session since session 18, and kept
@@ -214,6 +285,15 @@ class CodexSession {
214
285
  queuedInput = [];
215
286
  /** Null when the home was injected — an injected home is the whole truth. */
216
287
  repairHome;
288
+ /**
289
+ * The login this process started under (#422 S4), kept to its end: the machine
290
+ * may be switched while it runs, and its limits, its card and a refusal it meets
291
+ * all belong to THIS account (R14). Codex itself keeps a running session on the
292
+ * account it started with – it will not refresh into another account's file.
293
+ */
294
+ account;
295
+ accountId;
296
+ listRoot;
217
297
  /** Open elicitations, oldest first — Codex may have several in flight. */
218
298
  questions = new Map();
219
299
  threadId = null;
@@ -279,6 +359,12 @@ class CodexSession {
279
359
  this.model = spec.model;
280
360
  this.effort = spec.effort;
281
361
  this.repairHome = deps.repairHome ?? (deps.codexHome ? null : repairCodexAuth);
362
+ this.listRoot = deps.listRoot;
363
+ this.account = codexSessionAccount(home);
364
+ this.accountId = this.account.id;
365
+ // Released in `finish`, and only by THIS session: «forget» refuses a login a
366
+ // live process refreshes into.
367
+ noteCodexSessionAccount(spec.sessionId, this.accountId, this);
282
368
  const wiring = {
283
369
  // The cage's two numbers ride along (#398 S5). Built here rather than
284
370
  // inside `scrubbedEnv`, which has no access to the session — and merged
@@ -346,6 +432,13 @@ class CodexSession {
346
432
  throw new Error(`codex is using an unexpected CODEX_HOME (${reportedHome ?? 'not reported'}) — refusing to start the session`);
347
433
  }
348
434
  this.client.notify('initialized', {});
435
+ // The app-server reads `auth.json` while it starts, so the login it ended
436
+ // up with is the one the link points at NOW, not the one this object read
437
+ // before the process existed. A switch in that window is rare and would
438
+ // otherwise put the previous account's name on this session's card and its
439
+ // limits (found by the independent check of S4). The row it is noted under
440
+ // moves with it, so «forget» refuses the right one.
441
+ this.rereadAccount();
349
442
  // The helper set is per PROCESS (#113, #382): a session relaunched after a
350
443
  // runner restart must not go on showing the helpers of its previous life,
351
444
  // and nothing else would say so until the next one starts or ends.
@@ -675,7 +768,7 @@ class CodexSession {
675
768
  input: [{ type: 'text', text, text_elements: [] }],
676
769
  approvalPolicy: policy.approvalPolicy,
677
770
  // #418: the roots live here and only here — see `writableRootsFor`.
678
- sandboxPolicy: sandboxPolicyFor(policy.sandbox, this.spec),
771
+ sandboxPolicy: sandboxPolicyFor(policy.sandbox, this.spec, this.listRoot),
679
772
  ...(this.model ? { model: this.model } : {}),
680
773
  // "Override the reasoning effort for this turn and subsequent turns" —
681
774
  // the same sticky-override channel the model uses (there is still no
@@ -1835,6 +1928,13 @@ class CodexSession {
1835
1928
  this.emitRateLimits();
1836
1929
  }
1837
1930
  emitRateLimits(blocked = null) {
1931
+ // Whose figures these are (#422 §8): the subscription this session started
1932
+ // under. Codex reports the windows of the account its process is signed in
1933
+ // with, and that is the account captured at the start – so the signature is
1934
+ // true for the whole life of the session. No key (an API-key login) – no
1935
+ // signature, and the panel draws as before #422 (R12).
1936
+ const orgId = this.account.orgId;
1937
+ noteCodexUsage(orgId, this.rateLimitWindows);
1838
1938
  // An empty snapshot is «no plan», not «nothing happened»: Codex sends this
1839
1939
  // for API-key accounts too, and the popup has to be able to say so.
1840
1940
  this.emit({
@@ -1845,6 +1945,7 @@ class CodexSession {
1845
1945
  planType: this.ratePlanType,
1846
1946
  measuredAt: new Date().toISOString(),
1847
1947
  windows: this.rateLimitWindows,
1948
+ ...(orgId ? { orgId } : {}),
1848
1949
  },
1849
1950
  });
1850
1951
  }
@@ -2194,6 +2295,27 @@ class CodexSession {
2194
2295
  this.capabilitiesInFlight = false;
2195
2296
  });
2196
2297
  }
2298
+ /** Who the home belongs to now – after the app-server has read its credential. */
2299
+ rereadAccount() {
2300
+ if (!this.repairHome)
2301
+ return;
2302
+ try {
2303
+ const now = codexSessionAccount(this.repairHome());
2304
+ if (now.id === this.account.id)
2305
+ return;
2306
+ log.info('codex: the machine changed its Codex login while this session was starting', {
2307
+ sessionId: this.spec.sessionId,
2308
+ from: this.account.id,
2309
+ to: now.id,
2310
+ });
2311
+ this.account = now;
2312
+ this.accountId = now.id;
2313
+ noteCodexSessionAccount(this.spec.sessionId, now.id, this);
2314
+ }
2315
+ catch (error) {
2316
+ log.warn('codex: could not re-read the account after start', { error: describe(error) });
2317
+ }
2318
+ }
2197
2319
  async publishCapabilities() {
2198
2320
  // Every probe carries its own catch: an unknown method on a newer or older
2199
2321
  // codex must degrade a picker, not kill the session.
@@ -2218,7 +2340,7 @@ class CodexSession {
2218
2340
  currentMode: this.mode,
2219
2341
  ...(currentModel ? { currentModel } : {}),
2220
2342
  ...(currentEffort ? { currentEffort } : {}),
2221
- ...(account ? { account } : {}),
2343
+ account: this.sessionAccountCard(account),
2222
2344
  mcpServers,
2223
2345
  interrupt: true,
2224
2346
  };
@@ -2358,6 +2480,21 @@ class CodexSession {
2358
2480
  }
2359
2481
  return options;
2360
2482
  }
2483
+ /**
2484
+ * The card's account: what the live process says about itself (`account/read`),
2485
+ * under the row and the subscription the session started with (#422 §8) – the
2486
+ * `id` a «forget» is refused for, the `orgId` the limits panel matches against.
2487
+ */
2488
+ sessionAccountCard(live) {
2489
+ const plan = live?.plan ?? (this.account.plan ? `ChatGPT ${this.account.plan}` : undefined);
2490
+ const email = live?.email ?? this.account.email;
2491
+ return {
2492
+ id: this.account.id,
2493
+ ...(email ? { email } : {}),
2494
+ ...(plan ? { plan } : {}),
2495
+ ...(this.account.orgId ? { orgId: this.account.orgId } : {}),
2496
+ };
2497
+ }
2361
2498
  async readAccount() {
2362
2499
  const result = asRecord(await this.client.request('account/read', {}, 15_000));
2363
2500
  const account = asRecord(result['account']);
@@ -2505,6 +2642,7 @@ class CodexSession {
2505
2642
  return home.auth === 'missing' ? 'missing' : 'expired';
2506
2643
  }
2507
2644
  finish() {
2645
+ releaseCodexSessionAccount(this.spec.sessionId, this);
2508
2646
  this.stopped = true;
2509
2647
  this.clearLimitSettle();
2510
2648
  this.subagents.close();
@@ -2514,7 +2652,7 @@ class CodexSession {
2514
2652
  }
2515
2653
  class ResumeFailed extends Error {
2516
2654
  }
2517
- function sandboxPolicyFor(mode, spec) {
2655
+ function sandboxPolicyFor(mode, spec, listRoot) {
2518
2656
  // turn/start takes the structured SandboxPolicy, while thread/start takes the
2519
2657
  // CLI-style SandboxMode string. Same intent, two shapes — and only this one
2520
2658
  // can name the writable roots, which is why #418 lives here.
@@ -2526,7 +2664,7 @@ function sandboxPolicyFor(mode, spec) {
2526
2664
  default:
2527
2665
  return {
2528
2666
  type: 'workspaceWrite',
2529
- writableRoots: writableRootsFor(spec),
2667
+ writableRoots: writableRootsFor(spec, listRoot),
2530
2668
  // Unchanged by #418, on purpose: the permission is about the file
2531
2669
  // system, and letting the network out with it would be a second
2532
2670
  // boundary nobody asked to move. A network command still fails in the
@@ -2796,11 +2934,11 @@ export class CodexAdapter {
2796
2934
  // Re-assert rather than reuse: `repairCodexAuth` is a couple of lstat calls
2797
2935
  // and it puts a credential link back if something removed it since boot.
2798
2936
  //
2799
- // The configured mode has to travel with it. Repairing with the default
2800
- // would silently re-link the host user's credential into a home the owner
2801
- // explicitly asked to keep isolated — turning off a security control by
2802
- // accident.
2803
- const repair = () => repairCodexAuth({ auth: this.deps.authMode ?? 'link' });
2937
+ // The configured mode has to travel with it – and only a configured one.
2938
+ // Repairing with a default would silently re-link the host user's credential
2939
+ // into a home the owner explicitly asked to keep isolated, and undo a switch
2940
+ // to a saved login at every session start (#422 S4 item 2).
2941
+ const repair = () => repairCodexAuth(this.deps.authMode ? { auth: this.deps.authMode } : {});
2804
2942
  const home = this.deps.codexHome ?? repair();
2805
2943
  return new CodexSession(spec, home, {
2806
2944
  ...this.deps,
@@ -29,6 +29,12 @@ export interface ClaudeAccountRecord {
29
29
  */
30
30
  loginExpiredAt?: string;
31
31
  }
32
+ /**
33
+ * One saved Codex login of this machine (#422 S4) – the same shape as a Claude
34
+ * record. `lastSeenIdentity.orgId` carries `tokens.account_id` of its `auth.json`:
35
+ * the key of a Codex subscription (plan §8), one subscription – one row.
36
+ */
37
+ export type CodexAccountRecord = ClaudeAccountRecord;
32
38
  /** What the runner remembers about the machine row itself – it has no record of its own. */
33
39
  export interface ClaudeMachineRecord {
34
40
  lastSeenIdentity?: ClaudeIdentity;
@@ -52,6 +58,13 @@ export interface AgentAuthFile {
52
58
  /** Absent means the machine login – the pre-#422 file reads exactly as before. */
53
59
  claudeActiveAccount?: string;
54
60
  claudeMachine?: ClaudeMachineRecord;
61
+ /**
62
+ * The saved Codex logins of this machine (#422 S4). Their files live in the
63
+ * runner's CODEX_HOME (`accounts/<id>/auth.json`); WHICH one is active is not
64
+ * here but in the home itself – the `auth.json` link and its mark – because
65
+ * that is what the CLI reads.
66
+ */
67
+ codexAccounts?: CodexAccountRecord[];
55
68
  }
56
69
  /** Everything this runner keeps in `agent-auth.json`, read forgivingly. */
57
70
  export declare function readAgentAuth(): AgentAuthFile;
@@ -65,6 +65,29 @@ function readIdentity(value) {
65
65
  at,
66
66
  };
67
67
  }
68
+ /** A list of account records, each read on its own: one unreadable entry drops only itself. */
69
+ function parseRecords(entries) {
70
+ const seen = new Set();
71
+ return entries.flatMap((entry) => {
72
+ if (!isObject(entry))
73
+ return [];
74
+ const id = shortString(entry['id'], 64);
75
+ const addedAt = isoString(entry['addedAt']);
76
+ if (!id || !addedAt || seen.has(id))
77
+ return [];
78
+ seen.add(id);
79
+ const identity = readIdentity(entry['lastSeenIdentity']);
80
+ const expiredAt = isoString(entry['loginExpiredAt']);
81
+ return [
82
+ {
83
+ id,
84
+ addedAt,
85
+ ...(identity ? { lastSeenIdentity: identity } : {}),
86
+ ...(expiredAt ? { loginExpiredAt: expiredAt } : {}),
87
+ },
88
+ ];
89
+ });
90
+ }
68
91
  /**
69
92
  * The file as it was written, field by field.
70
93
  *
@@ -81,28 +104,10 @@ function parse(raw) {
81
104
  file.claudeOauthToken = raw['claudeOauthToken'];
82
105
  if (typeof raw['updatedAt'] === 'string')
83
106
  file.updatedAt = raw['updatedAt'];
84
- if (Array.isArray(raw['claudeAccounts'])) {
85
- const seen = new Set();
86
- file.claudeAccounts = raw['claudeAccounts'].flatMap((entry) => {
87
- if (!isObject(entry))
88
- return [];
89
- const id = shortString(entry['id'], 64);
90
- const addedAt = isoString(entry['addedAt']);
91
- if (!id || !addedAt || seen.has(id))
92
- return [];
93
- seen.add(id);
94
- const identity = readIdentity(entry['lastSeenIdentity']);
95
- const expiredAt = isoString(entry['loginExpiredAt']);
96
- return [
97
- {
98
- id,
99
- addedAt,
100
- ...(identity ? { lastSeenIdentity: identity } : {}),
101
- ...(expiredAt ? { loginExpiredAt: expiredAt } : {}),
102
- },
103
- ];
104
- });
105
- }
107
+ if (Array.isArray(raw['claudeAccounts']))
108
+ file.claudeAccounts = parseRecords(raw['claudeAccounts']);
109
+ if (Array.isArray(raw['codexAccounts']))
110
+ file.codexAccounts = parseRecords(raw['codexAccounts']);
106
111
  const active = shortString(raw['claudeActiveAccount'], 64);
107
112
  if (active)
108
113
  file.claudeActiveAccount = active;
@@ -122,6 +127,7 @@ const KNOWN_KEYS = new Set([
122
127
  'claudeAccounts',
123
128
  'claudeActiveAccount',
124
129
  'claudeMachine',
130
+ 'codexAccounts',
125
131
  ]);
126
132
  const RECORD_KEYS = new Set(['id', 'addedAt', 'lastSeenIdentity', 'loginExpiredAt']);
127
133
  const MACHINE_KEYS = new Set(['lastSeenIdentity', 'loginExpiredAt']);
@@ -142,6 +148,28 @@ function mergeObject(raw, known, keys, identityRaw) {
142
148
  merged['lastSeenIdentity'] = { ...unknownFields(identityRaw, IDENTITY_KEYS), ...identity };
143
149
  return merged;
144
150
  }
151
+ /** The lists of account records – both agents keep theirs in the same shape (#422 S4). */
152
+ const RECORD_LISTS = new Set(['claudeAccounts', 'codexAccounts']);
153
+ /**
154
+ * One list of records as it is to be written: every record this runner knows,
155
+ * over the unknown fields the same record had on disk, followed by the records it
156
+ * could not read at all. `undefined` – the list is not written.
157
+ */
158
+ function mergeRecordList(original, list, known) {
159
+ const rawAccounts = Array.isArray(original[list]) ? original[list] : [];
160
+ const rawById = new Map(rawAccounts.filter(isObject).map((entry) => [String(entry['id']), entry]));
161
+ const readable = new Set((parse(original)[list] ?? []).map((record) => record.id));
162
+ const accounts = (known ?? []).map((record) => {
163
+ const rawEntry = rawById.get(record.id);
164
+ return mergeObject(rawEntry, record, RECORD_KEYS, isObject(rawEntry) ? rawEntry['lastSeenIdentity'] : undefined);
165
+ });
166
+ const unreadable = rawAccounts.filter((entry) => !isObject(entry) || !readable.has(String(entry['id'])));
167
+ if (accounts.length + unreadable.length === 0)
168
+ return undefined;
169
+ if (!known && unreadable.length === 0)
170
+ return undefined;
171
+ return [...accounts, ...unreadable];
172
+ }
145
173
  /**
146
174
  * The file to write: what this runner knows, over everything a newer runner put
147
175
  * there – at the top level, inside each account record and its identity, and
@@ -151,20 +179,16 @@ function mergeObject(raw, known, keys, identityRaw) {
151
179
  function withForeignFields(original, known) {
152
180
  const body = unknownFields(original, KNOWN_KEYS);
153
181
  for (const [key, value] of Object.entries(known)) {
154
- if (value === undefined || key === 'claudeAccounts' || key === 'claudeMachine')
182
+ if (value === undefined || RECORD_LISTS.has(key))
183
+ continue;
184
+ if (key === 'claudeMachine')
155
185
  continue;
156
186
  body[key] = value;
157
187
  }
158
- const rawAccounts = Array.isArray(original['claudeAccounts']) ? original['claudeAccounts'] : [];
159
- const rawById = new Map(rawAccounts.filter(isObject).map((entry) => [String(entry['id']), entry]));
160
- const readable = new Set((parse(original).claudeAccounts ?? []).map((record) => record.id));
161
- const accounts = (known.claudeAccounts ?? []).map((record) => {
162
- const rawEntry = rawById.get(record.id);
163
- return mergeObject(rawEntry, record, RECORD_KEYS, isObject(rawEntry) ? rawEntry['lastSeenIdentity'] : undefined);
164
- });
165
- const unreadable = rawAccounts.filter((entry) => !isObject(entry) || !readable.has(String(entry['id'])));
166
- if (accounts.length + unreadable.length > 0 && (known.claudeAccounts || unreadable.length > 0)) {
167
- body['claudeAccounts'] = [...accounts, ...unreadable];
188
+ for (const list of RECORD_LISTS) {
189
+ const records = mergeRecordList(original, list, known[list]);
190
+ if (records)
191
+ body[list] = records;
168
192
  }
169
193
  if (known.claudeMachine) {
170
194
  const rawMachine = original['claudeMachine'];
@@ -105,7 +105,7 @@ export declare class AuthRelay {
105
105
  * (it prints an inference-only token, D1), so a «saved account» made from it
106
106
  * would be a row with no login. `machine` keeps the fallback it always had.
107
107
  */
108
- startAccountLogin(target: AccountLoginTarget): Promise<LoginStartResult>;
108
+ startAccountLogin(target: AccountLoginTarget, agent?: RelayAgent): Promise<LoginStartResult>;
109
109
  /** The CLI and the pty helper are both there – or a sentence saying which is not. */
110
110
  private assertCanRun;
111
111
  private startWith;
@@ -198,6 +198,10 @@ export declare function agentAuthFailureActive(agent: RelayAgent, accountId?: st
198
198
  * Codex reports its own login state via an exit code (0 signed in / 1 not).
199
199
  * Probed against the RUNNER's home: the host user can be signed in while our
200
200
  * isolated home is not, and it is ours that sessions use.
201
+ *
202
+ * About the login new sessions start under (#422 S4): the machine login through
203
+ * the link, or the saved login the link points at – named in `activeAccount`,
204
+ * read locally from the file, no CLI for it (R14).
201
205
  */
202
206
  export declare function codexAuthStatus(): Promise<AgentAuthStatus>;
203
207
  export declare function agentAuthStatuses(): Promise<{