agl 40.4.0 → 40.5.1
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 +33 -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 +172 -33
- package/dist_ts_authswitch/authority-contract.js +54 -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 +99 -30
- package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
- package/dist_ts_authswitch/classes.authorityclient.js +45 -15
- 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 +23 -8
- package/dist_ts_authswitch/classes.claudenative.js +49 -21
- package/dist_ts_authswitch/classes.claudesignin.d.ts +14 -2
- package/dist_ts_authswitch/classes.claudesignin.js +72 -23
- package/dist_ts_authswitch/classes.codexmanaged.d.ts +8 -0
- package/dist_ts_authswitch/classes.codexmanaged.js +33 -18
- package/dist_ts_authswitch/classes.harnessprocesses.d.ts +8 -0
- package/dist_ts_authswitch/classes.harnessprocesses.js +41 -9
- 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 +199 -22
- 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 +201 -34
- 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 +112 -32
- package/ts_authswitch/classes.authorityclient.ts +66 -19
- 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 +51 -22
- package/ts_authswitch/classes.claudesignin.ts +74 -24
- package/ts_authswitch/classes.codexmanaged.ts +30 -16
- package/ts_authswitch/classes.harnessprocesses.ts +39 -8
- package/ts_authswitch/native-contracts.md +82 -21
- 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 +7 -1
- package/ts_web/accounts.templates.ts +20 -11
- package/ts_web/classes.accountsreadmodel.ts +326 -71
- package/ts_web/elements.accounts.ts +219 -20
- package/ts_web/elements.codexconnections.ts +3 -1
- package/ts_web/plugins.ts +4 -2
|
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import { authSwitchRelease } from './packageidentity.js';
|
|
3
3
|
import type { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
|
|
4
4
|
import { requireVerifiedCodexServer, verifyManagedCodexContract, type IManagedCodexContract } from './codexcontract.js';
|
|
5
|
+
import { psLstartInstant } from './classes.harnessprocesses.js';
|
|
5
6
|
|
|
6
7
|
export interface IAuthSwitchManagedCodexOptions {
|
|
7
8
|
/** Private runtime root; only the disposable control socket lives here. */
|
|
@@ -71,6 +72,21 @@ const claimsManagedCodexSocket = (argv: readonly Buffer[], socketPath: string):
|
|
|
71
72
|
return false;
|
|
72
73
|
};
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Rows of `ps -o pid=,lstart=` run with `TZ=UTC0`: the start is read by the shared `psLstartInstant`, null when it
|
|
77
|
+
* cannot be read (a claimant with a null start fails the inspection).
|
|
78
|
+
*/
|
|
79
|
+
export const managedCodexProcessRows = (output: string): Array<{ pid: number; startedAt: string | null }> => {
|
|
80
|
+
const rows: Array<{ pid: number; startedAt: string | null }> = [];
|
|
81
|
+
for (const line of output.split('\n')) {
|
|
82
|
+
const parts = line.trim().split(/\s+/);
|
|
83
|
+
if (parts.length !== 6) continue;
|
|
84
|
+
const pid = Number(parts[0]);
|
|
85
|
+
if (Number.isSafeInteger(pid) && pid > 0) rows.push({ pid, startedAt: psLstartInstant(parts.slice(1, 6)) });
|
|
86
|
+
}
|
|
87
|
+
return rows;
|
|
88
|
+
};
|
|
89
|
+
|
|
74
90
|
/** Same-user Linux process inspection for a bounded socket set, using exact /proc argv boundaries. */
|
|
75
91
|
export const observeManagedCodexProcesses = async (
|
|
76
92
|
socketPaths: readonly string[],
|
|
@@ -85,30 +101,28 @@ export const observeManagedCodexProcesses = async (
|
|
|
85
101
|
}
|
|
86
102
|
const uid = process.getuid?.();
|
|
87
103
|
if (uid === undefined) throw new Error('Managed Codex process owner inspection is unavailable.');
|
|
104
|
+
// ps prints lstart in its TZ; under UTC0 the shared reader takes it as UTC (never Date.parse, which reads local time).
|
|
88
105
|
const output = await new Promise<string>((resolve, reject) => {
|
|
89
106
|
plugins.childProcess.execFile('ps', ['-ww', '-u', String(uid), '-o', 'pid=,lstart='],
|
|
90
|
-
{ timeout: 5_000, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C' } },
|
|
107
|
+
{ timeout: 5_000, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C', TZ: 'UTC0' } },
|
|
91
108
|
(error, stdout) => error ? reject(new Error('Managed Codex process inspection failed.')) : resolve(stdout));
|
|
92
109
|
});
|
|
93
|
-
const processes: Array<{ pid: number; startedAt: string; argv: Buffer[] }> = [];
|
|
94
|
-
for (const
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
const pid = Number(parts[0]);
|
|
98
|
-
const date = Date.parse(parts.slice(1, 6).join(' '));
|
|
99
|
-
if (Number.isSafeInteger(pid) && pid > 0 && Number.isFinite(date)) {
|
|
100
|
-
const argv = await linuxArgv(pid);
|
|
101
|
-
if (argv) processes.push({ pid, startedAt: new Date(date).toISOString(), argv });
|
|
102
|
-
}
|
|
110
|
+
const processes: Array<{ pid: number; startedAt: string | null; argv: Buffer[] }> = [];
|
|
111
|
+
for (const row of managedCodexProcessRows(output)) {
|
|
112
|
+
const argv = await linuxArgv(row.pid);
|
|
113
|
+
if (argv) processes.push({ ...row, argv });
|
|
103
114
|
}
|
|
104
115
|
const result = new Map<string, TManagedCodexProcessObservation>();
|
|
105
116
|
for (const socketPath of unique) {
|
|
106
117
|
const matches = processes.filter(process => claimsManagedCodexSocket(process.argv, socketPath));
|
|
107
|
-
|
|
108
|
-
? { state: 'absent' }
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
if (matches.length !== 1) {
|
|
119
|
+
result.set(socketPath, matches.length === 0 ? { state: 'absent' } : { state: 'multiple_claimants' });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const [claimant] = matches;
|
|
123
|
+
// A claimant whose start cannot be read has no identity to compare: the inspection fails, never reads as absent.
|
|
124
|
+
if (claimant.startedAt === null) throw new Error('Managed Codex process inspection failed.');
|
|
125
|
+
result.set(socketPath, { state: 'observed', pid: claimant.pid, startedAt: claimant.startedAt });
|
|
112
126
|
}
|
|
113
127
|
return result;
|
|
114
128
|
};
|
|
@@ -41,12 +41,31 @@ const executablePatterns: Record<THarnessBinary, RegExp[]> = {
|
|
|
41
41
|
const MAX_COMMAND_LINE = 200;
|
|
42
42
|
const POSIX_FIELDS = 'pid=,ppid=,uid=,lstart=,comm=,args=';
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
const LSTART_WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
45
|
+
const LSTART_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The instant of `ps -o lstart` under `LC_ALL=C` and `TZ=UTC0`: the five tokens `weekday month day hh:mm:ss year`,
|
|
49
|
+
* read as UTC with `Date.UTC`. Never `Date.parse`, which reads them in local time: in the repeated hour of a DST
|
|
50
|
+
* fall-back that dates a process an hour early. Anything that is not exactly that shape, or names no real day, is null.
|
|
51
|
+
*/
|
|
52
|
+
export const psLstartInstant = (tokens: readonly string[]): string | null => {
|
|
53
|
+
if (tokens.length !== 5) return null;
|
|
54
|
+
const [weekdayToken, monthToken, dayToken, timeToken, yearToken] = tokens;
|
|
55
|
+
const weekday = LSTART_WEEKDAYS.indexOf(weekdayToken);
|
|
56
|
+
const month = LSTART_MONTHS.indexOf(monthToken);
|
|
57
|
+
const time = /^(\d{2}):(\d{2}):(\d{2})$/.exec(timeToken);
|
|
58
|
+
if (weekday < 0 || month < 0 || !time || !/^\d{1,2}$/.test(dayToken) || !/^\d{4}$/.test(yearToken)) return null;
|
|
59
|
+
const [day, year, hours, minutes, seconds] = [dayToken, yearToken, time[1], time[2], time[3]].map(Number);
|
|
60
|
+
if (hours > 23 || minutes > 59 || seconds > 59) return null;
|
|
61
|
+
const instant = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
|
|
62
|
+
if (instant.getUTCFullYear() !== year || instant.getUTCMonth() !== month || instant.getUTCDate() !== day
|
|
63
|
+
|| instant.getUTCDay() !== weekday) return null;
|
|
64
|
+
return instant.toISOString();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Rows of `ps -o pid=,ppid=,uid=,lstart=,comm=,args=` run with `TZ=UTC0`, for the process owner `uid` only. */
|
|
68
|
+
export const parsePosixProcessTable = (output: string, uid: number): IEnumeratedProcess[] => {
|
|
50
69
|
const processes: IEnumeratedProcess[] = [];
|
|
51
70
|
for (const line of output.split('\n')) {
|
|
52
71
|
// pid ppid uid, then the five fixed tokens of lstart, then comm and the full argument vector.
|
|
@@ -54,17 +73,29 @@ const enumeratePosix = (): IEnumeratedProcess[] => {
|
|
|
54
73
|
if (parts.length < 9) continue;
|
|
55
74
|
const [pid, parentPid, owner] = parts.slice(0, 3).map(Number);
|
|
56
75
|
if (!Number.isInteger(pid) || pid <= 0 || owner !== uid) continue;
|
|
57
|
-
const started = Date.parse(parts.slice(3, 8).join(' '));
|
|
58
76
|
const args = parts.slice(9);
|
|
59
77
|
processes.push({
|
|
60
78
|
pid, parentPid: Number.isInteger(parentPid) ? parentPid : null, command: parts[8],
|
|
61
79
|
executable: args[0] ?? parts[8], commandLine: (args.length ? args.join(' ') : parts[8]).slice(0, MAX_COMMAND_LINE),
|
|
62
|
-
startedAt:
|
|
80
|
+
startedAt: psLstartInstant(parts.slice(3, 8)),
|
|
63
81
|
});
|
|
64
82
|
}
|
|
65
83
|
return processes;
|
|
66
84
|
};
|
|
67
85
|
|
|
86
|
+
/**
|
|
87
|
+
* A bounded read of the current user's process table. Never a pattern-based kill list. `ps` prints lstart in its
|
|
88
|
+
* `TZ`, so it runs with `TZ=UTC0` and the start is read as UTC (`psLstartInstant`).
|
|
89
|
+
*/
|
|
90
|
+
const enumeratePosix = (): IEnumeratedProcess[] => {
|
|
91
|
+
const uid = process.getuid!();
|
|
92
|
+
const output = plugins.childProcess.execFileSync('ps', ['-u', String(uid), '-o', POSIX_FIELDS], {
|
|
93
|
+
encoding: 'utf8', timeout: 5000, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C', TZ: 'UTC0' },
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
});
|
|
96
|
+
return parsePosixProcessTable(output, uid);
|
|
97
|
+
};
|
|
98
|
+
|
|
68
99
|
/** Windows reports an image name and a pid, with no owner, parent or start time in this view. */
|
|
69
100
|
const enumerateWindows = (): IEnumeratedProcess[] => {
|
|
70
101
|
const output = plugins.childProcess.execFileSync('tasklist', ['/FO', 'CSV', '/NH'], {
|
|
@@ -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.
|
|
@@ -438,7 +446,7 @@ remote agent from `~/.claude/remote/ccd-cli/<version>` (since 2.1.295 a build na
|
|
|
438
446
|
number as the command name while argv[0] carries the identity -- and a forked helper can
|
|
439
447
|
carry the same `comm` while running something else entirely (`ugrep`). `HarnessProcesses`
|
|
440
448
|
therefore matches argv[0] and those install paths (`ps -u <uid> -o pid=,ppid=,uid=,lstart=,comm=,args=`
|
|
441
|
-
under `LC_ALL=C`, filtered on the uid column). It deliberately never matches the rest of the
|
|
449
|
+
under `LC_ALL=C` and `TZ=UTC0`, filtered on the uid column; lstart is read as UTC with `Date.UTC`). It deliberately never matches the rest of the
|
|
442
450
|
command line, which would count an unrelated process that merely mentions `claude`. The
|
|
443
451
|
consequence is a known blind spot: an instance started through a wrapper that execs
|
|
444
452
|
`node <path>/cli.js` has argv[0] `node`, so it is not listed. Listing signals nothing; the
|
|
@@ -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`.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { authSwitchApiKeyVerifier } from './authority-apikeys.js';
|
|
2
3
|
import { AuthSwitchAuthorityDaemon, type IAuthSwitchAuthorityDaemonOptions } from './classes.authoritydaemon.js';
|
|
3
4
|
import { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
|
|
4
5
|
import { ClaudeAccountStatus } from './classes.claudestatus.js';
|
|
@@ -107,9 +108,9 @@ export interface IAuthSwitchFixtureUsage {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
|
-
* Anthropic's profile, usage and token endpoints for exactly the fixture tokens it knows,
|
|
111
|
-
* fixture API keys it issued, and ChatGPT's `wham/usage` for the fixture ChatGPT logins; every
|
|
112
|
-
* an error, so nothing a test starts reaches a real provider.
|
|
111
|
+
* Anthropic's profile, usage and token endpoints for exactly the fixture tokens it knows, Anthropic's and OpenAI's
|
|
112
|
+
* model lists for the fixture API keys it issued, and ChatGPT's `wham/usage` for the fixture ChatGPT logins; every
|
|
113
|
+
* other request fails with an error, so nothing a test starts reaches a real provider.
|
|
113
114
|
*/
|
|
114
115
|
export class AuthSwitchFakeClaudeProvider {
|
|
115
116
|
/** Access token -> the account name it belongs to. */
|
|
@@ -138,6 +139,27 @@ export class AuthSwitchFakeClaudeProvider {
|
|
|
138
139
|
return apiKey;
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The OpenAI API keys OpenAI accepts -> the account name each was issued for, and whether it may read the model list
|
|
144
|
+
* (a restricted key without that permission may not); delete one to revoke it.
|
|
145
|
+
*/
|
|
146
|
+
public readonly openAiApiKeys = new Map<string, { name: string; modelRead: boolean }>();
|
|
147
|
+
/** While true, OpenAI's model list answers 503. */
|
|
148
|
+
public openAiModelsDown = false;
|
|
149
|
+
public openAiModelListCalls = 0;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Issues `name`'s OpenAI API key, which OpenAI accepts from then on, and answers it: `sk-proj-fixture-<name>`, a
|
|
153
|
+
* project key's prefix, so the authority tells it from an Anthropic key as it would a real one. `modelRead: false`
|
|
154
|
+
* issues a restricted key that may not read the model list.
|
|
155
|
+
*/
|
|
156
|
+
public issueOpenAiApiKey(name: string, options: { modelRead?: boolean } = {}): string {
|
|
157
|
+
assertFixtureName(name);
|
|
158
|
+
const apiKey = `sk-proj-fixture-${name}`;
|
|
159
|
+
this.openAiApiKeys.set(apiKey, { name, modelRead: options.modelRead ?? true });
|
|
160
|
+
return apiKey;
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
/** Accepts the access token of `name`'s login at `generation`, and its refresh token for the next generation. */
|
|
142
164
|
public know(name: string, generation: number): void {
|
|
143
165
|
assertFixtureName(name);
|
|
@@ -168,6 +190,33 @@ export class AuthSwitchFakeClaudeProvider {
|
|
|
168
190
|
}
|
|
169
191
|
return Response.json({ data: [], has_more: false });
|
|
170
192
|
}
|
|
193
|
+
if (url === 'https://api.openai.com/v1/models') {
|
|
194
|
+
// The model list of an OpenAI API key, which the authority reads to learn whether OpenAI accepts a key.
|
|
195
|
+
this.openAiModelListCalls++;
|
|
196
|
+
if (this.openAiModelsDown) {
|
|
197
|
+
return Response.json({ error: { message: 'overloaded', type: 'server_error' } }, { status: 503 });
|
|
198
|
+
}
|
|
199
|
+
const headers = new Headers(init?.headers);
|
|
200
|
+
// Only the request the authority's verifier sends is a fixture request: anything else is answered as malformed.
|
|
201
|
+
if ((init?.method ?? 'GET') !== 'GET' || init?.redirect !== 'error' || headers.has('x-api-key')
|
|
202
|
+
|| headers.has('openai-organization') || headers.has('openai-project')) {
|
|
203
|
+
return Response.json({ error: { message: 'not a fixture request', type: 'invalid_request_error' } },
|
|
204
|
+
{ status: 400 });
|
|
205
|
+
}
|
|
206
|
+
const presented = (headers.get('authorization') ?? '').replace(/^Bearer /, '');
|
|
207
|
+
const issued = this.openAiApiKeys.get(presented);
|
|
208
|
+
if (!issued) {
|
|
209
|
+
// OpenAI quotes the key it refused (masked); the fake quotes it whole, so a test proves nothing repeats it.
|
|
210
|
+
return Response.json({ error: { message: `Incorrect API key provided: ${presented}`,
|
|
211
|
+
type: 'invalid_request_error', param: null, code: 'invalid_api_key' } }, { status: 401 });
|
|
212
|
+
}
|
|
213
|
+
if (!issued.modelRead) {
|
|
214
|
+
return Response.json({ error: { message: 'You have insufficient permissions for this operation. Missing scopes:'
|
|
215
|
+
+ ' api.model.read.', type: 'invalid_request_error', param: null, code: null } }, { status: 403 });
|
|
216
|
+
}
|
|
217
|
+
return Response.json({ object: 'list', data: [{ id: 'gpt-fixture', object: 'model', created: 1_700_000_000,
|
|
218
|
+
owned_by: 'openai' }] });
|
|
219
|
+
}
|
|
171
220
|
if (url === 'https://api.anthropic.com/api/oauth/usage') {
|
|
172
221
|
const token = (new Headers(init?.headers).get('Authorization') ?? '').replace(/^Bearer /, '');
|
|
173
222
|
const name = this.accessTokens.get(token);
|
|
@@ -395,7 +444,7 @@ export const authSwitchSeedClaudeAccount = async (daemon: AuthSwitchAuthorityDae
|
|
|
395
444
|
} else {
|
|
396
445
|
const account: IStoredAuthorityAccount = { id: accountId, providerId: 'anthropic', issuer: 'anthropic',
|
|
397
446
|
subject: identity.accountUuid, workspaceId: identity.organizationUuid, label: name,
|
|
398
|
-
email: identity.emailAddress, plan: 'max', primaryGrantId: loginId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, removed: false, revision: 1,
|
|
447
|
+
email: identity.emailAddress, plan: 'max', primaryGrantId: loginId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, spendLimitMicroUsd: null, keyHint: null, removed: false, revision: 1,
|
|
399
448
|
statusObservedAt: now, updateId };
|
|
400
449
|
await database.changeAccountAndGrant(updateId, accountId, loginId, () => ({ account, grant }));
|
|
401
450
|
}
|
|
@@ -418,8 +467,8 @@ export const authSwitchSeedClaudeAccount = async (daemon: AuthSwitchAuthorityDae
|
|
|
418
467
|
* It serves the management and runtime sockets exactly as the installed authority does, with the Claude native
|
|
419
468
|
* authority on a fixture host home, container homes confined to `homesDirectory` and Claude sign-ins made in
|
|
420
469
|
* `claudeSignInDirectory`. Its OpenAI provider, its
|
|
421
|
-
* Claude usage reads and its checks of an Anthropic API key go to the fake provider, which refuses everything
|
|
422
|
-
* not a fixture request, and no Claude Code process is ever seen running on a home.
|
|
470
|
+
* Claude usage reads and its checks of an Anthropic or OpenAI API key go to the fake provider, which refuses everything
|
|
471
|
+
* that is not a fixture request, and no Claude Code process is ever seen running on a home.
|
|
423
472
|
*/
|
|
424
473
|
export class AuthSwitchTestAuthority {
|
|
425
474
|
private stopping: Promise<void> | null = null;
|
|
@@ -468,9 +517,7 @@ export class AuthSwitchTestAuthority {
|
|
|
468
517
|
dataDirectory: plugins.path.join(directory, 'db'), socketPath: databaseSocketPath,
|
|
469
518
|
authoritySocketPath, runtimeSocketPath,
|
|
470
519
|
broker: { provider: new AuthSwitchOpenAiProvider({ fetch: provider.fetch }), secrets: plainCodec, proactive: false,
|
|
471
|
-
|
|
472
|
-
await plugins.flexAnthropic.listAnthropicApiKeyModels({ apiKey }, { signal, fetch: provider.fetch });
|
|
473
|
-
} },
|
|
520
|
+
verifyApiKey: authSwitchApiKeyVerifier(provider.fetch) },
|
|
474
521
|
...claude.options,
|
|
475
522
|
codexNative: { home: new CodexNativeHome({ codexHome: codexHost.codexHome, executable: codexHost.binary,
|
|
476
523
|
scope: codexHost.scope }), secrets: plainCodec },
|
|
@@ -663,7 +710,7 @@ export class AuthSwitchTestAuthority {
|
|
|
663
710
|
await database.changeAccountAndGrant(updateId, account.accountId, account.loginId, () => ({
|
|
664
711
|
account: { id: account.accountId, providerId: 'anthropic', issuer: 'anthropic', subject: identity.accountUuid,
|
|
665
712
|
workspaceId: identity.organizationUuid, label: name, email: identity.emailAddress, plan: 'max',
|
|
666
|
-
primaryGrantId: account.loginId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, removed: false, revision: 1, statusObservedAt: now, updateId },
|
|
713
|
+
primaryGrantId: account.loginId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, spendLimitMicroUsd: null, keyHint: null, removed: false, revision: 1, statusObservedAt: now, updateId },
|
|
667
714
|
grant: { id: account.loginId, accountId: account.accountId, providerId: 'anthropic', purpose: 'claude_host_native',
|
|
668
715
|
audience: 'claude-code-host', state: 'native', owner: 'claude_native', grantGeneration: 1, authorizationGeneration: 1,
|
|
669
716
|
revision: 1, ciphertext: null, accessExpiresAt: null, attemptId: null, retryAt: null, retryCount: 0,
|
|
@@ -701,7 +748,7 @@ export class AuthSwitchTestAuthority {
|
|
|
701
748
|
const identity = authSwitchFixtureOpenAiIdentity(name);
|
|
702
749
|
return { id: accountId, providerId: 'openai', issuer: 'openai', subject: identity.userId,
|
|
703
750
|
workspaceId: identity.chatgptAccountId, label: name, email: identity.email, plan: 'pro', primaryGrantId: loginId,
|
|
704
|
-
maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, removed: false, revision: 1, statusObservedAt: now, updateId };
|
|
751
|
+
maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, spendLimitMicroUsd: null, keyHint: null, removed: false, revision: 1, statusObservedAt: now, updateId };
|
|
705
752
|
}
|
|
706
753
|
|
|
707
754
|
/**
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gives every account recorded before API keys carried a spend limit the two members the account record now states.
|
|
5
|
+
*
|
|
6
|
+
* An API key account may carry a monthly spend limit (`spendLimitMicroUsd`) and names how its key ends (`keyHint`);
|
|
7
|
+
* the record states each, or `null` for none. Every account recorded earlier has neither: it gains
|
|
8
|
+
* `spendLimitMicroUsd: null` and `keyHint: null` -- an Anthropic key held from before keeps no hint, since its key is
|
|
9
|
+
* sealed -- and nothing else about it changes, neither its revision nor its update id, since no one changed the
|
|
10
|
+
* account. A record that already carries a member keeps it exactly as it is, which is what makes an interrupted run
|
|
11
|
+
* safe to repeat.
|
|
12
|
+
*/
|
|
13
|
+
export const addAccountApiKeyFields = async (db: plugins.nosqldb.SmartdataDb): Promise<void> => {
|
|
14
|
+
const accounts = db.mongoDb.collection('authswitch_authority_accounts');
|
|
15
|
+
for (const member of ['spendLimitMicroUsd', 'keyHint'] as const) {
|
|
16
|
+
const missing = { [member]: { $exists: false } };
|
|
17
|
+
if (!await accounts.findOne(missing)) continue;
|
|
18
|
+
await accounts.updateMany(missing, { $set: { [member]: null } });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -9,6 +9,7 @@ import { addAccountMaxUsage } from './0007_account_max_usage.js';
|
|
|
9
9
|
import { addAccountLapse } from './0008_account_lapse.js';
|
|
10
10
|
import { addAccountReserveLift } from './0009_account_reserve_lift.js';
|
|
11
11
|
import { prepareCodexHostRelease } from './0010_codex_host_release.js';
|
|
12
|
+
import { addAccountApiKeyFields } from './0011_account_api_key_fields.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Runs on every start of an existing store, in this fixed order. Each module recognises the published shapes it
|
|
@@ -25,4 +26,5 @@ export const runAuthorityMigrations = async (db: plugins.nosqldb.SmartdataDb): P
|
|
|
25
26
|
await addAccountLapse(db);
|
|
26
27
|
await addAccountReserveLift(db);
|
|
27
28
|
await prepareCodexHostRelease(db);
|
|
29
|
+
await addAccountApiKeyFields(db);
|
|
28
30
|
};
|
|
@@ -147,10 +147,10 @@ export const findAuthorityDaemonLogin = (
|
|
|
147
147
|
};
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
* The login AGL's inference proxy binds for an account, and its purpose, like `findAuthorityDaemonLogin`:
|
|
151
|
-
*
|
|
152
|
-
* sign-in (`claude_host_native`), the one that can back a binding
|
|
153
|
-
* (`anthropic_api_key`).
|
|
150
|
+
* The login AGL's inference proxy binds for an account, and its purpose, like `findAuthorityDaemonLogin`: an OpenAI
|
|
151
|
+
* account's daemon-held login -- a ChatGPT sign-in (`openai_managed`) or an API key (`openai_api_key`) -- or an
|
|
152
|
+
* Anthropic account's daemon-held login -- a Claude sign-in (`claude_host_native`), the one that can back a binding
|
|
153
|
+
* when the account holds several, or an API key (`anthropic_api_key`).
|
|
154
154
|
*/
|
|
155
155
|
export const findAuthorityGatewayLogin = (
|
|
156
156
|
projectionArg: IControllerAuthorityProjection,
|
|
@@ -159,17 +159,37 @@ export const findAuthorityGatewayLogin = (
|
|
|
159
159
|
const snapshot = projectionArg.snapshot;
|
|
160
160
|
const account = snapshot?.accounts.find((accountArg) => accountArg.id === accountIdArg);
|
|
161
161
|
if (!snapshot || !account || account.removed) return 'no_account';
|
|
162
|
-
if (account.providerId !== 'anthropic')
|
|
162
|
+
if (account.providerId !== 'anthropic') {
|
|
163
|
+
const managed = findAuthorityDaemonLogin(projectionArg, accountIdArg);
|
|
164
|
+
return managed !== 'no_login' ? managed : snapshot.logins.find((loginArg) => loginArg.accountId === accountIdArg
|
|
165
|
+
&& loginArg.purpose === 'openai_api_key' && loginArg.owner === 'daemon') ?? 'no_login';
|
|
166
|
+
}
|
|
163
167
|
const logins = snapshot.logins.filter((loginArg) => loginArg.accountId === accountIdArg
|
|
164
168
|
&& (loginArg.purpose === 'claude_host_native' || loginArg.purpose === 'anthropic_api_key')
|
|
165
169
|
&& loginArg.owner === 'daemon');
|
|
166
170
|
return logins.find((loginArg) => loginArg.capabilities.canBind) ?? logins[0] ?? 'no_login';
|
|
167
171
|
};
|
|
168
172
|
|
|
173
|
+
/**
|
|
174
|
+
* The login an API key's answers are reported on (`authswitch.authority.usage.spend`): the key's live gateway login, or
|
|
175
|
+
* once the key was removed the login it had, as the projection publishes removed accounts and their logins too; the
|
|
176
|
+
* accounts service records a removed key's spend. `undefined` when the projection names no API-key login of the account.
|
|
177
|
+
*/
|
|
178
|
+
export const findAuthorityApiKeyLogin = (
|
|
179
|
+
projectionArg: IControllerAuthorityProjection,
|
|
180
|
+
accountIdArg: string,
|
|
181
|
+
): TControllerAccountLogin | undefined => {
|
|
182
|
+
const isKeyLogin = (loginArg: TControllerAccountLogin) => loginArg.accountId === accountIdArg && loginArg.owner === 'daemon'
|
|
183
|
+
&& (loginArg.purpose === 'anthropic_api_key' || loginArg.purpose === 'openai_api_key');
|
|
184
|
+
const live = findAuthorityGatewayLogin(projectionArg, accountIdArg);
|
|
185
|
+
if (typeof live !== 'string') return isKeyLogin(live) ? live : undefined;
|
|
186
|
+
return projectionArg.snapshot?.logins.filter(isKeyLogin).at(-1);
|
|
187
|
+
};
|
|
188
|
+
|
|
169
189
|
/**
|
|
170
190
|
* A pool of AGL's inference proxy: its provider, and what its accounts authenticate with -- a subscription sign-in
|
|
171
|
-
* (a ChatGPT or a Claude account), or an
|
|
172
|
-
* Claude models only on the API-key pool.
|
|
191
|
+
* (a ChatGPT or a Claude account), or an API key of the provider. Claude Code runs on the Claude subscription pool,
|
|
192
|
+
* Flex's Claude models only on the Anthropic API-key pool.
|
|
173
193
|
*/
|
|
174
194
|
export interface IInferencePool {
|
|
175
195
|
provider: 'openai' | 'anthropic';
|
|
@@ -179,7 +199,7 @@ export interface IInferencePool {
|
|
|
179
199
|
/**
|
|
180
200
|
* The pool of AGL's inference proxy an account serves in, or `undefined` for none: an account whose gateway login
|
|
181
201
|
* (`findAuthorityGatewayLogin`) the authority holds ready, can bind and keeps in no tool's home -- a ChatGPT account
|
|
182
|
-
* this host's Codex does not run on, a Claude account, or an Anthropic API key.
|
|
202
|
+
* this host's Codex does not run on, a Claude account, or an Anthropic or OpenAI API key.
|
|
183
203
|
*/
|
|
184
204
|
export const findInferencePool = (
|
|
185
205
|
projectionArg: IControllerAuthorityProjection,
|
|
@@ -197,6 +217,9 @@ export const findInferencePool = (
|
|
|
197
217
|
if (account.providerId === 'anthropic' && login.purpose === 'anthropic_api_key') {
|
|
198
218
|
return { provider: 'anthropic', credential: 'api_key' };
|
|
199
219
|
}
|
|
220
|
+
if (account.providerId === 'openai' && login.purpose === 'openai_api_key') {
|
|
221
|
+
return { provider: 'openai', credential: 'api_key' };
|
|
222
|
+
}
|
|
200
223
|
return undefined;
|
|
201
224
|
};
|
|
202
225
|
|
|
@@ -230,6 +253,18 @@ export const isFlexAuthorityConnectionId = (valueArg: unknown): valueArg is stri
|
|
|
230
253
|
*/
|
|
231
254
|
export const flexClaudePoolConnectionId = 'flex:proxy:anthropic';
|
|
232
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Whether an account serves in the Claude pool Flex's Claude models run on: an Anthropic API key in the proxy's
|
|
258
|
+
* Anthropic API-key pool (`findInferencePool`). An OpenAI key serves the OpenAI API-key pool, which Flex does not use.
|
|
259
|
+
*/
|
|
260
|
+
export const isFlexClaudePoolMember = (
|
|
261
|
+
projectionArg: IControllerAuthorityProjection,
|
|
262
|
+
accountIdArg: string,
|
|
263
|
+
): boolean => {
|
|
264
|
+
const pool = findInferencePool(projectionArg, accountIdArg);
|
|
265
|
+
return pool?.provider === 'anthropic' && pool.credential === 'api_key';
|
|
266
|
+
};
|
|
267
|
+
|
|
233
268
|
/** Whether a value is a Flex connection id: an authority account's, or the Claude pool's. */
|
|
234
269
|
export const isFlexConnectionId = (valueArg: unknown): valueArg is string =>
|
|
235
270
|
valueArg === flexClaudePoolConnectionId || isFlexAuthorityConnectionId(valueArg);
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
TControllerBrowserPaintAvailability,
|
|
6
6
|
TControllerBrowserPaintTermination,
|
|
7
7
|
} from './browserpaint.js';
|
|
8
|
-
export const controllerProtocolVersion =
|
|
8
|
+
export const controllerProtocolVersion = 45 as const;
|
|
9
9
|
export const controllerUpgradeManagementVersion = 2 as const;
|
|
10
10
|
export const controllerPackageName = 'agl' as const;
|
|
11
11
|
/** The systemd user unit `agl service install` writes; a controller it runs reports it in its status. */
|
package/ts_interfaces/plugins.ts
CHANGED
|
@@ -2,9 +2,12 @@ import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
|
|
2
2
|
import type * as browserRuntime from '@push.rocks/smartbrowser/runtime';
|
|
3
3
|
// The account-management contract on its own. Its own doc comment declares it credential-free and
|
|
4
4
|
// safe for browser code, while the package root is Node-only, so the browser-facing interfaces take
|
|
5
|
-
// the subpath rather than the root export.
|
|
6
|
-
// publishes -- the account label rule -- are taken from it as values rather than
|
|
5
|
+
// the subpath rather than the root export. Besides types it imports only the model id rule, itself a
|
|
6
|
+
// leaf, so the rules it publishes -- the account label rule -- are taken from it as values rather than
|
|
7
|
+
// restated.
|
|
7
8
|
import * as authswitchAuthority from '../ts_authswitch/authority-contract.js';
|
|
9
|
+
// An API key's monthly spend: the spend limit rule and dollar amounts, a leaf that imports nothing.
|
|
10
|
+
import * as authswitchSpend from '../ts_authswitch/authority-spend.js';
|
|
8
11
|
import type {
|
|
9
12
|
AuthenticationResponseJSON,
|
|
10
13
|
PublicKeyCredentialCreationOptionsJSON,
|
|
@@ -14,7 +17,7 @@ import type {
|
|
|
14
17
|
|
|
15
18
|
export { typedrequestInterfaces };
|
|
16
19
|
export type { browserRuntime };
|
|
17
|
-
export { authswitchAuthority };
|
|
20
|
+
export { authswitchAuthority, authswitchSpend };
|
|
18
21
|
|
|
19
22
|
export type {
|
|
20
23
|
AuthenticationResponseJSON,
|