agl 41.0.0 → 41.1.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.
Files changed (82) hide show
  1. package/changelog.md +18 -0
  2. package/dist_serve/bundle.js +881 -881
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/aglapi/aglapisurface.document.js +6 -2
  6. package/dist_ts/classes.authmodels.d.ts +15 -0
  7. package/dist_ts/classes.authmodels.js +78 -7
  8. package/dist_ts/classes.authstore.js +23 -6
  9. package/dist_ts/classes.cli.js +8 -3
  10. package/dist_ts/classes.controller.d.ts +13 -0
  11. package/dist_ts/classes.controller.js +52 -15
  12. package/dist_ts/classes.controllermanagement.d.ts +53 -6
  13. package/dist_ts/classes.controllermanagement.js +180 -72
  14. package/dist_ts/classes.inferencegateway.d.ts +6 -6
  15. package/dist_ts/classes.inferencegateway.js +25 -13
  16. package/dist_ts/classes.mcpreexec.js +4 -4
  17. package/dist_ts/classes.processinspection.d.ts +3 -1
  18. package/dist_ts/classes.processinspection.js +4 -2
  19. package/dist_ts/classes.proxycontrol.d.ts +22 -7
  20. package/dist_ts/classes.proxycontrol.js +5 -3
  21. package/dist_ts/classes.proxycontrolclient.d.ts +4 -3
  22. package/dist_ts/classes.proxycontrolclient.js +7 -5
  23. package/dist_ts/classes.proxydaemon.d.ts +6 -4
  24. package/dist_ts/classes.proxydaemon.js +42 -17
  25. package/dist_ts/classes.proxyleaseholder.d.ts +11 -2
  26. package/dist_ts/classes.proxyleaseholder.js +25 -2
  27. package/dist_ts/classes.proxystore.d.ts +3 -2
  28. package/dist_ts/classes.proxystore.js +5 -2
  29. package/dist_ts/classes.upgradecoordinator.d.ts +2 -1
  30. package/dist_ts/classes.upgradecoordinator.js +13 -10
  31. package/dist_ts/classes.upgradetransaction.d.ts +6 -0
  32. package/dist_ts/classes.upgradetransaction.js +38 -13
  33. package/dist_ts/classes.workercallers.d.ts +2 -0
  34. package/dist_ts/classes.workercallers.js +11 -8
  35. package/dist_ts/functions.controllermanagementsurface.d.ts +37 -0
  36. package/dist_ts/functions.controllermanagementsurface.js +110 -0
  37. package/dist_ts/functions.inferencecli.d.ts +6 -3
  38. package/dist_ts/functions.inferencecli.js +9 -5
  39. package/dist_ts/functions.inferencecliarguments.d.ts +1 -1
  40. package/dist_ts/functions.inferencecliarguments.js +19 -7
  41. package/dist_ts/functions.inferencelaunch.d.ts +30 -9
  42. package/dist_ts/functions.inferencelaunch.js +49 -23
  43. package/dist_ts/functions.workerprocessproof.d.ts +12 -3
  44. package/dist_ts/functions.workerprocessproof.js +17 -6
  45. package/dist_ts/interfaces.auth.d.ts +14 -3
  46. package/dist_ts/interfaces.auth.js +4 -3
  47. package/dist_ts_interfaces/index.d.ts +1 -0
  48. package/dist_ts_interfaces/index.js +2 -1
  49. package/dist_ts_interfaces/interfaces.d.ts +5 -0
  50. package/dist_ts_interfaces/interfaces.js +1 -1
  51. package/dist_ts_interfaces/management.d.ts +33 -0
  52. package/dist_ts_interfaces/management.js +35 -0
  53. package/package.json +2 -1
  54. package/readme.md +118 -31
  55. package/ts/00_commitinfo_data.ts +1 -1
  56. package/ts/aglapi/aglapisurface.document.ts +5 -1
  57. package/ts/classes.authmodels.ts +74 -5
  58. package/ts/classes.authstore.ts +24 -4
  59. package/ts/classes.cli.ts +9 -2
  60. package/ts/classes.controller.ts +99 -60
  61. package/ts/classes.controllermanagement.ts +284 -107
  62. package/ts/classes.inferencegateway.ts +28 -17
  63. package/ts/classes.mcpreexec.ts +3 -3
  64. package/ts/classes.processinspection.ts +2 -1
  65. package/ts/classes.proxycontrol.ts +25 -9
  66. package/ts/classes.proxycontrolclient.ts +7 -4
  67. package/ts/classes.proxydaemon.ts +43 -17
  68. package/ts/classes.proxyleaseholder.ts +36 -7
  69. package/ts/classes.proxystore.ts +8 -3
  70. package/ts/classes.upgradecoordinator.ts +20 -11
  71. package/ts/classes.upgradetransaction.ts +53 -2
  72. package/ts/classes.workercallers.ts +12 -7
  73. package/ts/functions.controllermanagementsurface.ts +149 -0
  74. package/ts/functions.inferencecli.ts +15 -7
  75. package/ts/functions.inferencecliarguments.ts +16 -6
  76. package/ts/functions.inferencelaunch.ts +57 -23
  77. package/ts/functions.workerprocessproof.ts +17 -3
  78. package/ts/interfaces.auth.ts +14 -3
  79. package/ts_interfaces/index.ts +1 -0
  80. package/ts_interfaces/interfaces.ts +5 -0
  81. package/ts_interfaces/management.ts +52 -0
  82. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -72,6 +72,8 @@ export interface IAglProxyStatus {
72
72
  export interface IAglProxyConfinedRunStatus {
73
73
  /** The run's id: the first 12 hex characters of its credential's SHA-256 digest, never the credential. */
74
74
  id: string;
75
+ /** The pool the run is served from: Claude accounts (`anthropic`) or ChatGPT accounts (`openai`). */
76
+ provider: TInferenceProvider;
75
77
  createdAt: string;
76
78
  /** False while a restarted proxy serves the credential for its reclaim window, waiting for its holder to take it back. */
77
79
  held: boolean;
@@ -148,24 +150,35 @@ export interface IReq_AglProxyReclaim extends ITypedRequest {
148
150
 
149
151
  /**
150
152
  * A confined run's credential: accepted on the confined listener alone, never on the loopback one, and served there from
151
- * the Claude pool exactly as the loopback listener serves a Claude client, its sessions those of the client
152
- * `aglProxyConfinedClient`. Like a lease it lives as long as the connection that asked for it, a restarted proxy keeps it
153
- * for its holder to take back (`agl.proxy.reclaim`), and the proxy keeps only its digest. `agl.proxy.confined.end` ends it
154
- * at once.
153
+ * the pool `provider` names -- the Claude pool when it names none -- exactly as the loopback listener serves Claude Code on
154
+ * that pool, its sessions those of the client `aglProxyConfinedClient` on that pool. A ChatGPT-pool run is served by the
155
+ * pool's ChatGPT accounts alone, since a translated Messages request never goes to an API key. Like a lease it lives as
156
+ * long as the connection that asked for it, a restarted proxy keeps it for its holder to take back
157
+ * (`agl.proxy.reclaim`), and the proxy keeps only its digest. `agl.proxy.confined.end` ends it at once.
155
158
  */
156
159
  export interface IReq_AglProxyConfined extends ITypedRequest {
157
160
  method: 'agl.proxy.confined';
158
- request: Record<string, never>;
161
+ /** An empty request asks for a Claude-pool run, as every proxy since confined runs takes it. */
162
+ request: { provider?: TInferenceProvider };
159
163
  response: IAglProxyConfinedGrant;
160
164
  }
161
165
 
162
- /** What a confined run is given: its id, its credential, the confined listener's socket and the origin to name with it. */
166
+ /**
167
+ * What a confined run is given: its id, its pool, its credential, the confined listener's socket and the origin to name
168
+ * with it, and, for a ChatGPT-pool run, the pool's models as the proxy read them when it issued the credential, which the
169
+ * launcher configures Claude Code with as `agl proxy run claude --provider openai` does. A Claude-pool run gets none:
170
+ * Claude Code reads that pool's models through the confined listener.
171
+ */
163
172
  export interface IAglProxyConfinedGrant {
164
173
  /** See `IAglProxyConfinedRunStatus.id`. */
165
174
  id: string;
175
+ /** The run's pool; a proxy older than ChatGPT-pool runs names none, and issues Claude-pool runs alone. */
176
+ provider?: TInferenceProvider;
166
177
  token: string;
167
178
  socketPath: string;
168
179
  baseUrl: string;
180
+ /** A ChatGPT-pool run's models, the pool's default among them; absent for a Claude-pool run. */
181
+ models?: TInferenceModel[];
169
182
  }
170
183
 
171
184
  /**
@@ -207,7 +220,7 @@ export interface IAglProxyControlHandlers {
207
220
  catalog(requestArg: IReq_AglProxyCatalog['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyCatalog['response']>;
208
221
  lease(requestArg: IReq_AglProxyLease['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyLease['response']>;
209
222
  reclaim(requestArg: IReq_AglProxyReclaim['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyReclaim['response']>;
210
- confined(connectionArg: IAglProxyConnection): Promise<IReq_AglProxyConfined['response']>;
223
+ confined(requestArg: IReq_AglProxyConfined['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyConfined['response']>;
211
224
  confinedEnd(requestArg: IReq_AglProxyConfinedEnd['request']): Promise<IReq_AglProxyConfinedEnd['response']>;
212
225
  usage(requestArg: IReq_AglProxyUsage['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyUsage['response']>;
213
226
  }
@@ -303,8 +316,11 @@ export class AglProxyControlServer {
303
316
  return this.handlers!.reclaim(requestArg, connectionArg);
304
317
  }));
305
318
  this.router.addTypedHandler(handler<IReq_AglProxyConfined>('agl.proxy.confined', (requestArg, connectionArg) => {
306
- if (!isRecord(requestArg) || Object.keys(requestArg).length !== 0) throw new AglProxyControlRefusal('Invalid proxy confined request.');
307
- return this.handlers!.confined(connectionArg);
319
+ if (!isRecord(requestArg) || Object.keys(requestArg).some((keyArg) => keyArg !== 'provider')
320
+ || (requestArg.provider !== undefined && !inferenceProviders.includes(requestArg.provider))) {
321
+ throw new AglProxyControlRefusal('Invalid proxy confined request.');
322
+ }
323
+ return this.handlers!.confined(requestArg, connectionArg);
308
324
  }));
309
325
  this.router.addTypedHandler(handler<IReq_AglProxyConfinedEnd>('agl.proxy.confined.end', (requestArg) => {
310
326
  if (!isRecord(requestArg) || typeof requestArg.id !== 'string' || !aglProxyConfinedIdPattern.test(requestArg.id)
@@ -190,12 +190,15 @@ export class AglProxyControlClient {
190
190
  }
191
191
 
192
192
  /**
193
- * A confined run's credential, the confined listener's socket and the origin to name with it; the credential lives as
194
- * long as the returned connection (see `IReq_AglProxyConfined`).
193
+ * A confined run's credential on the pool `providerArg` names, the confined listener's socket and the origin to name
194
+ * with it; the credential lives as long as the returned connection (see `IReq_AglProxyConfined`). A Claude-pool run is
195
+ * asked for with the empty request every proxy since confined runs takes.
195
196
  */
196
- public async confined(signalArg: AbortSignal): Promise<IReq_AglProxyConfined['response'] & IAglProxyLeaseConnection> {
197
+ public async confined(providerArg: TInferenceProvider,
198
+ signalArg: AbortSignal): Promise<IReq_AglProxyConfined['response'] & IAglProxyLeaseConnection> {
197
199
  let held: { socket: net.Socket; rest: Buffer } | undefined;
198
- const response = await exchange<IReq_AglProxyConfined['response']>(this.socketPath, 'agl.proxy.confined', {},
200
+ const request: IReq_AglProxyConfined['request'] = providerArg === 'anthropic' ? {} : { provider: providerArg };
201
+ const response = await exchange<IReq_AglProxyConfined['response']>(this.socketPath, 'agl.proxy.confined', request,
199
202
  signalArg, (socketArg, restArg) => { held = { socket: socketArg, rest: restArg }; });
200
203
  return { ...response, ...leaseConnection(held) };
201
204
  }
@@ -8,7 +8,7 @@ import { InferenceRoutingError, type IInferenceRoutingPolicy } from './classes.i
8
8
  import { aglProxyConfinedClient, aglProxyConfinedOrigin, proxyClientNamePattern, type TInferenceProvider,
9
9
  type TProxyCredentialDelivery } from './functions.proxycontract.js';
10
10
  import { AglProxyControlRefusal, AglProxyControlServer, type IAglProxyConfinedGrant, type IAglProxyConnection,
11
- type IAglProxyStatus, type IReq_AglProxyCatalog, type IReq_AglProxyReclaim,
11
+ type IAglProxyStatus, type IReq_AglProxyCatalog, type IReq_AglProxyConfined, type IReq_AglProxyReclaim,
12
12
  type TAglProxyLeaseCatalog } from './classes.proxycontrol.js';
13
13
  import { ProxyStore, type IProxySecretCodec } from './classes.proxystore.js';
14
14
  import type { InferenceUsageStore } from './classes.inferenceusage.js';
@@ -51,8 +51,16 @@ const catalogRefusal = (errorArg: unknown): AglProxyControlRefusal => new AglPro
51
51
  errorArg instanceof InferenceRoutingError ? errorArg.code : undefined,
52
52
  );
53
53
 
54
- /** The policy of every confined run's credential: the Claude pool, unrestricted, as Claude Code's own lease. */
55
- const confinedPolicy: IInferenceRoutingPolicy = Object.freeze({ provider: 'anthropic' });
54
+ /**
55
+ * The policy of a confined run's credential, by its pool: the pool, unrestricted, as Claude Code's own lease on the Claude
56
+ * pool and `agl proxy run claude --provider openai`'s on the ChatGPT pool.
57
+ */
58
+ const confinedPolicies: Readonly<Record<TInferenceProvider, IInferenceRoutingPolicy>> = Object.freeze({
59
+ anthropic: Object.freeze({ provider: 'anthropic' }),
60
+ // A ChatGPT-pool run's policy is the one `agl proxy run claude --provider openai` leases: its translated Messages
61
+ // requests go to the pool's ChatGPT accounts alone, never to an API key (see `IReq_AglProxyConfined`).
62
+ openai: Object.freeze({ provider: 'openai' }),
63
+ });
56
64
 
57
65
  /** A confined run's id, as the operator names it: the first 12 hex characters of its credential's digest. */
58
66
  const confinedRunId = (digestArg: string): string => digestArg.slice(0, 12);
@@ -61,6 +69,8 @@ const confinedRunId = (digestArg: string): string => digestArg.slice(0, 12);
61
69
  interface IHeldLease {
62
70
  grant: IInferenceGatewayGrant;
63
71
  route: TInferenceGatewayRoute;
72
+ /** The pool the lease is served from; a policy that names none is the ChatGPT pool's, as the router reads it. */
73
+ provider: TInferenceProvider;
64
74
  createdAt: Date;
65
75
  /** The connection the lease lives as long as, while a holder has it. */
66
76
  holder?: IAglProxyConnection;
@@ -146,7 +156,7 @@ export class AglProxyDaemon {
146
156
  catalog: (requestArg, connectionArg) => this.catalog(requestArg, connectionArg),
147
157
  lease: (requestArg, connectionArg) => this.lease(requestArg.policy, requestArg.client, connectionArg),
148
158
  reclaim: async (requestArg, connectionArg) => this.reclaim(requestArg.token, connectionArg),
149
- confined: (connectionArg) => this.confined(connectionArg),
159
+ confined: (requestArg, connectionArg) => this.confined(requestArg, connectionArg),
150
160
  confinedEnd: async (requestArg) => this.confinedEnd(requestArg.id),
151
161
  usage: async (requestArg, connectionArg) => 'since' in requestArg ? this.usage!.changes(requestArg)
152
162
  : await this.usage!.snapshot(requestArg, connectionArg.signal),
@@ -168,8 +178,8 @@ export class AglProxyDaemon {
168
178
  recentPlacements: status.recentPlacements, confinedSocketPath: this.options.confinedSocketPath ?? null,
169
179
  confinedRuns: [...this.leases].filter(([, heldArg]) => heldArg.route === 'confined')
170
180
  .sort(([, left], [, right]) => left.createdAt.getTime() - right.createdAt.getTime())
171
- .map(([digestArg, heldArg]) => ({ id: confinedRunId(digestArg), createdAt: heldArg.createdAt.toISOString(),
172
- held: heldArg.reclaimTimer === undefined })),
181
+ .map(([digestArg, heldArg]) => ({ id: confinedRunId(digestArg), provider: heldArg.provider,
182
+ createdAt: heldArg.createdAt.toISOString(), held: heldArg.reclaimTimer === undefined })),
173
183
  };
174
184
  }
175
185
 
@@ -289,7 +299,8 @@ export class AglProxyDaemon {
289
299
  // The lease continues the sessions of the client it is taken for (see `IReq_AglProxyLease`).
290
300
  try { lease = await service.open(policyArg, { sessionScope: clientArg }); }
291
301
  catch (errorArg) { throw new AglProxyControlRefusal(errorArg instanceof Error ? errorArg.message : 'The lease could not be opened.'); }
292
- const held: IHeldLease = { grant: lease, route: 'loopback', createdAt: new Date(), holder: connectionArg };
302
+ const held: IHeldLease = { grant: lease, route: 'loopback', provider: policyArg.provider ?? 'openai', createdAt: new Date(),
303
+ holder: connectionArg };
293
304
  this.leases.set(lease.digest, held);
294
305
  try {
295
306
  await this.trackLeaseWrite(this.store!.recordLease({ digest: lease.digest, client: clientArg, policy: policyArg,
@@ -319,30 +330,45 @@ export class AglProxyDaemon {
319
330
  * proves the caller holds the credential the lease was issued with.
320
331
  */
321
332
  /**
322
- * A confined run's credential (see `IReq_AglProxyConfined`): a lease of the Claude pool that continues the confined
323
- * client's sessions and is accepted on the confined listener alone. It is kept, as its digest, before it is handed out,
324
- * and ends when its holder's connection closes or the operator ends it (`confinedEnd`). No model list is read for it:
325
- * Claude Code reads the pool's models through the confined listener, and the pool is judged at each request.
333
+ * A confined run's credential (see `IReq_AglProxyConfined`): a lease of the pool the request names -- the Claude pool
334
+ * when it names none -- that continues the confined client's sessions on that pool and is accepted on the confined
335
+ * listener alone. It is kept, as its digest, before it is handed out, and ends when its holder's connection closes or
336
+ * the operator ends it (`confinedEnd`). A Claude-pool run gets no model list: Claude Code reads the pool's models
337
+ * through the confined listener, and the pool is judged at each request. A ChatGPT-pool run gets the pool's models, as
338
+ * an `agl proxy run` lease does, for its launcher to configure Claude Code with.
326
339
  */
327
- private async confined(connectionArg: IAglProxyConnection): Promise<IAglProxyConfinedGrant> {
340
+ private async confined(requestArg: IReq_AglProxyConfined['request'],
341
+ connectionArg: IAglProxyConnection): Promise<IAglProxyConfinedGrant> {
328
342
  const socketPath = this.options.confinedSocketPath;
329
343
  if (socketPath === undefined) throw new AglProxyControlRefusal('This AGL proxy has no confined listener.');
344
+ const provider = requestArg.provider ?? 'anthropic';
345
+ const policy = confinedPolicies[provider];
346
+ const service = this.service!;
330
347
  let lease: IInferenceGatewayLease;
331
- try { lease = await this.service!.open(confinedPolicy, { sessionScope: aglProxyConfinedClient, route: 'confined' }); }
348
+ try { lease = await service.open(policy, { sessionScope: aglProxyConfinedClient, route: 'confined' }); }
332
349
  catch (errorArg) {
333
350
  throw new AglProxyControlRefusal(errorArg instanceof Error ? errorArg.message : 'The confined credential could not be issued.');
334
351
  }
335
- const held: IHeldLease = { grant: lease, route: 'confined', createdAt: new Date(), holder: connectionArg };
352
+ const held: IHeldLease = { grant: lease, route: 'confined', provider, createdAt: new Date(), holder: connectionArg };
336
353
  this.leases.set(lease.digest, held);
337
354
  try {
338
355
  await this.trackLeaseWrite(this.store!.recordLease({ digest: lease.digest, client: aglProxyConfinedClient,
339
- policy: confinedPolicy, createdAt: held.createdAt, route: 'confined' }));
356
+ policy, createdAt: held.createdAt, route: 'confined' }));
340
357
  } catch (errorArg) {
341
358
  this.endLease(lease.digest, held);
342
359
  throw new AglProxyControlRefusal(errorArg instanceof Error ? errorArg.message : 'The confined credential could not be recorded.');
343
360
  }
344
361
  connectionArg.onClose(() => this.endLease(lease.digest, held));
345
- return { id: confinedRunId(lease.digest), token: lease.token, socketPath, baseUrl: aglProxyConfinedOrigin };
362
+ const grant: IAglProxyConfinedGrant = { id: confinedRunId(lease.digest), provider, token: lease.token, socketPath,
363
+ baseUrl: aglProxyConfinedOrigin };
364
+ if (provider === 'anthropic') return grant;
365
+ try {
366
+ // A launcher that goes away stops the model list read it asked for.
367
+ return { ...grant, models: await service.router.models(policy, AbortSignal.any([connectionArg.signal, AbortSignal.timeout(60_000)])) };
368
+ } catch (errorArg) {
369
+ this.endLease(lease.digest, held);
370
+ throw catalogRefusal(errorArg);
371
+ }
346
372
  }
347
373
 
348
374
  /**
@@ -414,7 +440,7 @@ export class AglProxyDaemon {
414
440
  await this.forgetLease(kept.digest);
415
441
  continue;
416
442
  }
417
- const held: IHeldLease = { grant, route: kept.route, createdAt: kept.createdAt };
443
+ const held: IHeldLease = { grant, route: kept.route, provider: kept.policy.provider ?? 'openai', createdAt: kept.createdAt };
418
444
  held.reclaimTimer = setTimeout(() => this.endLease(kept.digest, held), Math.max(0, deadline - Date.now()));
419
445
  this.leases.set(kept.digest, held);
420
446
  }
@@ -1,6 +1,7 @@
1
1
  import * as plugins from './plugins.js';
2
- import type { IInferenceRoutingPolicy } from './classes.inferencerouter.js';
3
- import type { IReq_AglProxyReclaim, TAglProxyLeaseCatalog } from './classes.proxycontrol.js';
2
+ import type { IInferenceRoutingPolicy, TInferenceModel } from './classes.inferencerouter.js';
3
+ import type { IAglProxyConfinedGrant, IReq_AglProxyReclaim, TAglProxyLeaseCatalog } from './classes.proxycontrol.js';
4
+ import type { TInferenceProvider } from './functions.proxycontract.js';
4
5
  import { AglProxyControlClient, AglProxyRefusedError, type IAglProxyLeaseConnection } from './classes.proxycontrolclient.js';
5
6
 
6
7
  /** The delays between attempts to take a lease back from a restarting proxy; the last one repeats. */
@@ -159,28 +160,56 @@ export class AglProxyLeaseHolder extends AglProxyHeldCredential {
159
160
  }
160
161
  }
161
162
 
163
+ export interface IAglProxyConfinedHolderOptions extends IAglProxyHolderOptions {
164
+ /** The run's pool: Claude accounts (`anthropic`) or, translated, the ChatGPT accounts (`openai`). */
165
+ provider: TInferenceProvider;
166
+ }
167
+
168
+ /**
169
+ * Why a confined grant does not serve the pool it was asked for, or `undefined` when it does: a proxy older than
170
+ * ChatGPT-pool runs names no pool and issues Claude-pool runs alone, and a ChatGPT-pool run comes with the pool's models.
171
+ */
172
+ const confinedGrantMismatch = (providerArg: TInferenceProvider, grantArg: IAglProxyConfinedGrant): string | undefined => {
173
+ if (providerArg === 'anthropic') {
174
+ return grantArg.provider === undefined || grantArg.provider === 'anthropic' ? undefined
175
+ : 'The AGL proxy issued a confined run of another pool than the Claude pool asked for.';
176
+ }
177
+ return grantArg.provider === 'openai' && Array.isArray(grantArg.models) ? undefined
178
+ : 'The AGL proxy issued no ChatGPT-pool confined run with the pool\'s models; it may run an AGL older than ChatGPT-pool runs.';
179
+ };
180
+
162
181
  /**
163
182
  * A confined run's credential (see `IReq_AglProxyConfined`), held for as long as its container runs. It is configured
164
183
  * with the confined listener's socket, so one taken back on another socket cannot continue.
165
184
  */
166
185
  export class AglProxyConfinedHolder extends AglProxyHeldCredential {
167
186
  /** Takes a new confined run's credential of the proxy on `optionsArg.socketPath`, its control socket. */
168
- public static async take(optionsArg: IAglProxyHolderOptions, signalArg: AbortSignal): Promise<AglProxyConfinedHolder> {
187
+ public static async take(optionsArg: IAglProxyConfinedHolderOptions, signalArg: AbortSignal): Promise<AglProxyConfinedHolder> {
169
188
  const control = new AglProxyControlClient(optionsArg.socketPath);
170
- const grant = await control.confined(signalArg);
189
+ const grant = await control.confined(optionsArg.provider, signalArg);
190
+ const mismatch = confinedGrantMismatch(optionsArg.provider, grant);
191
+ if (mismatch !== undefined) {
192
+ grant.close();
193
+ throw new Error(mismatch);
194
+ }
171
195
  return new AglProxyConfinedHolder(control, grant, optionsArg);
172
196
  }
173
197
 
174
198
  /** The run's id, as `agl proxy confined end` and `agl proxy status` name it. */
175
199
  public readonly id: string;
200
+ /** The run's pool. */
201
+ public readonly provider: TInferenceProvider;
202
+ /** A ChatGPT-pool run's models as the proxy read them when it issued the credential; empty for a Claude-pool run. */
203
+ public readonly models: readonly TInferenceModel[];
176
204
  /** The confined listener's socket, on the host. */
177
205
  public readonly confinedSocketPath: string;
178
206
 
179
- private constructor(controlArg: AglProxyControlClient,
180
- grantArg: { id: string; baseUrl: string; token: string; socketPath: string } & IAglProxyLeaseConnection,
181
- optionsArg: IAglProxyHolderOptions) {
207
+ private constructor(controlArg: AglProxyControlClient, grantArg: IAglProxyConfinedGrant & IAglProxyLeaseConnection,
208
+ optionsArg: IAglProxyConfinedHolderOptions) {
182
209
  super(controlArg, grantArg, optionsArg);
183
210
  this.id = grantArg.id;
211
+ this.provider = optionsArg.provider;
212
+ this.models = grantArg.models ?? [];
184
213
  this.confinedSocketPath = grantArg.socketPath;
185
214
  }
186
215
 
@@ -151,8 +151,9 @@ export interface IProxyLeaseDocument {
151
151
  */
152
152
  orphanedAt?: Date;
153
153
  /**
154
- * `confined` for a confined run's credential, accepted on the confined listener alone, with the Claude pool's policy
155
- * and the confined client's sessions; absent for a lease of the loopback listener.
154
+ * `confined` for a confined run's credential, accepted on the confined listener alone, with the policy of its pool,
155
+ * the Claude pool's or the ChatGPT pool's, unrestricted, and the confined client's sessions on that pool; absent for a
156
+ * lease of the loopback listener.
156
157
  */
157
158
  route?: 'confined';
158
159
  }
@@ -174,6 +175,10 @@ const assertLeasePolicy = (valueArg: unknown): IProxyLeasePolicyDocument => {
174
175
  return value;
175
176
  };
176
177
 
178
+ /** A confined run's policy, as kept: one pool, unrestricted (see `IProxyLeaseDocument.route`). */
179
+ const confinedLeasePolicies: ReadonlySet<string> = new Set([JSON.stringify({ provider: 'anthropic' }),
180
+ JSON.stringify({ provider: 'openai' })]);
181
+
177
182
  const assertLease: (valueArg: unknown) => asserts valueArg is IProxyLeaseDocument = (valueArg) => {
178
183
  const value = valueArg as Partial<IProxyLeaseDocument> | null;
179
184
  if (!value || typeof value !== 'object'
@@ -182,7 +187,7 @@ const assertLease: (valueArg: unknown) => asserts valueArg is IProxyLeaseDocumen
182
187
  || typeof value.client !== 'string' || !proxyClientNamePattern.test(value.client)
183
188
  || !isDate(value.createdAt) || (Object.hasOwn(value, 'orphanedAt') && !isDate(value.orphanedAt))
184
189
  || (Object.hasOwn(value, 'route') && (value.route !== 'confined' || value.client !== aglProxyConfinedClient
185
- || JSON.stringify(value.policy) !== JSON.stringify({ provider: 'anthropic' })))) {
190
+ || !confinedLeasePolicies.has(JSON.stringify(value.policy))))) {
186
191
  throw new Error('Invalid proxy lease.');
187
192
  }
188
193
  assertLeasePolicy(value.policy);
@@ -85,6 +85,18 @@ interface IUpgradeSemver {
85
85
  prerelease: string[];
86
86
  }
87
87
 
88
+ const upgradeOwnerCommands = [
89
+ '__serve', '__upgrade-worker', 'upgrade', 'start', 'restart', 'service', 'foreground', 'temp-password', 'config',
90
+ ] as const;
91
+ /** The CLI commands this release runs under an upgrade lock or a start lease. */
92
+ type TUpgradeOwnerCommand = (typeof upgradeOwnerCommands)[number];
93
+ const upgradeOwnerCommandSet: ReadonlySet<string> = new Set(upgradeOwnerCommands);
94
+ /**
95
+ * A command a later release's CLI holds a start lease under (`agl remote` from 42 on), read back after a rollback:
96
+ * its owner is live while that command runs and stale once it ended, like any other.
97
+ */
98
+ const laterReleaseStartCommandPattern = /^[a-z][a-z0-9-]{0,31}$/;
99
+
88
100
  interface IUpgradeProcessOwner {
89
101
  version: typeof coordinationVersion;
90
102
  kind: TUpgradeOwnerKind;
@@ -94,7 +106,8 @@ interface IUpgradeProcessOwner {
94
106
  processGroupId: number;
95
107
  fingerprint: string;
96
108
  cliPath: string;
97
- command: '__serve' | '__upgrade-worker' | 'upgrade' | 'start' | 'restart' | 'service' | 'foreground' | 'temp-password' | 'config';
109
+ /** A `TUpgradeOwnerCommand`, or for a start lease also a later release's command. */
110
+ command: string;
98
111
  createdAt: number;
99
112
  }
100
113
 
@@ -504,15 +517,11 @@ const parseOwner = (valueArg: unknown): IUpgradeProcessOwner => {
504
517
  || value.fingerprint.length > 256
505
518
  || typeof value.cliPath !== 'string'
506
519
  || !plugins.path.isAbsolute(value.cliPath)
507
- || (value.command !== '__serve'
508
- && value.command !== '__upgrade-worker'
509
- && value.command !== 'start'
510
- && value.command !== 'upgrade'
511
- && value.command !== 'foreground'
512
- && value.command !== 'temp-password'
513
- && value.command !== 'config'
514
- && value.command !== 'restart'
515
- && value.command !== 'service')
520
+ || typeof value.command !== 'string'
521
+ || (
522
+ !upgradeOwnerCommandSet.has(value.command)
523
+ && (value.kind !== 'start' || !laterReleaseStartCommandPattern.test(value.command))
524
+ )
516
525
  ) {
517
526
  throw new Error('Upgrade owner metadata is invalid.');
518
527
  }
@@ -1330,7 +1339,7 @@ const ownerForCurrentProcess = async (
1330
1339
  kindArg: TUpgradeOwnerKind,
1331
1340
  tokenArg: string,
1332
1341
  cliPathArg: string,
1333
- commandArg: IUpgradeProcessOwner['command'],
1342
+ commandArg: TUpgradeOwnerCommand,
1334
1343
  ): Promise<IUpgradeProcessOwner> => {
1335
1344
  const identity = await readControllerProcessIdentity(process.pid);
1336
1345
  if (!identity) throw new Error('Unable to establish upgrade process ownership.');
@@ -9,6 +9,7 @@ import {
9
9
  } from '../ts_interfaces/index.js';
10
10
  import { resolveAGLHomePaths } from './classes.aglhome.js';
11
11
  import {
12
+ isControllerManagementProtocolVersion,
12
13
  isLoopbackPortListening,
13
14
  queryControllerStatus,
14
15
  requestControllerUpgradeFinalize,
@@ -144,8 +145,22 @@ export interface IPnpmGlobalInstallation {
144
145
  packageDirectory: string;
145
146
  cliPath: string;
146
147
  packageVersion: string;
148
+ /**
149
+ * The management protocol the installed `package.json` declares (`controllerManagementProtocolVersion`), which
150
+ * decides how the controller this package starts is reached before it reports its status; absent when it declares
151
+ * none, as every release older than the loopback management endpoint.
152
+ */
153
+ managementProtocolVersion?: number;
147
154
  }
148
155
 
156
+ /**
157
+ * What a `package.json` declares as `controllerManagementProtocolVersion`: `undefined` for no declaration, `false`
158
+ * for one that is not a safe integer of 1 or more, which makes the package metadata incompatible.
159
+ */
160
+ const readDeclaredManagementProtocolVersion = (valueArg: unknown): number | undefined | false => (
161
+ valueArg === undefined ? undefined : isControllerManagementProtocolVersion(valueArg) ? valueArg : false
162
+ );
163
+
149
164
  export interface IDetachedUpgradeWorker {
150
165
  pid: number;
151
166
  processGroupId: number;
@@ -597,15 +612,20 @@ export const resolvePnpmGlobalInstallation = async (): Promise<IPnpmGlobalInstal
597
612
  version?: unknown;
598
613
  bin?: unknown;
599
614
  controllerUpgradeManagementVersion?: unknown;
615
+ controllerManagementProtocolVersion?: unknown;
600
616
  };
601
617
  const currentIdentity: { cliName: string; cliRelativePath: string; managementVersion: number } = (
602
618
  currentPackageIdentity
603
619
  );
620
+ const managementProtocolVersion = readDeclaredManagementProtocolVersion(
621
+ packageJson.controllerManagementProtocolVersion,
622
+ );
604
623
  if (
605
624
  packageJson.name !== controllerPackageName
606
625
  || packageJson.version !== dependency.version
607
626
  || packageJson.controllerUpgradeManagementVersion !== currentIdentity.managementVersion
608
627
  || Number(controllerUpgradeManagementVersion) !== currentIdentity.managementVersion
628
+ || managementProtocolVersion === false
609
629
  || !declaresControllerCommands(packageJson.bin, currentIdentity)
610
630
  ) {
611
631
  throw new Error(`The pnpm-global ${controllerPackageName} package metadata is incompatible.`);
@@ -621,6 +641,7 @@ export const resolvePnpmGlobalInstallation = async (): Promise<IPnpmGlobalInstal
621
641
  packageDirectory,
622
642
  cliPath,
623
643
  packageVersion: dependency.version,
644
+ ...(managementProtocolVersion === undefined ? {} : { managementProtocolVersion }),
624
645
  };
625
646
  };
626
647
 
@@ -749,10 +770,14 @@ const readExactGlobalPackage = async (
749
770
  }
750
771
  const manifest = packageJson as Record<string, unknown>;
751
772
  const bin = manifest.bin;
773
+ const managementProtocolVersion = readDeclaredManagementProtocolVersion(
774
+ manifest.controllerManagementProtocolVersion,
775
+ );
752
776
  if (
753
777
  manifest.name !== identityArg.packageName
754
778
  || manifest.version !== identityArg.version
755
779
  || manifest.controllerUpgradeManagementVersion !== identityArg.managementVersion
780
+ || managementProtocolVersion === false
756
781
  || !bin
757
782
  || typeof bin !== 'object'
758
783
  || Array.isArray(bin)
@@ -772,6 +797,7 @@ const readExactGlobalPackage = async (
772
797
  packageDirectory,
773
798
  cliPath,
774
799
  packageVersion: identityArg.version,
800
+ ...(managementProtocolVersion === undefined ? {} : { managementProtocolVersion }),
775
801
  packageName: identityArg.packageName,
776
802
  cliName: identityArg.cliName,
777
803
  cliRelativePath: identityArg.cliRelativePath,
@@ -952,11 +978,15 @@ const resolveAuthoritativeRecoveryInstallation = async (
952
978
  }
953
979
  const manifest = packageJson as Record<string, unknown>;
954
980
  const bin = manifest.bin;
981
+ const managementProtocolVersion = readDeclaredManagementProtocolVersion(
982
+ manifest.controllerManagementProtocolVersion,
983
+ );
955
984
  if (
956
985
  manifest.name !== currentIdentity.packageName
957
986
  || typeof manifest.version !== 'string'
958
987
  || !currentIdentity.versions.includes(manifest.version)
959
988
  || manifest.controllerUpgradeManagementVersion !== currentIdentity.managementVersion
989
+ || managementProtocolVersion === false
960
990
  || !declaresControllerCommands(bin, currentIdentity)
961
991
  ) throw new Error('The authoritative recovery package identity is incompatible.');
962
992
  if (!(await plugins.fs.promises.stat(cliPath)).isFile()) {
@@ -970,6 +1000,7 @@ const resolveAuthoritativeRecoveryInstallation = async (
970
1000
  packageDirectory,
971
1001
  cliPath,
972
1002
  packageVersion: manifest.version,
1003
+ ...(managementProtocolVersion === undefined ? {} : { managementProtocolVersion }),
973
1004
  };
974
1005
  };
975
1006
 
@@ -1956,6 +1987,10 @@ const grantControllerAction = async (
1956
1987
  },
1957
1988
  });
1958
1989
 
1990
+ /**
1991
+ * Reads the status of a controller the upgrade just started from `installationArg`, over the transport that
1992
+ * package's `package.json` declares.
1993
+ */
1959
1994
  const queryUpgradeManagedController = async (
1960
1995
  portArg: number,
1961
1996
  identityArg: {
@@ -1963,10 +1998,12 @@ const queryUpgradeManagedController = async (
1963
1998
  packageVersion: string;
1964
1999
  managementVersion: number;
1965
2000
  },
2001
+ installationArg: IPnpmGlobalInstallation,
1966
2002
  ): Promise<IControllerStatus> => queryControllerStatus(portArg, {
1967
2003
  packageName: identityArg.packageName,
1968
2004
  packageVersion: identityArg.packageVersion,
1969
2005
  upgradeManagementVersion: identityArg.managementVersion,
2006
+ managementProtocolVersion: installationArg.managementProtocolVersion,
1970
2007
  });
1971
2008
 
1972
2009
  const finalizeControllerUpgrade = async (
@@ -1980,7 +2017,7 @@ const finalizeControllerUpgrade = async (
1980
2017
  try {
1981
2018
  response = await requestControllerUpgradeFinalize(statusArg.controllerPid > 0
1982
2019
  ? (await coordinatorArg.readTransaction(tokenArg)).port
1983
- : 0, tokenArg, modeArg);
2020
+ : 0, statusArg, tokenArg, modeArg);
1984
2021
  } finally {
1985
2022
  await coordinatorArg.removeControllerActionGrant('finalize', tokenArg);
1986
2023
  }
@@ -2103,6 +2140,7 @@ export const prepareAndStopRunningController = async (
2103
2140
  try {
2104
2141
  await (dependenciesArg.requestPrepareBegin ?? requestControllerUpgradePrepareBegin)(
2105
2142
  optionsArg.port,
2143
+ optionsArg.runningStatus,
2106
2144
  optionsArg.token,
2107
2145
  optionsArg.targetVersion,
2108
2146
  optionsArg.gracePeriodMs,
@@ -2323,6 +2361,7 @@ const runPackageTransitionUpgradeWorker = async (optionsArg: {
2323
2361
  packageVersion: initialTransaction.sourceVersion,
2324
2362
  protocolVersion: controllerProtocolVersion,
2325
2363
  upgradeManagementVersion: initialTransaction.sourceManagementVersion,
2364
+ managementProtocolVersion: currentInstallation.managementProtocolVersion,
2326
2365
  });
2327
2366
  assertExpectedController(runningStatus, payload.expectedController);
2328
2367
  if (
@@ -2470,6 +2509,7 @@ const runPackageTransitionUpgradeWorker = async (optionsArg: {
2470
2509
  const restoredStatus = await queryUpgradeManagedController(
2471
2510
  payload.port,
2472
2511
  sourceControllerIdentity(initialTransaction),
2512
+ sourceTopology.source,
2473
2513
  );
2474
2514
  await finalizeControllerUpgrade(
2475
2515
  coordinator,
@@ -2520,6 +2560,7 @@ const runPackageTransitionUpgradeWorker = async (optionsArg: {
2520
2560
  const targetStatus = await queryUpgradeManagedController(
2521
2561
  payload.port,
2522
2562
  targetControllerIdentity(initialTransaction),
2563
+ targetInstallation,
2523
2564
  );
2524
2565
  await updateWorkerProgress(
2525
2566
  coordinator,
@@ -2728,6 +2769,7 @@ export async function runUpgradeWorker(
2728
2769
  packageVersion: currentInstallation.packageVersion,
2729
2770
  protocolVersion: controllerProtocolVersion,
2730
2771
  upgradeManagementVersion: controllerUpgradeManagementVersion,
2772
+ managementProtocolVersion: currentInstallation.managementProtocolVersion,
2731
2773
  });
2732
2774
  assertExpectedController(runningStatus, payloadArg.expectedController);
2733
2775
  if (
@@ -2812,6 +2854,7 @@ export async function runUpgradeWorker(
2812
2854
  packageVersion: restored.packageVersion,
2813
2855
  managementVersion: controllerUpgradeManagementVersion,
2814
2856
  },
2857
+ restored,
2815
2858
  );
2816
2859
  await finalizeControllerUpgrade(
2817
2860
  coordinator,
@@ -2851,7 +2894,7 @@ export async function runUpgradeWorker(
2851
2894
  packageName: controllerPackageName,
2852
2895
  packageVersion: latestVersion,
2853
2896
  managementVersion: controllerUpgradeManagementVersion,
2854
- });
2897
+ }, upgradedInstallation);
2855
2898
  await updateWorkerProgress(
2856
2899
  coordinator,
2857
2900
  payloadArg.token,
@@ -2997,6 +3040,7 @@ const recoverPackageTransition = async (optionsArg: {
2997
3040
  packageName: current.targetPackageName,
2998
3041
  packageVersion: upgradeTransactionTargetVersion(current)!,
2999
3042
  upgradeManagementVersion: current.targetManagementVersion,
3043
+ managementProtocolVersion: targetInstallation.managementProtocolVersion,
3000
3044
  }).catch(() => undefined);
3001
3045
  if (!status) {
3002
3046
  if (await retainedUpgradeTargetIsAuthoritative({
@@ -3019,6 +3063,7 @@ const recoverPackageTransition = async (optionsArg: {
3019
3063
  status = await queryUpgradeManagedController(
3020
3064
  current.port,
3021
3065
  targetControllerIdentity(current),
3066
+ targetInstallation,
3022
3067
  );
3023
3068
  }
3024
3069
  await finalizeControllerUpgrade(
@@ -3051,6 +3096,7 @@ const recoverPackageTransition = async (optionsArg: {
3051
3096
  packageName: transaction.sourcePackageName,
3052
3097
  packageVersion: transaction.sourceVersion,
3053
3098
  upgradeManagementVersion: transaction.sourceManagementVersion,
3099
+ managementProtocolVersion: sourceInstallation.managementProtocolVersion,
3054
3100
  }).catch(() => undefined);
3055
3101
  if (!status) {
3056
3102
  await startRestoredController(
@@ -3062,6 +3108,7 @@ const recoverPackageTransition = async (optionsArg: {
3062
3108
  status = await queryUpgradeManagedController(
3063
3109
  transaction.port,
3064
3110
  sourceControllerIdentity(transaction),
3111
+ sourceInstallation,
3065
3112
  );
3066
3113
  }
3067
3114
  await finalizeControllerUpgrade(
@@ -3130,6 +3177,7 @@ const recoverStalledUpgradeUnderLock = async (optionsArg: {
3130
3177
  packageName: controllerPackageName,
3131
3178
  packageVersion: targetInstallation.packageVersion,
3132
3179
  upgradeManagementVersion: controllerUpgradeManagementVersion,
3180
+ managementProtocolVersion: targetInstallation.managementProtocolVersion,
3133
3181
  }).catch(() => undefined);
3134
3182
  if (!status) {
3135
3183
  if (await retainedUpgradeTargetIsAuthoritative({
@@ -3156,6 +3204,7 @@ const recoverStalledUpgradeUnderLock = async (optionsArg: {
3156
3204
  packageVersion: targetInstallation.packageVersion,
3157
3205
  managementVersion: controllerUpgradeManagementVersion,
3158
3206
  },
3207
+ targetInstallation,
3159
3208
  );
3160
3209
  }
3161
3210
  await finalizeControllerUpgrade(
@@ -3187,6 +3236,7 @@ const recoverStalledUpgradeUnderLock = async (optionsArg: {
3187
3236
  packageName: controllerPackageName,
3188
3237
  packageVersion: sourceInstallation.packageVersion,
3189
3238
  upgradeManagementVersion: controllerUpgradeManagementVersion,
3239
+ managementProtocolVersion: sourceInstallation.managementProtocolVersion,
3190
3240
  }).catch(() => undefined);
3191
3241
  if (!status) {
3192
3242
  await startRestoredController(
@@ -3202,6 +3252,7 @@ const recoverStalledUpgradeUnderLock = async (optionsArg: {
3202
3252
  packageVersion: sourceInstallation.packageVersion,
3203
3253
  managementVersion: controllerUpgradeManagementVersion,
3204
3254
  },
3255
+ sourceInstallation,
3205
3256
  );
3206
3257
  }
3207
3258
  await finalizeControllerUpgrade(