agl 40.4.0 → 40.5.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/changelog.md +27 -0
- package/dist_serve/bundle.js +2228 -2177
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +2 -2
- package/dist_ts/classes.accountsactions.d.ts +11 -1
- package/dist_ts/classes.accountsactions.js +59 -2
- package/dist_ts/classes.controller.js +5 -5
- package/dist_ts/classes.flexaccountaccess.js +3 -2
- package/dist_ts/classes.harnessreleases.d.ts +14 -4
- package/dist_ts/classes.harnessreleases.js +28 -4
- package/dist_ts/classes.inferencegateway.d.ts +28 -3
- package/dist_ts/classes.inferencegateway.js +177 -61
- package/dist_ts/classes.inferenceplacement.d.ts +5 -3
- package/dist_ts/classes.inferenceplacement.js +2 -2
- package/dist_ts/classes.inferencerouter.d.ts +349 -91
- package/dist_ts/classes.inferencerouter.js +804 -297
- package/dist_ts/classes.inferenceservice.d.ts +14 -2
- package/dist_ts/classes.inferenceservice.js +53 -14
- package/dist_ts/classes.inferencespend.d.ts +339 -0
- package/dist_ts/classes.inferencespend.js +815 -0
- package/dist_ts/classes.inferenceusagecache.d.ts +8 -0
- package/dist_ts/classes.inferenceusagecache.js +9 -5
- package/dist_ts/classes.inferenceusagetap.d.ts +50 -9
- package/dist_ts/classes.inferenceusagetap.js +185 -31
- package/dist_ts/classes.proxycontrol.d.ts +3 -2
- package/dist_ts/classes.proxycontrol.js +1 -2
- package/dist_ts/classes.proxydaemon.js +3 -2
- package/dist_ts/classes.proxystore.d.ts +6 -0
- package/dist_ts/classes.proxystore.js +11 -2
- package/dist_ts/fleet/classes.claudeworkers.d.ts +5 -4
- package/dist_ts/fleet/classes.claudeworkers.js +10 -9
- package/dist_ts/functions.claudecoderelease.d.ts +12 -6
- package/dist_ts/functions.claudecoderelease.js +13 -7
- package/dist_ts/functions.inferencecli.js +12 -8
- package/dist_ts/interfaces.auth.d.ts +1 -1
- package/dist_ts/interfaces.auth.js +3 -1
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_authswitch/authority-apikeys.d.ts +32 -0
- package/dist_ts_authswitch/authority-apikeys.js +65 -0
- package/dist_ts_authswitch/authority-contract.d.ts +142 -28
- package/dist_ts_authswitch/authority-contract.js +48 -1
- package/dist_ts_authswitch/authority-overview.js +8 -1
- package/dist_ts_authswitch/authority-placement.d.ts +7 -2
- package/dist_ts_authswitch/authority-placement.js +10 -3
- package/dist_ts_authswitch/authority-runtime-contract.d.ts +2 -2
- package/dist_ts_authswitch/authority-spend.d.ts +103 -0
- package/dist_ts_authswitch/authority-spend.js +110 -0
- package/dist_ts_authswitch/authority-subscription.d.ts +12 -1
- package/dist_ts_authswitch/authority-subscription.js +24 -1
- package/dist_ts_authswitch/authority-usagelimit.d.ts +7 -4
- package/dist_ts_authswitch/authority-usagelimit.js +89 -39
- package/dist_ts_authswitch/authority-usagewindows.d.ts +4 -1
- package/dist_ts_authswitch/authority-usagewindows.js +41 -16
- package/dist_ts_authswitch/classes.authoritybroker.d.ts +30 -19
- package/dist_ts_authswitch/classes.authoritybroker.js +152 -61
- package/dist_ts_authswitch/classes.authoritycli.d.ts +9 -4
- package/dist_ts_authswitch/classes.authoritycli.js +94 -29
- package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
- package/dist_ts_authswitch/classes.authorityclient.js +31 -11
- package/dist_ts_authswitch/classes.authoritydaemon.js +39 -12
- package/dist_ts_authswitch/classes.authoritydatabase.d.ts +28 -5
- package/dist_ts_authswitch/classes.authoritydatabase.js +171 -9
- package/dist_ts_authswitch/classes.authorityimport.js +2 -2
- package/dist_ts_authswitch/classes.authoritymodels.d.ts +71 -3
- package/dist_ts_authswitch/classes.authoritymodels.js +249 -20
- package/dist_ts_authswitch/classes.authorityregistry.js +4 -2
- package/dist_ts_authswitch/classes.authorityusage.d.ts +69 -7
- package/dist_ts_authswitch/classes.authorityusage.js +118 -4
- package/dist_ts_authswitch/classes.claudenative.d.ts +16 -7
- package/dist_ts_authswitch/classes.claudenative.js +29 -16
- package/dist_ts_authswitch/classes.claudesignin.js +5 -2
- package/dist_ts_authswitch/testsupport.d.ts +24 -5
- package/dist_ts_authswitch/testsupport.js +56 -12
- package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.d.ts +12 -0
- package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.js +21 -0
- package/dist_ts_authswitch/ts_migration/index.js +3 -1
- package/dist_ts_interfaces/functions.authorityscope.d.ts +18 -7
- package/dist_ts_interfaces/functions.authorityscope.js +35 -8
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/plugins.d.ts +2 -1
- package/dist_ts_interfaces/plugins.js +7 -4
- package/dist_ts_interfaces/requests.accounts.d.ts +56 -0
- package/dist_ts_interfaces/requests.accounts.js +11 -1
- package/dist_ts_interfaces/requests.d.ts +2 -2
- package/dist_ts_interfaces/requests.js +1 -1
- package/docs/authswitch.md +75 -12
- package/package.json +5 -5
- package/readme.md +198 -21
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +2 -2
- package/ts/classes.accountsactions.ts +75 -1
- package/ts/classes.controller.ts +4 -4
- package/ts/classes.flexaccountaccess.ts +2 -1
- package/ts/classes.harnessreleases.ts +25 -4
- package/ts/classes.inferencegateway.ts +184 -64
- package/ts/classes.inferenceplacement.ts +6 -4
- package/ts/classes.inferencerouter.ts +1026 -314
- package/ts/classes.inferenceservice.ts +56 -13
- package/ts/classes.inferencespend.ts +870 -0
- package/ts/classes.inferenceusagecache.ts +16 -4
- package/ts/classes.inferenceusagetap.ts +159 -34
- package/ts/classes.proxycontrol.ts +3 -3
- package/ts/classes.proxydaemon.ts +2 -1
- package/ts/classes.proxystore.ts +11 -1
- package/ts/fleet/classes.claudeworkers.ts +9 -8
- package/ts/functions.claudecoderelease.ts +13 -6
- package/ts/functions.inferencecli.ts +11 -7
- package/ts/interfaces.auth.ts +2 -0
- package/ts/plugins.ts +7 -0
- package/ts_authswitch/authority-apikeys.ts +85 -0
- package/ts_authswitch/authority-contract.ts +167 -29
- package/ts_authswitch/authority-overview.ts +7 -0
- package/ts_authswitch/authority-placement.ts +12 -4
- package/ts_authswitch/authority-runtime-contract.ts +2 -2
- package/ts_authswitch/authority-spend.ts +168 -0
- package/ts_authswitch/authority-subscription.ts +28 -1
- package/ts_authswitch/authority-usagelimit.ts +95 -40
- package/ts_authswitch/authority-usagewindows.ts +42 -17
- package/ts_authswitch/classes.authoritybroker.ts +161 -68
- package/ts_authswitch/classes.authoritycli.ts +108 -32
- package/ts_authswitch/classes.authorityclient.ts +41 -10
- package/ts_authswitch/classes.authoritydaemon.ts +43 -13
- package/ts_authswitch/classes.authoritydatabase.ts +178 -8
- package/ts_authswitch/classes.authorityimport.ts +1 -1
- package/ts_authswitch/classes.authoritymodels.ts +165 -19
- package/ts_authswitch/classes.authorityregistry.ts +3 -0
- package/ts_authswitch/classes.authorityusage.ts +171 -10
- package/ts_authswitch/classes.claudenative.ts +28 -15
- package/ts_authswitch/classes.claudesignin.ts +4 -1
- package/ts_authswitch/native-contracts.md +81 -20
- package/ts_authswitch/readme.md +1 -1
- package/ts_authswitch/testsupport.ts +58 -11
- package/ts_authswitch/ts_migration/0011_account_api_key_fields.ts +20 -0
- package/ts_authswitch/ts_migration/index.ts +2 -0
- package/ts_interfaces/functions.authorityscope.ts +43 -8
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/plugins.ts +6 -3
- package/ts_interfaces/requests.accounts.ts +48 -0
- package/ts_interfaces/requests.ts +4 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/accounts.plugins.ts +3 -1
- package/ts_web/accounts.templates.ts +20 -11
- package/ts_web/classes.accountsreadmodel.ts +322 -70
- package/ts_web/elements.accounts.ts +219 -20
- package/ts_web/elements.codexconnections.ts +3 -1
- package/ts_web/plugins.ts +4 -2
|
@@ -8,6 +8,9 @@ import { authSwitchCombineUsageObservations, authSwitchObservedUsage,
|
|
|
8
8
|
type TAuthSwitchUsageObservation } from './authority-usageheaders.js';
|
|
9
9
|
import { usageWindowPeriod } from './accounts.js';
|
|
10
10
|
import type { IAuthSwitchTokenUsage } from './authority-usagetokens.js';
|
|
11
|
+
import { authSwitchSpendPeriod, type IAuthSwitchSpendReading } from './authority-spend.js';
|
|
12
|
+
import type { IAuthSwitchSpendAnswer, TAuthSwitchPaymentProblemCause as TCause } from './authority-contract.js';
|
|
13
|
+
import { authSwitchPaymentProblemHeld } from './authority-subscription.js';
|
|
11
14
|
import { AuthSwitchUsageEstimator, authSwitchRolledOpenAiLimits, type IAuthSwitchUsageEstimateState,
|
|
12
15
|
type IAuthSwitchUsageEstimatorOptions, type TAuthSwitchEstimateBasis } from './classes.authorityusageestimate.js';
|
|
13
16
|
|
|
@@ -16,12 +19,12 @@ type TRateLimits = IOpenAiAccountRateLimits;
|
|
|
16
19
|
/**
|
|
17
20
|
* Why a usage reading is missing or incomplete. `held_in_container`: the Claude login runs in a registered container
|
|
18
21
|
* home, whose files are the container's; the authority reads its usage again once the login is moved out. `pending`:
|
|
19
|
-
* the account's first read has not finished yet.
|
|
22
|
+
* the account's first read has not finished yet. `spend_unavailable`: an API key's spend ledger could not be read.
|
|
20
23
|
*/
|
|
21
24
|
export type TAuthSwitchUsageProblem =
|
|
22
25
|
| 'none' | 'unsupported' | 'access_unavailable' | 'provider_unavailable'
|
|
23
26
|
| 'provider_rejected' | 'profile_unavailable' | 'usage_unavailable'
|
|
24
|
-
| 'context_changed' | 'expired' | 'held_in_container' | 'pending';
|
|
27
|
+
| 'context_changed' | 'expired' | 'held_in_container' | 'pending' | 'spend_unavailable';
|
|
25
28
|
|
|
26
29
|
export interface IAuthSwitchNativeUsageSource {
|
|
27
30
|
homeId: string;
|
|
@@ -69,10 +72,18 @@ export interface IAuthSwitchClaudeStatus {
|
|
|
69
72
|
rateLimit: { retryAt: string | null } | null;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
/** A usage reading the authority reads from a provider for a subscription sign-in, and stores between its runs. */
|
|
76
|
+
export type TAuthSwitchProviderUsageReading =
|
|
73
77
|
| { kind: 'openai'; rateLimits: TRateLimits }
|
|
74
78
|
| { kind: 'anthropic'; status: IAuthSwitchClaudeStatus };
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* A login's usage reading: a subscription sign-in's, as its provider stated it, or an API key's monthly spend
|
|
82
|
+
* (`spend`), which the authority answers from what AGL's inference proxy reported and never reads from a provider.
|
|
83
|
+
*/
|
|
84
|
+
export type TAuthSwitchUsageReading = TAuthSwitchProviderUsageReading
|
|
85
|
+
| { kind: 'spend'; spend: IAuthSwitchSpendReading };
|
|
86
|
+
|
|
76
87
|
export interface IAuthSwitchUsageAccess {
|
|
77
88
|
accessToken: string;
|
|
78
89
|
accountId: string;
|
|
@@ -96,10 +107,33 @@ export interface IAuthSwitchUsageAccess {
|
|
|
96
107
|
export interface IAuthSwitchPaymentProblem {
|
|
97
108
|
at: string;
|
|
98
109
|
subscription: IClaudeSubscription | null;
|
|
110
|
+
/**
|
|
111
|
+
* What the provider refused for (`TAuthSwitchPaymentProblemCause`), present only for `spend_cap` and `quota`, which
|
|
112
|
+
* only an API key's login is held with; absent, the cause is `billing`, on a Claude login and an API key alike. A
|
|
113
|
+
* problem with a cause has no subscription.
|
|
114
|
+
*/
|
|
115
|
+
cause?: Exclude<TAuthSwitchPaymentProblemCause, 'billing'>;
|
|
116
|
+
/**
|
|
117
|
+
* When a `spend_cap` lifts by itself: the end of the month in UTC `at` falls in (`authSwitchSpendPeriod(at).end`),
|
|
118
|
+
* present exactly for that cause; once it passed the problem holds nothing (`authSwitchPaymentProblemHeld`). Every
|
|
119
|
+
* other problem holds until it is cleared: a Claude login's by a read, an API key's by a re-check (`usage.recheck`).
|
|
120
|
+
*/
|
|
121
|
+
until?: string;
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
/**
|
|
102
|
-
*
|
|
125
|
+
* Why a provider refused inference on an API key for money, as AGL's inference proxy read the refusal: `billing`, the
|
|
126
|
+
* organization's billing (Anthropic HTTP 402 `billing_error`, and every Claude sign-in's payment problem); `spend_cap`,
|
|
127
|
+
* the monthly spend cap of the key's usage tier, which lifts at 00:00 UTC on the first of the next month (Anthropic 429
|
|
128
|
+
* `enforced_spend_limit_reached`); `quota`, the organization's or project's credit or spend limit (OpenAI 429
|
|
129
|
+
* `insufficient_quota`).
|
|
130
|
+
*/
|
|
131
|
+
export type TAuthSwitchPaymentProblemCause = 'billing' | 'spend_cap' | 'quota';
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* How a re-check went (`AuthSwitchAuthorityUsage.recheck`). `cleared`: the login is an API key, whose hold a re-check
|
|
135
|
+
* lifts without asking the provider, which states no billing or quota on a model list; the next request shows whether the
|
|
136
|
+
* provider takes the key again. Every other outcome is a Claude login's read: `healthy` -- the profile was found, the sign-in
|
|
103
137
|
* accepted and the identity matched -- clears a payment problem; every other outcome clears nothing. `credential_rejected`:
|
|
104
138
|
* Anthropic rejected the sign-in, renewed too. `token_unusable`: no access token usable for a profile read (it could not
|
|
105
139
|
* be resolved, is not fresh enough, or lacks the `user:profile` scope). `profile_missing`: the profile read failed or its
|
|
@@ -108,7 +142,7 @@ export interface IAuthSwitchPaymentProblem {
|
|
|
108
142
|
* lies ahead.
|
|
109
143
|
*/
|
|
110
144
|
export type TAuthSwitchRecheckOutcome = 'healthy' | 'credential_rejected' | 'token_unusable' | 'profile_missing'
|
|
111
|
-
| 'mismatch' | 'read_failed';
|
|
145
|
+
| 'mismatch' | 'read_failed' | 'cleared';
|
|
112
146
|
|
|
113
147
|
/** A re-check's answer: the usage the authority holds once its read finished, and how that read went. */
|
|
114
148
|
export interface IAuthSwitchUsageRecheck {
|
|
@@ -120,7 +154,7 @@ export interface IAuthSwitchUsageRecheck {
|
|
|
120
154
|
export interface IAuthSwitchStoredUsage {
|
|
121
155
|
context: IAuthSwitchUsageContext;
|
|
122
156
|
updateId: string;
|
|
123
|
-
reading:
|
|
157
|
+
reading: TAuthSwitchProviderUsageReading | null;
|
|
124
158
|
observedAt: string | null;
|
|
125
159
|
currentUntil: string | null;
|
|
126
160
|
staleUntil: string | null;
|
|
@@ -262,6 +296,15 @@ export interface IAuthSwitchAuthorityUsageOptions {
|
|
|
262
296
|
estimate?: IAuthSwitchUsageEstimatorOptions;
|
|
263
297
|
/** Where the reader's notes go, one line each: standard error, the service's journal, unless set. */
|
|
264
298
|
log?: (lineArg: string) => void;
|
|
299
|
+
/**
|
|
300
|
+
* An API key's spend ledger: its month's reading at `nowArg`, and a spend report recorded in one transaction
|
|
301
|
+
* (`AuthSwitchAuthorityDatabase.readSpend`, `.recordSpend`). Without it the authority answers no key's usage.
|
|
302
|
+
*/
|
|
303
|
+
spend?: {
|
|
304
|
+
read(accountId: string, loginId: string, nowArg: number): Promise<IAuthSwitchSpendReading>;
|
|
305
|
+
record(accountId: string, loginId: string, answers: readonly IAuthSwitchSpendAnswer[],
|
|
306
|
+
nowArg: number): Promise<IAuthSwitchSpendReading>;
|
|
307
|
+
};
|
|
265
308
|
}
|
|
266
309
|
|
|
267
310
|
/** What a client that served requests on a login reports about the account: see `report`. */
|
|
@@ -345,6 +388,15 @@ const usable = (context: IAuthSwitchUsageContext): boolean =>
|
|
|
345
388
|
|| (context.owner === 'claude_native' && context.nativeSource !== null
|
|
346
389
|
&& context.grantState === 'native'))));
|
|
347
390
|
|
|
391
|
+
/** An API key of its account's provider, held or removed: its spend is recorded either way. */
|
|
392
|
+
const apiKeyOfAccount = (context: IAuthSwitchUsageContext): boolean => context.workspaceId === 'api-key'
|
|
393
|
+
&& ((context.providerId === 'anthropic' && context.purpose === 'anthropic_api_key')
|
|
394
|
+
|| (context.providerId === 'openai' && context.purpose === 'openai_api_key'));
|
|
395
|
+
|
|
396
|
+
/** An API key's login the authority holds: a key nothing else renews. Its usage is its spend ledger. */
|
|
397
|
+
const apiKeyLogin = (context: IAuthSwitchUsageContext): boolean => apiKeyOfAccount(context) && context.owner === 'daemon'
|
|
398
|
+
&& context.nativeSource === null;
|
|
399
|
+
|
|
348
400
|
/** A login read through Claude Code's own home: its context names the home only once that home is opened for a read. */
|
|
349
401
|
const native = (context: IAuthSwitchUsageContext): boolean => context.providerId === 'anthropic'
|
|
350
402
|
&& context.purpose === 'claude_host_native' && context.owner === 'claude_native' && context.grantState === 'native';
|
|
@@ -664,7 +716,8 @@ export class AuthSwitchAuthorityUsage {
|
|
|
664
716
|
* carries it. Refused for any login but a Claude login the authority renews, and for one with no usage record of its
|
|
665
717
|
* sign-in yet, which the proxy never routed to. The provider is not asked.
|
|
666
718
|
*/
|
|
667
|
-
public reportPaymentProblem(accountId: string, loginId: string, observedAt: string
|
|
719
|
+
public reportPaymentProblem(accountId: string, loginId: string, observedAt: string,
|
|
720
|
+
cause: TCause = 'billing'): Promise<IAuthSwitchUsageSnapshot> {
|
|
668
721
|
if (this.closed) return Promise.reject(new Error('Authority usage helper is closed.'));
|
|
669
722
|
return this.admit((async () => {
|
|
670
723
|
if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId) || !Number.isFinite(Date.parse(observedAt))) {
|
|
@@ -672,9 +725,21 @@ export class AuthSwitchAuthorityUsage {
|
|
|
672
725
|
}
|
|
673
726
|
for (let attempt = 1; ; attempt++) {
|
|
674
727
|
const context = await this.currentContext(accountId, loginId);
|
|
728
|
+
if (context && context.accountId === accountId && context.loginId === loginId && !context.removed
|
|
729
|
+
&& apiKeyLogin(context) && context.grantState === 'ready') {
|
|
730
|
+
if (await this.holdApiKey(context, observedAt, cause)) return this.spendAnswer(context);
|
|
731
|
+
if (attempt >= paymentProblemWriteAttempts) {
|
|
732
|
+
throw new AuthSwitchRefusal('account_changed',
|
|
733
|
+
'The key\'s record kept changing, so its payment problem was not written; report it again.');
|
|
734
|
+
}
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
675
737
|
if (!context || context.accountId !== accountId || context.loginId !== loginId || !paymentProblemLogin(context)) {
|
|
676
738
|
throw new AuthSwitchRefusal('login_unavailable',
|
|
677
|
-
'Only a Claude login the account service renews is held with a payment problem.');
|
|
739
|
+
'Only a Claude login the account service renews, or an API key it holds, is held with a payment problem.');
|
|
740
|
+
}
|
|
741
|
+
if (cause !== 'billing') {
|
|
742
|
+
throw new AuthSwitchRefusal('login_unavailable', 'A Claude login is held only for a billing error.');
|
|
678
743
|
}
|
|
679
744
|
const stored = await this.options.store.read(accountId, loginId);
|
|
680
745
|
const held = stored && authSwitchSameUsageSource(context, stored.context) ? stored : null;
|
|
@@ -714,9 +779,13 @@ export class AuthSwitchAuthorityUsage {
|
|
|
714
779
|
throw new Error('Invalid authority usage identity.');
|
|
715
780
|
}
|
|
716
781
|
const context = await this.currentContext(accountId, loginId);
|
|
782
|
+
if (context && context.accountId === accountId && context.loginId === loginId && !context.removed
|
|
783
|
+
&& apiKeyLogin(context)) {
|
|
784
|
+
return this.clearApiKey(context);
|
|
785
|
+
}
|
|
717
786
|
if (!context || context.accountId !== accountId || context.loginId !== loginId
|
|
718
787
|
|| context.providerId !== 'anthropic' || context.purpose !== 'claude_host_native') {
|
|
719
|
-
throw new AuthSwitchRefusal('login_unavailable', 'Only a Claude login is re-checked.');
|
|
788
|
+
throw new AuthSwitchRefusal('login_unavailable', 'Only a Claude login or an API key is re-checked.');
|
|
720
789
|
}
|
|
721
790
|
const key = this.key(accountId, loginId);
|
|
722
791
|
await this.answer(accountId, loginId, null);
|
|
@@ -737,6 +806,97 @@ export class AuthSwitchAuthorityUsage {
|
|
|
737
806
|
})());
|
|
738
807
|
}
|
|
739
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Records what answers relayed on an API key's login cost (`authswitch.authority.usage.spend`) and answers the key's
|
|
811
|
+
* usage once the report committed. A removed key's spend is recorded too; its usage then answers unavailable.
|
|
812
|
+
*/
|
|
813
|
+
public recordSpend(accountId: string, loginId: string,
|
|
814
|
+
answers: readonly IAuthSwitchSpendAnswer[]): Promise<IAuthSwitchUsageSnapshot> {
|
|
815
|
+
if (this.closed) return Promise.reject(new Error('Authority usage helper is closed.'));
|
|
816
|
+
return this.admit((async () => {
|
|
817
|
+
if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) {
|
|
818
|
+
throw new Error('Invalid authority usage identity.');
|
|
819
|
+
}
|
|
820
|
+
const context = await this.currentContext(accountId, loginId);
|
|
821
|
+
const spend = this.options.spend;
|
|
822
|
+
if (!spend || !context || context.accountId !== accountId || context.loginId !== loginId || !apiKeyOfAccount(context)) {
|
|
823
|
+
throw new AuthSwitchRefusal('login_unavailable', 'Only an API key\'s login records spend.');
|
|
824
|
+
}
|
|
825
|
+
await spend.record(accountId, loginId, answers, this.now());
|
|
826
|
+
return this.answer(accountId, loginId, null);
|
|
827
|
+
})());
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* An API key's usage: its month's spend from the ledger, current as of now, which stays current until the month ends
|
|
832
|
+
* or a report or a limit change moves it, and the payment problem it is held with, while it holds.
|
|
833
|
+
*/
|
|
834
|
+
private async spendAnswer(context: IAuthSwitchUsageContext): Promise<IAuthSwitchUsageSnapshot> {
|
|
835
|
+
const { accountId, loginId } = context;
|
|
836
|
+
if (context.removed || !this.options.spend) {
|
|
837
|
+
return this.unavailable(accountId, loginId, context.removed ? 'access_unavailable' : 'unsupported');
|
|
838
|
+
}
|
|
839
|
+
const now = this.now();
|
|
840
|
+
let spend: IAuthSwitchSpendReading;
|
|
841
|
+
let held: IAuthSwitchStoredUsage | null;
|
|
842
|
+
try {
|
|
843
|
+
[spend, held] = await Promise.all([this.options.spend.read(accountId, loginId, now),
|
|
844
|
+
this.options.store.read(accountId, loginId)]);
|
|
845
|
+
} catch {
|
|
846
|
+
this.log('Authswitch could not read an API key\'s spend; the key is answered without a reading.');
|
|
847
|
+
return this.unavailable(accountId, loginId, 'spend_unavailable');
|
|
848
|
+
}
|
|
849
|
+
const paymentProblem = held && authSwitchSameUsageSource(context, held.context) && held.paymentProblem
|
|
850
|
+
&& authSwitchPaymentProblemHeld(held.paymentProblem, now) ? held.paymentProblem : undefined;
|
|
851
|
+
const readAt = timestamp(now);
|
|
852
|
+
return { accountId, loginId, state: 'current', reading: { kind: 'spend', spend }, observedAt: readAt, readAt,
|
|
853
|
+
currentUntil: spend.resetAt, staleUntil: spend.resetAt, lastAttemptAt: null, nextAttemptAt: null,
|
|
854
|
+
lastReadError: null, problem: 'none', ...(paymentProblem ? { paymentProblem } : {}) };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Holds an API key with a payment problem on its usage record, which holds nothing else: when it was seen -- a time
|
|
859
|
+
* ahead of the authority's clock counts as now -- and its cause; `billing` is written as no cause, and a `spend_cap`
|
|
860
|
+
* lifts at the end of the month it was seen in. False when another write of the record landed first.
|
|
861
|
+
*/
|
|
862
|
+
private async holdApiKey(context: IAuthSwitchUsageContext, observedAt: string, cause: TCause): Promise<boolean> {
|
|
863
|
+
const stored = await this.options.store.read(context.accountId, context.loginId);
|
|
864
|
+
const held = stored && authSwitchSameUsageSource(context, stored.context) ? stored : null;
|
|
865
|
+
const now = this.now();
|
|
866
|
+
const at = timestamp(Math.min(Date.parse(observedAt), now));
|
|
867
|
+
const paymentProblem: IAuthSwitchPaymentProblem = cause === 'spend_cap'
|
|
868
|
+
? { at, subscription: null, cause, until: authSwitchSpendPeriod(Date.parse(at)).end }
|
|
869
|
+
: cause === 'quota' ? { at, subscription: null, cause } : { at, subscription: null };
|
|
870
|
+
const next: IAuthSwitchStoredUsage = { context, updateId: plugins.crypto.randomUUID(), reading: null,
|
|
871
|
+
observedAt: null, currentUntil: null, staleUntil: null, lastAttemptAt: at, nextAttemptAt: at, failures: 0,
|
|
872
|
+
problem: 'none', paymentProblem };
|
|
873
|
+
return this.options.store.publishIfCurrent(context, held && sameContext(context, held.context) ? held.updateId : null,
|
|
874
|
+
next);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* A re-check of an API key: lifts the payment problem it is held with, without asking the provider, whose model list
|
|
879
|
+
* states no billing or quota; the next request shows whether the provider takes the key again.
|
|
880
|
+
*/
|
|
881
|
+
private async clearApiKey(context: IAuthSwitchUsageContext): Promise<IAuthSwitchUsageRecheck> {
|
|
882
|
+
for (let attempt = 1; ; attempt++) {
|
|
883
|
+
const stored = await this.options.store.read(context.accountId, context.loginId);
|
|
884
|
+
const held = stored && authSwitchSameUsageSource(context, stored.context) && stored.paymentProblem ? stored : null;
|
|
885
|
+
if (!held) return { usage: await this.spendAnswer(context), outcome: 'cleared' };
|
|
886
|
+
// A record without the member: the store's transition drops a problem the draft does not hold.
|
|
887
|
+
const { paymentProblem: _lifted, ...rest } = held;
|
|
888
|
+
const next: IAuthSwitchStoredUsage = { ...rest, context, updateId: plugins.crypto.randomUUID() };
|
|
889
|
+
if (await this.options.store.publishIfCurrent(context, sameContext(context, held.context) ? held.updateId : null,
|
|
890
|
+
next)) {
|
|
891
|
+
return { usage: await this.spendAnswer(context), outcome: 'cleared' };
|
|
892
|
+
}
|
|
893
|
+
if (attempt >= paymentProblemWriteAttempts) {
|
|
894
|
+
throw new AuthSwitchRefusal('account_changed', 'The key\'s record kept changing, so its hold was not lifted; '
|
|
895
|
+
+ 're-check it again.');
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
740
900
|
/** Logs a Claude subscription status the first time this run reads it, up to `maximumLoggedSubscriptionStatuses`. */
|
|
741
901
|
private noteSubscriptionStatus(statusArg: string | null): void {
|
|
742
902
|
if (statusArg === null || this.subscriptionStatusesCapped || this.loggedSubscriptionStatuses.has(statusArg)) return;
|
|
@@ -774,6 +934,7 @@ export class AuthSwitchAuthorityUsage {
|
|
|
774
934
|
if (context.accountId !== accountId || context.loginId !== loginId) {
|
|
775
935
|
return this.unavailable(accountId, loginId, 'context_changed');
|
|
776
936
|
}
|
|
937
|
+
if (apiKeyLogin(context)) return this.spendAnswer(context);
|
|
777
938
|
if (!readable(context)) {
|
|
778
939
|
this.unschedule(accountId, loginId);
|
|
779
940
|
return this.unavailable(accountId, loginId,
|
|
@@ -999,7 +1160,7 @@ export class AuthSwitchAuthorityUsage {
|
|
|
999
1160
|
previous = latest && sameContext(context, latest.context) ? latest : null;
|
|
1000
1161
|
held = latest && authSwitchSameUsageSource(context, latest.context) ? latest : null;
|
|
1001
1162
|
};
|
|
1002
|
-
let reading:
|
|
1163
|
+
let reading: TAuthSwitchProviderUsageReading | null = null;
|
|
1003
1164
|
/** Whether the read stated the account's windows: only then is it a reading the schedule waits a full slot after. */
|
|
1004
1165
|
let windowsRead = false;
|
|
1005
1166
|
let rateLimited = false;
|
|
@@ -14,11 +14,20 @@ export const CLAUDE_NATIVE_MINIMUM_VERSION = '2.1.273';
|
|
|
14
14
|
/**
|
|
15
15
|
* The releases whose lock table, live credential pickup and native-login selector were read from their own bundle
|
|
16
16
|
* (native-contracts.md). The usage of an adopted home's login is read from the home only while every Claude Code
|
|
17
|
-
* session running there runs one of these or a
|
|
18
|
-
* (`claudeNativeReleaseReadsAsVerified`, owner ruling 2026-10-22, AGL-685); an installed release
|
|
19
|
-
*
|
|
17
|
+
* session running there runs one of these or a release from `CLAUDE_NATIVE_READ_AS_VERIFIED_FROM` on
|
|
18
|
+
* (`claudeNativeReleaseReadsAsVerified`, owner ruling 2026-10-22, AGL-685); an installed release below that line that
|
|
19
|
+
* is not listed is read from only while no session runs in that home. The controller's verified release (2.1.306,
|
|
20
|
+
* AGL-686) and its fleet floor (2.1.282) are listed. 2.1.288, the release AGL's own Claude chats run through the Agent
|
|
21
|
+
* SDK, had these items read from the SDK's binary (AGL-688) and is listed for the usage read only: the fleet never ran
|
|
22
|
+
* it, so it is never the fleet's floor or the verified release.
|
|
20
23
|
*/
|
|
21
|
-
export const CLAUDE_NATIVE_VERIFIED_RELEASES: readonly string[] = ['2.1.273', '2.1.282'];
|
|
24
|
+
export const CLAUDE_NATIVE_VERIFIED_RELEASES: readonly string[] = ['2.1.273', '2.1.282', '2.1.288', '2.1.306'];
|
|
25
|
+
/**
|
|
26
|
+
* The line from which the usage read takes any release, listed or not, as verified (owner ruling 2026-10-22, AGL-685):
|
|
27
|
+
* the controller's fleet floor (`claudeCodeFleetFloor`), which a test holds equal to it. It does not move when a newer
|
|
28
|
+
* release is verified.
|
|
29
|
+
*/
|
|
30
|
+
export const CLAUDE_NATIVE_READ_AS_VERIFIED_FROM = '2.1.282';
|
|
22
31
|
/**
|
|
23
32
|
* The releases whose credential custody contract was read from their own bundle (native-contracts.md): the OAuth
|
|
24
33
|
* refresh lock and its legacy twin, the credential store's write lock and the global config lock; the store read
|
|
@@ -126,7 +135,8 @@ export class ClaudeNativeEffectiveAuthUnsupportedError extends Error {
|
|
|
126
135
|
public readonly sessions: readonly IClaudeNativeRefusedSession[] = []) {
|
|
127
136
|
super((reason === 'unsupported_release'
|
|
128
137
|
? `Reading Claude Code's own login requires Claude Code ${CLAUDE_NATIVE_MINIMUM_VERSION} or newer, and a session `
|
|
129
|
-
+ `running in this home must run a verified release (${CLAUDE_NATIVE_VERIFIED_RELEASES.join(', ')}) or
|
|
138
|
+
+ `running in this home must run a verified release (${CLAUDE_NATIVE_VERIFIED_RELEASES.join(', ')}) or any release `
|
|
139
|
+
+ `from ${CLAUDE_NATIVE_READ_AS_VERIFIED_FROM} on.`
|
|
130
140
|
: `Cannot prove that Claude Code uses its native claude.ai login (${reason}).`)
|
|
131
141
|
+ (sessions.length ? ` Refused: ${sessions.map(describeClaudeNativeRefusedSession).join('; ')}.` : ''));
|
|
132
142
|
}
|
|
@@ -251,17 +261,15 @@ const atLeast = (version: [number, number, number], minimum: [number, number, nu
|
|
|
251
261
|
const claudeBuildPattern = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-[0-9a-f]{1,64})?$/;
|
|
252
262
|
/**
|
|
253
263
|
* Whether a session of this build is read like a verified release when its home's login is read for usage: a listed
|
|
254
|
-
* release, or a build of a release
|
|
255
|
-
*
|
|
264
|
+
* release, or a build of a release from `CLAUDE_NATIVE_READ_AS_VERIFIED_FROM` on. A hashed build is ordered by its
|
|
265
|
+
* release. Custody never uses this: it matches whole build names (`CLAUDE_NATIVE_CUSTODY_RELEASES`).
|
|
256
266
|
*/
|
|
257
267
|
export const claudeNativeReleaseReadsAsVerified = (build: string | null): boolean => {
|
|
258
268
|
if (build === null) return false;
|
|
259
269
|
if (CLAUDE_NATIVE_VERIFIED_RELEASES.includes(build)) return true;
|
|
260
270
|
const match = claudeBuildPattern.exec(build);
|
|
261
271
|
if (!match) return false;
|
|
262
|
-
|
|
263
|
-
.reduce((left, right) => atLeast(left, right) ? left : right);
|
|
264
|
-
return atLeast([Number(match[1]), Number(match[2]), Number(match[3])], newest);
|
|
272
|
+
return atLeast([Number(match[1]), Number(match[2]), Number(match[3])], parseClaudeVersion(CLAUDE_NATIVE_READ_AS_VERIFIED_FROM)!);
|
|
265
273
|
};
|
|
266
274
|
|
|
267
275
|
/**
|
|
@@ -458,10 +466,15 @@ export const claudeNativeCredentialStoreDirectory = (env: NodeJS.ProcessEnv): st
|
|
|
458
466
|
? plugins.path.join(env.HOME, '.claude').normalize('NFC') : null;
|
|
459
467
|
};
|
|
460
468
|
|
|
469
|
+
/**
|
|
470
|
+
* An environment selects this home and keeps its credential store there too: a store elsewhere
|
|
471
|
+
* (`CLAUDE_SECURESTORAGE_CONFIG_DIR`) selects another login, so it is refused as an override.
|
|
472
|
+
*/
|
|
461
473
|
const assertNativeHome = (env: NodeJS.ProcessEnv, configDir: string): void => {
|
|
462
|
-
const selected
|
|
463
|
-
|
|
464
|
-
|
|
474
|
+
for (const selected of [selectedNativeHome(env), claudeNativeCredentialStoreDirectory(env)]) {
|
|
475
|
+
if (!selected || !plugins.path.isAbsolute(selected) || plugins.path.normalize(selected) !== configDir) {
|
|
476
|
+
throw new ClaudeNativeEffectiveAuthUnsupportedError('override');
|
|
477
|
+
}
|
|
465
478
|
}
|
|
466
479
|
};
|
|
467
480
|
|
|
@@ -908,8 +921,8 @@ export class ClaudeNativeAdapter {
|
|
|
908
921
|
|
|
909
922
|
/**
|
|
910
923
|
* The whole support check of reading the login Claude Code selects in the home: the home's custody, and the proof
|
|
911
|
-
* that Claude Code selects its native login there -- for the daemon's own environment
|
|
912
|
-
* process running on the home.
|
|
924
|
+
* that Claude Code selects its native login there -- for the daemon's own environment (its home and its credential
|
|
925
|
+
* store are both this home) and settings, and for every process running on the home.
|
|
913
926
|
*/
|
|
914
927
|
private assertSupported(): IClaudeNativeSupport {
|
|
915
928
|
this.assertHomeDirectories();
|
|
@@ -107,8 +107,11 @@ export class ClaudeSignInAuthority {
|
|
|
107
107
|
const now = this.now();
|
|
108
108
|
const signIn: IClaudeSignIn = {
|
|
109
109
|
operation: { id: operationId, state: 'pending',
|
|
110
|
+
// The credential store is pinned to the home as well: one an inherited CLAUDE_SECURESTORAGE_CONFIG_DIR names
|
|
111
|
+
// would take the new login, over whatever login it holds, and the home would never get it.
|
|
110
112
|
prompt: { flow: 'terminal', directory,
|
|
111
|
-
command: `CLAUDE_CONFIG_DIR=${shellWord(directory)}
|
|
113
|
+
command: `CLAUDE_CONFIG_DIR=${shellWord(directory)} CLAUDE_SECURESTORAGE_CONFIG_DIR=${shellWord(directory)} `
|
|
114
|
+
+ `claude ${authSwitchClaudeSignInArguments.join(' ')}` },
|
|
112
115
|
result: null, error: null, createdAt: new Date(now).toISOString(), updatedAt: new Date(now).toISOString(),
|
|
113
116
|
expiresAt: new Date(now + signInWindowMs).toISOString(), finishedAt: null, revision: 1 },
|
|
114
117
|
directory, parent, timer: null, commit: null,
|
|
@@ -312,10 +312,11 @@ custody item, so it is on `CLAUDE_NATIVE_CUSTODY_RELEASES`:
|
|
|
312
312
|
- Logout `performLogout` (@34545518) clears two account caches more than 2.1.282's, `metricsStatusCache` and
|
|
313
313
|
`metricsStatusCacheByPrincipal`. Both are now on `CLAUDE_NATIVE_ACCOUNT_CACHES`.
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
Its lock table, live pickup and selector are the usage read's items too, so 2.1.288 is on
|
|
316
|
+
`CLAUDE_NATIVE_VERIFIED_RELEASES` (AGL-686) for the usage read only. The fleet never runs the SDK's binary, so its fleet
|
|
317
|
+
items were never confirmed: 2.1.288 is never the fleet's floor (`claudeCodeFleetFloor`) or the verified release
|
|
318
|
+
(`claudeCodeVerifiedRelease`). Both lists name releases, so a natively installed 2.1.288 is admitted like the SDK's
|
|
319
|
+
build; only the SDK's `linux-x64` binary was read. The SDK's binary is named through
|
|
319
320
|
`CLAUDE_NATIVE_AGENT_SDK_RELEASES` (SDK 0.3.288 bundles 2.1.288), see process detection below.
|
|
320
321
|
|
|
321
322
|
Claude Code 2.1.306 (AGL-686, read 2026-10-22). Linux ELF bundle `~/.local/share/claude/versions/2.1.306`, BuildID
|
|
@@ -407,6 +408,11 @@ process to a home by both directories (`claudeNativeHomeRelation`):
|
|
|
407
408
|
as `override`.
|
|
408
409
|
- A process whose home or store cannot be resolved is refused as uninspectable (`running_session` in the usage read).
|
|
409
410
|
- The container-home and sign-in checks count a process whose store is their home.
|
|
411
|
+
- The usage read requires the accounts service's own environment to keep its store on the home as well
|
|
412
|
+
(`assertNativeHome` applies `claudeNativeCredentialStoreDirectory` to it): a store elsewhere is refused as `override`.
|
|
413
|
+
- A sign-in pins the variable to its home, in the command it prints and when `account add claude` starts Claude Code.
|
|
414
|
+
Inherited, the variable would send the new login to another store, over the login that store holds, while the sign-in
|
|
415
|
+
home never gets a `.credentials.json` and the sign-in expires.
|
|
410
416
|
|
|
411
417
|
Which credential a 2.1.282 process uses (`jc` @16151504, `Cc` @16150518): `ANTHROPIC_AUTH_TOKEN` and `apiKeyHelper`
|
|
412
418
|
outrank the stored claude.ai login and disable it for requests, though the stored login is still read (`dF`) and side
|
|
@@ -423,10 +429,12 @@ Until 38.0.0 a switch of an adopted home proved an installed release above the 2
|
|
|
423
429
|
switch: from 38.0.0 on authswitch never moves a login into Claude Code's own home, and reads an adopted home's login for
|
|
424
430
|
usage only while no session of such a release runs there.
|
|
425
431
|
|
|
426
|
-
Newer releases (owner ruling 2026-10-22, AGL-685): the usage read takes a release
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
432
|
+
Newer releases (owner ruling 2026-10-22, AGL-685): the usage read takes a listed release and every release from
|
|
433
|
+
2.1.282 on as verified (`claudeNativeReleaseReadsAsVerified`, line `CLAUDE_NATIVE_READ_AS_VERIFIED_FROM`, held equal to
|
|
434
|
+
the fleet's floor by a test), installed or running, and a remote agent's hashed build (`2.1.295-8b2928c330c4`) is
|
|
435
|
+
ordered by its release there. The line does not move when a newer release is verified (arbiter ruling 2026-10-23,
|
|
436
|
+
AGL-686). An unlisted release below it (2.1.274-2.1.281) stays refused, since its selector was never read. Custody does
|
|
437
|
+
not follow: it matches whole build names against
|
|
430
438
|
`CLAUDE_NATIVE_CUSTODY_RELEASES`, so a newer release and every hashed build stay refused until their custody contract is
|
|
431
439
|
read, and the refusal names each process by pid, start and build. AGL mentions each newer release it meets once to the
|
|
432
440
|
driver, who files the compatibility check below.
|
|
@@ -504,17 +512,20 @@ can invalidate the grant. Requests to Claude account endpoints report an HTTP 42
|
|
|
504
512
|
|
|
505
513
|
## Claude Code compatibility check
|
|
506
514
|
|
|
507
|
-
AGL verified Claude Code 2.1.
|
|
508
|
-
`CLAUDE_NATIVE_VERIFIED_RELEASES
|
|
509
|
-
|
|
515
|
+
AGL verified Claude Code 2.1.306 (`claudeCodeVerifiedRelease` in `ts/functions.claudecoderelease.ts`, on
|
|
516
|
+
`CLAUDE_NATIVE_VERIFIED_RELEASES`; 2.1.282 before it). Its fleet's floor is a separate constant, `claudeCodeFleetFloor`
|
|
517
|
+
(2.1.282): the oldest release whose fleet output the tests still pin. It rises only when the adapter stops handling an
|
|
518
|
+
older release's output, never because a newer release was verified (arbiter ruling 2026-10-23, AGL-686). AGL runs a
|
|
519
|
+
release newer than the verified one optimistically -- fleet workers, Claude Code sessions in the session feed and the
|
|
520
|
+
usage read -- refuses one older than the floor in the fleet, and keeps custody (handing Claude Code's own
|
|
510
521
|
sign-in to the accounts service) refused on it until its custody items were read (`CLAUDE_NATIVE_CUSTODY_RELEASES`, which
|
|
511
522
|
may take a release before its fleet items are confirmed, as it took 2.1.306). The first time the controller's fleet or configuration probe meets a
|
|
512
523
|
newer release it records it (`agl_harness_releases`) and publishes one `harness.release.discovered` fleet event to every
|
|
513
524
|
owner's watch and the built-in Driver, so the driver files a compatibility check for that release on the board. The
|
|
514
525
|
check works through this list against the release's own bundle (`~/.local/share/claude/versions/<release>`) and
|
|
515
|
-
disposable homes, never a credential file. A release that keeps every item moves the verified
|
|
516
|
-
constant
|
|
517
|
-
and SHA-256 recorded here and
|
|
526
|
+
disposable homes, never a credential file. A release that keeps every item moves the verified release (the controller
|
|
527
|
+
constant `claudeCodeVerifiedRelease`, `CLAUDE_NATIVE_VERIFIED_RELEASES`, and `CLAUDE_NATIVE_CUSTODY_RELEASES` for the
|
|
528
|
+
custody items), with its BuildID and SHA-256 recorded here and its output added to the tests beside the floor's.
|
|
518
529
|
|
|
519
530
|
An Agent SDK update gets the same check against the binary its platform package ships, the custody and usage-read items
|
|
520
531
|
at least, since AGL's own chats run that binary on the controller's home. Record the SDK version and the release it
|
|
@@ -542,9 +553,9 @@ are marked:
|
|
|
542
553
|
- Relay stream-json: `system`/`init` and `result` (`subtype`, `is_error`, `permission_denials`, `api_error_status`,
|
|
543
554
|
`terminal_reason`, `errors`).
|
|
544
555
|
- Transcript growth against the 4 MiB window: the fleet reads a transcript's last 4 MiB and refuses an incremental read
|
|
545
|
-
of more as an observation gap.
|
|
546
|
-
|
|
547
|
-
|
|
556
|
+
of more as an observation gap. Measure what the release adds and how much time and how many turns the window still
|
|
557
|
+
covers, and check that the Agent SDK's chain reader (`getSessionMessages`, used for history) skips record types it does
|
|
558
|
+
not know.
|
|
548
559
|
|
|
549
560
|
The managed policy hook (`ts/configuration/policyhook/claude.ts`): the PreToolUse payload's `tool_name` (`Bash`,
|
|
550
561
|
`mcp__agl__job_start`), `tool_input.command` and `tool_input.argv`, and exit code 2 as a denial. A renamed `tool_name`,
|
|
@@ -587,9 +598,59 @@ offsets, as for 2.1.280-2.1.282 above:
|
|
|
587
598
|
"diskless" session (`ee()`, a launch option a hosting server sets; off on the host). A new mode like it must not let a
|
|
588
599
|
process that has a disk refresh without the lock.
|
|
589
600
|
|
|
590
|
-
2.1.306's
|
|
591
|
-
|
|
592
|
-
`
|
|
601
|
+
2.1.306's fleet items (AGL-686, 2026-10-23). Sources: its bundle's `strings -n 6` dump (offsets as in its entry above),
|
|
602
|
+
compared with 2.1.282's after renaming minified names; 56 transcripts 2.1.306 processes wrote on this host (6 sessions,
|
|
603
|
+
50 subagents, 18,732 records with `version` 2.1.306); and the live controller on AGL 40.4.0, which watched the fleet's two
|
|
604
|
+
2.1.306 driver workers from 23:35 UTC on 2026-10-22. AGL's own parsers (`parseFleetClaudeTranscript`,
|
|
605
|
+
`fleetClaudeAssistantTexts`, `fleetClaudeCredentialFailures`, `fleetClaudePermissionMode`) and the history projection
|
|
606
|
+
read every one of those records, and each file's 4 MiB window, without an error. Every item holds, so 2.1.306 is the
|
|
607
|
+
verified release and on `CLAUDE_NATIVE_VERIFIED_RELEASES`; the fleet's floor stays 2.1.282. Items marked bundle only
|
|
608
|
+
did not occur on 2.1.306 after the upgrade; the first natural occurrence gets a confirming line here, and one that
|
|
609
|
+
contradicts the bundle read gets a fix ticket:
|
|
610
|
+
- Inventory. `printAgentsJson` (@56964267) is 2.1.282's after renaming. A status goes through `p` (@56963968): `idle`,
|
|
611
|
+
`waiting`, anything else `busy`, so the session registry's `shell` reads `busy`. A pid and a status come from the same
|
|
612
|
+
live session entry, and a job's kind is `background`. The controller read the inventory at every poll after the
|
|
613
|
+
upgrade and published no Claude `worker.observation.failed`.
|
|
614
|
+
- `--bg` and wake output, all 2.1.282's:
|
|
615
|
+
- `backgrounded · <id>[ · <name>]` (`lqe` @31980520);
|
|
616
|
+
- the wake note (@31978597), printed only when the job saved options;
|
|
617
|
+
- the copy note (`xn` @31973173, case `own-options`);
|
|
618
|
+
- the message saved for the job's next start (@31977716);
|
|
619
|
+
- `--mcp-config <configs...>` (@35482194), still variadic;
|
|
620
|
+
- the trust key `hasTrustDialogAccepted`.
|
|
621
|
+
|
|
622
|
+
New: `Couldn't tell whether <flag> is a flag or text here, so nothing was started` (@31974605, exit 1) when no `--bg`
|
|
623
|
+
flag can be stripped. The fleet passes `--bg` first, and exit 1 fails visibly. A wake's text is recorded as
|
|
624
|
+
`origin.kind` `human` with string content (three wakes on 2026-10-22). The output itself is bundle only: the fleet
|
|
625
|
+
started, woke and relayed nothing on 2.1.306 after the upgrade.
|
|
626
|
+
- Held and delivery notices (bundle only). `Held peer message — from …` (@47687878) is unchanged. It is still appended to the session's
|
|
627
|
+
transcript as a `system`/`informational` record with `level: "warning"`, built like the release's other transcript
|
|
628
|
+
notices, and the loggable check `Mw` (@25426540) keeps a system record unless it says `persist:false`. Delivery notices (@47169054) still start
|
|
629
|
+
with `[Cross-session delivery notice]`. The hold now reads `… was held by that session. NOT delivered: …` instead of
|
|
630
|
+
`… held for the recipient user's approval …`. No 2.1.306 session held a message here, so neither text is in a 2.1.306
|
|
631
|
+
transcript yet.
|
|
632
|
+
- SendMessage receipts. A peer send by name answers `{success: true, msg_id, message}` (6 in 2.1.306 transcripts) or
|
|
633
|
+
`{success: false, message}` (1). The caveat (@50880854) now reads `; in that session's inbox, not yet read by its
|
|
634
|
+
Claude — a [Cross-session delivery notice] follows if …`. It sits in the tool result, which the relay check skips. The
|
|
635
|
+
tool's input gained `recipient_kind`; `to` and `message` are unchanged.
|
|
636
|
+
- API error records: `isApiErrorMessage: true` with `error` (9 records, `rate_limit`), built at @29824350. The error list
|
|
637
|
+
(@17985651) is 2.1.282's, `authentication_failed` and `oauth_org_not_allowed` included. `apiErrorStatus` and
|
|
638
|
+
`apiErrorIsTransient` are new beside them.
|
|
639
|
+
- `system`/`turn_duration` records: 342 written. The 15 the two driver workers wrote after the upgrade matched the
|
|
640
|
+
controller's 15 `worker.turn.ended` events one for one.
|
|
641
|
+
- `permission-mode` records (no `version` field; 435, one per turn as before), and `user.origin.kind` `human` and `peer`
|
|
642
|
+
with `origin.body` (4 peer messages). Copies keeping their uuids are bundle only: no copy was seen, and the copy path
|
|
643
|
+
of the dispatcher is 2.1.282's.
|
|
644
|
+
- Relay stream-json (bundle only): `system`/`init` (@33617187), and the `result` fields `permission_denials`, `api_error_status`,
|
|
645
|
+
`terminal_reason`, `errors` and `is_api_error_message` are in the bundle. No relay ran on 2.1.306.
|
|
646
|
+
- Transcript growth. `api-request`, `api-request-shape` and `api-request-blob` records are 27 % of the bytes written after
|
|
647
|
+
each file's first 2.1.306 record (26 of 95 MB, three quarters of it blobs), and 26 % of the two driver workers' 4 MiB
|
|
648
|
+
windows.
|
|
649
|
+
- Those windows still cover 4.5 h and 105 turns, and 3.7 h and 61 turns. Without the new records they would cover 4.9 h
|
|
650
|
+
and 4.2 h; two 2.1.282 drivers' windows covered 2.4 h and 7.8 h.
|
|
651
|
+
- The most written within 2 s was 0.7 MB, far from the 4 MiB gap, and every window held a `permission-mode` record.
|
|
652
|
+
- The Agent SDK's chain reader (`jM` in SDK 0.3.288) keeps only `user`, `assistant`, `progress`, `system` and
|
|
653
|
+
`attachment` records with a `uuid`, so it skips the new records.
|
|
593
654
|
|
|
594
655
|
## Managed Codex app-server contract (Codex 0.157.0, 2026-09-25)
|
|
595
656
|
|
package/ts_authswitch/readme.md
CHANGED
|
@@ -8,6 +8,6 @@ Keep the authority in its own process. The controller never owns refresh grants.
|
|
|
8
8
|
|
|
9
9
|
Native credential files belong to their tools: the authority writes one only through a verified, journaled handoff, and never destroys or overwrites a native credential it has not first proven it can restore. Every operation that stops the owned Codex app-server restarts it on both success and failure. Structured authority state uses the existing NoSQLDB models and versioned migrations.
|
|
10
10
|
|
|
11
|
-
Tests isolate all harness homes and use injected provider/process adapters. Never run account-mutating tests against the developer's own installations. Module import, CLI help/version and the browser leaves (`authority-contract.ts`, `authority-usagewindows.ts`, `authority-usagelimit.ts`, `authority-usageheaders.ts`, `authority-usagetokens.ts`, `authority-placement.ts`, `authority-subscription.ts`, `preuse-text.ts`) must not initialize a controller or touch credentials. `authority-subscription.ts` holds the one map of what Claude's `subscription_status` values mean (empty until a value is confirmed) and the words of a payment problem and a re-check, which the command line and the Accounts screen share; `classes.authorityusage.ts` holds a payment problem on the usage record (`reportPaymentProblem`) and clears it on a read that finds a changed subscription or a healthy re-check (`recheck`); a problem held without a subscription takes the one the next healthy read finds, and that read clears nothing. `authority-placement.ts` is the one rule of an account's lifecycle against its `lapsesAt` and of the placement score the proxy places new sessions by. `authority-usageheaders.ts` reads the usage a provider's answer headers state and joins it to the authority's own reading; the authority holds such observations in memory and answers every usage read with them (`authswitch.authority.usage.observe`). `classes.authorityusage.ts` owns the one usage-read schedule: each account is read from the provider every 10 minutes on the authority's timer and at once after an inference 429, a failed read keeps the held reading `stale` and waits for `retry-after` or the next slot, and no question waits for a read or an unseal. `authority-usagetokens.ts` (browser-safe) weighs the tokens the proxy reports by Anthropic's published prices, and `classes.authorityusageestimate.ts` carries each Claude window forward between reads with a ratio learned per account and window, in memory only. `authority-usagelimit.ts` is the one rule of whether an account's usage, against its `maxUsagePercent`, lets it take work; the proxy, Flex, the CLI and the Accounts screen all call it rather than restating it.
|
|
11
|
+
Tests isolate all harness homes and use injected provider/process adapters. Never run account-mutating tests against the developer's own installations. Module import, CLI help/version and the browser leaves (`authority-contract.ts`, `authority-spend.ts`, `authority-usagewindows.ts`, `authority-usagelimit.ts`, `authority-usageheaders.ts`, `authority-usagetokens.ts`, `authority-placement.ts`, `authority-subscription.ts`, `preuse-text.ts`) must not initialize a controller or touch credentials. `authority-subscription.ts` holds the one map of what Claude's `subscription_status` values mean (empty until a value is confirmed) and the words of a payment problem and a re-check, which the command line and the Accounts screen share; `classes.authorityusage.ts` holds a payment problem on the usage record (`reportPaymentProblem`) and clears it on a read that finds a changed subscription or a healthy re-check (`recheck`); a problem held without a subscription takes the one the next healthy read finds, and that read clears nothing. `authority-placement.ts` is the one rule of an account's lifecycle against its `lapsesAt` and of the placement score the proxy places new sessions by. `authority-usageheaders.ts` reads the usage a provider's answer headers state and joins it to the authority's own reading; the authority holds such observations in memory and answers every usage read with them (`authswitch.authority.usage.observe`). `classes.authorityusage.ts` owns the one usage-read schedule: each account is read from the provider every 10 minutes on the authority's timer and at once after an inference 429, a failed read keeps the held reading `stale` and waits for `retry-after` or the next slot, and no question waits for a read or an unseal. `authority-usagetokens.ts` (browser-safe) weighs the tokens the proxy reports by Anthropic's published prices, and `classes.authorityusageestimate.ts` carries each Claude window forward between reads with a ratio learned per account and window, in memory only. `authority-usagelimit.ts` is the one rule of whether an account's usage, against its `maxUsagePercent`, lets it take work -- a switch on the reading's `kind`, where an API key's `spend` reading is judged by its spend limit alone; the proxy, Flex, the CLI and the Accounts screen all call it rather than restating it. `authority-spend.ts` (imports nothing) is the calendar month in UTC a key's spend counts in (`authSwitchSpendPeriod`, the one calendar function), the spend reading, the spend limit rule and dollar amounts. `authority-apikeys.ts` holds the key verifier (`authSwitchApiKeyVerifier`, a read of the provider's model list) and the guard against a key of the other provider; `AuthSwitchAuthorityDatabase.recordSpend` / `readSpend` keep the monthly ledger (`authswitch_authority_spend_entries`, `authswitch_authority_spend_periods`), and `classes.authorityusage.ts` answers a key's usage from it and holds a key's payment problem, by cause, on the key's usage record. A sealed key is `{ kind: 'anthropicApiKey' | 'openaiApiKey', apiKey }`, one kind per provider; never add a kind a reader must keep forever.
|
|
12
12
|
|
|
13
13
|
Imported from authswitch 11.4.1, commit `c3fed26`; future fixes and releases belong to AGL. OpenAI adaptations retain their headers and the notices in the repository's `third-party-notices.md` and `openai-codex-license.txt`.
|