@bridge4dev/runner 0.65.1 → 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.
@@ -0,0 +1,79 @@
1
+ import { type CodexHome } from './adapters/codex-home.js';
2
+ import type { AgentRateLimitWindow } from './adapters/types.js';
3
+ import type { AccountCard, AccountList } from './claude-homes.js';
4
+ /** Which row the home uses: a saved one only while its directory is there. */
5
+ export declare function activeCodexAccountId(home?: string): string;
6
+ /**
7
+ * The list of Codex logins on this machine (§8 `agent_accounts`).
8
+ *
9
+ * Repairs the home first, without forcing anything (S4 item 2), so a real file
10
+ * the CLI left in place of the link is back in its store before the rows are
11
+ * read. No subprocess anywhere in it.
12
+ */
13
+ export declare function listCodexAccounts(homedir?: string): AccountList;
14
+ /** The card of a row that just signed in – for the relay's log and the tests. */
15
+ export declare function codexAccountCard(id: string): AccountCard;
16
+ /**
17
+ * Make a row the login new Codex sessions start under (§8 `agent_account_activate`).
18
+ *
19
+ * Running sessions keep the tokens they started with; a refreshed login is
20
+ * returned to its own store before the link moves (S4 item 7). The machine row is
21
+ * refused only where the machine's owner wrote `[codex] auth = "own"`: there the
22
+ * host login is kept away from the runner on purpose, and the next repair would
23
+ * take the link out again.
24
+ */
25
+ export declare function activateCodexAccount(id: string): string;
26
+ /**
27
+ * Forget a saved Codex login: its file goes, its record goes (§8 `agent_account_forget`).
28
+ *
29
+ * The machine row cannot be forgotten. A row a live session runs under is
30
+ * refused – its process refreshes into that file until it ends. The row in use is
31
+ * switched to the machine login FIRST, so the link never points at nothing.
32
+ */
33
+ export declare function forgetCodexAccount(id: string): {
34
+ active: string;
35
+ };
36
+ /** The account a Codex session runs under – captured at its start, kept to its end (R14). */
37
+ export interface CodexSessionAccount {
38
+ id: string;
39
+ email?: string;
40
+ orgId?: string;
41
+ plan?: string;
42
+ }
43
+ /**
44
+ * Who the home a session is starting with belongs to: the row it is set to, and
45
+ * the identity of the login the link resolves to right now – read locally.
46
+ */
47
+ export declare function codexSessionAccount(home: CodexHome): CodexSessionAccount;
48
+ /**
49
+ * `owner` is the session object itself: a session can end twice (the boot catch
50
+ * calls `stop()`, and the process exit calls it again), and a bare delete by
51
+ * sessionId then wiped the note of the RELAUNCHED session that had taken the
52
+ * same id – «forget» stopped refusing the account that session was using (found
53
+ * by the independent check of S4).
54
+ */
55
+ export declare function noteCodexSessionAccount(sessionId: string, accountId: string, owner: object): void;
56
+ export declare function releaseCodexSessionAccount(sessionId: string, owner: object): void;
57
+ /** Codex rows some live session process runs under right now. */
58
+ export declare function liveCodexAccountIds(): Set<string>;
59
+ /** Who the active row is, for `auth_status.activeAccount` (§8) – read locally, no CLI. */
60
+ export declare function codexActiveAccountSummary(id: string, homedir?: string): {
61
+ id: string;
62
+ email?: string;
63
+ orgId?: string;
64
+ };
65
+ /** A refusal persisted on a saved row that still stands against its login file. */
66
+ export declare function codexRowRefused(id: string): boolean;
67
+ /** A session under this saved row was refused (D2): mark it, remove nothing. */
68
+ export declare function markCodexLoginExpired(id: string): void;
69
+ /** The row worked again. Writes only when there was a mark to clear. */
70
+ export declare function clearCodexLoginExpired(id: string): void;
71
+ /**
72
+ * A Codex session reported its account's windows. Codex sends these after every
73
+ * model request, for the account the session runs under – so for a card they
74
+ * are that subscription's figures, as fresh as its last working session.
75
+ */
76
+ export declare function noteCodexUsage(orgId: string | undefined, windows: AgentRateLimitWindow[]): void;
77
+ /** Forget every remembered reading – tests. */
78
+ export declare function resetCodexUsage(): void;
79
+ //# sourceMappingURL=codex-accounts.d.ts.map
@@ -0,0 +1,386 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { readAgentAuth, updateAgentAuth } from './agent-auth.js';
5
+ import { codexAccountAuthFile, codexAccountDir, codexAccountsDir, codexHomePath, configuredCodexAuth, hostCodexAuthFile, markedCodexAccount, readCodexCredentialFile, repairCodexAuth, setCodexActiveLogin, } from './adapters/codex-home.js';
6
+ import { AccountError, MACHINE_ACCOUNT_ID, isAccountId, refusalActive } from './login-marks.js';
7
+ import { log } from './log.js';
8
+ /**
9
+ * Several Codex logins on one machine, as rows (#422 S4, plan §8).
10
+ *
11
+ * `adapters/codex-home.ts` moves the files; this module is what the wire and a
12
+ * session read: the list of cards, switching, forgetting, which account a
13
+ * session starts under, and the verdict on a saved row. The rows have the SAME
14
+ * shape as Claude's (`AccountCard`), so the API, the mirror and the window need
15
+ * nothing new to serve Codex.
16
+ *
17
+ * Differences from Claude, each on purpose:
18
+ * - identity is read from the login file itself (claims of `id_token`) – there
19
+ * is no public `auth status` with an e-mail in it, and reading a local file
20
+ * costs no subprocess, so the list may read it every time;
21
+ * - the machine row is the host's `~/.codex/auth.json`, which DevBridge never
22
+ * wrote and does not start writing: no «Log in again» for it (R13), and a
23
+ * missing file is `expired`, not a reason to drop the row (S4 item 6);
24
+ * - limits: Codex measures nothing by itself – a session reports its account's
25
+ * windows as it works, and the card shows the last ones this daemon saw.
26
+ */
27
+ // ─── Rows ────────────────────────────────────────────────────────────
28
+ function recordsOf() {
29
+ return readAgentAuth().codexAccounts ?? [];
30
+ }
31
+ function recordOf(id) {
32
+ return recordsOf().find((record) => record.id === id);
33
+ }
34
+ function dirExists(id, home) {
35
+ try {
36
+ return fs.lstatSync(codexAccountDir(id, home)).isDirectory();
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /**
43
+ * A saved login whose record went missing (the record file was set aside as
44
+ * unreadable, say) is still a login on this disk: it gets its record back,
45
+ * dated by its directory, instead of becoming a file nobody can list or forget.
46
+ */
47
+ function adoptOrphanDirectories(home) {
48
+ let names;
49
+ try {
50
+ names = fs.readdirSync(codexAccountsDir(home));
51
+ }
52
+ catch {
53
+ return;
54
+ }
55
+ const known = new Set(recordsOf().map((record) => record.id));
56
+ const orphans = names.filter((name) => isAccountId(name) && !known.has(name));
57
+ if (orphans.length === 0)
58
+ return;
59
+ updateAgentAuth((file) => {
60
+ const records = file.codexAccounts ?? [];
61
+ for (const id of orphans) {
62
+ if (records.some((record) => record.id === id))
63
+ continue;
64
+ let addedAt = new Date().toISOString();
65
+ try {
66
+ addedAt = new Date(Math.floor(fs.statSync(codexAccountDir(id, home)).mtimeMs)).toISOString();
67
+ }
68
+ catch {
69
+ /* dated now */
70
+ }
71
+ records.push({ id, addedAt });
72
+ }
73
+ file.codexAccounts = records;
74
+ });
75
+ log.warn('codex-accounts: saved logins without a record got one back', { count: orphans.length });
76
+ }
77
+ /** Did a refusal persisted on the row outlive the login file? A file written after it wins. */
78
+ function markHolds(markedAt, writtenMs) {
79
+ if (!markedAt)
80
+ return false;
81
+ const marked = Date.parse(markedAt);
82
+ if (!Number.isFinite(marked))
83
+ return false;
84
+ return writtenMs === undefined || writtenMs <= marked;
85
+ }
86
+ function loginOf(credential, refused) {
87
+ const login = credential.status === 'unknown'
88
+ ? 'unknown'
89
+ : credential.status === 'ok' && !refused
90
+ ? 'ok'
91
+ : 'expired';
92
+ return { login, ...(credential.expiresAt ? { loginUntil: credential.expiresAt } : {}) };
93
+ }
94
+ function identityFields(identity) {
95
+ return {
96
+ ...(identity?.email ? { email: identity.email } : {}),
97
+ ...(identity?.orgId ? { orgId: identity.orgId } : {}),
98
+ ...(identity?.plan ? { plan: identity.plan } : {}),
99
+ };
100
+ }
101
+ const sameIdentity = (a, b) => (a?.email ?? '') === (b.email ?? '') &&
102
+ (a?.orgId ?? '') === (b.orgId ?? '') &&
103
+ (a?.plan ?? '') === (b.plan ?? '');
104
+ /** Which row the home uses: a saved one only while its directory is there. */
105
+ export function activeCodexAccountId(home = codexHomePath()) {
106
+ const marked = markedCodexAccount(home);
107
+ return marked && dirExists(marked, home) ? marked : MACHINE_ACCOUNT_ID;
108
+ }
109
+ /**
110
+ * The list of Codex logins on this machine (§8 `agent_accounts`).
111
+ *
112
+ * Repairs the home first, without forcing anything (S4 item 2), so a real file
113
+ * the CLI left in place of the link is back in its store before the rows are
114
+ * read. No subprocess anywhere in it.
115
+ */
116
+ export function listCodexAccounts(homedir = os.homedir()) {
117
+ const home = codexHomePath();
118
+ if (fs.existsSync(home))
119
+ repairCodexAuth({ homedir });
120
+ adoptOrphanDirectories(home);
121
+ const active = activeCodexAccountId(home);
122
+ const host = readCodexCredentialFile(hostCodexAuthFile(homedir));
123
+ const machineCard = {
124
+ id: MACHINE_ACCOUNT_ID,
125
+ kind: 'machine',
126
+ ...identityFields(host.identity),
127
+ active: active === MACHINE_ACCOUNT_ID,
128
+ ...loginOf(host, refusalActive('codex', MACHINE_ACCOUNT_ID)),
129
+ ...usageOf(host.identity.orgId),
130
+ };
131
+ const refreshed = new Map();
132
+ const saved = recordsOf()
133
+ .filter((record) => isAccountId(record.id) && dirExists(record.id, home))
134
+ .sort((a, b) => Date.parse(a.addedAt) - Date.parse(b.addedAt))
135
+ .map((record) => {
136
+ const credential = readCodexCredentialFile(codexAccountAuthFile(record.id, home));
137
+ const readable = Object.keys(credential.identity).length > 0;
138
+ if (readable && !sameIdentity(record.lastSeenIdentity, credential.identity)) {
139
+ refreshed.set(record.id, credential.identity);
140
+ }
141
+ // What the file says now; what it said last time when it cannot be read
142
+ // (a row whose login is gone keeps its name, D2).
143
+ const identity = readable ? credential.identity : record.lastSeenIdentity;
144
+ const refused = refusalActive('codex', record.id) || markHolds(record.loginExpiredAt, credential.writtenMs);
145
+ const same = identity?.orgId !== undefined &&
146
+ host.identity.orgId !== undefined &&
147
+ identity.orgId === host.identity.orgId;
148
+ return {
149
+ id: record.id,
150
+ kind: 'saved',
151
+ ...identityFields(identity),
152
+ addedAt: record.addedAt,
153
+ active: active === record.id,
154
+ ...loginOf(credential, refused),
155
+ ...usageOf(identity?.orgId),
156
+ ...(same ? { sameAsMachine: true } : {}),
157
+ };
158
+ });
159
+ if (refreshed.size > 0) {
160
+ try {
161
+ updateAgentAuth((file) => {
162
+ const at = new Date().toISOString();
163
+ for (const record of file.codexAccounts ?? []) {
164
+ const identity = refreshed.get(record.id);
165
+ if (identity)
166
+ record.lastSeenIdentity = { ...identity, at };
167
+ }
168
+ });
169
+ }
170
+ catch (error) {
171
+ log.warn('codex-accounts: could not remember who the saved logins are', {
172
+ error: String(error),
173
+ });
174
+ }
175
+ }
176
+ return { accounts: [machineCard, ...saved], active };
177
+ }
178
+ function cardOf(id) {
179
+ const card = listCodexAccounts().accounts.find((row) => row.id === id);
180
+ if (!card)
181
+ throw new AccountError('the account is not on this server any more');
182
+ return card;
183
+ }
184
+ /** The card of a row that just signed in – for the relay's log and the tests. */
185
+ export function codexAccountCard(id) {
186
+ return cardOf(id);
187
+ }
188
+ // ─── Switching and forgetting ────────────────────────────────────────
189
+ /**
190
+ * Make a row the login new Codex sessions start under (§8 `agent_account_activate`).
191
+ *
192
+ * Running sessions keep the tokens they started with; a refreshed login is
193
+ * returned to its own store before the link moves (S4 item 7). The machine row is
194
+ * refused only where the machine's owner wrote `[codex] auth = "own"`: there the
195
+ * host login is kept away from the runner on purpose, and the next repair would
196
+ * take the link out again.
197
+ */
198
+ export function activateCodexAccount(id) {
199
+ const home = codexHomePath();
200
+ if (id === MACHINE_ACCOUNT_ID) {
201
+ if (configuredCodexAuth() === 'own') {
202
+ throw new AccountError('this server keeps Codex away from the login of the machine ([codex] auth = "own" in the runner config) – switch to a saved login instead');
203
+ }
204
+ assertNoLiveSessionsForMachineSwitch();
205
+ }
206
+ else {
207
+ if (!isAccountId(id) || !recordOf(id))
208
+ throw new AccountError('no such account on this server');
209
+ if (!dirExists(id, home)) {
210
+ throw new AccountError('the login of this account is gone from this server – forget it');
211
+ }
212
+ }
213
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
214
+ setCodexActiveLogin(id, home);
215
+ return activeCodexAccountId(home);
216
+ }
217
+ /**
218
+ * Forget a saved Codex login: its file goes, its record goes (§8 `agent_account_forget`).
219
+ *
220
+ * The machine row cannot be forgotten. A row a live session runs under is
221
+ * refused – its process refreshes into that file until it ends. The row in use is
222
+ * switched to the machine login FIRST, so the link never points at nothing.
223
+ */
224
+ export function forgetCodexAccount(id) {
225
+ if (id === MACHINE_ACCOUNT_ID) {
226
+ throw new AccountError('the login of this machine cannot be forgotten');
227
+ }
228
+ const record = isAccountId(id) ? recordOf(id) : undefined;
229
+ if (!record)
230
+ throw new AccountError('no such account on this server');
231
+ if (liveCodexAccountIds().has(id)) {
232
+ throw new AccountError('a running session uses this account – stop it first, then forget it');
233
+ }
234
+ const home = codexHomePath();
235
+ if (markedCodexAccount(home) === id) {
236
+ assertNoLiveSessionsForMachineSwitch();
237
+ if (configuredCodexAuth() === 'own') {
238
+ throw new AccountError('this is the only login Codex may use on this server ([codex] auth = "own") – switch to another saved login first');
239
+ }
240
+ setCodexActiveLogin(MACHINE_ACCOUNT_ID, home);
241
+ }
242
+ fs.rmSync(codexAccountDir(id, home), { recursive: true, force: true });
243
+ updateAgentAuth((file) => {
244
+ file.codexAccounts = (file.codexAccounts ?? []).filter((entry) => entry.id !== id);
245
+ if (file.codexAccounts.length === 0)
246
+ delete file.codexAccounts;
247
+ });
248
+ if (record.lastSeenIdentity?.orgId)
249
+ usageByKey.delete(record.lastSeenIdentity.orgId);
250
+ log.info('codex-accounts: an account was forgotten', { account: id });
251
+ return { active: activeCodexAccountId(home) };
252
+ }
253
+ // ─── What a session starts under ─────────────────────────────────────
254
+ /**
255
+ * Moving the link to the MACHINE login while Codex sessions run is refused.
256
+ *
257
+ * Codex writes a refreshed token through whatever `auth.json` points at when the
258
+ * refresh comes back, and it only compares accounts when it RE-READS the file, not
259
+ * when it writes. A switch in that second would put a session's tokens into the
260
+ * file the link points at by then – and the host user's own `~/.codex/auth.json`
261
+ * is the one file DevBridge must never write (R13). Between saved logins the same
262
+ * second costs a re-login of a saved account and is left open (the plan: a switch
263
+ * acts on what starts after it); over the host's file it is not ours to risk.
264
+ */
265
+ function assertNoLiveSessionsForMachineSwitch() {
266
+ const live = liveCodexAccountIds().size;
267
+ if (live === 0)
268
+ return;
269
+ throw new AccountError(live === 1
270
+ ? 'a Codex session is running on this server – close or pause it before switching to the login of this machine'
271
+ : `${live} Codex sessions are running on this server – close or pause them before switching to the login of this machine`);
272
+ }
273
+ /**
274
+ * Who the home a session is starting with belongs to: the row it is set to, and
275
+ * the identity of the login the link resolves to right now – read locally.
276
+ */
277
+ export function codexSessionAccount(home) {
278
+ const id = home.auth === 'linked' || !home.accountId ? MACHINE_ACCOUNT_ID : home.accountId;
279
+ if (home.auth === 'missing')
280
+ return { id };
281
+ const identity = readCodexCredentialFile(path.join(home.path, 'auth.json')).identity;
282
+ return { id, ...identity };
283
+ }
284
+ /** sessionId → the Codex row that session's process runs under, and which process said so. */
285
+ const sessionAccounts = new Map();
286
+ /**
287
+ * `owner` is the session object itself: a session can end twice (the boot catch
288
+ * calls `stop()`, and the process exit calls it again), and a bare delete by
289
+ * sessionId then wiped the note of the RELAUNCHED session that had taken the
290
+ * same id – «forget» stopped refusing the account that session was using (found
291
+ * by the independent check of S4).
292
+ */
293
+ export function noteCodexSessionAccount(sessionId, accountId, owner) {
294
+ sessionAccounts.set(sessionId, { accountId, owner });
295
+ }
296
+ export function releaseCodexSessionAccount(sessionId, owner) {
297
+ if (sessionAccounts.get(sessionId)?.owner === owner)
298
+ sessionAccounts.delete(sessionId);
299
+ }
300
+ /** Codex rows some live session process runs under right now. */
301
+ export function liveCodexAccountIds() {
302
+ return new Set([...sessionAccounts.values()].map((entry) => entry.accountId));
303
+ }
304
+ // ─── The verdict on a saved row ──────────────────────────────────────
305
+ /** Who the active row is, for `auth_status.activeAccount` (§8) – read locally, no CLI. */
306
+ export function codexActiveAccountSummary(id, homedir = os.homedir()) {
307
+ const file = id === MACHINE_ACCOUNT_ID ? hostCodexAuthFile(homedir) : codexAccountAuthFile(id);
308
+ const identity = readCodexCredentialFile(file).identity;
309
+ const known = identity.orgId || identity.email ? identity : recordOf(id)?.lastSeenIdentity;
310
+ return {
311
+ id,
312
+ ...(known?.email ? { email: known.email } : {}),
313
+ ...(known?.orgId ? { orgId: known.orgId } : {}),
314
+ };
315
+ }
316
+ /** A refusal persisted on a saved row that still stands against its login file. */
317
+ export function codexRowRefused(id) {
318
+ if (id === MACHINE_ACCOUNT_ID || !isAccountId(id))
319
+ return false;
320
+ const record = recordOf(id);
321
+ if (!record?.loginExpiredAt)
322
+ return false;
323
+ return markHolds(record.loginExpiredAt, readCodexCredentialFile(codexAccountAuthFile(id)).writtenMs);
324
+ }
325
+ /** A session under this saved row was refused (D2): mark it, remove nothing. */
326
+ export function markCodexLoginExpired(id) {
327
+ if (!isAccountId(id))
328
+ return;
329
+ try {
330
+ updateAgentAuth((file) => {
331
+ const record = file.codexAccounts?.find((entry) => entry.id === id);
332
+ if (record)
333
+ record.loginExpiredAt = new Date().toISOString();
334
+ });
335
+ }
336
+ catch (error) {
337
+ log.warn('codex-accounts: could not record the refusal', { account: id, error: String(error) });
338
+ }
339
+ }
340
+ /** The row worked again. Writes only when there was a mark to clear. */
341
+ export function clearCodexLoginExpired(id) {
342
+ if (!isAccountId(id) || !recordOf(id)?.loginExpiredAt)
343
+ return;
344
+ try {
345
+ updateAgentAuth((file) => {
346
+ const record = file.codexAccounts?.find((entry) => entry.id === id);
347
+ if (record)
348
+ delete record.loginExpiredAt;
349
+ });
350
+ }
351
+ catch (error) {
352
+ log.warn('codex-accounts: could not clear the refusal', { account: id, error: String(error) });
353
+ }
354
+ }
355
+ // ─── Limits a session saw ────────────────────────────────────────────
356
+ /** subscription key → the windows a Codex session of it last reported, in this daemon. */
357
+ const usageByKey = new Map();
358
+ /**
359
+ * A Codex session reported its account's windows. Codex sends these after every
360
+ * model request, for the account the session runs under – so for a card they
361
+ * are that subscription's figures, as fresh as its last working session.
362
+ */
363
+ export function noteCodexUsage(orgId, windows) {
364
+ if (!orgId)
365
+ return;
366
+ // The two windows a card has words for; a window Codex names otherwise stays in
367
+ // the session's own panel.
368
+ const rows = windows.flatMap((window) => (window.key === 'five_hour' || window.key === 'seven_day') &&
369
+ typeof window.usedPercent === 'number'
370
+ ? [{ key: window.key, label: null, percent: window.usedPercent, resetsAt: window.resetsAt }]
371
+ : []);
372
+ if (rows.length === 0)
373
+ return;
374
+ usageByKey.set(orgId, { rows, measuredAtMs: Date.now() });
375
+ }
376
+ function usageOf(orgId) {
377
+ const reading = orgId ? usageByKey.get(orgId) : undefined;
378
+ if (!reading)
379
+ return {};
380
+ return { usage: reading.rows, usageMeasuredAt: new Date(reading.measuredAtMs).toISOString() };
381
+ }
382
+ /** Forget every remembered reading – tests. */
383
+ export function resetCodexUsage() {
384
+ usageByKey.clear();
385
+ }
386
+ //# sourceMappingURL=codex-accounts.js.map
@@ -1,6 +1,7 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { assertClaudeInstalled, claudeExecutableOption } from './agent-binary.js';
3
3
  import { scrubbedEnv } from './adapters/claude.js';
4
+ import { activeAccountHome, withAccountHome } from './claude-homes.js';
4
5
  import { gitBranchDiff } from './gitops.js';
5
6
  import { isSecretPath, maskString } from './policy.js';
6
7
  /**
@@ -160,7 +161,9 @@ export async function proposeCommitMessage(input, queryFn = query) {
160
161
  prompt: buildPrompt(input, diff),
161
162
  options: {
162
163
  cwd: input.worktreePath,
163
- env: scrubbedEnv(),
164
+ // The machine's active account (#422), with the same rule as a session:
165
+ // its home, and no operator token above it.
166
+ env: withAccountHome(scrubbedEnv(), activeAccountHome()),
164
167
  // The second place Claude `Options` are built. It must follow the same
165
168
  // constant, or after C3 commit messages would still be written by the
166
169
  // bundled binary while sessions had moved to the system one.
package/dist/config.d.ts CHANGED
@@ -15,12 +15,12 @@ declare const ConfigSchema: z.ZodObject<{
15
15
  name: z.ZodString;
16
16
  token: z.ZodString;
17
17
  }, "strip", z.ZodTypeAny, {
18
- name: string;
19
18
  id: string;
19
+ name: string;
20
20
  token: string;
21
21
  }, {
22
- name: string;
23
22
  id: string;
23
+ name: string;
24
24
  token: string;
25
25
  }>;
26
26
  mcp: z.ZodOptional<z.ZodObject<{
@@ -34,9 +34,9 @@ declare const ConfigSchema: z.ZodObject<{
34
34
  token: string;
35
35
  }>>;
36
36
  codex: z.ZodOptional<z.ZodObject<{
37
- auth: z.ZodDefault<z.ZodEnum<["link", "own"]>>;
37
+ auth: z.ZodOptional<z.ZodEnum<["link", "own"]>>;
38
38
  }, "strip", z.ZodTypeAny, {
39
- auth: "link" | "own";
39
+ auth?: "link" | "own" | undefined;
40
40
  }, {
41
41
  auth?: "link" | "own" | undefined;
42
42
  }>>;
@@ -179,10 +179,13 @@ declare const ConfigSchema: z.ZodObject<{
179
179
  ws_url: string;
180
180
  };
181
181
  server: {
182
- name: string;
183
182
  id: string;
183
+ name: string;
184
184
  token: string;
185
185
  };
186
+ codex?: {
187
+ auth?: "link" | "own" | undefined;
188
+ } | undefined;
186
189
  checkpoints?: {
187
190
  enabled: boolean;
188
191
  } | undefined;
@@ -190,8 +193,8 @@ declare const ConfigSchema: z.ZodObject<{
190
193
  url: string;
191
194
  token: string;
192
195
  } | undefined;
193
- codex?: {
194
- auth: "link" | "own";
196
+ agents?: {
197
+ install_enabled: boolean;
195
198
  } | undefined;
196
199
  limits?: {
197
200
  max_sessions?: number | undefined;
@@ -204,19 +207,19 @@ declare const ConfigSchema: z.ZodObject<{
204
207
  verify?: {
205
208
  enabled: boolean;
206
209
  } | undefined;
207
- agents?: {
208
- install_enabled: boolean;
209
- } | undefined;
210
210
  }, {
211
211
  api: {
212
212
  url: string;
213
213
  ws_url: string;
214
214
  };
215
215
  server: {
216
- name: string;
217
216
  id: string;
217
+ name: string;
218
218
  token: string;
219
219
  };
220
+ codex?: {
221
+ auth?: "link" | "own" | undefined;
222
+ } | undefined;
220
223
  checkpoints?: {
221
224
  enabled?: boolean | undefined;
222
225
  } | undefined;
@@ -224,8 +227,8 @@ declare const ConfigSchema: z.ZodObject<{
224
227
  url: string;
225
228
  token: string;
226
229
  } | undefined;
227
- codex?: {
228
- auth?: "link" | "own" | undefined;
230
+ agents?: {
231
+ install_enabled?: boolean | undefined;
229
232
  } | undefined;
230
233
  limits?: {
231
234
  max_sessions?: number | undefined;
@@ -238,9 +241,6 @@ declare const ConfigSchema: z.ZodObject<{
238
241
  verify?: {
239
242
  enabled?: boolean | undefined;
240
243
  } | undefined;
241
- agents?: {
242
- install_enabled?: boolean | undefined;
243
- } | undefined;
244
244
  }>;
245
245
  export type RunnerConfig = z.infer<typeof ConfigSchema>;
246
246
  export declare function loadConfig(): RunnerConfig | null;
package/dist/config.js CHANGED
@@ -30,9 +30,12 @@ const ConfigSchema = z.object({
30
30
  // the owner also uses codex interactively on this machine:
31
31
  // OpenAI rotates refresh tokens, and two processes sharing
32
32
  // one credential store can invalidate each other.
33
+ // No schema default (#422 S4 item 2): «absent» and «written as link» are two
34
+ // different facts. Only a written mode is forced, and neither is forced over a
35
+ // saved login the dashboard switched to – the home's own mark decides then.
33
36
  codex: z
34
37
  .object({
35
- auth: z.enum(['link', 'own']).default('link'),
38
+ auth: z.enum(['link', 'own']).optional(),
36
39
  })
37
40
  .optional(),
38
41
  // Layer-1 resource guard (plan §8.6): a ceiling the dashboard cannot raise.