@bridge4dev/runner 0.66.0 → 0.68.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/dist/account-commands.d.ts +27 -8
- package/dist/account-commands.js +103 -13
- package/dist/adapters/codex-home.d.ts +144 -21
- package/dist/adapters/codex-home.js +626 -77
- package/dist/adapters/codex.d.ts +25 -3
- package/dist/adapters/codex.js +149 -11
- package/dist/agent-auth.d.ts +13 -0
- package/dist/agent-auth.js +57 -33
- package/dist/auth-relay.d.ts +7 -1
- package/dist/auth-relay.js +162 -67
- package/dist/claude-homes.d.ts +6 -7
- package/dist/claude-homes.js +20 -26
- package/dist/codex-accounts.d.ts +79 -0
- package/dist/codex-accounts.js +386 -0
- package/dist/config.d.ts +3 -3
- package/dist/config.js +4 -1
- package/dist/index.js +26 -10
- package/dist/login-marks.d.ts +11 -0
- package/dist/login-marks.js +23 -0
- package/dist/supervisor.d.ts +30 -0
- package/dist/supervisor.js +97 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/auth-relay.js
CHANGED
|
@@ -10,7 +10,8 @@ import { applyStoredClaudeToken, environmentCarriesStoredToken, extractOauthToke
|
|
|
10
10
|
import { invalidateUsageCache } from './adapters/claude-usage.js';
|
|
11
11
|
import { AccountError, activeAccountId, adoptLoginResult as adoptClaudeLogin, buildAccountList, clearLoginExpired, discardStagingHome as discardClaudeStagingHome, identityProbeEnv, machineLoginStatus, markLoginExpired, prepareStagingHome as prepareClaudeStagingHome, readActiveAccountSummary, refreshAccountIdentity, savedLoginStatus, setActiveAccount, } from './claude-homes.js';
|
|
12
12
|
import { MACHINE_ACCOUNT_ID, clearRefusal, noteRefusal, refusalActive } from './login-marks.js';
|
|
13
|
-
import { adoptLoginResult, discardStagingHome, prepareStagingHome,
|
|
13
|
+
import { adoptLoginResult as adoptCodexLogin, discardStagingHome as discardCodexStagingHome, prepareStagingHome as prepareCodexStagingHome, readCodexCredentialFile, repairCodexAuth, } from './adapters/codex-home.js';
|
|
14
|
+
import { activeCodexAccountId, clearCodexLoginExpired, codexActiveAccountSummary, codexRowRefused, markCodexLoginExpired, } from './codex-accounts.js';
|
|
14
15
|
const execFileAsync = promisify(execFile);
|
|
15
16
|
/* eslint-disable no-control-regex -- this module parses raw pty output, so
|
|
16
17
|
matching ANSI/OSC escape bytes (\x1b, \x07) is exactly the point. */
|
|
@@ -21,7 +22,13 @@ const CODE_EXCHANGE_TIMEOUT_MS = 60_000;
|
|
|
21
22
|
const RELAY_MAX_LIFETIME_MS = 16 * 60_000;
|
|
22
23
|
const URL_PATTERNS = {
|
|
23
24
|
claude: /https:\/\/(?:claude\.com|claude\.ai)\/[^\s\x07\x1b"']+/,
|
|
24
|
-
|
|
25
|
+
// A page a person opens, never an endpoint of the provider's API (D26, S4 item
|
|
26
|
+
// 5a). When codex fails to get a device code it prints «Error logging in with
|
|
27
|
+
// device code: error sending request for url
|
|
28
|
+
// (https://auth.openai.com/api/accounts/deviceauth/usercode)», and the old
|
|
29
|
+
// any-https pattern handed that address to the window as the sign-in link –
|
|
30
|
+
// without a code, onto a page that answers «Authentication Error».
|
|
31
|
+
codex: /https:\/\/(?![^\s\x07\x1b"']*\/api\/)[^\s\x07\x1b"']+/,
|
|
25
32
|
};
|
|
26
33
|
/**
|
|
27
34
|
* Rejoin a secret the pty split across lines, so the masker can see it.
|
|
@@ -219,12 +226,30 @@ export class AuthRelay {
|
|
|
219
226
|
* (it prints an inference-only token, D1), so a «saved account» made from it
|
|
220
227
|
* would be a row with no login. `machine` keeps the fallback it always had.
|
|
221
228
|
*/
|
|
222
|
-
async startAccountLogin(target) {
|
|
229
|
+
async startAccountLogin(target, agent = 'claude', options = {}) {
|
|
230
|
+
// Default true: that is what a sign-in has always done, and it is what an
|
|
231
|
+
// older API – which sends no such argument – must keep getting.
|
|
232
|
+
const activate = options.activate !== false;
|
|
233
|
+
if (agent === 'codex') {
|
|
234
|
+
// R13: the machine row of Codex is the host user's own `~/.codex/auth.json`,
|
|
235
|
+
// which DevBridge has never written and does not start writing. A sign-in
|
|
236
|
+
// from the window always adds (or renews) a saved login.
|
|
237
|
+
if (target === 'machine') {
|
|
238
|
+
throw new AccountError('the login of this machine is the Codex login of its own user – sign in again in the terminal on the machine (`codex login`)');
|
|
239
|
+
}
|
|
240
|
+
this.cancel();
|
|
241
|
+
this.assertCanRun('codex');
|
|
242
|
+
return this.startWith('codex', this.commands.codex ?? '', false, {
|
|
243
|
+
flow: 'account',
|
|
244
|
+
target: 'saved',
|
|
245
|
+
activate,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
223
248
|
this.cancel();
|
|
224
249
|
this.assertCanRun('claude');
|
|
225
250
|
const command = this.commands.claude ?? '';
|
|
226
251
|
try {
|
|
227
|
-
return await this.startWith('claude', command, false, { flow: 'account', target });
|
|
252
|
+
return await this.startWith('claude', command, false, { flow: 'account', target, activate });
|
|
228
253
|
}
|
|
229
254
|
catch (error) {
|
|
230
255
|
if (!looksLikeUnsupportedSubcommand(String(error)))
|
|
@@ -233,7 +258,11 @@ export class AuthRelay {
|
|
|
233
258
|
throw new Error('the Claude CLI on this server is too old to sign in to a saved account – update Claude Code and try again', { cause: error });
|
|
234
259
|
}
|
|
235
260
|
log.warn('auth-relay: this claude has no `auth login` — falling back to setup-token');
|
|
236
|
-
return this.startWith('claude', CLAUDE_LEGACY_LOGIN, true, {
|
|
261
|
+
return this.startWith('claude', CLAUDE_LEGACY_LOGIN, true, {
|
|
262
|
+
flow: 'account',
|
|
263
|
+
target,
|
|
264
|
+
activate,
|
|
265
|
+
});
|
|
237
266
|
}
|
|
238
267
|
}
|
|
239
268
|
/** The CLI and the pty helper are both there – or a sentence saying which is not. */
|
|
@@ -258,7 +287,9 @@ export class AuthRelay {
|
|
|
258
287
|
// sign-in (or letting it time out) left the server permanently signed out
|
|
259
288
|
// with no way back except restarting the daemon — and writing through the
|
|
260
289
|
// link would have overwritten the host user's own account (QA-100 MINOR-5).
|
|
261
|
-
|
|
290
|
+
// Whichever door started it, the result is a saved login (#422 S4): Codex
|
|
291
|
+
// has no «machine login» DevBridge may write.
|
|
292
|
+
const stagingHome = agent === 'codex' ? prepareCodexStagingHome() : null;
|
|
262
293
|
// A saved Claude account signs in inside its own staging home (#422 R2).
|
|
263
294
|
// `captureToken` never reaches here with `saved` – see `startAccountLogin`.
|
|
264
295
|
const claudeStaging = agent === 'claude' && door.flow === 'account' && door.target === 'saved'
|
|
@@ -266,12 +297,16 @@ export class AuthRelay {
|
|
|
266
297
|
: null;
|
|
267
298
|
const proc = spawn('script', ['-qec', command, '/dev/null'], {
|
|
268
299
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
269
|
-
env:
|
|
270
|
-
? //
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
300
|
+
env: stagingHome
|
|
301
|
+
? // Codex must log in to a home WE control, never the host user's ~/.codex –
|
|
302
|
+
// whichever door started it (the account door once ran it without this
|
|
303
|
+
// variable, and the CLI wrote its login into the user's own home).
|
|
304
|
+
relayEnv({ CODEX_HOME: stagingHome })
|
|
305
|
+
: door.flow === 'account'
|
|
306
|
+
? // The staging home, or the machine's own `~/.claude` (no variable);
|
|
307
|
+
// and in both, no operator token and no API key (§8).
|
|
308
|
+
identityProbeEnv(claudeStaging, relayEnv())
|
|
309
|
+
: relayEnv(),
|
|
275
310
|
});
|
|
276
311
|
const relay = {
|
|
277
312
|
agent,
|
|
@@ -283,7 +318,9 @@ export class AuthRelay {
|
|
|
283
318
|
captureToken,
|
|
284
319
|
flow: door.flow,
|
|
285
320
|
target: door.target,
|
|
321
|
+
activate: door.activate !== false,
|
|
286
322
|
claudeStaging,
|
|
323
|
+
codexStaging: stagingHome,
|
|
287
324
|
exchanging: false,
|
|
288
325
|
killTimer: setTimeout(() => {
|
|
289
326
|
if (this.active === relay)
|
|
@@ -304,28 +341,46 @@ export class AuthRelay {
|
|
|
304
341
|
// exits once the browser side is confirmed. That exit IS the completion
|
|
305
342
|
// signal — promote the staging credential, or throw it away.
|
|
306
343
|
//
|
|
307
|
-
// Only when this relay is still the current one:
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
if (relay.agent !== 'codex' || this.active !== relay)
|
|
344
|
+
// Only when this relay is still the current one: a cancelled login was
|
|
345
|
+
// abandoned (its home removed) by whoever took the slot, and its late exit
|
|
346
|
+
// must not store anything in its name.
|
|
347
|
+
if (relay.agent !== 'codex' || this.active !== relay || !relay.codexStaging)
|
|
348
|
+
return;
|
|
349
|
+
if (code !== 0) {
|
|
350
|
+
this.discardStaging(relay);
|
|
311
351
|
return;
|
|
352
|
+
}
|
|
353
|
+
const staging = relay.codexStaging;
|
|
354
|
+
relay.codexStaging = null;
|
|
312
355
|
try {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
356
|
+
// By rename, one subscription – one row, and the signed-in row is the one
|
|
357
|
+
// new sessions start under (S4 item 5, R15). The window learns it from the
|
|
358
|
+
// list (§8: device sign-in finishes without `login_code`).
|
|
359
|
+
// The one-login window's door (`flow: 'legacy'`) replaces the login in
|
|
360
|
+
// use instead of adding a row beside it: with the organization's option
|
|
361
|
+
// off, «a token overwrites the token» is what a sign-in has always meant
|
|
362
|
+
// (D27), and DevBridge writes no machine login for Codex to overwrite.
|
|
363
|
+
const stored = adoptCodexLogin(staging, {
|
|
364
|
+
replaceActive: relay.flow === 'legacy',
|
|
365
|
+
// «Log in, but do not switch to it» (S4.1): the row is stored and
|
|
366
|
+
// listed, the link the sessions start from stays where it is.
|
|
367
|
+
activate: relay.activate,
|
|
368
|
+
});
|
|
369
|
+
log.info('codex: device login completed — credential stored', {
|
|
370
|
+
account: stored.id,
|
|
371
|
+
replaced: Boolean(stored.replaced),
|
|
372
|
+
});
|
|
373
|
+
// The ONLY place a Codex sign-in can be reported as finished. Its
|
|
374
|
+
// flow never sends `login_code` (device auth needs no paste-back),
|
|
375
|
+
// so without this line a refusal we recorded earlier would keep the
|
|
376
|
+
// panel demanding a re-login the user has just done — #121's own
|
|
377
|
+
// complaint, reproduced on the other half of the panel (QA-117 H1).
|
|
378
|
+
clearAgentAuthFailure('codex', stored.id);
|
|
325
379
|
}
|
|
326
380
|
catch (error) {
|
|
327
|
-
log.warn('codex: could not finish the device login', {
|
|
328
|
-
|
|
381
|
+
log.warn('codex: could not finish the device login', {
|
|
382
|
+
error: maskString(String(error)).slice(0, 300),
|
|
383
|
+
});
|
|
329
384
|
}
|
|
330
385
|
});
|
|
331
386
|
proc.on('error', (error) => {
|
|
@@ -336,15 +391,20 @@ export class AuthRelay {
|
|
|
336
391
|
const deadline = Date.now() + URL_START_TIMEOUT_MS;
|
|
337
392
|
for (;;) {
|
|
338
393
|
const url = extractLoginUrl(agent, relay.buffer);
|
|
339
|
-
|
|
394
|
+
// Codex: a link is half of the sign-in, the one-time code the other half,
|
|
395
|
+
// and the CLI prints them on two lines that can arrive in two reads. A link
|
|
396
|
+
// without its code sends the person to a page that asks for something the
|
|
397
|
+
// window never showed (D26) – so both, or the CLI's own words about why not.
|
|
398
|
+
const code = agent === 'codex' ? extractDeviceCode(relay.buffer) : null;
|
|
399
|
+
const ready = url !== null && (agent !== 'codex' || code !== null);
|
|
400
|
+
if (ready && this.active !== relay) {
|
|
340
401
|
// A newer start took the slot while this one waited: its CLI is killed,
|
|
341
402
|
// and a link to it would lead nowhere – a code pasted for it would reach
|
|
342
403
|
// the newer CLI (found by the tests of the S2 check).
|
|
343
404
|
this.abandon(relay);
|
|
344
405
|
throw new Error('a newer sign-in was started on this server – use its link');
|
|
345
406
|
}
|
|
346
|
-
if (url) {
|
|
347
|
-
const code = agent === 'codex' ? extractDeviceCode(relay.buffer) : null;
|
|
407
|
+
if (ready && url) {
|
|
348
408
|
return { url, ...(code ? { code } : {}), expectsCode: agent === 'claude' };
|
|
349
409
|
}
|
|
350
410
|
if (relay.exited) {
|
|
@@ -354,7 +414,9 @@ export class AuthRelay {
|
|
|
354
414
|
}
|
|
355
415
|
if (Date.now() > deadline) {
|
|
356
416
|
this.abandon(relay);
|
|
357
|
-
throw new Error(
|
|
417
|
+
throw new Error(url
|
|
418
|
+
? `${agent} login printed a link but no one-time code in time – start again`
|
|
419
|
+
: `${agent} login did not print a sign-in URL in time`);
|
|
358
420
|
}
|
|
359
421
|
await sleep(200);
|
|
360
422
|
}
|
|
@@ -404,7 +466,7 @@ export class AuthRelay {
|
|
|
404
466
|
};
|
|
405
467
|
}
|
|
406
468
|
try {
|
|
407
|
-
const adopted = await adoptClaudeLogin(relay.claudeStaging);
|
|
469
|
+
const adopted = await adoptClaudeLogin(relay.claudeStaging, { activate: relay.activate });
|
|
408
470
|
// Adopted: the staging path no longer exists, nothing left to remove.
|
|
409
471
|
relay.claudeStaging = null;
|
|
410
472
|
clearAgentAuthFailure('claude', adopted.account.id);
|
|
@@ -526,7 +588,8 @@ export class AuthRelay {
|
|
|
526
588
|
log.info('auth-relay: stored a long-lived Claude token for this runner');
|
|
527
589
|
clearAgentAuthFailure('claude');
|
|
528
590
|
forgetClaudeUsage();
|
|
529
|
-
|
|
591
|
+
if (relay.activate)
|
|
592
|
+
activateMachineLogin();
|
|
530
593
|
return { ok: true, detail: 'signed in with a long-lived token stored on this server' };
|
|
531
594
|
}
|
|
532
595
|
// `claude auth login` writes the credential just before it exits; give the
|
|
@@ -540,7 +603,8 @@ export class AuthRelay {
|
|
|
540
603
|
if (status.status === 'ok') {
|
|
541
604
|
clearAgentAuthFailure('claude');
|
|
542
605
|
forgetClaudeUsage();
|
|
543
|
-
|
|
606
|
+
if (relay.activate)
|
|
607
|
+
activateMachineLogin();
|
|
544
608
|
return { ok: true };
|
|
545
609
|
}
|
|
546
610
|
await sleep(300);
|
|
@@ -581,12 +645,15 @@ export class AuthRelay {
|
|
|
581
645
|
}
|
|
582
646
|
/** A sign-in that will not be adopted leaves nothing behind (§8, S2 item 2). */
|
|
583
647
|
discardStaging(relay) {
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
return;
|
|
648
|
+
const claude = relay.claudeStaging;
|
|
649
|
+
const codex = relay.codexStaging;
|
|
587
650
|
relay.claudeStaging = null;
|
|
651
|
+
relay.codexStaging = null;
|
|
588
652
|
try {
|
|
589
|
-
|
|
653
|
+
if (claude)
|
|
654
|
+
discardClaudeStagingHome(claude);
|
|
655
|
+
if (codex)
|
|
656
|
+
discardCodexStagingHome(codex);
|
|
590
657
|
}
|
|
591
658
|
catch (error) {
|
|
592
659
|
log.warn('auth-relay: could not remove an abandoned sign-in home', {
|
|
@@ -654,8 +721,19 @@ export async function claudeAuthStatus(homedir = os.homedir(), options = {}) {
|
|
|
654
721
|
export function noteAgentAuthFailure(agent, accountId = MACHINE_ACCOUNT_ID) {
|
|
655
722
|
noteRefusal(agent, accountId);
|
|
656
723
|
log.warn('auth-relay: agent sign-in refused during a session', { agent, account: accountId });
|
|
657
|
-
if (agent
|
|
724
|
+
if (agent === 'codex') {
|
|
725
|
+
// A saved Codex login keeps the mark on disk, as a Claude one does (D2) –
|
|
726
|
+
// but only while it is the login in use. Codex refuses to refresh a session
|
|
727
|
+
// whose account is no longer the one `auth.json` points at («you have since
|
|
728
|
+
// signed in to another account»), and that refusal says nothing about the
|
|
729
|
+
// login of the account the session started under: marking it would put
|
|
730
|
+
// «sign in again» on a login that is perfectly good (found by the
|
|
731
|
+
// independent check of S4).
|
|
732
|
+
if (accountId !== MACHINE_ACCOUNT_ID && accountId === activeCodexAccountId()) {
|
|
733
|
+
markCodexLoginExpired(accountId);
|
|
734
|
+
}
|
|
658
735
|
return;
|
|
736
|
+
}
|
|
659
737
|
if (accountId !== MACHINE_ACCOUNT_ID) {
|
|
660
738
|
markLoginExpired(accountId);
|
|
661
739
|
return;
|
|
@@ -676,8 +754,11 @@ export function clearAgentAuthFailure(agent, accountId = MACHINE_ACCOUNT_ID) {
|
|
|
676
754
|
if (clearRefusal(agent, accountId)) {
|
|
677
755
|
log.info('auth-relay: agent sign-in is working again', { agent, account: accountId });
|
|
678
756
|
}
|
|
679
|
-
if (agent
|
|
757
|
+
if (agent === 'codex') {
|
|
758
|
+
if (accountId !== MACHINE_ACCOUNT_ID)
|
|
759
|
+
clearCodexLoginExpired(accountId);
|
|
680
760
|
return;
|
|
761
|
+
}
|
|
681
762
|
// A machine session that worked on the credentials FILE says nothing about the
|
|
682
763
|
// captured token: its mark is lifted only by a session that ran with it.
|
|
683
764
|
if (accountId !== MACHINE_ACCOUNT_ID || environmentCarriesStoredToken()) {
|
|
@@ -714,14 +795,35 @@ function logVerdictChange(agent, status) {
|
|
|
714
795
|
* Codex reports its own login state via an exit code (0 signed in / 1 not).
|
|
715
796
|
* Probed against the RUNNER's home: the host user can be signed in while our
|
|
716
797
|
* isolated home is not, and it is ours that sessions use.
|
|
798
|
+
*
|
|
799
|
+
* About the login new sessions start under (#422 S4): the machine login through
|
|
800
|
+
* the link, or the saved login the link points at – named in `activeAccount`,
|
|
801
|
+
* read locally from the file, no CLI for it (R14).
|
|
717
802
|
*/
|
|
718
803
|
export async function codexAuthStatus() {
|
|
719
804
|
// Re-assert the credential before judging it. The link into the shared store
|
|
720
805
|
// can disappear under a live daemon (codex has its own auth.json removal
|
|
721
806
|
// path), and reporting "login expired" about a credential that is merely
|
|
722
807
|
// unlinked — while the user's own file is valid for another week — is the
|
|
723
|
-
// exact complaint this fixes.
|
|
808
|
+
// exact complaint this fixes. Forcing nothing (S4 item 2): a saved login in
|
|
809
|
+
// use stays in use.
|
|
724
810
|
const home = repairCodexAuth();
|
|
811
|
+
const accountId = home.accountId ?? MACHINE_ACCOUNT_ID;
|
|
812
|
+
const activeAccount = codexActiveAccountSummary(accountId);
|
|
813
|
+
const verdict = await codexHomeVerdict(home);
|
|
814
|
+
// A saved login a session was refused with stays refused until its file is
|
|
815
|
+
// written again (D2) – the same rule as a saved Claude login.
|
|
816
|
+
if (verdict.status === 'ok' && codexRowRefused(accountId)) {
|
|
817
|
+
return {
|
|
818
|
+
...verdict,
|
|
819
|
+
status: 'expired',
|
|
820
|
+
detail: 'the agent was refused with this account – sign in again',
|
|
821
|
+
activeAccount,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return { ...verdict, activeAccount };
|
|
825
|
+
}
|
|
826
|
+
async function codexHomeVerdict(home) {
|
|
725
827
|
const local = readCodexCredential(home.path);
|
|
726
828
|
if (home.auth === 'missing' && !local) {
|
|
727
829
|
return { status: 'missing', detail: 'not signed in on this server' };
|
|
@@ -733,7 +835,11 @@ export async function codexAuthStatus() {
|
|
|
733
835
|
});
|
|
734
836
|
return {
|
|
735
837
|
status: 'ok',
|
|
736
|
-
detail: home.auth === 'linked'
|
|
838
|
+
detail: home.auth === 'linked'
|
|
839
|
+
? 'signed in with ChatGPT (shared login)'
|
|
840
|
+
: home.auth === 'account'
|
|
841
|
+
? 'signed in with ChatGPT (saved login)'
|
|
842
|
+
: 'signed in with ChatGPT',
|
|
737
843
|
...(local?.expiresAt ? { expiresAt: local.expiresAt } : {}),
|
|
738
844
|
};
|
|
739
845
|
}
|
|
@@ -763,30 +869,19 @@ export async function codexAuthStatus() {
|
|
|
763
869
|
/**
|
|
764
870
|
* Read what the runner's own auth.json says, without ever logging a token.
|
|
765
871
|
* Used only to tell "no credential" apart from "credential past its date" —
|
|
766
|
-
* the CLI probe is local and cannot detect a server-side revocation.
|
|
872
|
+
* the CLI probe is local and cannot detect a server-side revocation. The
|
|
873
|
+
* reading itself lives in `codex-home.ts`, where the saved logins are judged by
|
|
874
|
+
* the same lines.
|
|
767
875
|
*/
|
|
768
876
|
function readCodexCredential(homePath) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if (!access)
|
|
773
|
-
return null;
|
|
774
|
-
// A refresh token means the access token's own expiry is not the whole
|
|
775
|
-
// story — codex renews it on its own.
|
|
776
|
-
if (raw.tokens?.refresh_token)
|
|
777
|
-
return { expired: false };
|
|
778
|
-
const payload = access.split('.')[1];
|
|
779
|
-
if (!payload)
|
|
780
|
-
return { expired: false };
|
|
781
|
-
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
782
|
-
if (typeof claims.exp !== 'number')
|
|
783
|
-
return { expired: false };
|
|
784
|
-
const expiresAt = new Date(claims.exp * 1000).toISOString();
|
|
785
|
-
return { expiresAt, expired: claims.exp * 1000 < Date.now() };
|
|
786
|
-
}
|
|
787
|
-
catch {
|
|
877
|
+
const credential = readCodexCredentialFile(path.join(homePath, 'auth.json'));
|
|
878
|
+
// Only a ChatGPT login has tokens to date; an API key is left to `codex login status`.
|
|
879
|
+
if (credential.kind !== 'chatgpt')
|
|
788
880
|
return null;
|
|
789
|
-
|
|
881
|
+
return {
|
|
882
|
+
...(credential.expiresAt ? { expiresAt: credential.expiresAt } : {}),
|
|
883
|
+
expired: credential.status === 'expired',
|
|
884
|
+
};
|
|
790
885
|
}
|
|
791
886
|
/**
|
|
792
887
|
* A verdict read off disk, overruled by what a real session experienced.
|
|
@@ -807,7 +902,7 @@ function withObservedFailures(agent, status, accountId = MACHINE_ACCOUNT_ID) {
|
|
|
807
902
|
export async function agentAuthStatuses() {
|
|
808
903
|
const [claudeRaw, codexRaw] = await Promise.all([claudeAuthStatus(), codexAuthStatus()]);
|
|
809
904
|
const claude = withObservedFailures('claude', claudeRaw, claudeRaw.activeAccount?.id ?? MACHINE_ACCOUNT_ID);
|
|
810
|
-
const codex = withObservedFailures('codex', codexRaw);
|
|
905
|
+
const codex = withObservedFailures('codex', codexRaw, codexRaw.activeAccount?.id ?? MACHINE_ACCOUNT_ID);
|
|
811
906
|
logVerdictChange('claude', claude);
|
|
812
907
|
logVerdictChange('codex', codex);
|
|
813
908
|
return { claude, codex };
|
package/dist/claude-homes.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ClaudeIdentity } from './agent-auth.js';
|
|
2
2
|
import { type UsageReading, type UsageRow, type UsageSignature } from './adapters/claude-usage.js';
|
|
3
|
+
import { AccountError, isAccountId } from './login-marks.js';
|
|
3
4
|
/**
|
|
4
5
|
* Several Claude logins on one machine – one HOME per account (#422, D1).
|
|
5
6
|
*
|
|
@@ -41,14 +42,10 @@ export declare function claudeHomesDir(): string;
|
|
|
41
42
|
* exactly the definition of «the machine login».
|
|
42
43
|
*/
|
|
43
44
|
export declare function machineHome(homedir?: string): string;
|
|
44
|
-
/**
|
|
45
|
-
export
|
|
45
|
+
/** The id format and the refusal live beside the machine row's id – shared by both agents. */
|
|
46
|
+
export { AccountError, isAccountId };
|
|
46
47
|
/** The home of a saved account. Throws on anything that is not an account id. */
|
|
47
48
|
export declare function accountHome(id: string): string;
|
|
48
|
-
/** A refusal meant for a person: the wire (S2) sends its message as it is. */
|
|
49
|
-
export declare class AccountError extends Error {
|
|
50
|
-
constructor(message: string);
|
|
51
|
-
}
|
|
52
49
|
/**
|
|
53
50
|
* What a saved home reaches through a link rather than owning (R20).
|
|
54
51
|
*
|
|
@@ -410,7 +407,9 @@ export interface AdoptResult {
|
|
|
410
407
|
* BEFORE a new home is moved into place, so a failed write never leaves a login
|
|
411
408
|
* nobody can list or forget. The signed-in row becomes active (R15).
|
|
412
409
|
*/
|
|
413
|
-
export declare function adoptLoginResult(stagingDir: string
|
|
410
|
+
export declare function adoptLoginResult(stagingDir: string, options?: {
|
|
411
|
+
activate?: boolean;
|
|
412
|
+
}): Promise<AdoptResult>;
|
|
414
413
|
/**
|
|
415
414
|
* Bring a home that already holds a login into the store – by rename (D17, R7).
|
|
416
415
|
*
|
package/dist/claude-homes.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import crypto from 'node:crypto';
|
|
3
2
|
import fs from 'node:fs';
|
|
4
3
|
import os from 'node:os';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { sessionClaudePath } from './agent-binary.js';
|
|
7
6
|
import { readAgentAuth, storedClaudeToken, storedClaudeTokenRefused, updateAgentAuth, } from './agent-auth.js';
|
|
8
7
|
import { invalidateUsageCache, lastUsageRows, } from './adapters/claude-usage.js';
|
|
9
|
-
import { MACHINE_ACCOUNT_ID, clearRefusal, refusalActive } from './login-marks.js';
|
|
8
|
+
import { AccountError, MACHINE_ACCOUNT_ID, clearRefusal, isAccountId, newAccountId, refusalActive, } from './login-marks.js';
|
|
10
9
|
import { log } from './log.js';
|
|
11
10
|
import { stateDir } from './paths.js';
|
|
12
11
|
import { lowerPriority } from './process-priority.js';
|
|
@@ -66,30 +65,14 @@ function configFileOf(home, homedir = os.homedir()) {
|
|
|
66
65
|
function credentialsFileOf(home, homedir = os.homedir()) {
|
|
67
66
|
return path.join(home ?? machineHome(homedir), '.credentials.json');
|
|
68
67
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export function isAccountId(id) {
|
|
72
|
-
return typeof id === 'string' && ACCOUNT_ID.test(id);
|
|
73
|
-
}
|
|
74
|
-
function newAccountId() {
|
|
75
|
-
// base32-ish from random bytes: 12 characters of [a-z0-9].
|
|
76
|
-
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
77
|
-
const bytes = crypto.randomBytes(12);
|
|
78
|
-
return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');
|
|
79
|
-
}
|
|
68
|
+
/** The id format and the refusal live beside the machine row's id – shared by both agents. */
|
|
69
|
+
export { AccountError, isAccountId };
|
|
80
70
|
/** The home of a saved account. Throws on anything that is not an account id. */
|
|
81
71
|
export function accountHome(id) {
|
|
82
72
|
if (!isAccountId(id))
|
|
83
73
|
throw new AccountError(`not an account id: ${String(id).slice(0, 40)}`);
|
|
84
74
|
return path.join(claudeHomesDir(), id);
|
|
85
75
|
}
|
|
86
|
-
/** A refusal meant for a person: the wire (S2) sends its message as it is. */
|
|
87
|
-
export class AccountError extends Error {
|
|
88
|
-
constructor(message) {
|
|
89
|
-
super(message);
|
|
90
|
-
this.name = 'AccountError';
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
76
|
function lstatOrNull(target) {
|
|
94
77
|
try {
|
|
95
78
|
return fs.lstatSync(target);
|
|
@@ -1364,7 +1347,12 @@ export function discardStagingHome(dir) {
|
|
|
1364
1347
|
* BEFORE a new home is moved into place, so a failed write never leaves a login
|
|
1365
1348
|
* nobody can list or forget. The signed-in row becomes active (R15).
|
|
1366
1349
|
*/
|
|
1367
|
-
export async function adoptLoginResult(stagingDir) {
|
|
1350
|
+
export async function adoptLoginResult(stagingDir, options = {}) {
|
|
1351
|
+
// «And make it the one new sessions start on» – the window's own tick, on
|
|
1352
|
+
// unless the person turned it off (S4.1). Off: the row is stored and listed,
|
|
1353
|
+
// and the active account does not move. Default true, because that is what a
|
|
1354
|
+
// sign-in has always done and what an older API still asks for.
|
|
1355
|
+
const activate = options.activate !== false;
|
|
1368
1356
|
if (!isStagingDir(stagingDir) || !lstatOrNull(stagingDir)?.isDirectory()) {
|
|
1369
1357
|
throw new AccountError('no sign-in in progress – start again');
|
|
1370
1358
|
}
|
|
@@ -1412,10 +1400,12 @@ export async function adoptLoginResult(stagingDir) {
|
|
|
1412
1400
|
});
|
|
1413
1401
|
throw error;
|
|
1414
1402
|
}
|
|
1415
|
-
|
|
1416
|
-
file
|
|
1417
|
-
|
|
1418
|
-
|
|
1403
|
+
if (activate) {
|
|
1404
|
+
updateAgentAuth((file) => {
|
|
1405
|
+
file.claudeActiveAccount = id;
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
log.info('claude-homes: a new account was added', { account: id, active: activate });
|
|
1419
1409
|
return { account: cardOf(id), active: activeAccountId() };
|
|
1420
1410
|
}
|
|
1421
1411
|
const home = accountHome(same.id);
|
|
@@ -1442,7 +1432,11 @@ export async function adoptLoginResult(stagingDir) {
|
|
|
1442
1432
|
record.lastSeenIdentity = stamped;
|
|
1443
1433
|
delete record.loginExpiredAt;
|
|
1444
1434
|
}
|
|
1445
|
-
|
|
1435
|
+
// The row that signed in again is the SAME row: leaving the pointer alone
|
|
1436
|
+
// when the tick is off keeps whatever was active active – including this
|
|
1437
|
+
// row, when it already was.
|
|
1438
|
+
if (activate)
|
|
1439
|
+
file.claudeActiveAccount = same.id;
|
|
1446
1440
|
});
|
|
1447
1441
|
// A fresh login outranks anything remembered about the one it replaced – on
|
|
1448
1442
|
// disk above, and in this process's memory of refusals here.
|
|
@@ -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
|