agl 41.0.1 → 41.2.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 (87) hide show
  1. package/changelog.md +24 -0
  2. package/dist_serve/bundle.js +1448 -1448
  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 +7 -3
  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 +51 -14
  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 +28 -15
  16. package/dist_ts/classes.inferenceusage.d.ts +19 -7
  17. package/dist_ts/classes.inferenceusage.js +49 -13
  18. package/dist_ts/classes.inferenceusagetap.d.ts +15 -3
  19. package/dist_ts/classes.inferenceusagetap.js +16 -9
  20. package/dist_ts/classes.processinspection.d.ts +3 -1
  21. package/dist_ts/classes.processinspection.js +4 -2
  22. package/dist_ts/classes.proxycontrol.d.ts +22 -7
  23. package/dist_ts/classes.proxycontrol.js +5 -3
  24. package/dist_ts/classes.proxycontrolclient.d.ts +4 -3
  25. package/dist_ts/classes.proxycontrolclient.js +7 -5
  26. package/dist_ts/classes.proxydaemon.d.ts +6 -4
  27. package/dist_ts/classes.proxydaemon.js +42 -17
  28. package/dist_ts/classes.proxyleaseholder.d.ts +11 -2
  29. package/dist_ts/classes.proxyleaseholder.js +25 -2
  30. package/dist_ts/classes.proxystore.d.ts +3 -2
  31. package/dist_ts/classes.proxystore.js +5 -2
  32. package/dist_ts/classes.upgradecoordinator.d.ts +2 -1
  33. package/dist_ts/classes.upgradecoordinator.js +13 -10
  34. package/dist_ts/classes.upgradetransaction.d.ts +6 -0
  35. package/dist_ts/classes.upgradetransaction.js +38 -13
  36. package/dist_ts/classes.usagefeed.js +3 -2
  37. package/dist_ts/functions.controllermanagementsurface.d.ts +37 -0
  38. package/dist_ts/functions.controllermanagementsurface.js +110 -0
  39. package/dist_ts/functions.inferencecli.d.ts +6 -3
  40. package/dist_ts/functions.inferencecli.js +9 -5
  41. package/dist_ts/functions.inferencecliarguments.d.ts +1 -1
  42. package/dist_ts/functions.inferencecliarguments.js +19 -7
  43. package/dist_ts/functions.inferencelaunch.d.ts +30 -9
  44. package/dist_ts/functions.inferencelaunch.js +49 -23
  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 +6 -1
  50. package/dist_ts_interfaces/interfaces.js +2 -2
  51. package/dist_ts_interfaces/management.d.ts +33 -0
  52. package/dist_ts_interfaces/management.js +35 -0
  53. package/dist_ts_interfaces/metrics.d.ts +29 -7
  54. package/dist_ts_interfaces/metrics.js +14 -1
  55. package/package.json +6 -5
  56. package/readme.md +123 -28
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/aglapi/aglapisurface.document.ts +6 -2
  59. package/ts/classes.authmodels.ts +74 -5
  60. package/ts/classes.authstore.ts +24 -4
  61. package/ts/classes.cli.ts +9 -2
  62. package/ts/classes.controller.ts +98 -59
  63. package/ts/classes.controllermanagement.ts +284 -107
  64. package/ts/classes.inferencegateway.ts +31 -19
  65. package/ts/classes.inferenceusage.ts +58 -19
  66. package/ts/classes.inferenceusagetap.ts +27 -8
  67. package/ts/classes.processinspection.ts +2 -1
  68. package/ts/classes.proxycontrol.ts +25 -9
  69. package/ts/classes.proxycontrolclient.ts +7 -4
  70. package/ts/classes.proxydaemon.ts +43 -17
  71. package/ts/classes.proxyleaseholder.ts +36 -7
  72. package/ts/classes.proxystore.ts +8 -3
  73. package/ts/classes.upgradecoordinator.ts +20 -11
  74. package/ts/classes.upgradetransaction.ts +53 -2
  75. package/ts/classes.usagefeed.ts +2 -1
  76. package/ts/functions.controllermanagementsurface.ts +149 -0
  77. package/ts/functions.inferencecli.ts +15 -7
  78. package/ts/functions.inferencecliarguments.ts +16 -6
  79. package/ts/functions.inferencelaunch.ts +57 -23
  80. package/ts/interfaces.auth.ts +14 -3
  81. package/ts_interfaces/index.ts +1 -0
  82. package/ts_interfaces/interfaces.ts +6 -1
  83. package/ts_interfaces/management.ts +52 -0
  84. package/ts_interfaces/metrics.ts +34 -7
  85. package/ts_web/00_commitinfo_data.ts +1 -1
  86. package/ts_web/classes.controllersocketclient.ts +5 -2
  87. package/ts_web/elements.metricsboard.ts +68 -18
@@ -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(
@@ -68,7 +68,8 @@ const bucketOrder = (leftArg: IControllerUsageBucket, rightArg: IControllerUsage
68
68
 
69
69
  const sameCounts = (leftArg: IControllerUsageBucket, rightArg: IControllerUsageBucket): boolean => leftArg.calls === rightArg.calls
70
70
  && leftArg.inputTokens === rightArg.inputTokens && leftArg.outputTokens === rightArg.outputTokens
71
- && leftArg.cacheReadTokens === rightArg.cacheReadTokens && leftArg.cacheWriteTokens === rightArg.cacheWriteTokens;
71
+ && leftArg.cacheReadTokens === rightArg.cacheReadTokens && leftArg.cacheWriteTokens === rightArg.cacheWriteTokens
72
+ && leftArg.cacheReadUnknownCalls === rightArg.cacheReadUnknownCalls && leftArg.cacheWriteUnknownCalls === rightArg.cacheWriteUnknownCalls;
72
73
 
73
74
  /**
74
75
  * The metrics board's usage feed in the controller. The proxy keeps the token usage and has no push, so the feed reads
@@ -0,0 +1,149 @@
1
+ import * as plugins from './plugins.js';
2
+ import {
3
+ controllerManagementMethods,
4
+ controllerManagementRequestMaximumBodyBytes,
5
+ controllerManagementTypedRequestPath,
6
+ } from '../ts_interfaces/index.js';
7
+
8
+ /** The direct peer addresses the management endpoint answers: this host's loopback, as Node reports it. */
9
+ const loopbackPeerAddresses = new Set(['127.0.0.1', '::ffff:127.0.0.1', '::1']);
10
+
11
+ /**
12
+ * Headers a forwarding proxy adds, and the transport secret a trusted TLS proxy injects. A request that carries one
13
+ * came through a proxy, even one on this host whose connection is loopback, so it is not a local caller's.
14
+ */
15
+ const forwardingHeaders = [
16
+ 'forwarded',
17
+ 'x-forwarded-for',
18
+ 'x-forwarded-host',
19
+ 'x-forwarded-proto',
20
+ 'x-real-ip',
21
+ 'x-harness-controller-proxy-secret',
22
+ ] as const;
23
+
24
+ const envelopeKeys = new Set(['method', 'request', 'response', 'correlation', 'requestInstanceId']);
25
+
26
+ /**
27
+ * The connection a management request arrived on, granted to its handler as `localData.peer`: one per request, so
28
+ * the handlers' per-peer operation and preflight budgets count it as a connection of its own.
29
+ */
30
+ export interface IControllerManagementPeer {
31
+ id: string;
32
+ context: {
33
+ headers: Headers;
34
+ state: Record<string, unknown>;
35
+ };
36
+ }
37
+
38
+ export const isLoopbackPeerAddress = (addressArg: string | undefined): boolean => (
39
+ addressArg !== undefined && loopbackPeerAddresses.has(addressArg)
40
+ );
41
+
42
+ /**
43
+ * Admits a request to the management endpoint before its body is read: from a loopback peer, a POST of
44
+ * `application/json` to the exact path, addressed to `127.0.0.1:<port>`, with no `Origin` (a browser always sends
45
+ * one with a POST) and no forwarding header. Anything else is refused; nothing about the request is logged.
46
+ */
47
+ export const admitControllerManagementRequest = (
48
+ contextArg: plugins.smartserve.IRequestContext,
49
+ portArg: number,
50
+ ): true | Response => {
51
+ if (!isLoopbackPeerAddress(contextArg.connectionInfo?.remoteAddr)) {
52
+ return new Response('Forbidden', { status: 403 });
53
+ }
54
+ const url = contextArg.url;
55
+ if (
56
+ url.pathname !== controllerManagementTypedRequestPath
57
+ || url.search !== ''
58
+ || url.hash !== ''
59
+ || url.username !== ''
60
+ || url.password !== ''
61
+ || url.hostname !== '127.0.0.1'
62
+ || url.port !== String(portArg)
63
+ || contextArg.headers.get('host') !== `127.0.0.1:${portArg}`
64
+ ) {
65
+ return new Response('Not Found', { status: 404 });
66
+ }
67
+ if (
68
+ contextArg.headers.has('origin')
69
+ || forwardingHeaders.some((headerArg) => contextArg.headers.has(headerArg))
70
+ ) {
71
+ return new Response('Forbidden', { status: 403 });
72
+ }
73
+ if (contextArg.method !== 'POST') {
74
+ return new Response('Method Not Allowed', { status: 405, headers: { Allow: 'POST' } });
75
+ }
76
+ if (contextArg.headers.get('content-type') !== 'application/json') {
77
+ return new Response('Unsupported Media Type', { status: 415 });
78
+ }
79
+ return true;
80
+ };
81
+
82
+ /**
83
+ * Admits one envelope: exactly the keys a TypedRequest client sends, a management method, a request object and a
84
+ * request-phase correlation. It grants the request its own peer and nothing else: every upgrade method still
85
+ * authorises itself against the upgrade token it is handed, as it does over the controller socket.
86
+ */
87
+ export const admitControllerManagementTypedRequest = (
88
+ requestArg: plugins.typedrequestInterfaces.ITypedRequest,
89
+ contextArg: plugins.smartserve.IRequestContext,
90
+ ): { trustedLocalData: { peer: IControllerManagementPeer } } | Response => {
91
+ const envelope: unknown = requestArg;
92
+ if (
93
+ typeof envelope !== 'object'
94
+ || envelope === null
95
+ || Array.isArray(envelope)
96
+ || !Object.keys(envelope).every((keyArg) => envelopeKeys.has(keyArg))
97
+ ) return new Response('Invalid request', { status: 400 });
98
+ const { method, request, correlation, requestInstanceId } = envelope as Record<string, unknown>;
99
+ if (
100
+ !controllerManagementMethods.some((methodArg) => methodArg === method)
101
+ || typeof request !== 'object'
102
+ || request === null
103
+ || Array.isArray(request)
104
+ || typeof correlation !== 'object'
105
+ || correlation === null
106
+ || (correlation as Record<string, unknown>).phase !== 'request'
107
+ || typeof (correlation as Record<string, unknown>).id !== 'string'
108
+ || typeof requestInstanceId !== 'string'
109
+ || requestInstanceId.length === 0
110
+ ) return new Response('Invalid request', { status: 400 });
111
+ return {
112
+ trustedLocalData: {
113
+ peer: {
114
+ id: `management:${plugins.crypto.randomUUID()}`,
115
+ context: { headers: contextArg.headers, state: {} },
116
+ },
117
+ },
118
+ };
119
+ };
120
+
121
+ /**
122
+ * The management endpoint as a surface of the controller's listener: it matches only the exact host and path, so
123
+ * every other request on the port keeps reaching the browser surface.
124
+ */
125
+ export const createControllerManagementSurface = (optionsArg: {
126
+ port: number;
127
+ router: plugins.typedrequest.TypedRouter;
128
+ }): plugins.typedserver.ITypedServerSurface => ({
129
+ name: 'controller-management',
130
+ match: { hostnames: ['127.0.0.1'], pathPrefixes: [controllerManagementTypedRequestPath] },
131
+ httpTypedRouter: optionsArg.router,
132
+ typedRequestPath: controllerManagementTypedRequestPath,
133
+ typedRequestMaxBodyBytes: controllerManagementRequestMaximumBodyBytes,
134
+ includeBuiltinTypedHandlers: false,
135
+ serveDir: null,
136
+ spaFallback: false,
137
+ healthzEndpoint: false,
138
+ cors: false,
139
+ noCache: true,
140
+ requestAdmission: (contextArg) => admitControllerManagementRequest(contextArg, optionsArg.port),
141
+ typedRequestAdmission: admitControllerManagementTypedRequest,
142
+ securityHeaders: {
143
+ xFrameOptions: 'DENY',
144
+ xContentTypeOptions: true,
145
+ referrerPolicy: 'no-referrer',
146
+ crossOriginOpenerPolicy: 'same-origin',
147
+ crossOriginResourcePolicy: 'same-origin',
148
+ },
149
+ });
@@ -19,12 +19,15 @@ type TAccountPlacementKey = 'lifecycle' | 'lapsesAt' | 'placementScore' | 'place
19
19
 
20
20
  /**
21
21
  * The status a running proxy answers with: one started from an AGL before 38.3.0 reports no accounts and no usage
22
- * observations, one before 38.4.0 no lifecycle, placement or placement journal, and one before confined runs neither
23
- * its confined listener nor its runs.
22
+ * observations, one before 38.4.0 no lifecycle, placement or placement journal, one before confined runs neither
23
+ * its confined listener nor its runs, and one before ChatGPT-pool confined runs no run's pool: its runs are all of the
24
+ * Claude pool.
24
25
  */
25
26
  export type TReportedProxyStatus = Omit<IAglProxyStatus, 'accounts' | 'usageObservations' | 'nextPlacement'
26
27
  | 'recentPlacements' | 'confinedSocketPath' | 'confinedRuns'>
27
- & Partial<Pick<IAglProxyStatus, 'usageObservations' | 'nextPlacement' | 'recentPlacements' | 'confinedSocketPath' | 'confinedRuns'>>
28
+ & Partial<Pick<IAglProxyStatus, 'usageObservations' | 'nextPlacement' | 'recentPlacements' | 'confinedSocketPath'>>
29
+ & { confinedRuns?: Array<Omit<IAglProxyStatus['confinedRuns'][number], 'provider'>
30
+ & Partial<Pick<IAglProxyStatus['confinedRuns'][number], 'provider'>>> }
28
31
  & { accounts?: Array<Omit<IAglProxyStatus['accounts'][number], TAccountPlacementKey>
29
32
  & Partial<Pick<IAglProxyStatus['accounts'][number], TAccountPlacementKey>>> };
30
33
 
@@ -73,7 +76,8 @@ export const formatProxyStatus = (unitArg: string, statusArg: TReportedProxyStat
73
76
  if (statusArg.confinedSocketPath !== undefined) {
74
77
  lines.push(`Confined listener: ${statusArg.confinedSocketPath ?? 'none'}`);
75
78
  for (const run of statusArg.confinedRuns ?? []) {
76
- lines.push(`Confined run ${run.id}: issued ${run.createdAt}`
79
+ // An older proxy names no run's pool: it issues Claude-pool runs alone.
80
+ lines.push(`Confined run ${run.id}: ${run.provider === 'openai' ? 'ChatGPT pool' : 'Claude pool'}, issued ${run.createdAt}`
77
81
  + `${run.held ? '' : ', waiting for its launcher to take it back after a proxy restart'}`
78
82
  + ` (end it with \`agl proxy confined end ${run.id}\`)`);
79
83
  }
@@ -213,13 +217,17 @@ const run = (optionsArg: IInferenceCliArguments): Promise<void> => withSignals(a
213
217
  const confinedRun = (optionsArg: IInferenceCliArguments): Promise<void> => withSignals(async (signalArg) => {
214
218
  await preflightAGLHomeMigration();
215
219
  const paths = resolveAGLHomePaths();
216
- const run = await AglProxyConfinedHolder.take({ socketPath: paths.proxySocket }, AbortSignal.any([signalArg, AbortSignal.timeout(90_000)]));
220
+ const run = await AglProxyConfinedHolder.take({ socketPath: paths.proxySocket, provider: optionsArg.provider ?? 'anthropic' },
221
+ AbortSignal.any([signalArg, AbortSignal.timeout(90_000)]));
217
222
  let lost: AglProxyLeaseLostError | undefined;
218
223
  try {
219
224
  const launch = confinedDockerLaunch({ args: optionsArg.args, socketPath: run.confinedSocketPath, baseUrl: run.baseUrl,
220
- token: run.token, environment: process.env });
225
+ token: run.token, environment: process.env,
226
+ pool: run.provider === 'anthropic' ? { provider: 'anthropic' } : { provider: 'openai', models: run.models } });
227
+ // A Claude-pool run says what it said before ChatGPT-pool runs; a ChatGPT-pool run says so.
221
228
  process.stderr.write(`AGL proxy confined run ${run.id}: ${plugins.path.dirname(run.confinedSocketPath)} is mounted read-only `
222
- + `at ${aglProxyConfinedMountPoint}; end it at once with \`agl proxy confined end ${run.id}\`.\n`);
229
+ + `at ${aglProxyConfinedMountPoint}${run.provider === 'openai' ? ', served from the ChatGPT pool' : ''}; `
230
+ + `end it at once with \`agl proxy confined end ${run.id}\`.\n`);
223
231
  signalArg.throwIfAborted();
224
232
  const child = plugins.childProcess.spawn(launch.command, launch.args, { env: launch.environment, stdio: 'inherit', shell: false });
225
233
  let killTimer: ReturnType<typeof setTimeout> | undefined;
@@ -26,7 +26,7 @@ export const inferenceCliHelp = `Usage:
26
26
  agl proxy status [--json]
27
27
  agl proxy credential <client> [--provider openai|anthropic] [--delivery file|none] [--write <path>] [--rotate] | --version
28
28
  agl proxy run codex|claude|opencode [--provider openai|anthropic] [--account <id>] [--plan <name>] [--model <id>] -- [harness arguments]
29
- agl proxy confined run -- docker run [docker options] <image> [command]
29
+ agl proxy confined run [--provider anthropic|openai] -- docker run [docker options] <image> [command]
30
30
  agl proxy confined end <run id>
31
31
 
32
32
  The proxy runs as the ${aglProxyUnitName} user unit (serve runs it in a terminal instead).
@@ -40,12 +40,15 @@ client starts on Claude accounts, every other client on ChatGPT accounts; --prov
40
40
  Run gives one child a private credential that ends when the child exits; run claude uses Claude accounts
41
41
  unless --provider openai asks for ChatGPT models behind Claude Code.
42
42
  confined run starts Claude Code's container on a credential of its own, accepted only on the proxy's confined
43
- listener: a Unix socket whose directory the run mounts read-only at ${aglProxyConfinedMountPoint}, served the Claude pool's
44
- three Claude Code endpoints alone. It adds the mount and Claude Code's settings (ANTHROPIC_UNIX_SOCKET, ANTHROPIC_BASE_URL,
45
- ANTHROPIC_AUTH_TOKEN) right after docker run, prints the run's id on standard error, and ends the credential when the
43
+ listener: a Unix socket whose directory the run mounts read-only at ${aglProxyConfinedMountPoint}, served Claude Code's
44
+ three endpoints of the run's pool alone: Claude accounts unless --provider openai asks for the ChatGPT pool, whose
45
+ ChatGPT accounts answer it translated, as for run claude --provider openai. It adds the mount and Claude Code's settings
46
+ (ANTHROPIC_UNIX_SOCKET, ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, and on the ChatGPT pool the model variables run claude
47
+ --provider openai sets) right after docker run, prints the run's id on standard error, and ends the credential when the
46
48
  container exits. The socket is the proxy user's (0600): run the container as that uid (--user), or as root under
47
49
  rootless Docker. Root of a rootful daemon connects as well while it keeps CAP_DAC_OVERRIDE (--cap-drop ALL takes it).
48
- confined end ends a run's credential at once, and its launcher stops the container; agl proxy status lists the runs.
50
+ confined end ends a run's credential at once, and its launcher stops the container; agl proxy status lists the runs
51
+ with their pools.
49
52
  Repeat --account or --plan to restrict automatic routing. Only accounts the account service holds are eligible.
50
53
  ChatGPT pools: /openai/v1/responses, /anthropic/v1/messages (translated), and each prefix's /models.
51
54
  Claude pools: /anthropic/v1/messages, /anthropic/v1/messages/count_tokens and /anthropic/v1/models, sent on as Claude Code sent them.
@@ -124,7 +127,14 @@ export const parseInferenceCliArguments = (argvArg: readonly string[]): IInferen
124
127
  result.confinedId = id;
125
128
  return result;
126
129
  }
127
- if (action !== 'run' || argv.shift() !== '--' || !argv.length) throw new Error(inferenceCliHelp);
130
+ if (action !== 'run') throw new Error(inferenceCliHelp);
131
+ if (argv[0] === '--provider') {
132
+ argv.shift();
133
+ const value = argv.shift();
134
+ if (!inferenceProviders.includes(value as TInferenceProvider)) throw new Error('--provider is openai or anthropic.');
135
+ result.provider = value as TInferenceProvider;
136
+ }
137
+ if (argv.shift() !== '--' || !argv.length) throw new Error(inferenceCliHelp);
128
138
  result.confinedAction = 'run';
129
139
  result.args = argv;
130
140
  return result;