@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.
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { log } from './log.js';
3
4
  import { configDir } from './paths.js';
4
5
  /**
5
6
  * A Claude login this runner owns, for the one CLI that cannot store one.
@@ -40,17 +41,235 @@ export function agentAuthPath() {
40
41
  * Eleven months keeps it inside the token's own life while still expiring.
41
42
  */
42
43
  const TOKEN_MAX_AGE_MS = 334 * 24 * 60 * 60 * 1000;
44
+ const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
45
+ const shortString = (value, max) => typeof value === 'string' && value.length > 0 && value.length <= max ? value : undefined;
46
+ const isoString = (value) => {
47
+ const text = shortString(value, 40);
48
+ return text !== undefined && Number.isFinite(Date.parse(text)) ? text : undefined;
49
+ };
50
+ function readIdentity(value) {
51
+ if (!isObject(value))
52
+ return undefined;
53
+ const at = isoString(value['at']);
54
+ if (!at)
55
+ return undefined;
56
+ const email = shortString(value['email'], 320);
57
+ const orgId = shortString(value['orgId'], 100);
58
+ const orgName = shortString(value['orgName'], 200);
59
+ const plan = shortString(value['plan'], 60);
60
+ return {
61
+ ...(email ? { email } : {}),
62
+ ...(orgId ? { orgId } : {}),
63
+ ...(orgName ? { orgName } : {}),
64
+ ...(plan ? { plan } : {}),
65
+ at,
66
+ };
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
+ }
91
+ /**
92
+ * The file as it was written, field by field.
93
+ *
94
+ * Forgiving one level down, like every JSON this project stores (CLAUDE.md: never
95
+ * `.strict()` on stored JSON): a record we cannot read is dropped, not the file.
96
+ * A person can edit this file, and a pre-#422 runner never wrote the new fields –
97
+ * both have to read as a working machine login, not as «signed out» (К7).
98
+ */
99
+ function parse(raw) {
100
+ if (!isObject(raw))
101
+ return {};
102
+ const file = {};
103
+ if (typeof raw['claudeOauthToken'] === 'string')
104
+ file.claudeOauthToken = raw['claudeOauthToken'];
105
+ if (typeof raw['updatedAt'] === 'string')
106
+ file.updatedAt = raw['updatedAt'];
107
+ if (Array.isArray(raw['claudeAccounts']))
108
+ file.claudeAccounts = parseRecords(raw['claudeAccounts']);
109
+ if (Array.isArray(raw['codexAccounts']))
110
+ file.codexAccounts = parseRecords(raw['codexAccounts']);
111
+ const active = shortString(raw['claudeActiveAccount'], 64);
112
+ if (active)
113
+ file.claudeActiveAccount = active;
114
+ if (isObject(raw['claudeMachine'])) {
115
+ const identity = readIdentity(raw['claudeMachine']['lastSeenIdentity']);
116
+ const expiredAt = isoString(raw['claudeMachine']['loginExpiredAt']);
117
+ file.claudeMachine = {
118
+ ...(identity ? { lastSeenIdentity: identity } : {}),
119
+ ...(expiredAt ? { loginExpiredAt: expiredAt } : {}),
120
+ };
121
+ }
122
+ return file;
123
+ }
124
+ const KNOWN_KEYS = new Set([
125
+ 'claudeOauthToken',
126
+ 'updatedAt',
127
+ 'claudeAccounts',
128
+ 'claudeActiveAccount',
129
+ 'claudeMachine',
130
+ 'codexAccounts',
131
+ ]);
132
+ const RECORD_KEYS = new Set(['id', 'addedAt', 'lastSeenIdentity', 'loginExpiredAt']);
133
+ const MACHINE_KEYS = new Set(['lastSeenIdentity', 'loginExpiredAt']);
134
+ const IDENTITY_KEYS = new Set(['email', 'orgId', 'orgName', 'plan', 'at']);
135
+ /** The fields of `raw` this runner does not know – to be written back untouched. */
136
+ function unknownFields(raw, known) {
137
+ if (!isObject(raw))
138
+ return {};
139
+ return Object.fromEntries(Object.entries(raw).filter(([key]) => !known.has(key)));
140
+ }
141
+ /** A known object written over the unknown fields the same object had on disk. */
142
+ function mergeObject(raw, known, keys, identityRaw) {
143
+ if (!known)
144
+ return undefined;
145
+ const merged = { ...unknownFields(raw, keys), ...known };
146
+ const identity = known.lastSeenIdentity;
147
+ if (identity)
148
+ merged['lastSeenIdentity'] = { ...unknownFields(identityRaw, IDENTITY_KEYS), ...identity };
149
+ return merged;
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
+ }
173
+ /**
174
+ * The file to write: what this runner knows, over everything a newer runner put
175
+ * there – at the top level, inside each account record and its identity, and
176
+ * whole records this runner could not read at all (a newer id format must not
177
+ * lose its home's record on the first write after a downgrade).
178
+ */
179
+ function withForeignFields(original, known) {
180
+ const body = unknownFields(original, KNOWN_KEYS);
181
+ for (const [key, value] of Object.entries(known)) {
182
+ if (value === undefined || RECORD_LISTS.has(key))
183
+ continue;
184
+ if (key === 'claudeMachine')
185
+ continue;
186
+ body[key] = value;
187
+ }
188
+ for (const list of RECORD_LISTS) {
189
+ const records = mergeRecordList(original, list, known[list]);
190
+ if (records)
191
+ body[list] = records;
192
+ }
193
+ if (known.claudeMachine) {
194
+ const rawMachine = original['claudeMachine'];
195
+ body['claudeMachine'] = mergeObject(rawMachine, known.claudeMachine, MACHINE_KEYS, isObject(rawMachine) ? rawMachine['lastSeenIdentity'] : undefined);
196
+ }
197
+ return body;
198
+ }
43
199
  function read() {
44
200
  try {
45
- const parsed = JSON.parse(fs.readFileSync(agentAuthPath(), 'utf8'));
46
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
47
- return {};
48
- return parsed;
201
+ return parse(JSON.parse(fs.readFileSync(agentAuthPath(), 'utf8')));
49
202
  }
50
203
  catch {
51
204
  return {};
52
205
  }
53
206
  }
207
+ /** Everything this runner keeps in `agent-auth.json`, read forgivingly. */
208
+ export function readAgentAuth() {
209
+ return read();
210
+ }
211
+ /**
212
+ * Change some fields of the file and keep every other one.
213
+ *
214
+ * Read → change → write, never «write what I know». The two writers this file
215
+ * had before #422 wrote the WHOLE file (`storeClaudeToken`) or deleted it
216
+ * (`clearStoredClaudeToken`), and with a list of accounts beside the token either
217
+ * would have erased every saved login's record on its way past (S1 item 3).
218
+ *
219
+ * A file that exists but cannot be parsed is moved aside, not overwritten: it may
220
+ * be the only record of which home belongs to which account, and «unreadable»
221
+ * must not quietly become «empty». A file that cannot be READ (EACCES) throws –
222
+ * writing over something we could not look at is the same mistake.
223
+ *
224
+ * Synchronous from read to rename, so two changes inside this process cannot
225
+ * interleave. Same write-then-rename as the config, 0600.
226
+ */
227
+ export function updateAgentAuth(change) {
228
+ const dir = configDir();
229
+ const target = agentAuthPath();
230
+ let current = {};
231
+ // What a NEWER runner wrote and this one does not know – kept as it is, so a
232
+ // downgrade followed by a login does not erase what the upgrade recorded.
233
+ let original = {};
234
+ let raw = null;
235
+ try {
236
+ raw = fs.readFileSync(target, 'utf8');
237
+ }
238
+ catch (error) {
239
+ if (error.code !== 'ENOENT')
240
+ throw error;
241
+ }
242
+ if (raw !== null) {
243
+ try {
244
+ const parsed = JSON.parse(raw);
245
+ if (!isObject(parsed))
246
+ throw new Error('not an object');
247
+ current = parse(parsed);
248
+ original = parsed;
249
+ }
250
+ catch {
251
+ const aside = `${target}.unreadable-${Date.now()}`;
252
+ fs.renameSync(target, aside);
253
+ log.warn('agent-auth: the file could not be parsed – moved aside, starting a new one', {
254
+ aside,
255
+ });
256
+ }
257
+ }
258
+ change(current);
259
+ const body = withForeignFields(original, current);
260
+ if (Object.keys(body).length === 0) {
261
+ fs.rmSync(target, { force: true });
262
+ return current;
263
+ }
264
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
265
+ // A torn file here reads as «signed out» and sends somebody through the whole
266
+ // login again.
267
+ const tmp = path.join(dir, `.agent-auth.${process.pid}.tmp`);
268
+ fs.writeFileSync(tmp, `${JSON.stringify(body, null, 2)}\n`, { mode: FILE_MODE });
269
+ fs.renameSync(tmp, target);
270
+ fs.chmodSync(target, FILE_MODE);
271
+ return current;
272
+ }
54
273
  /** The token we hold for Claude, or null once it is too old to trust. */
55
274
  export function storedClaudeToken() {
56
275
  const file = read();
@@ -62,23 +281,30 @@ export function storedClaudeToken() {
62
281
  return null;
63
282
  return value;
64
283
  }
284
+ /** When the stored token was captured, in ms – null without one. */
285
+ export function storedClaudeTokenAtMs() {
286
+ const file = read();
287
+ if (typeof file.claudeOauthToken !== 'string' || !file.claudeOauthToken)
288
+ return null;
289
+ const stamped = typeof file.updatedAt === 'string' ? Date.parse(file.updatedAt) : NaN;
290
+ return Number.isFinite(stamped) ? stamped : null;
291
+ }
65
292
  export function storeClaudeToken(token) {
66
- const dir = configDir();
67
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
68
- const file = agentAuthPath();
69
- // Same write-then-rename as the config: a torn file here reads as «signed
70
- // out» and sends somebody through the whole login again.
71
- const tmp = path.join(dir, `.agent-auth.${process.pid}.tmp`);
72
- fs.writeFileSync(tmp, `${JSON.stringify({ claudeOauthToken: token, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: FILE_MODE });
73
- fs.renameSync(tmp, file);
74
- fs.chmodSync(file, FILE_MODE);
293
+ updateAgentAuth((file) => {
294
+ file.claudeOauthToken = token;
295
+ file.updatedAt = new Date().toISOString();
296
+ });
75
297
  }
298
+ /** Drop the stored token – and nothing else: the accounts beside it stay (#422). */
76
299
  export function clearStoredClaudeToken() {
77
300
  try {
78
- fs.rmSync(agentAuthPath(), { force: true });
301
+ updateAgentAuth((file) => {
302
+ delete file.claudeOauthToken;
303
+ delete file.updatedAt;
304
+ });
79
305
  }
80
306
  catch {
81
- /* nothing to clear */
307
+ /* nothing to clear, or nothing we may touch */
82
308
  }
83
309
  }
84
310
  /**
@@ -98,9 +324,41 @@ export function applyStoredClaudeToken() {
98
324
  const token = storedClaudeToken();
99
325
  if (!token)
100
326
  return false;
327
+ // A token a session was refused with stays on disk (#422, D2), but it is not
328
+ // put back into the environment: there it would outrank a fresh
329
+ // `~/.claude/.credentials.json` for every session, on every daemon start,
330
+ // until somebody found this file by hand.
331
+ if (storedClaudeTokenRefused())
332
+ return false;
101
333
  process.env['CLAUDE_CODE_OAUTH_TOKEN'] = token;
102
334
  return true;
103
335
  }
336
+ /**
337
+ * Was a session refused with the token this runner captured – after it was
338
+ * captured? A token stored after the refusal is newer evidence and outranks it.
339
+ * A token with no readable capture date is judged by the mark alone.
340
+ */
341
+ export function storedClaudeTokenRefused() {
342
+ const file = read();
343
+ if (!file.claudeOauthToken)
344
+ return false;
345
+ const marked = file.claudeMachine?.loginExpiredAt;
346
+ if (!marked)
347
+ return false;
348
+ const markedMs = Date.parse(marked);
349
+ if (!Number.isFinite(markedMs))
350
+ return false;
351
+ const capturedMs = typeof file.updatedAt === 'string' ? Date.parse(file.updatedAt) : NaN;
352
+ return !Number.isFinite(capturedMs) || capturedMs <= markedMs;
353
+ }
354
+ /**
355
+ * The daemon's environment carries exactly the token this runner captured – so
356
+ * a machine-login session refused now was refused WITH it.
357
+ */
358
+ export function environmentCarriesStoredToken() {
359
+ const token = storedClaudeToken();
360
+ return !!token && process.env['CLAUDE_CODE_OAUTH_TOKEN'] === token;
361
+ }
104
362
  /**
105
363
  * The OAuth token `claude setup-token` printed, reassembled out of pty output.
106
364
  *
@@ -1,4 +1,5 @@
1
1
  import { type RelayAgentName } from './agent-registry.js';
2
+ import { type AccountCard } from './claude-homes.js';
2
3
  /**
3
4
  * The agents the login relay can drive — the registry's wire alphabet (Р15).
4
5
  *
@@ -18,6 +19,23 @@ export interface LoginCodeResult {
18
19
  ok: boolean;
19
20
  detail?: string;
20
21
  }
22
+ /** Where a sign-in through the account commands lands (#422 §8). */
23
+ export type AccountLoginTarget = 'machine' | 'saved';
24
+ /**
25
+ * `agent_account_login_code` – the row that signed in, or why not.
26
+ *
27
+ * A wrong code is an answer, not an exception, exactly as in the old flow: the
28
+ * CLI usually asks again, and the relay stays alive for the corrected code.
29
+ */
30
+ export type AccountLoginCodeResult = {
31
+ ok: true;
32
+ account: AccountCard;
33
+ active: string;
34
+ replaced?: string;
35
+ } | {
36
+ ok: false;
37
+ detail: string;
38
+ };
21
39
  export interface AgentAuthStatus {
22
40
  /**
23
41
  * `missing` — never signed in here (or the CLI is not installed).
@@ -31,6 +49,16 @@ export interface AgentAuthStatus {
31
49
  status: 'ok' | 'expired' | 'missing' | 'unknown';
32
50
  expiresAt?: string;
33
51
  detail?: string;
52
+ /**
53
+ * Which account this verdict is about (#422 §8) – the one the next session
54
+ * starts under. Identity from what the runner last read, never from a CLI call
55
+ * in the poll (R14). Optional on the wire: an older API strips it.
56
+ */
57
+ activeAccount?: {
58
+ id: string;
59
+ email?: string;
60
+ orgId?: string;
61
+ };
34
62
  }
35
63
  /**
36
64
  * Rejoin a secret the pty split across lines, so the masker can see it.
@@ -60,9 +88,48 @@ export declare class AuthRelay {
60
88
  constructor(commands?: Record<RelayAgent, string>, deps?: AuthRelayDeps);
61
89
  /** Start (or restart) a login flow and wait until the sign-in URL appears. */
62
90
  start(agent: RelayAgent): Promise<LoginStartResult>;
91
+ /**
92
+ * `agent_account_login_start` for Claude (#422 §8, R2).
93
+ *
94
+ * `saved` signs in inside a fresh staging home (`CLAUDE_CONFIG_DIR`), which
95
+ * becomes a saved account only when the code is accepted – the machine's own
96
+ * login is not touched by it at all. `machine` is today's sign-in into
97
+ * `~/.claude`, the one legitimate way to write that file, pressed by a person.
98
+ *
99
+ * Either way the CLI runs WITHOUT the operator's `CLAUDE_CODE_OAUTH_TOKEN`
100
+ * (and API keys): the CLI puts that variable above every login file, so a
101
+ * sign-in started with it could report the token's blind identity instead of
102
+ * the account that just signed in.
103
+ *
104
+ * No `setup-token` fallback for `saved`: that command stores nothing in a home
105
+ * (it prints an inference-only token, D1), so a «saved account» made from it
106
+ * would be a row with no login. `machine` keeps the fallback it always had.
107
+ */
108
+ startAccountLogin(target: AccountLoginTarget, agent?: RelayAgent): Promise<LoginStartResult>;
109
+ /** The CLI and the pty helper are both there – or a sentence saying which is not. */
110
+ private assertCanRun;
63
111
  private startWith;
64
112
  /** Paste the confirmation code back into the waiting CLI (Claude flow). */
65
113
  submitCode(agent: RelayAgent, code: string): Promise<LoginCodeResult>;
114
+ /**
115
+ * `agent_account_login_code` – finish a sign-in the account commands started.
116
+ *
117
+ * `saved`: the staging home is adopted by rename – one subscription, one row,
118
+ * the signed-in row active (`adoptLoginResult`, R15). `machine`: the same
119
+ * check the old door makes, then the machine login becomes active and its
120
+ * identity is asked once, so the answer names who ACTUALLY signed in (§2:
121
+ * «Signed in as B, not A») rather than who the machine was before.
122
+ */
123
+ submitAccountCode(code: string): Promise<AccountLoginCodeResult>;
124
+ /**
125
+ * Write the code, wait for the CLI's answer.
126
+ *
127
+ * `{ raw }` – the CLI exited 0 and the relay is released (its staging home, if
128
+ * any, is kept for the caller to adopt). Anything else is the answer to hand
129
+ * back: a non-zero exit or a timeout ends the sign-in; a line saying «invalid»
130
+ * keeps it alive, because the CLI usually asks again.
131
+ */
132
+ private exchangeCode;
66
133
  /**
67
134
  * The CLI exited 0 — but is the machine actually signed in?
68
135
  *
@@ -73,47 +140,68 @@ export declare class AuthRelay {
73
140
  */
74
141
  private confirmSignedIn;
75
142
  cancel(): void;
143
+ /**
144
+ * End THIS sign-in: its process, its slot if it still holds it, its staging
145
+ * home. A loop driving an older sign-in must never end a newer one that took
146
+ * the slot meanwhile – with `cancel()` in its place, two starts close together
147
+ * killed each other, and a stale code exchange killed the newer start (found by
148
+ * the independent check of S2).
149
+ */
150
+ private abandon;
151
+ /** Stop driving a relay: its process, its timer, its slot. Its staging home stays. */
152
+ private release;
153
+ /** A sign-in that will not be adopted leaves nothing behind (§8, S2 item 2). */
154
+ private discardStaging;
76
155
  }
77
156
  /**
78
- * Claude: the subscription login is recorded in the CLI's own credentials file.
79
- * The RUNNER reads it (its own host user's file) — the agent itself is still
80
- * denied this path by layer-1 policy.
157
+ * Claude: the verdict on the account the next session starts under (#422 R14).
81
158
  *
82
- * `expiresAt` is NOT the login. It is the expiry of a short-lived access token
83
- * (~8 hours on a live file), and next to it sits `refreshToken` with
84
- * `refreshTokenExpiresAt` ~26 days out, which the CLI spends silently on its
85
- * next run. Judging the login by `expiresAt` alone is why every server nobody
86
- * had touched since the morning reported "login expired — re-login needed"
87
- * over a login that was good for another three weeks (#121). Codex has carried
88
- * exactly this guard since day one (`readCodexCredential`); Claude did not.
159
+ * The machine login – the pre-#422 rule word for word: the operator's variable,
160
+ * then `~/.claude/.credentials.json`, then a token this runner captured. A
161
+ * saved account – ONLY its own credentials file: a session under it has the
162
+ * operator's token taken out of its environment, so letting the variable or the
163
+ * captured token answer here would paint a green verdict over a switch that
164
+ * does not work (#121, К14). Nothing here runs a CLI; the file judge and its
165
+ * reasons live in `claude-homes.ts` so the account list reads the same lines.
89
166
  *
90
- * Deliberately NOT asking the CLI. `claude auth status --json` looks like an
91
- * arbiter and is not one: measured live (SDK binary 2.1.218), it answers
92
- * `loggedIn: true` for a credential whose access token has expired AND which
93
- * carries no refresh token at all — i.e. for a genuinely dead login. It never
94
- * leaves the machine, so it cannot see a server-side revocation either. It
95
- * would have cost a ~800 ms / ~300 MB subprocess per poll under `MemoryMax=2G`
96
- * (gotcha #100) and echoed the account's e-mail and org name to every member of
97
- * the organization, in exchange for no truth at all. A revoked login is caught
98
- * instead by `noteAgentAuthFailure` below — from a real refusal, not a guess.
167
+ * The RUNNER reads these files (its own host user's) — the agent itself is still
168
+ * denied these paths by layer-1 policy.
169
+ *
170
+ * `account: 'machine'` asks about the machine login whatever is active – what the
171
+ * sign-in relay needs after writing `~/.claude`.
99
172
  */
100
- export declare function claudeAuthStatus(homedir?: string): Promise<AgentAuthStatus>;
101
- /** A session just failed to authenticate as this agent. */
102
- export declare function noteAgentAuthFailure(agent: RelayAgent): void;
103
- /** The agent just worked — whatever was wrong with the sign-in is not. */
104
- export declare function clearAgentAuthFailure(agent: RelayAgent): void;
173
+ export declare function claudeAuthStatus(homedir?: string, options?: {
174
+ account?: 'active' | 'machine';
175
+ }): Promise<AgentAuthStatus>;
105
176
  /**
106
- * Is a refusal still being held against this agent?
177
+ * A session just failed to authenticate as this agent – under this account.
178
+ *
179
+ * `accountId` is the account the SESSION started under (R14), not whatever the
180
+ * machine has active now. Nothing is deleted any more (#422 S1 item 3, D2): the
181
+ * old code threw a captured Claude token away on the spot, and that meant
182
+ * erasing the file the saved accounts are listed in. A Claude refusal is kept as
183
+ * a mark on the row instead – it outlives a daemon restart, and a login written
184
+ * after it (a refresh, a new sign-in) outranks it.
185
+ */
186
+ export declare function noteAgentAuthFailure(agent: RelayAgent, accountId?: string): void;
187
+ /** The agent just worked under this account — whatever was wrong with its sign-in is not. */
188
+ export declare function clearAgentAuthFailure(agent: RelayAgent, accountId?: string): void;
189
+ /**
190
+ * Is a refusal still being held against this agent (and account)?
107
191
  *
108
192
  * Exported so the wiring in `supervisor.ts` can be pinned by a test without
109
193
  * shelling out to the agent CLIs: this predicate IS the mechanism the panel's
110
194
  * demotion reads, and three unguarded lines were carrying it (QA-117 M6).
111
195
  */
112
- export declare function agentAuthFailureActive(agent: RelayAgent): boolean;
196
+ export declare function agentAuthFailureActive(agent: RelayAgent, accountId?: string): boolean;
113
197
  /**
114
198
  * Codex reports its own login state via an exit code (0 signed in / 1 not).
115
199
  * Probed against the RUNNER's home: the host user can be signed in while our
116
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).
117
205
  */
118
206
  export declare function codexAuthStatus(): Promise<AgentAuthStatus>;
119
207
  export declare function agentAuthStatuses(): Promise<{