@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.
- package/dist/account-commands.d.ts +43 -0
- package/dist/account-commands.js +142 -0
- package/dist/adapters/claude-usage.d.ts +79 -15
- package/dist/adapters/claude-usage.js +143 -67
- package/dist/adapters/claude.d.ts +30 -1
- package/dist/adapters/claude.js +171 -11
- package/dist/adapters/codex-home.d.ts +143 -21
- package/dist/adapters/codex-home.js +623 -77
- package/dist/adapters/codex.d.ts +25 -3
- package/dist/adapters/codex.js +149 -11
- package/dist/adapters/types.d.ts +22 -0
- package/dist/agent-auth.d.ts +100 -0
- package/dist/agent-auth.js +273 -15
- package/dist/auth-relay.d.ts +114 -26
- package/dist/auth-relay.js +464 -278
- package/dist/claude-homes.d.ts +433 -0
- package/dist/claude-homes.js +1550 -0
- package/dist/codex-accounts.d.ts +79 -0
- package/dist/codex-accounts.js +386 -0
- package/dist/commit-message.js +4 -1
- package/dist/config.d.ts +16 -16
- package/dist/config.js +4 -1
- package/dist/index.js +78 -10
- package/dist/login-marks.d.ts +42 -0
- package/dist/login-marks.js +70 -0
- package/dist/protocol.d.ts +38 -38
- package/dist/protocol.js +3 -1
- package/dist/supervisor.js +17 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import { type AgentAdapter, type AgentQuestion, type AgentSession, type SessionSpec } from './types.js';
|
|
2
|
+
import { type AgentAdapter, type AgentMode, type AgentQuestion, type AgentSession, type SessionSpec } from './types.js';
|
|
3
|
+
import { type SessionAccount } from '../claude-homes.js';
|
|
3
4
|
export declare function truncate(text: string, limit?: number): string;
|
|
4
5
|
/** Exported for the one-shot commit-message run (session 14), same rules. */
|
|
5
6
|
export declare function scrubbedEnv(): Record<string, string>;
|
|
7
|
+
/**
|
|
8
|
+
* The one env var the allowlist above refuses, added back for exactly one mode
|
|
9
|
+
* (ticket #156).
|
|
10
|
+
*
|
|
11
|
+
* The CLI's own guard, read out of the binary verbatim:
|
|
12
|
+
*
|
|
13
|
+
* ```js
|
|
14
|
+
* if (t === "bypassPermissions" || r) {
|
|
15
|
+
* if (typeof process.getuid === "function" && process.getuid() === 0
|
|
16
|
+
* && process.env.IS_SANDBOX !== "1" && !Z.CLAUDE_CODE_BUBBLEWRAP)
|
|
17
|
+
* console.error("--dangerously-skip-permissions cannot be used with root/sudo privileges …"),
|
|
18
|
+
* process.exit(1)
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The runner's default install is root (`install.sh`), so on an ordinary
|
|
23
|
+
* machine «Unrestricted» did not degrade — it killed the session at launch with
|
|
24
|
+
* exit code 1. On the machine this was found on it happened to start, because
|
|
25
|
+
* `~/.claude/settings.json` carries `env: { IS_SANDBOX: "1" }` and this adapter
|
|
26
|
+
* loads user settings; that is a hole in the scrub covering a bug, not a fix.
|
|
27
|
+
*
|
|
28
|
+
* The condition here is the CLI's own, no wider: `full` AND uid 0. It is safe
|
|
29
|
+
* precisely where it is applied — `bypassPermissions` is the mode in which the
|
|
30
|
+
* SDK never calls `canUseTool` (it says so itself), so layer 1 is already inert
|
|
31
|
+
* and there is nothing left for `IS_SANDBOX` to weaken. In every other mode the
|
|
32
|
+
* scrub stands, which is what the comment on `ENV_ALLOWLIST` has always meant.
|
|
33
|
+
*/
|
|
34
|
+
export declare function agentEnv(mode: AgentMode, sessionId: string, account?: Pick<SessionAccount, 'kind' | 'home'>): Record<string, string>;
|
|
6
35
|
/**
|
|
7
36
|
* The whole `AskUserQuestion` payload, not just its first line.
|
|
8
37
|
*
|
package/dist/adapters/claude.js
CHANGED
|
@@ -13,6 +13,7 @@ import { percentFromUtilization, RATE_WINDOW_MINUTES, rateWindowKey } from './ra
|
|
|
13
13
|
import { AgentTaskTray } from './agent-tasks.js';
|
|
14
14
|
import { applyUsagePercentages, lastUsageRows, readUsageRows } from './claude-usage.js';
|
|
15
15
|
import { assertClaudeInstalled, claudeExecutableOption, sessionClaudePath, } from '../agent-binary.js';
|
|
16
|
+
import { noteSessionAccount, refreshAccountIdentity, releaseSessionAccount, resolveSessionAccount, currentHomeOrgId, usageSignature, withAccountHome, } from '../claude-homes.js';
|
|
16
17
|
/** Same 2KB the SDK keeps: enough for the CLI's last words, not a log sink. */
|
|
17
18
|
const STDERR_TAIL_LIMIT = 2048;
|
|
18
19
|
import { answerSummary, answerValue, discussMessage, invalidationMessage, mirrorOptions, newAskId, MAX_OPTIONS, MAX_QUESTIONS, OPTION_TEXT_LIMIT, QUESTION_TEXT_LIMIT, } from './questions.js';
|
|
@@ -101,8 +102,18 @@ export function scrubbedEnv() {
|
|
|
101
102
|
* and there is nothing left for `IS_SANDBOX` to weaken. In every other mode the
|
|
102
103
|
* scrub stands, which is what the comment on `ENV_ALLOWLIST` has always meant.
|
|
103
104
|
*/
|
|
104
|
-
function agentEnv(mode, sessionId) {
|
|
105
|
-
|
|
105
|
+
export function agentEnv(mode, sessionId, account = { kind: 'machine', home: null }) {
|
|
106
|
+
/**
|
|
107
|
+
* The account's home (#422 S1 item 4) – HERE, in the session's environment, and
|
|
108
|
+
* never in the daemon's (К1): the daemon's environment is also what the verdict
|
|
109
|
+
* and `doctor` would read, and a variable there turns every verdict green.
|
|
110
|
+
*
|
|
111
|
+
* For a saved account the operator's `CLAUDE_CODE_OAUTH_TOKEN` comes OUT: the
|
|
112
|
+
* CLI ranks it above every login file, so with it left in, switching accounts
|
|
113
|
+
* would change nothing under a green verdict (К14). For the machine login the
|
|
114
|
+
* environment is exactly what it was before #422.
|
|
115
|
+
*/
|
|
116
|
+
const env = withAccountHome(scrubbedEnv(), account);
|
|
106
117
|
if (mode === 'full' && process.getuid?.() === 0)
|
|
107
118
|
env['IS_SANDBOX'] = '1';
|
|
108
119
|
/**
|
|
@@ -480,8 +491,30 @@ class ClaudeSession {
|
|
|
480
491
|
this.beginTurnFacts();
|
|
481
492
|
}
|
|
482
493
|
events = this.output;
|
|
494
|
+
/**
|
|
495
|
+
* The account this process runs under, captured at its start and kept to its
|
|
496
|
+
* end (#422 R14, R16): the limits it measures, the refusal it may meet and the
|
|
497
|
+
* account its card names all belong to THIS account, whatever the machine is
|
|
498
|
+
* switched to meanwhile.
|
|
499
|
+
*/
|
|
500
|
+
account;
|
|
501
|
+
accountId;
|
|
502
|
+
/** Started on the operator's token: its limits go out unsigned (see the constructor). */
|
|
503
|
+
onOperatorToken;
|
|
483
504
|
constructor(spec, queryFn) {
|
|
484
505
|
this.spec = spec;
|
|
506
|
+
const account = resolveSessionAccount();
|
|
507
|
+
// A machine-login session that runs on a TOKEN is not the account of the
|
|
508
|
+
// credentials file: `claude auth status` is asked without the token, so the
|
|
509
|
+
// identity on file names the file's login, and this session's card and its
|
|
510
|
+
// limits would carry somebody else's name (found by the independent check of
|
|
511
|
+
// S1). Such a session keeps only the row id – unsigned, as before #422.
|
|
512
|
+
this.onOperatorToken =
|
|
513
|
+
account.kind === 'machine' && Boolean(process.env['CLAUDE_CODE_OAUTH_TOKEN']);
|
|
514
|
+
this.account = this.onOperatorToken
|
|
515
|
+
? { id: account.id, kind: account.kind, home: null }
|
|
516
|
+
: account;
|
|
517
|
+
this.accountId = this.account.id;
|
|
485
518
|
// Refused rather than honoured, and BEFORE anything is built from it: on a
|
|
486
519
|
// STRICT workspace `full` would launch the CLI in `bypassPermissions`, and
|
|
487
520
|
// there layer 1 is never consulted — the manager's shield would be spent by
|
|
@@ -522,7 +555,7 @@ class ClaudeSession {
|
|
|
522
555
|
this.mcpConfigFile = mcpConfigFile;
|
|
523
556
|
const options = {
|
|
524
557
|
cwd: spec.cwd,
|
|
525
|
-
env: agentEnv(this.mode, spec.sessionId),
|
|
558
|
+
env: agentEnv(this.mode, spec.sessionId, this.account),
|
|
526
559
|
// Empty while `USE_BUNDLED_CLAUDE` — the SDK keeps resolving its own
|
|
527
560
|
// bundled binary, exactly as before. After C3 this pins the system
|
|
528
561
|
// `claude`, which is the file the card measures and the button installs.
|
|
@@ -615,7 +648,17 @@ class ClaudeSession {
|
|
|
615
648
|
? { mcpServers }
|
|
616
649
|
: {}),
|
|
617
650
|
};
|
|
618
|
-
|
|
651
|
+
// Registered before the process exists, released when it is gone
|
|
652
|
+
// (`consume`'s finally): `forget` refuses an account a live process writes
|
|
653
|
+
// into, and a replaced login's old home waits for exactly these to end.
|
|
654
|
+
noteSessionAccount(spec.sessionId, this.account.id);
|
|
655
|
+
try {
|
|
656
|
+
this.q = queryFn({ prompt: this.input, options });
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
releaseSessionAccount(spec.sessionId);
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
619
662
|
void this.consume();
|
|
620
663
|
// Ticket #119 / QA-114 MAJOR-3: if the protected file could not be written,
|
|
621
664
|
// say so where a person will see it, not only in journald.
|
|
@@ -897,12 +940,25 @@ class ClaudeSession {
|
|
|
897
940
|
refreshUsage() {
|
|
898
941
|
if (this.stopped)
|
|
899
942
|
return;
|
|
900
|
-
|
|
943
|
+
// THIS session's account (#422 R16): the reading is keyed by its
|
|
944
|
+
// subscription, measured under its home, and signed by what that home says
|
|
945
|
+
// right after – never whatever the machine is switched to now.
|
|
946
|
+
const account = {
|
|
947
|
+
id: this.account.id,
|
|
948
|
+
home: this.account.home,
|
|
949
|
+
...(this.account.orgId ? { orgId: this.account.orgId } : {}),
|
|
950
|
+
};
|
|
951
|
+
if (this.absorbUsageReading(lastUsageRows(account)))
|
|
901
952
|
this.emitRateLimits();
|
|
902
953
|
const binary = sessionClaudePath();
|
|
903
954
|
if (!binary)
|
|
904
955
|
return;
|
|
905
|
-
void readUsageRows({
|
|
956
|
+
void readUsageRows({
|
|
957
|
+
binary,
|
|
958
|
+
account,
|
|
959
|
+
env: withAccountHome({ ...process.env }, this.account),
|
|
960
|
+
sign: () => usageSignature(this.account),
|
|
961
|
+
})
|
|
906
962
|
.then((reading) => {
|
|
907
963
|
if (this.stopped)
|
|
908
964
|
return;
|
|
@@ -913,6 +969,48 @@ class ClaudeSession {
|
|
|
913
969
|
}
|
|
914
970
|
/** The measurement already folded in, so an older one cannot undo a newer event. */
|
|
915
971
|
usageAppliedAtMs = 0;
|
|
972
|
+
/**
|
|
973
|
+
* The CLI's own signature on the last reading folded in (#422 §8). Only ever
|
|
974
|
+
* this session's account: a reading signed by another subscription never
|
|
975
|
+
* reaches `absorbUsageReading`'s fold.
|
|
976
|
+
*/
|
|
977
|
+
usageAccountUuid;
|
|
978
|
+
/**
|
|
979
|
+
* The subscription of a session that could not name one at start: the first
|
|
980
|
+
* signed reading measured under its home, and never overwritten. Taken from
|
|
981
|
+
* every reading instead, it moved with the home – a terminal login into
|
|
982
|
+
* another subscription re-based a running session onto it, and the latch below
|
|
983
|
+
* never saw a change (found by the second independent check of S2).
|
|
984
|
+
*/
|
|
985
|
+
firstReadingOrgId;
|
|
986
|
+
/** Latched: this home was signed in to another subscription while the session ran. */
|
|
987
|
+
signedInElsewhere = false;
|
|
988
|
+
/**
|
|
989
|
+
* Has somebody signed this session's home in to a different subscription?
|
|
990
|
+
*
|
|
991
|
+
* «Log in again» on the machine row, or `claude auth login` in a terminal,
|
|
992
|
+
* rewrites the home the session reads by PATH, and the CLI process moves onto
|
|
993
|
+
* the new login at its next token refresh (gotcha 531). From then on its
|
|
994
|
+
* figures are the new subscription's, and signing them with the one captured at
|
|
995
|
+
* start would put B's percentages under A's name – exactly what the signature
|
|
996
|
+
* exists to prevent (found by the independent check of S2). The frame then goes
|
|
997
|
+
* unsigned, as before #422, until the process ends.
|
|
998
|
+
*/
|
|
999
|
+
homeSignedInElsewhere() {
|
|
1000
|
+
if (this.signedInElsewhere)
|
|
1001
|
+
return true;
|
|
1002
|
+
const captured = this.signatureOrgId();
|
|
1003
|
+
if (!captured)
|
|
1004
|
+
return false;
|
|
1005
|
+
const now = currentHomeOrgId(this.account.home);
|
|
1006
|
+
if (now !== undefined && now !== captured) {
|
|
1007
|
+
this.signedInElsewhere = true;
|
|
1008
|
+
log.info('claude: the session home was signed in to another subscription – limits unsigned', {
|
|
1009
|
+
sessionId: this.spec.sessionId,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return this.signedInElsewhere;
|
|
1013
|
+
}
|
|
916
1014
|
/**
|
|
917
1015
|
* Fold a `/usage` reading into this session's window map.
|
|
918
1016
|
*
|
|
@@ -936,6 +1034,23 @@ class ClaudeSession {
|
|
|
936
1034
|
absorbUsageReading(reading) {
|
|
937
1035
|
if (!reading || reading.rows.length === 0)
|
|
938
1036
|
return false;
|
|
1037
|
+
// Somebody else's figures (#422 S1 item 5): a reading signed by a different
|
|
1038
|
+
// subscription than this session's never lands in its windows. Unsigned on
|
|
1039
|
+
// either side is taken as before – that is every pre-#422 reading.
|
|
1040
|
+
const own = this.signatureOrgId();
|
|
1041
|
+
if (reading.orgId && own && reading.orgId !== own)
|
|
1042
|
+
return false;
|
|
1043
|
+
// A session that could not name its subscription takes the first signed
|
|
1044
|
+
// reading as its own – but only one of the subscription its home names NOW.
|
|
1045
|
+
// The machine's cache can still hold a reading from the home's previous
|
|
1046
|
+
// login (a terminal sign-in is not seen by `forgetClaudeUsage`), and fixing
|
|
1047
|
+
// that as the signature refused every fresh reading for the rest of the
|
|
1048
|
+
// session (found by the check of the second fix round of S2).
|
|
1049
|
+
if (reading.orgId && !own) {
|
|
1050
|
+
const homeNow = currentHomeOrgId(this.account.home);
|
|
1051
|
+
if (homeNow !== undefined && homeNow !== reading.orgId)
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
939
1054
|
const fresh = reading.measuredAtMs > this.usageAppliedAtMs;
|
|
940
1055
|
const before = this.rateLimitsFingerprint();
|
|
941
1056
|
// A percentage proves a plan as surely as the event does.
|
|
@@ -949,8 +1064,16 @@ class ClaudeSession {
|
|
|
949
1064
|
}
|
|
950
1065
|
if (fresh)
|
|
951
1066
|
this.usageAppliedAtMs = reading.measuredAtMs;
|
|
1067
|
+
if (reading.accountUuid)
|
|
1068
|
+
this.usageAccountUuid = reading.accountUuid;
|
|
1069
|
+
if (reading.orgId && !own)
|
|
1070
|
+
this.firstReadingOrgId = reading.orgId;
|
|
952
1071
|
return this.rateLimitsFingerprint() !== before;
|
|
953
1072
|
}
|
|
1073
|
+
/** The subscription this session signs with: captured at start, else its first signed reading. */
|
|
1074
|
+
signatureOrgId() {
|
|
1075
|
+
return this.account.orgId ?? this.firstReadingOrgId;
|
|
1076
|
+
}
|
|
954
1077
|
rateLimitsFingerprint() {
|
|
955
1078
|
return JSON.stringify([this.rateLimitsAvailable, [...this.rateLimitWindows.values()]]);
|
|
956
1079
|
}
|
|
@@ -961,9 +1084,20 @@ class ClaudeSession {
|
|
|
961
1084
|
return blocked;
|
|
962
1085
|
}
|
|
963
1086
|
emitRateLimits(blocked = null) {
|
|
1087
|
+
// Signed by THIS session's account (#422 §8, S2 item 4): the windows come
|
|
1088
|
+
// from its own stream and from readings measured under its own home. The
|
|
1089
|
+
// subscription is the one captured at start; a session that could not name
|
|
1090
|
+
// it borrows the reading's, which was measured in the very same home. A
|
|
1091
|
+
// session on the operator's token signs nothing: the home's file names the
|
|
1092
|
+
// file's login, not the token's (the constructor's note).
|
|
1093
|
+
const unsigned = this.onOperatorToken || this.homeSignedInElsewhere();
|
|
1094
|
+
const orgId = unsigned ? undefined : this.signatureOrgId();
|
|
1095
|
+
const accountUuid = unsigned ? undefined : this.usageAccountUuid;
|
|
964
1096
|
this.emit({
|
|
965
1097
|
type: 'rate_limits',
|
|
966
1098
|
limits: {
|
|
1099
|
+
...(accountUuid ? { accountUuid } : {}),
|
|
1100
|
+
...(orgId ? { orgId } : {}),
|
|
967
1101
|
blocked,
|
|
968
1102
|
available: this.rateLimitsAvailable,
|
|
969
1103
|
planType: this.ratePlanType,
|
|
@@ -1204,12 +1338,24 @@ class ClaudeSession {
|
|
|
1204
1338
|
currentMode: this.mode,
|
|
1205
1339
|
...(this.model ? { currentModel: this.model } : {}),
|
|
1206
1340
|
...(this.effort ? { currentEffort: this.effort } : {}),
|
|
1207
|
-
|
|
1341
|
+
// What the live CLI says first, what the runner last read about the home
|
|
1342
|
+
// second; plus WHICH of the machine's accounts this is (#422 §8), which
|
|
1343
|
+
// only the runner knows. No block at all when neither side knows anything
|
|
1344
|
+
// – exactly as before.
|
|
1345
|
+
...(account || this.account.email
|
|
1208
1346
|
? {
|
|
1209
1347
|
account: {
|
|
1210
|
-
|
|
1211
|
-
...(account
|
|
1212
|
-
|
|
1348
|
+
id: this.account.id,
|
|
1349
|
+
...((account?.email ?? this.account.email)
|
|
1350
|
+
? { email: account?.email ?? this.account.email }
|
|
1351
|
+
: {}),
|
|
1352
|
+
...((account?.organization ?? this.account.orgName)
|
|
1353
|
+
? { organization: account?.organization ?? this.account.orgName }
|
|
1354
|
+
: {}),
|
|
1355
|
+
...((account?.subscriptionType ?? this.account.plan)
|
|
1356
|
+
? { plan: account?.subscriptionType ?? this.account.plan }
|
|
1357
|
+
: {}),
|
|
1358
|
+
...(this.account.orgId ? { orgId: this.account.orgId } : {}),
|
|
1213
1359
|
},
|
|
1214
1360
|
}
|
|
1215
1361
|
: {}),
|
|
@@ -2502,6 +2648,7 @@ class ClaudeSession {
|
|
|
2502
2648
|
// the path that catches a session which died before the CLI ever
|
|
2503
2649
|
// answered.
|
|
2504
2650
|
this.removeMcpConfig();
|
|
2651
|
+
releaseSessionAccount(this.spec.sessionId);
|
|
2505
2652
|
this.stopped = true;
|
|
2506
2653
|
this.input.end();
|
|
2507
2654
|
this.output.end();
|
|
@@ -2735,7 +2882,20 @@ export class ClaudeAdapter {
|
|
|
2735
2882
|
*/
|
|
2736
2883
|
if (this.queryFn === query)
|
|
2737
2884
|
assertClaudeInstalled();
|
|
2738
|
-
|
|
2885
|
+
const session = new ClaudeSession(spec, this.queryFn);
|
|
2886
|
+
/**
|
|
2887
|
+
* Who the home is, asked once when nobody knows or the home says it changed
|
|
2888
|
+
* (#422 R14 c) – in the background, after the session is on its way. The
|
|
2889
|
+
* next session and the account list get the answer; this one keeps what it
|
|
2890
|
+
* started with.
|
|
2891
|
+
*
|
|
2892
|
+
* Only for the real SDK, for the reason above: an injected `query` is a test,
|
|
2893
|
+
* and a test must not run `claude auth status` against this machine's login.
|
|
2894
|
+
*/
|
|
2895
|
+
if (this.queryFn === query) {
|
|
2896
|
+
void refreshAccountIdentity(session.accountId).catch(() => undefined);
|
|
2897
|
+
}
|
|
2898
|
+
return session;
|
|
2739
2899
|
}
|
|
2740
2900
|
}
|
|
2741
2901
|
//# sourceMappingURL=claude.js.map
|
|
@@ -1,24 +1,90 @@
|
|
|
1
1
|
export type CodexAuthMode = 'link' | 'own';
|
|
2
2
|
export interface CodexHome {
|
|
3
3
|
path: string;
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* How the home is authenticated — reported in notices, not a secret.
|
|
6
|
+
* `account` – a saved login is linked (`accountId`); `own` – a real file the
|
|
7
|
+
* runner could not move into the store and keeps using where it is.
|
|
8
|
+
*/
|
|
9
|
+
auth: 'linked' | 'account' | 'own' | 'missing';
|
|
10
|
+
/** The saved login the home is set to – present with `account`, and with `missing` under it. */
|
|
11
|
+
accountId?: string;
|
|
6
12
|
}
|
|
7
13
|
export declare function codexHomePath(): string;
|
|
14
|
+
/** Where the saved Codex logins live – inside the one home, beside what they share. */
|
|
15
|
+
export declare function codexAccountsDir(home?: string): string;
|
|
16
|
+
/** The directory of one saved login. Throws on anything that is not an account id. */
|
|
17
|
+
export declare function codexAccountDir(id: string, home?: string): string;
|
|
18
|
+
/** The login file of one saved account. */
|
|
19
|
+
export declare function codexAccountAuthFile(id: string, home?: string): string;
|
|
20
|
+
/** The machine login of Codex – the host user's own file, never written by DevBridge (R13). */
|
|
21
|
+
export declare function hostCodexAuthFile(homedir?: string): string;
|
|
8
22
|
/**
|
|
9
|
-
* Throwaway
|
|
10
|
-
* into the
|
|
11
|
-
* cannot destroy a credential that was working.
|
|
23
|
+
* Throwaway homes for device-code logins, one per attempt. The flow runs there
|
|
24
|
+
* and its login is moved into the store only on success, so an abandoned or
|
|
25
|
+
* timed-out sign-in cannot destroy a credential that was working.
|
|
12
26
|
*/
|
|
13
27
|
export declare function stagingCodexHomePath(): string;
|
|
28
|
+
/**
|
|
29
|
+
* `[codex] auth` as the machine's owner WROTE it – `undefined` when the key is
|
|
30
|
+
* not in `config.toml`. Set once at daemon start.
|
|
31
|
+
*
|
|
32
|
+
* Only a mode that is really written may be forced, and even then not over a
|
|
33
|
+
* saved login (S4 item 2): the runner used to pass `link` by default from three
|
|
34
|
+
* places – the daemon start, every session start and the minute probe – and each
|
|
35
|
+
* of them would have undone a switch of account within a minute (К4).
|
|
36
|
+
*/
|
|
37
|
+
export declare function configureCodexAuth(mode: CodexAuthMode | undefined): void;
|
|
38
|
+
export declare function configuredCodexAuth(): CodexAuthMode | undefined;
|
|
39
|
+
/** Which saved login the home is set to, if any – the mark, read only. */
|
|
40
|
+
export declare function markedCodexAccount(home?: string): string | null;
|
|
41
|
+
/** Who a Codex login is, read locally from its own file – never a token. */
|
|
42
|
+
export interface CodexIdentity {
|
|
43
|
+
email?: string;
|
|
44
|
+
/** `tokens.account_id` – the key of a Codex subscription (plan §8, S4 item 5). */
|
|
45
|
+
orgId?: string;
|
|
46
|
+
/** `chatgpt_plan_type` – `plus`, `pro`, `team`, … */
|
|
47
|
+
plan?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface CodexCredential {
|
|
50
|
+
/**
|
|
51
|
+
* `ok` – a login the CLI can use; `expired` – a login past its date with no
|
|
52
|
+
* refresh token; `missing` – no file; `unreadable` – a file that is not a login
|
|
53
|
+
* (torn, hand-edited); `unknown` – we could not look (EACCES).
|
|
54
|
+
*/
|
|
55
|
+
status: 'ok' | 'expired' | 'missing' | 'unreadable' | 'unknown';
|
|
56
|
+
/** A ChatGPT login (tokens) or an API key – only for a file that is a login. */
|
|
57
|
+
kind?: 'chatgpt' | 'apikey';
|
|
58
|
+
identity: CodexIdentity;
|
|
59
|
+
/** Only when the access token's own date is the login's date (no refresh token). */
|
|
60
|
+
expiresAt?: string;
|
|
61
|
+
/** Last write of the file, whole ms – a refusal mark older than this no longer holds. */
|
|
62
|
+
writtenMs?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read a Codex login file without letting a token out of this function.
|
|
66
|
+
*
|
|
67
|
+
* The identity comes from the claims of `id_token`, decoded locally (S4 item 8):
|
|
68
|
+
* `email`, and under `https://api.openai.com/auth` the plan and the ChatGPT
|
|
69
|
+
* account; `tokens.account_id` first for the key, the claim when it is absent. An
|
|
70
|
+
* internal file of the CLI (§11): whatever cannot be read is simply not there –
|
|
71
|
+
* no identity is invented, and a row without a key merges with nothing.
|
|
72
|
+
*
|
|
73
|
+
* The verdict is the pre-S4 one (`readCodexCredential`): a refresh token means
|
|
74
|
+
* the CLI renews the access token on its own, so only a lone access token past
|
|
75
|
+
* its `exp` is `expired`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function readCodexCredentialFile(file: string): CodexCredential;
|
|
14
78
|
/**
|
|
15
79
|
* Create (or refresh) the runner's CODEX_HOME and return it.
|
|
16
80
|
*
|
|
17
|
-
* `
|
|
18
|
-
* runner uses their ChatGPT subscription and —
|
|
19
|
-
* credential store with their own CLI, so a token
|
|
20
|
-
* both working. `
|
|
21
|
-
* device-code login
|
|
81
|
+
* `link` (the default when nothing else is decided) symlinks the host user's
|
|
82
|
+
* `~/.codex/auth.json` so the runner uses their ChatGPT subscription and —
|
|
83
|
+
* importantly — shares one credential store with their own CLI, so a token
|
|
84
|
+
* refresh on either side keeps both working. `own` leaves the home
|
|
85
|
+
* unauthenticated until a device-code login is stored for it, which is the
|
|
86
|
+
* choice for full isolation. A saved login (`account:<id>`) is kept whatever
|
|
87
|
+
* the configured mode says (S4 item 2).
|
|
22
88
|
*
|
|
23
89
|
* A home under the OS temp dir still works, but codex then refuses to install
|
|
24
90
|
* its helper binaries ("Refusing to create helper binaries under temporary
|
|
@@ -35,6 +101,9 @@ export declare function ensureCodexHome(options?: {
|
|
|
35
101
|
* it only answers "is the credential still where we left it, and if not, can we
|
|
36
102
|
* put it back". This is what makes a credential disappearing under a running
|
|
37
103
|
* daemon self-healing instead of permanent.
|
|
104
|
+
*
|
|
105
|
+
* Forces nothing it was not told to by the machine's owner (S4 item 2): with no
|
|
106
|
+
* `[codex] auth` in `config.toml` the mark decides, and a saved login stays.
|
|
38
107
|
*/
|
|
39
108
|
export declare function repairCodexAuth(options?: {
|
|
40
109
|
auth?: CodexAuthMode;
|
|
@@ -42,20 +111,73 @@ export declare function repairCodexAuth(options?: {
|
|
|
42
111
|
}): CodexHome;
|
|
43
112
|
/**
|
|
44
113
|
* Drop a linked auth.json so a device-code login writes our own file instead.
|
|
45
|
-
*
|
|
46
|
-
* Only ever called AFTER a login has actually succeeded in the staging home —
|
|
47
|
-
* never as a pre-step. Detaching first meant an abandoned or timed-out sign-in
|
|
48
|
-
* left the server permanently "not signed in", recoverable only by restarting
|
|
49
|
-
* the daemon.
|
|
114
|
+
* Never a real credential: only a symlink is removed.
|
|
50
115
|
*/
|
|
51
116
|
export declare function detachLinkedAuth(dir?: string): void;
|
|
117
|
+
export interface StoredCodexLogin {
|
|
118
|
+
id: string;
|
|
119
|
+
/** The saved row whose login this one replaced – the same subscription (D19). */
|
|
120
|
+
replaced?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Move a login file into the store (§8 `agent_account_login_code` for Codex, S4
|
|
124
|
+
* items 4 and 5) – by rename, and one subscription, one row.
|
|
125
|
+
*
|
|
126
|
+
* A known `account_id` that a saved row already has REPLACES that row's login:
|
|
127
|
+
* the row keeps its id (refusal marks and a live session are keyed by it, R15)
|
|
128
|
+
* and its `addedAt` becomes now – the date is that of the login the row holds,
|
|
129
|
+
* and a second sign-in of the same subscription must change something the
|
|
130
|
+
* window can see (S3 hands this over: `deviceSignInResult`). An unknown key
|
|
131
|
+
* merges with nothing. The machine row takes no part: it is the host's own file.
|
|
132
|
+
*
|
|
133
|
+
* The record is written BEFORE the file moves, so a failure never leaves a login
|
|
134
|
+
* nobody can list or forget; a move that fails takes its new record back.
|
|
135
|
+
*/
|
|
136
|
+
export declare function storeCodexLogin(source: string, options: {
|
|
137
|
+
activate: boolean;
|
|
138
|
+
home?: string;
|
|
139
|
+
replaceActive?: boolean;
|
|
140
|
+
}): StoredCodexLogin;
|
|
52
141
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
142
|
+
* Make a row the one the home uses: the mark first, then the link.
|
|
143
|
+
*
|
|
144
|
+
* In that order on purpose – a daemon that dies between the two leaves a mark
|
|
145
|
+
* the next repair finishes (it points the link where the mark says), never a
|
|
146
|
+
* link that the next repair would undo.
|
|
147
|
+
*
|
|
148
|
+
* A real file in place of the link is dealt with FIRST (§8 `activate`, S4 item
|
|
149
|
+
* 7): the CLI may have refreshed the login in use a moment ago, and repointing
|
|
150
|
+
* over it would throw that token away.
|
|
151
|
+
*/
|
|
152
|
+
export declare function markActiveCodexLogin(id: string, dir?: string): void;
|
|
153
|
+
export declare function setCodexActiveLogin(id: string, dir?: string): CodexHome;
|
|
154
|
+
/**
|
|
155
|
+
* A fresh throwaway home for one device-code sign-in (config only, no credential).
|
|
156
|
+
*
|
|
157
|
+
* One directory per attempt, and every other one is removed first: only one
|
|
158
|
+
* sign-in runs on a machine at a time (the relay cancels the previous one before
|
|
159
|
+
* it gets here), and the pre-S4 runner kept a single fixed staging home that was
|
|
160
|
+
* never cleaned up – on this machine it had lain there since 01.08.2026.
|
|
55
161
|
*/
|
|
56
|
-
export declare function adoptLoginResult(stagingDir: string, dir?: string): boolean;
|
|
57
|
-
/** Seed a throwaway home for the device-code flow (config only, no credential). */
|
|
58
162
|
export declare function prepareStagingHome(): string;
|
|
59
|
-
/** Remove
|
|
60
|
-
export declare function discardStagingHome(): void;
|
|
163
|
+
/** Remove one sign-in's home whatever the outcome — it may hold a credential. */
|
|
164
|
+
export declare function discardStagingHome(dir: string): void;
|
|
165
|
+
/**
|
|
166
|
+
* Remove every sign-in home – at daemon start (nothing can be signing in yet)
|
|
167
|
+
* and before a new sign-in. Includes the fixed-path staging home of runners
|
|
168
|
+
* before S4.
|
|
169
|
+
*/
|
|
170
|
+
export declare function discardAbandonedCodexStagingHomes(): number;
|
|
171
|
+
/**
|
|
172
|
+
* A device-code sign-in in `stagingDir` finished: its login becomes a saved
|
|
173
|
+
* account – by rename, deduplicated by subscription – and the one in use (R15).
|
|
174
|
+
* The staging home is removed either way.
|
|
175
|
+
*
|
|
176
|
+
* `replaceActive` – the sign-in came through the one-login window, where it has
|
|
177
|
+
* always meant «this login from now on»: it takes the place of the login in use
|
|
178
|
+
* rather than adding a row (D27, for an organization without several logins).
|
|
179
|
+
*/
|
|
180
|
+
export declare function adoptLoginResult(stagingDir: string, options?: {
|
|
181
|
+
replaceActive?: boolean;
|
|
182
|
+
}): StoredCodexLogin;
|
|
61
183
|
//# sourceMappingURL=codex-home.d.ts.map
|