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.
- package/changelog.md +24 -0
- package/dist_serve/bundle.js +1448 -1448
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +7 -3
- package/dist_ts/classes.authmodels.d.ts +15 -0
- package/dist_ts/classes.authmodels.js +78 -7
- package/dist_ts/classes.authstore.js +23 -6
- package/dist_ts/classes.cli.js +8 -3
- package/dist_ts/classes.controller.d.ts +13 -0
- package/dist_ts/classes.controller.js +51 -14
- package/dist_ts/classes.controllermanagement.d.ts +53 -6
- package/dist_ts/classes.controllermanagement.js +180 -72
- package/dist_ts/classes.inferencegateway.d.ts +6 -6
- package/dist_ts/classes.inferencegateway.js +28 -15
- package/dist_ts/classes.inferenceusage.d.ts +19 -7
- package/dist_ts/classes.inferenceusage.js +49 -13
- package/dist_ts/classes.inferenceusagetap.d.ts +15 -3
- package/dist_ts/classes.inferenceusagetap.js +16 -9
- package/dist_ts/classes.processinspection.d.ts +3 -1
- package/dist_ts/classes.processinspection.js +4 -2
- package/dist_ts/classes.proxycontrol.d.ts +22 -7
- package/dist_ts/classes.proxycontrol.js +5 -3
- package/dist_ts/classes.proxycontrolclient.d.ts +4 -3
- package/dist_ts/classes.proxycontrolclient.js +7 -5
- package/dist_ts/classes.proxydaemon.d.ts +6 -4
- package/dist_ts/classes.proxydaemon.js +42 -17
- package/dist_ts/classes.proxyleaseholder.d.ts +11 -2
- package/dist_ts/classes.proxyleaseholder.js +25 -2
- package/dist_ts/classes.proxystore.d.ts +3 -2
- package/dist_ts/classes.proxystore.js +5 -2
- package/dist_ts/classes.upgradecoordinator.d.ts +2 -1
- package/dist_ts/classes.upgradecoordinator.js +13 -10
- package/dist_ts/classes.upgradetransaction.d.ts +6 -0
- package/dist_ts/classes.upgradetransaction.js +38 -13
- package/dist_ts/classes.usagefeed.js +3 -2
- package/dist_ts/functions.controllermanagementsurface.d.ts +37 -0
- package/dist_ts/functions.controllermanagementsurface.js +110 -0
- package/dist_ts/functions.inferencecli.d.ts +6 -3
- package/dist_ts/functions.inferencecli.js +9 -5
- package/dist_ts/functions.inferencecliarguments.d.ts +1 -1
- package/dist_ts/functions.inferencecliarguments.js +19 -7
- package/dist_ts/functions.inferencelaunch.d.ts +30 -9
- package/dist_ts/functions.inferencelaunch.js +49 -23
- package/dist_ts/interfaces.auth.d.ts +14 -3
- package/dist_ts/interfaces.auth.js +4 -3
- package/dist_ts_interfaces/index.d.ts +1 -0
- package/dist_ts_interfaces/index.js +2 -1
- package/dist_ts_interfaces/interfaces.d.ts +6 -1
- package/dist_ts_interfaces/interfaces.js +2 -2
- package/dist_ts_interfaces/management.d.ts +33 -0
- package/dist_ts_interfaces/management.js +35 -0
- package/dist_ts_interfaces/metrics.d.ts +29 -7
- package/dist_ts_interfaces/metrics.js +14 -1
- package/package.json +6 -5
- package/readme.md +123 -28
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +6 -2
- package/ts/classes.authmodels.ts +74 -5
- package/ts/classes.authstore.ts +24 -4
- package/ts/classes.cli.ts +9 -2
- package/ts/classes.controller.ts +98 -59
- package/ts/classes.controllermanagement.ts +284 -107
- package/ts/classes.inferencegateway.ts +31 -19
- package/ts/classes.inferenceusage.ts +58 -19
- package/ts/classes.inferenceusagetap.ts +27 -8
- package/ts/classes.processinspection.ts +2 -1
- package/ts/classes.proxycontrol.ts +25 -9
- package/ts/classes.proxycontrolclient.ts +7 -4
- package/ts/classes.proxydaemon.ts +43 -17
- package/ts/classes.proxyleaseholder.ts +36 -7
- package/ts/classes.proxystore.ts +8 -3
- package/ts/classes.upgradecoordinator.ts +20 -11
- package/ts/classes.upgradetransaction.ts +53 -2
- package/ts/classes.usagefeed.ts +2 -1
- package/ts/functions.controllermanagementsurface.ts +149 -0
- package/ts/functions.inferencecli.ts +15 -7
- package/ts/functions.inferencecliarguments.ts +16 -6
- package/ts/functions.inferencelaunch.ts +57 -23
- package/ts/interfaces.auth.ts +14 -3
- package/ts_interfaces/index.ts +1 -0
- package/ts_interfaces/interfaces.ts +6 -1
- package/ts_interfaces/management.ts +52 -0
- package/ts_interfaces/metrics.ts +34 -7
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.controllersocketclient.ts +5 -2
- 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
|
-
|
|
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
|
-
||
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
&& value.
|
|
511
|
-
|
|
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:
|
|
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(
|
package/ts/classes.usagefeed.ts
CHANGED
|
@@ -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,
|
|
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'
|
|
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
|
-
|
|
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
|
|
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}
|
|
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
|
|
44
|
-
three
|
|
45
|
-
|
|
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'
|
|
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;
|