agl 40.5.1 → 41.0.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 +43 -0
- package/dist_serve/bundle.js +3514 -3357
- package/dist_serve/bundle.js.map +2 -2
- package/dist_serve/bundle.js.third-party-notices.json +58 -32
- package/dist_serve/bundle.js.third-party-notices.txt +108 -33
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +64 -37
- package/dist_ts/classes.aglhome.d.ts +5 -0
- package/dist_ts/classes.aglhome.js +2 -1
- package/dist_ts/classes.authstore.js +5 -1
- package/dist_ts/classes.controller.d.ts +8 -0
- package/dist_ts/classes.controller.js +119 -22
- package/dist_ts/classes.controllereventdelivery.d.ts +72 -0
- package/dist_ts/classes.controllereventdelivery.js +233 -0
- package/dist_ts/classes.flexmodels.public.js +4 -1
- package/dist_ts/classes.flexprojectionreader.d.ts +6 -0
- package/dist_ts/classes.flexprojectionreader.js +29 -5
- package/dist_ts/classes.flexservice.d.ts +2 -1
- package/dist_ts/classes.flexservice.js +34 -6
- package/dist_ts/classes.flexstore.d.ts +41 -4
- package/dist_ts/classes.flexstore.js +266 -203
- package/dist_ts/classes.inferencegateway.d.ts +35 -1
- package/dist_ts/classes.inferencegateway.js +86 -26
- package/dist_ts/classes.inferenceservice.d.ts +4 -2
- package/dist_ts/classes.inferenceservice.js +5 -4
- package/dist_ts/classes.proxycontrol.d.ts +58 -4
- package/dist_ts/classes.proxycontrol.js +23 -44
- package/dist_ts/classes.proxycontrolclient.d.ts +15 -5
- package/dist_ts/classes.proxycontrolclient.js +67 -17
- package/dist_ts/classes.proxydaemon.d.ts +22 -3
- package/dist_ts/classes.proxydaemon.js +73 -12
- package/dist_ts/classes.proxyleaseholder.d.ts +43 -14
- package/dist_ts/classes.proxyleaseholder.js +66 -22
- package/dist_ts/classes.proxystore.d.ts +9 -0
- package/dist_ts/classes.proxystore.js +15 -7
- package/dist_ts/classes.ticketnotifier.js +4 -2
- package/dist_ts/classes.ticketstore.d.ts +50 -2
- package/dist_ts/classes.ticketstore.js +195 -78
- package/dist_ts/classes.workercallerbindings.d.ts +66 -0
- package/dist_ts/classes.workercallerbindings.js +211 -0
- package/dist_ts/classes.workercallers.d.ts +63 -2
- package/dist_ts/classes.workercallers.js +430 -65
- package/dist_ts/driver/classes.drivereventrelay.d.ts +13 -1
- package/dist_ts/driver/classes.drivereventrelay.js +99 -6
- package/dist_ts/driver/functions.driverinstructions.d.ts +1 -1
- package/dist_ts/driver/functions.driverinstructions.js +1 -1
- package/dist_ts/driver/functions.drivertools.d.ts +11 -2
- package/dist_ts/driver/functions.drivertools.js +19 -9
- package/dist_ts/fleet/classes.boardpump.d.ts +8 -2
- package/dist_ts/fleet/classes.boardpump.js +38 -20
- package/dist_ts/fleet/classes.ticketeffects.d.ts +17 -2
- package/dist_ts/fleet/classes.ticketeffects.js +16 -10
- package/dist_ts/fleet/classes.workerlaunches.d.ts +12 -5
- package/dist_ts/fleet/classes.workerlaunches.js +25 -19
- package/dist_ts/fleet/classes.workers.d.ts +21 -0
- package/dist_ts/fleet/classes.workers.js +110 -17
- package/dist_ts/fleet/functions.workervalidation.js +2 -2
- package/dist_ts/functions.flexprivatesnapshot.d.ts +102 -0
- package/dist_ts/functions.flexprivatesnapshot.js +461 -0
- package/dist_ts/functions.inferencecli.d.ts +3 -2
- package/dist_ts/functions.inferencecli.js +67 -5
- package/dist_ts/functions.inferencecliarguments.d.ts +6 -2
- package/dist_ts/functions.inferencecliarguments.js +29 -2
- package/dist_ts/functions.inferencelaunch.d.ts +24 -0
- package/dist_ts/functions.inferencelaunch.js +42 -2
- package/dist_ts/functions.mcptickettools.d.ts +3 -0
- package/dist_ts/functions.mcptickettools.js +2 -2
- package/dist_ts/functions.proxycontract.d.ts +11 -0
- package/dist_ts/functions.proxycontract.js +12 -1
- package/dist_ts/functions.proxysocket.d.ts +6 -0
- package/dist_ts/functions.proxysocket.js +46 -0
- package/dist_ts/functions.ticketcli.d.ts +1 -1
- package/dist_ts/functions.ticketcli.js +8 -4
- package/dist_ts/functions.ticketvalidation.d.ts +3 -0
- package/dist_ts/functions.ticketvalidation.js +7 -4
- package/dist_ts/functions.workerprocessproof.d.ts +6 -0
- package/dist_ts/functions.workerprocessproof.js +28 -4
- package/dist_ts/interfaces.flexipc.d.ts +21 -2
- package/dist_ts/interfaces.flexipc.js +33 -16
- package/dist_ts_interfaces/fleetworkers.d.ts +6 -0
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/requests.tickets.d.ts +8 -12
- package/dist_ts_interfaces/tickets.d.ts +37 -9
- package/dist_ts_interfaces/tickets.js +7 -1
- package/dist_ts_interfaces/workercallers.d.ts +43 -4
- package/package.json +7 -7
- package/readme.md +156 -23
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +63 -36
- package/ts/classes.aglhome.ts +6 -0
- package/ts/classes.authstore.ts +4 -0
- package/ts/classes.controller.ts +141 -32
- package/ts/classes.controllereventdelivery.ts +289 -0
- package/ts/classes.flexmodels.public.ts +10 -0
- package/ts/classes.flexprojectionreader.ts +30 -3
- package/ts/classes.flexservice.ts +33 -5
- package/ts/classes.flexstore.ts +381 -246
- package/ts/classes.inferencegateway.ts +104 -23
- package/ts/classes.inferenceservice.ts +6 -3
- package/ts/classes.proxycontrol.ts +76 -39
- package/ts/classes.proxycontrolclient.ts +72 -20
- package/ts/classes.proxydaemon.ts +85 -15
- package/ts/classes.proxyleaseholder.ts +84 -28
- package/ts/classes.proxystore.ts +16 -5
- package/ts/classes.ticketnotifier.ts +3 -1
- package/ts/classes.ticketstore.ts +201 -75
- package/ts/classes.workercallerbindings.ts +147 -0
- package/ts/classes.workercallers.ts +410 -59
- package/ts/driver/classes.drivereventrelay.ts +89 -4
- package/ts/driver/functions.driverinstructions.ts +1 -1
- package/ts/driver/functions.drivertools.ts +22 -8
- package/ts/fleet/classes.boardpump.ts +51 -19
- package/ts/fleet/classes.ticketeffects.ts +25 -11
- package/ts/fleet/classes.workerlaunches.ts +23 -15
- package/ts/fleet/classes.workers.ts +79 -17
- package/ts/fleet/functions.workervalidation.ts +1 -1
- package/ts/functions.flexprivatesnapshot.ts +601 -0
- package/ts/functions.inferencecli.ts +61 -7
- package/ts/functions.inferencecliarguments.ts +32 -1
- package/ts/functions.inferencelaunch.ts +49 -2
- package/ts/functions.mcptickettools.ts +1 -1
- package/ts/functions.proxycontract.ts +12 -0
- package/ts/functions.proxysocket.ts +39 -0
- package/ts/functions.ticketcli.ts +7 -3
- package/ts/functions.ticketvalidation.ts +7 -3
- package/ts/functions.workerprocessproof.ts +27 -4
- package/ts/interfaces.flexipc.ts +50 -21
- package/ts_interfaces/fleetworkers.ts +6 -0
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/requests.tickets.ts +9 -6
- package/ts_interfaces/tickets.ts +32 -1
- package/ts_interfaces/workercallers.ts +32 -4
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements.harnesscontrollerapp.ts +19 -8
- package/ts_web/elements.ticketboard.ts +16 -4
- package/ts_web/settings.general.ts +10 -4
|
@@ -3,13 +3,13 @@ import { commitinfo } from './00_commitinfo_data.js';
|
|
|
3
3
|
import { ensureControllerDataRoot, preflightAGLHomeMigration } from './functions.controllerdataroot.js';
|
|
4
4
|
import { resolveAGLHomePaths } from './classes.aglhome.js';
|
|
5
5
|
import { resolveProductionAuthoritySocketPaths } from './classes.authorityclient.js';
|
|
6
|
-
import { inferenceHarnessLaunch } from './functions.inferencelaunch.js';
|
|
6
|
+
import { confinedDockerLaunch, inferenceHarnessLaunch } from './functions.inferencelaunch.js';
|
|
7
7
|
import type { IAglProxyStatus } from './classes.proxycontrol.js';
|
|
8
8
|
import { AglProxyControlClient, AglProxyUnavailableError } from './classes.proxycontrolclient.js';
|
|
9
|
-
import { AglProxyLeaseHolder } from './classes.proxyleaseholder.js';
|
|
9
|
+
import { AglProxyConfinedHolder, AglProxyLeaseHolder, type AglProxyLeaseLostError } from './classes.proxyleaseholder.js';
|
|
10
10
|
import { AglProxyDaemon } from './classes.proxydaemon.js';
|
|
11
11
|
import { AglProxyService, resolveAglExecutable } from './classes.proxyservice.js';
|
|
12
|
-
import { aglProxyUnitName } from './functions.proxycontract.js';
|
|
12
|
+
import { aglProxyConfinedMountPoint, aglProxyUnitName } from './functions.proxycontract.js';
|
|
13
13
|
import { inferenceCliHelp, parseInferenceCliArguments, type IInferenceCliArguments } from './functions.inferencecliarguments.js';
|
|
14
14
|
import { runProxyCredentialCommand } from './functions.proxycredentialcli.js';
|
|
15
15
|
import { currentProxyCredentialHelper, writeProxyCredentialHelper } from './functions.proxycredentialhelper.js';
|
|
@@ -19,11 +19,12 @@ 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,
|
|
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.
|
|
23
24
|
*/
|
|
24
25
|
export type TReportedProxyStatus = Omit<IAglProxyStatus, 'accounts' | 'usageObservations' | 'nextPlacement'
|
|
25
|
-
| 'recentPlacements'>
|
|
26
|
-
& Partial<Pick<IAglProxyStatus, 'usageObservations' | 'nextPlacement' | 'recentPlacements'>>
|
|
26
|
+
| 'recentPlacements' | 'confinedSocketPath' | 'confinedRuns'>
|
|
27
|
+
& Partial<Pick<IAglProxyStatus, 'usageObservations' | 'nextPlacement' | 'recentPlacements' | 'confinedSocketPath' | 'confinedRuns'>>
|
|
27
28
|
& { accounts?: Array<Omit<IAglProxyStatus['accounts'][number], TAccountPlacementKey>
|
|
28
29
|
& Partial<Pick<IAglProxyStatus['accounts'][number], TAccountPlacementKey>>> };
|
|
29
30
|
|
|
@@ -69,6 +70,14 @@ export const formatProxyStatus = (unitArg: string, statusArg: TReportedProxyStat
|
|
|
69
70
|
+ `${client.generation}${client.delivery === 'file' ? ' (kept in a file)' : ''}, ${client.pending} in flight, `
|
|
70
71
|
+ `last used ${client.lastUsedAt ?? 'never'}`);
|
|
71
72
|
}
|
|
73
|
+
if (statusArg.confinedSocketPath !== undefined) {
|
|
74
|
+
lines.push(`Confined listener: ${statusArg.confinedSocketPath ?? 'none'}`);
|
|
75
|
+
for (const run of statusArg.confinedRuns ?? []) {
|
|
76
|
+
lines.push(`Confined run ${run.id}: issued ${run.createdAt}`
|
|
77
|
+
+ `${run.held ? '' : ', waiting for its launcher to take it back after a proxy restart'}`
|
|
78
|
+
+ ` (end it with \`agl proxy confined end ${run.id}\`)`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
72
81
|
if (!statusArg.accounts) {
|
|
73
82
|
// The proxy runs the older AGL it was started from until it restarts, as after an upgrade it was not restarted by.
|
|
74
83
|
lines.push(`Accounts: not reported, as the proxy runs AGL ${statusArg.version} and this command AGL ${commitinfo.version}. `
|
|
@@ -130,7 +139,7 @@ const serve = (portArg: number): Promise<void> => withSignals(async (signalArg)
|
|
|
130
139
|
const credentialHelper = await currentProxyCredentialHelper(paths.root,
|
|
131
140
|
() => resolveAglExecutable(process.env, plugins.os.homedir()));
|
|
132
141
|
const daemon = new AglProxyDaemon({ socketPath: paths.proxySocket, dataDirectory: plugins.path.join(paths.proxy, 'nosqldb'),
|
|
133
|
-
credentialDirectory: paths.proxyCredentials,
|
|
142
|
+
credentialDirectory: paths.proxyCredentials, confinedSocketPath: paths.proxyConfinedSocket,
|
|
134
143
|
authorityPaths: resolveProductionAuthoritySocketPaths(), port: portArg });
|
|
135
144
|
const stopping = () => { void daemon.stop().catch(() => undefined); };
|
|
136
145
|
signalArg.addEventListener('abort', stopping, { once: true });
|
|
@@ -196,6 +205,45 @@ const run = (optionsArg: IInferenceCliArguments): Promise<void> => withSignals(a
|
|
|
196
205
|
if (lost) throw new Error(`${lost.message} Run \`agl proxy run ${optionsArg.harness}\` again to continue.`);
|
|
197
206
|
});
|
|
198
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Runs a confined run's container (see `confinedDockerLaunch`) on a credential of its own, which ends when the container
|
|
210
|
+
* exits. A proxy that restarts meanwhile hands the credential back; one the proxy no longer hands back, or the operator
|
|
211
|
+
* ended, stops the container (`docker run` passes the signal on to it), and the run ends with the reason.
|
|
212
|
+
*/
|
|
213
|
+
const confinedRun = (optionsArg: IInferenceCliArguments): Promise<void> => withSignals(async (signalArg) => {
|
|
214
|
+
await preflightAGLHomeMigration();
|
|
215
|
+
const paths = resolveAGLHomePaths();
|
|
216
|
+
const run = await AglProxyConfinedHolder.take({ socketPath: paths.proxySocket }, AbortSignal.any([signalArg, AbortSignal.timeout(90_000)]));
|
|
217
|
+
let lost: AglProxyLeaseLostError | undefined;
|
|
218
|
+
try {
|
|
219
|
+
const launch = confinedDockerLaunch({ args: optionsArg.args, socketPath: run.confinedSocketPath, baseUrl: run.baseUrl,
|
|
220
|
+
token: run.token, environment: process.env });
|
|
221
|
+
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`);
|
|
223
|
+
signalArg.throwIfAborted();
|
|
224
|
+
const child = plugins.childProcess.spawn(launch.command, launch.args, { env: launch.environment, stdio: 'inherit', shell: false });
|
|
225
|
+
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
226
|
+
const terminate = () => {
|
|
227
|
+
child.kill('SIGTERM');
|
|
228
|
+
killTimer ??= setTimeout(() => child.kill('SIGKILL'), 10_000);
|
|
229
|
+
killTimer.unref();
|
|
230
|
+
};
|
|
231
|
+
signalArg.addEventListener('abort', terminate, { once: true });
|
|
232
|
+
void run.lost.then((errorArg) => { lost = errorArg; terminate(); });
|
|
233
|
+
try {
|
|
234
|
+
if (signalArg.aborted) terminate();
|
|
235
|
+
process.exitCode = await new Promise<number>((resolve, reject) => {
|
|
236
|
+
child.once('error', reject);
|
|
237
|
+
child.once('exit', (codeArg) => resolve(codeArg ?? 1));
|
|
238
|
+
});
|
|
239
|
+
} finally {
|
|
240
|
+
signalArg.removeEventListener('abort', terminate);
|
|
241
|
+
if (killTimer) clearTimeout(killTimer);
|
|
242
|
+
}
|
|
243
|
+
} finally { run.close(); }
|
|
244
|
+
if (lost) throw new Error(`${lost.message} The container was stopped; start the run again to continue.`);
|
|
245
|
+
});
|
|
246
|
+
|
|
199
247
|
const unitState = async (serviceArg: AglProxyService): Promise<string> => {
|
|
200
248
|
try {
|
|
201
249
|
const state = await serviceArg.inspect();
|
|
@@ -212,8 +260,14 @@ export const runInferenceCli = async (argvArg: readonly string[]): Promise<void>
|
|
|
212
260
|
const options = parseInferenceCliArguments(argvArg);
|
|
213
261
|
if (options.command === 'serve') { await preflightAGLHomeMigration(); await serve(options.port); return; }
|
|
214
262
|
if (options.command === 'run') { await run(options); return; }
|
|
263
|
+
if (options.command === 'confined' && options.confinedAction === 'run') { await confinedRun(options); return; }
|
|
215
264
|
const paths = resolveAGLHomePaths();
|
|
216
265
|
const client = new AglProxyControlClient(paths.proxySocket);
|
|
266
|
+
if (options.command === 'confined') {
|
|
267
|
+
await client.endConfined(options.confinedId!, AbortSignal.timeout(10_000));
|
|
268
|
+
process.stdout.write(`Ended confined run ${options.confinedId}: the proxy refuses its credential from now on.\n`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
217
271
|
if (options.command === 'credential') {
|
|
218
272
|
await runProxyCredentialCommand(options, (textArg) => process.stdout.write(textArg));
|
|
219
273
|
return;
|
|
@@ -2,6 +2,8 @@ import * as path from 'node:path';
|
|
|
2
2
|
import type { IInferenceRoutingPolicy } from './classes.inferencerouter.js';
|
|
3
3
|
import type { TInferenceHarness } from './functions.inferencelaunch.js';
|
|
4
4
|
import {
|
|
5
|
+
aglProxyConfinedIdPattern,
|
|
6
|
+
aglProxyConfinedMountPoint,
|
|
5
7
|
aglProxyDefaultPort,
|
|
6
8
|
aglProxyUnitName,
|
|
7
9
|
defaultProxyClientProvider,
|
|
@@ -24,6 +26,8 @@ export const inferenceCliHelp = `Usage:
|
|
|
24
26
|
agl proxy status [--json]
|
|
25
27
|
agl proxy credential <client> [--provider openai|anthropic] [--delivery file|none] [--write <path>] [--rotate] | --version
|
|
26
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]
|
|
30
|
+
agl proxy confined end <run id>
|
|
27
31
|
|
|
28
32
|
The proxy runs as the ${aglProxyUnitName} user unit (serve runs it in a terminal instead).
|
|
29
33
|
Every client (codex, claude, opencode, or a name of your own) has one credential, sealed by the proxy.
|
|
@@ -35,6 +39,13 @@ Each client is served from one pool: ChatGPT accounts (openai) or Claude account
|
|
|
35
39
|
client starts on Claude accounts, every other client on ChatGPT accounts; --provider moves a client.
|
|
36
40
|
Run gives one child a private credential that ends when the child exits; run claude uses Claude accounts
|
|
37
41
|
unless --provider openai asks for ChatGPT models behind Claude Code.
|
|
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
|
|
46
|
+
container exits. The socket is the proxy user's (0600): run the container as that uid (--user), or as root under
|
|
47
|
+
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.
|
|
38
49
|
Repeat --account or --plan to restrict automatic routing. Only accounts the account service holds are eligible.
|
|
39
50
|
ChatGPT pools: /openai/v1/responses, /anthropic/v1/messages (translated), and each prefix's /models.
|
|
40
51
|
Claude pools: /anthropic/v1/messages, /anthropic/v1/messages/count_tokens and /anthropic/v1/models, sent on as Claude Code sent them.
|
|
@@ -44,14 +55,18 @@ Use x-agl-session-id for custom clients; native session headers and prompt_cache
|
|
|
44
55
|
Switches use full history with a cold cache. Provider-only references require a full-history retry; opaque account state never crosses accounts.
|
|
45
56
|
`;
|
|
46
57
|
|
|
47
|
-
export type TInferenceCommand = 'serve' | 'run' | 'credential' | 'status' | 'service';
|
|
58
|
+
export type TInferenceCommand = 'serve' | 'run' | 'credential' | 'status' | 'service' | 'confined';
|
|
48
59
|
export type TInferenceServiceAction = 'install' | 'enable' | 'start' | 'stop' | 'status';
|
|
60
|
+
export type TInferenceConfinedAction = 'run' | 'end';
|
|
49
61
|
|
|
50
62
|
export interface IInferenceCliArguments {
|
|
51
63
|
command: TInferenceCommand;
|
|
52
64
|
harness?: TInferenceHarness;
|
|
53
65
|
client?: string;
|
|
54
66
|
serviceAction?: TInferenceServiceAction;
|
|
67
|
+
confinedAction?: TInferenceConfinedAction;
|
|
68
|
+
/** `confined end`: the run to end. */
|
|
69
|
+
confinedId?: string;
|
|
55
70
|
port: number;
|
|
56
71
|
policy: IInferenceRoutingPolicy;
|
|
57
72
|
/** `--provider`, when given. */
|
|
@@ -72,6 +87,7 @@ const commandOptions: Record<TInferenceCommand, readonly string[]> = {
|
|
|
72
87
|
credential: ['--provider', '--delivery', '--write', '--rotate'],
|
|
73
88
|
status: ['--json'],
|
|
74
89
|
service: ['--port'],
|
|
90
|
+
confined: [],
|
|
75
91
|
};
|
|
76
92
|
const flags = new Set(['--rotate', '--json']);
|
|
77
93
|
|
|
@@ -97,6 +113,21 @@ export const parseInferenceCliArguments = (argvArg: readonly string[]): IInferen
|
|
|
97
113
|
const action = argv.shift();
|
|
98
114
|
if (!['install', 'enable', 'start', 'stop', 'status'].includes(action ?? '')) throw new Error(inferenceCliHelp);
|
|
99
115
|
result.serviceAction = action as TInferenceServiceAction;
|
|
116
|
+
} else if (command === 'confined') {
|
|
117
|
+
const action = argv.shift();
|
|
118
|
+
if (action === 'end') {
|
|
119
|
+
const id = argv.shift();
|
|
120
|
+
if (id === undefined || !aglProxyConfinedIdPattern.test(id) || argv.length) {
|
|
121
|
+
throw new Error('Name the confined run to end by its id: 12 hex characters, as `agl proxy status` lists it.');
|
|
122
|
+
}
|
|
123
|
+
result.confinedAction = 'end';
|
|
124
|
+
result.confinedId = id;
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
if (action !== 'run' || argv.shift() !== '--' || !argv.length) throw new Error(inferenceCliHelp);
|
|
128
|
+
result.confinedAction = 'run';
|
|
129
|
+
result.args = argv;
|
|
130
|
+
return result;
|
|
100
131
|
}
|
|
101
132
|
const accounts: string[] = [];
|
|
102
133
|
const plans: string[] = [];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
1
2
|
import type { TInferenceModel } from './classes.inferencerouter.js';
|
|
2
3
|
import type { TAglProxyLeaseCatalog } from './classes.proxycontrol.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
4
|
+
import { aglProxyConfinedMountPoint } from './functions.proxycontract.js';
|
|
5
|
+
import { aglProxyProviderId, claudeProxyEnvironment, claudeProxyReservedVariables, claudeRoutingVariables, codexProxyProvider,
|
|
6
|
+
openCodeProxyProvider, proxyHarnessModels } from './functions.proxyharnessconfig.js';
|
|
5
7
|
|
|
6
8
|
export type TInferenceHarness = 'codex' | 'claude' | 'opencode';
|
|
7
9
|
export interface IInferenceLaunchOptions {
|
|
@@ -137,3 +139,48 @@ export const inferenceHarnessLaunch = (optionsArg: IInferenceLaunchOptions): { a
|
|
|
137
139
|
if (args[0] === 'run') return { environment, args: ['run', ...selection, ...args.slice(1)] };
|
|
138
140
|
return { environment, args: args[0] !== undefined && openCodeCommands.has(args[0]) ? args : [...selection, ...args] };
|
|
139
141
|
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A confined run's `docker run`: the given command, with the confined listener's directory mounted read-only at
|
|
145
|
+
* `aglProxyConfinedMountPoint` and Claude Code's settings for it added right after `run`, before the launcher's own
|
|
146
|
+
* options and image. Claude Code inside reaches the proxy through the socket alone (`ANTHROPIC_UNIX_SOCKET`, which
|
|
147
|
+
* Claude Code sends its API requests through as plain HTTP to `ANTHROPIC_BASE_URL`'s path), on the run's credential
|
|
148
|
+
* (`ANTHROPIC_AUTH_TOKEN`), reads the pool's models from the proxy and sends nothing beyond its model requests. The
|
|
149
|
+
* container gets each variable by name from the docker command's environment, so the credential never appears on a
|
|
150
|
+
* command line; that environment drops every variable that would give Claude Code another login or route, so `-e NAME`
|
|
151
|
+
* of one of them passes nothing. The mount is the socket's own directory, which holds nothing else and survives a proxy
|
|
152
|
+
* restart, unlike a mount of the socket file. Only `docker run` and `docker container run` are taken.
|
|
153
|
+
*/
|
|
154
|
+
export const confinedDockerLaunch = (optionsArg: {
|
|
155
|
+
args: readonly string[];
|
|
156
|
+
/** The confined listener's socket on the host. */
|
|
157
|
+
socketPath: string;
|
|
158
|
+
/** The origin Claude Code names; see `IAglProxyConfinedGrant.baseUrl`. */
|
|
159
|
+
baseUrl: string;
|
|
160
|
+
token: string;
|
|
161
|
+
environment: NodeJS.ProcessEnv;
|
|
162
|
+
}): { command: string; args: string[]; environment: NodeJS.ProcessEnv } => {
|
|
163
|
+
const [command, ...args] = optionsArg.args;
|
|
164
|
+
const runAt = args[0] === 'run' ? 1 : args[0] === 'container' && args[1] === 'run' ? 2 : 0;
|
|
165
|
+
if (command === undefined || path.basename(command) !== 'docker' || runAt === 0) {
|
|
166
|
+
throw new Error('A confined run starts its container with `docker run` (or `docker container run`) and its arguments.');
|
|
167
|
+
}
|
|
168
|
+
const directory = path.dirname(optionsArg.socketPath);
|
|
169
|
+
// `--mount` separates its fields with commas and reads quotes; a directory naming either cannot be passed safely.
|
|
170
|
+
if (/[,"'\n]/u.test(directory)) throw new Error(`The confined listener's directory ${directory} cannot be mounted: it holds a comma, quote or line break.`);
|
|
171
|
+
const variables: Record<string, string> = {
|
|
172
|
+
...claudeProxyEnvironment(optionsArg.baseUrl),
|
|
173
|
+
ANTHROPIC_UNIX_SOCKET: `${aglProxyConfinedMountPoint}/${path.basename(optionsArg.socketPath)}`,
|
|
174
|
+
ANTHROPIC_AUTH_TOKEN: optionsArg.token,
|
|
175
|
+
};
|
|
176
|
+
const environment = { ...optionsArg.environment };
|
|
177
|
+
for (const name of claudeProxyReservedVariables) delete environment[name];
|
|
178
|
+
return {
|
|
179
|
+
command,
|
|
180
|
+
args: [...args.slice(0, runAt),
|
|
181
|
+
'--mount', `type=bind,source=${directory},target=${aglProxyConfinedMountPoint},readonly`,
|
|
182
|
+
...Object.keys(variables).flatMap((nameArg) => ['--env', nameArg]),
|
|
183
|
+
...args.slice(runAt)],
|
|
184
|
+
environment: { ...environment, ...variables },
|
|
185
|
+
};
|
|
186
|
+
};
|
|
@@ -4,7 +4,7 @@ import type { TTicketOperation } from '../ts_interfaces/tickets.js';
|
|
|
4
4
|
const workflow = ' Caller identity comes from AGL, never a supplied name. Unidentified callers may list/get tickets with empty mine; every mutation requires a verified caller. Workers create in backlog and may update/move only their assigned tickets within ready, in_progress, needs_input and review. Link a branch with a full sha before Review. Only the driver or owner assigns, lands or releases tickets.';
|
|
5
5
|
export const ticketToolDefinitions = [
|
|
6
6
|
{ name: 'tickets_list', operation: 'list', description: 'Read the shared board across all projects. Filter by mine, project, status, label or text. mine uses your verified session. label also matches AutoLabels, which AGL computes and nobody sets: openQuestion marks a ticket with an unanswered question.' },
|
|
7
|
-
{ name: 'ticket_get', operation: 'get', description: 'Read a ticket by its AGL-<n> key
|
|
7
|
+
{ name: 'ticket_get', operation: 'get', description: 'Read a ticket by its AGL-<n> key: source, links, all its questions and their answers, and the newest page of its history, oldest first: up to limit entries (default 50, at most 500), fewer when the entries and their comments would pass 256 KiB. activity is the page; comments are the markdown comments it names, and a commented entry names its comment by id. historyBefore is present while older history exists: pass it as before for the next older page. commentId instead reads the one comment an activity entry or fleet event names (without before or limit).' },
|
|
8
8
|
{ name: 'ticket_create', operation: 'create', description: 'Capture discussed work as a ticket. Include project, title, description and sourceText (the original words); optional attachments are named URLs. Worker-created tickets start in backlog for driver triage.' },
|
|
9
9
|
{ name: 'ticket_update', operation: 'update', description: 'Update the title, markdown description, priority, labels, project or parent of your assigned ticket. Supply expectedRevision from the ticket you read; stale edits return TICKET_REVISION_CONFLICT with currentRevision. Reload before editing again. Status is changed only with ticket_move; assignment is driver/owner-only.' },
|
|
10
10
|
{ name: 'ticket_move', operation: 'move', description: 'Atomically move/reorder your assigned ticket. Omit beforeTicketId to append. Returns every record whose column or order changed. Review requires a linked branch and sha. Use needs_input to report a blocker.' },
|
|
@@ -33,3 +33,15 @@ export const proxyCredentialDeliveries: readonly TProxyCredentialDelivery[] = ['
|
|
|
33
33
|
|
|
34
34
|
export const aglProxyUnitName = 'agl-proxy.service';
|
|
35
35
|
export const aglProxyDefaultPort = 4100;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Confined runs (`agl proxy confined run`): Claude Code in a container that reaches the proxy only through the confined
|
|
39
|
+
* listener, a Unix socket alone in its directory, which the run mounts read-only at `aglProxyConfinedMountPoint`. Their
|
|
40
|
+
* sessions are those of the client `aglProxyConfinedClient`, and Claude Code names `aglProxyConfinedOrigin` as its
|
|
41
|
+
* endpoint's origin, whose host the socket makes irrelevant.
|
|
42
|
+
*/
|
|
43
|
+
export const aglProxyConfinedClient = 'confined';
|
|
44
|
+
export const aglProxyConfinedOrigin = 'http://localhost';
|
|
45
|
+
export const aglProxyConfinedMountPoint = '/run/agl-proxy';
|
|
46
|
+
/** A confined run's id: the first 12 hex characters of its credential's SHA-256 digest. */
|
|
47
|
+
export const aglProxyConfinedIdPattern = /^[a-f0-9]{12}$/;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claims the pathname of one of the proxy's Unix sockets: its directory is made private (0700, this user's, not a link),
|
|
5
|
+
* and a socket a killed proxy left there is removed, but only while it is an unchanged, owned socket nobody answers on.
|
|
6
|
+
* `nameArg` names the socket in the refusals; `busyArg` is the refusal for a socket another process answers on.
|
|
7
|
+
*/
|
|
8
|
+
export const claimProxySocketPath = async (socketPathArg: string, nameArg: string, busyArg: string): Promise<void> => {
|
|
9
|
+
const directory = plugins.path.dirname(socketPathArg);
|
|
10
|
+
await plugins.fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
11
|
+
const stat = await plugins.fs.promises.lstat(directory);
|
|
12
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.()) {
|
|
13
|
+
throw new Error(`The ${nameArg} directory is not private.`);
|
|
14
|
+
}
|
|
15
|
+
await plugins.fs.promises.chmod(directory, 0o700);
|
|
16
|
+
let before: plugins.fs.Stats;
|
|
17
|
+
try { before = await plugins.fs.promises.lstat(socketPathArg); }
|
|
18
|
+
catch (errorArg) {
|
|
19
|
+
if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
20
|
+
throw errorArg;
|
|
21
|
+
}
|
|
22
|
+
if (!before.isSocket() || before.uid !== process.getuid?.()) throw new Error(`The ${nameArg} path is unsafe.`);
|
|
23
|
+
const refused = await new Promise<boolean>((resolve, reject) => {
|
|
24
|
+
const probe = plugins.net.createConnection(socketPathArg);
|
|
25
|
+
probe.setTimeout(1_000, () => { probe.destroy(); reject(new Error(`The ${nameArg} probe timed out.`)); });
|
|
26
|
+
probe.once('connect', () => { probe.destroy(); resolve(false); });
|
|
27
|
+
probe.once('error', (errorArg) => {
|
|
28
|
+
probe.destroy();
|
|
29
|
+
if ((errorArg as NodeJS.ErrnoException).code === 'ECONNREFUSED') resolve(true);
|
|
30
|
+
else reject(errorArg);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
if (!refused) throw new Error(busyArg);
|
|
34
|
+
const after = await plugins.fs.promises.lstat(socketPathArg);
|
|
35
|
+
if (!after.isSocket() || after.dev !== before.dev || after.ino !== before.ino) {
|
|
36
|
+
throw new Error(`The ${nameArg} path changed during startup.`);
|
|
37
|
+
}
|
|
38
|
+
await plugins.fs.promises.unlink(socketPathArg);
|
|
39
|
+
};
|
|
@@ -8,7 +8,10 @@ import type { ITicketChanges, ITicketPerson, TTicketOperation, TTicketRequest, T
|
|
|
8
8
|
export const ticketCliHelp = `Usage: agl ticket <list|get|create|update|move|comment|link|assign|ask|answer> [AGL-<n>] [options]
|
|
9
9
|
list [--mine] [--project ID] [--status COLUMN] [--label LABEL] [--text TEXT]
|
|
10
10
|
--label also matches AutoLabels, which AGL computes: openQuestion (an unanswered question)
|
|
11
|
-
get AGL-<n>
|
|
11
|
+
get AGL-<n> [--limit N] [--before HISTORY-ID] [--comment COMMENT-ID]
|
|
12
|
+
The ticket, its questions and the newest N history entries (default 50, at most 500, within 256 KiB);
|
|
13
|
+
historyBefore names the next older page: pass it as --before.
|
|
14
|
+
--comment reads the one comment an activity entry names instead (not with --limit or --before)
|
|
12
15
|
create --project ID --title TEXT [--description MARKDOWN] [--kind epic|task|subtask]
|
|
13
16
|
[--parent AGL-<n>] [--priority none|low|normal|high|urgent] [--labels a,b]
|
|
14
17
|
[--source TEXT] [--attachments JSON] [--column COLUMN]
|
|
@@ -38,7 +41,7 @@ export interface ITicketCliArguments {
|
|
|
38
41
|
}
|
|
39
42
|
const common = ['port', 'json'];
|
|
40
43
|
const actionOptions: Record<string, string[]> = {
|
|
41
|
-
list: ['mine', 'project', 'status', 'label', 'text'], get: [],
|
|
44
|
+
list: ['mine', 'project', 'status', 'label', 'text'], get: ['limit', 'before', 'comment'],
|
|
42
45
|
create: ['project', 'title', 'description', 'kind', 'parent', 'priority', 'labels', 'source', 'attachments', 'column'],
|
|
43
46
|
update: ['expected-revision', 'changes', 'title', 'description', 'priority', 'project', 'labels', 'parent', 'clear-parent'],
|
|
44
47
|
move: ['column', 'before'], comment: ['body'], link: ['repo', 'branch', 'sha', 'package', 'version'],
|
|
@@ -78,7 +81,8 @@ export const parseTicketCliArguments = (argumentsArg: string[]): ITicketCliArgum
|
|
|
78
81
|
const operation = action === 'assign' ? 'update' : action as TTicketOperation;
|
|
79
82
|
if (action === 'list') request = { ...(options.has('mine') ? { mine: true } : {}), ...optional('project'),
|
|
80
83
|
...optional('status'), ...optional('label'), ...optional('text') };
|
|
81
|
-
else if (action === 'get') request = { ticketId
|
|
84
|
+
else if (action === 'get') request = { ticketId, ...optional('before'), ...optional('comment', 'commentId'),
|
|
85
|
+
...(options.has('limit') ? { limit: Number(required('limit')) } : {}) };
|
|
82
86
|
else if (action === 'create') request = { project: required('project'), title: required('title'), ...optional('description'),
|
|
83
87
|
...optional('kind'), ...optional('parent', 'parentId'), ...optional('priority'), ...labels(), ...optional('source', 'sourceText'),
|
|
84
88
|
...optional('column', 'columnId'), ...(options.has('attachments') ? { attachments: JSON.parse(required('attachments')) } : {}) };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import { ticketAutoLabels, ticketColumns, type TTicketOperation, type TTicketRequest } from '../ts_interfaces/tickets.js';
|
|
2
|
+
import { ticketAutoLabels, ticketColumns, ticketHistoryPage, type TTicketOperation, type TTicketRequest } from '../ts_interfaces/tickets.js';
|
|
3
3
|
|
|
4
4
|
const text = (maximumArg: number) => plugins.z.string().max(maximumArg);
|
|
5
5
|
const identifier = text(1024).min(1).refine((valueArg) => !/[\x00-\x1f\x7f]/u.test(valueArg));
|
|
@@ -38,7 +38,11 @@ const changes = plugins.z.object({
|
|
|
38
38
|
export const ticketInputSchemas = {
|
|
39
39
|
list: filters, board: filters,
|
|
40
40
|
search: plugins.z.object({ query: text(1024), label: text(100).min(1).optional() }).strict(),
|
|
41
|
-
get: plugins.z.object({ ticketId
|
|
41
|
+
get: plugins.z.object({ ticketId, before: identifier.optional(),
|
|
42
|
+
limit: plugins.z.number().int().min(1).max(ticketHistoryPage.maximum).optional(), commentId: identifier.optional() }).strict()
|
|
43
|
+
.refine((valueArg) => valueArg.commentId === undefined || (valueArg.before === undefined && valueArg.limit === undefined),
|
|
44
|
+
'commentId reads one comment and excludes before and limit.'),
|
|
45
|
+
comments: plugins.z.object({ ticketId }).strict(),
|
|
42
46
|
create: plugins.z.object({ project: identifier, kind: kind.optional(), title: text(200).trim().min(1),
|
|
43
47
|
description: text(64 * 1024).optional(), columnId: plugins.z.enum(ticketColumns).optional(), parentId: ticketId.optional(),
|
|
44
48
|
priority: priority.optional(), assignee: person.optional(), labels: labels.optional(), sourceText: text(64 * 1024).optional(),
|
|
@@ -117,7 +121,7 @@ export const assertTicketResponse = (methodArg: string, valueArg: unknown): void
|
|
|
117
121
|
id: plugins.z.enum(ticketColumns), title: plugins.z.string(), done: plugins.z.boolean().optional() }).strict()), tickets: plugins.z.array(ticket),
|
|
118
122
|
viewer: person }).strict(),
|
|
119
123
|
get: plugins.z.object({ ticket, comments: plugins.z.array(ticketCommentSchema), activity: plugins.z.array(ticketActivitySchema),
|
|
120
|
-
questions: plugins.z.array(ticketQuestionSchema) }).strict(),
|
|
124
|
+
questions: plugins.z.array(ticketQuestionSchema), historyBefore: identifier.optional() }).strict(),
|
|
121
125
|
comments: plugins.z.object({ comments: plugins.z.array(ticketCommentSchema) }).strict(),
|
|
122
126
|
create: plugins.z.object({ ticket }).strict(), update: plugins.z.object({ ticket }).strict(),
|
|
123
127
|
link: plugins.z.object({ ticket }).strict(), move: plugins.z.object({ tickets: plugins.z.array(ticket) }).strict(),
|
|
@@ -142,21 +142,44 @@ const processOwnsLoopbackConnection = async (connectionArg: IWorkerCallerConnect
|
|
|
142
142
|
return matches.length === 1 && await processOwnsInode(pidArg, matches[0][9]);
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* The bridge process `bridgePidArg`, proved to hold the caller's end of the direct loopback connection and to run the
|
|
147
|
+
* approved command, whatever its parent. The command is the one recorded when the launch was prepared: an upgrade
|
|
148
|
+
* installs AGL under another path, and a command whose files are gone is refused as such.
|
|
149
|
+
*/
|
|
150
|
+
export const readWorkerBridge = async (
|
|
146
151
|
connectionArg: IWorkerCallerConnection | undefined,
|
|
147
152
|
bridgePidArg: number,
|
|
148
153
|
commandArg: IWorkerBridgeCommand,
|
|
149
|
-
parentArg: IWorkerProcessSnapshot,
|
|
150
154
|
): Promise<IWorkerProcessSnapshot> => {
|
|
155
|
+
for (const file of [commandArg.executable, commandArg.entry]) {
|
|
156
|
+
let resolved: string;
|
|
157
|
+
try { resolved = await plugins.fs.promises.realpath(file); } catch (errorArg) {
|
|
158
|
+
if (isWorkerCallerProofRefusal(errorArg)) throw new WorkerCallerProofError('The approved bridge command no longer exists.');
|
|
159
|
+
throw errorArg;
|
|
160
|
+
}
|
|
161
|
+
if (resolved !== file) throw new WorkerCallerProofError('The approved bridge command no longer exists.');
|
|
162
|
+
}
|
|
151
163
|
if (!await processOwnsLoopbackConnection(connectionArg, bridgePidArg)) {
|
|
152
164
|
throw new WorkerCallerProofError('Worker transport ownership could not be proved.');
|
|
153
165
|
}
|
|
154
166
|
const bridge = await readWorkerProcessSnapshot(bridgePidArg);
|
|
155
167
|
const entry = await plugins.fs.promises.realpath(bridge.arguments[1] ?? '');
|
|
156
|
-
if (bridge.
|
|
157
|
-
||
|
|
168
|
+
if (bridge.executable !== commandArg.executable || entry !== commandArg.entry
|
|
169
|
+
|| !plugins.util.isDeepStrictEqual(bridge.arguments.slice(2), commandArg.arguments)) {
|
|
158
170
|
throw new WorkerCallerProofError('Worker bridge does not match the approved launch.');
|
|
159
171
|
}
|
|
172
|
+
return bridge;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const verifyWorkerBridge = async (
|
|
176
|
+
connectionArg: IWorkerCallerConnection | undefined,
|
|
177
|
+
bridgePidArg: number,
|
|
178
|
+
commandArg: IWorkerBridgeCommand,
|
|
179
|
+
parentArg: IWorkerProcessSnapshot,
|
|
180
|
+
): Promise<IWorkerProcessSnapshot> => {
|
|
181
|
+
const bridge = await readWorkerBridge(connectionArg, bridgePidArg, commandArg);
|
|
182
|
+
if (bridge.parentPid !== parentArg.pid) throw new WorkerCallerProofError('Worker bridge does not match the approved launch.');
|
|
160
183
|
if (!workerSnapshotsEqual(parentArg, await readWorkerProcessSnapshot(parentArg.pid))
|
|
161
184
|
|| !workerSnapshotsEqual(bridge, await readWorkerProcessSnapshot(bridge.pid))) {
|
|
162
185
|
throw new WorkerCallerProofError('Worker transport or process incarnation could not be proved.');
|
package/ts/interfaces.flexipc.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type TFlexCreateSessionOptions = plugins.flexharness.IFlexCreateSessionOptions;
|
|
24
24
|
type TFlexMessage = plugins.flexharness.IFlexMessage;
|
|
25
25
|
type TFlexMessagePage = plugins.flexharness.IFlexMessagePage;
|
|
26
|
+
type TFlexMessageMetadataPage = plugins.flexharness.IFlexMessageMetadataPage;
|
|
26
27
|
type TFlexPermissionRequest = plugins.flexharness.IFlexPermissionRequest;
|
|
27
28
|
export type TFlexPromptQueueEntry = plugins.flexharness.IFlexPromptQueueEntry;
|
|
28
29
|
type TFlexPromptOptions = plugins.flexharness.IFlexPromptOptions;
|
|
@@ -67,7 +68,7 @@ export type TFlexTurnReversionCaptureInspectionV2 =
|
|
|
67
68
|
export type TFlexTurnReversionApplyInspection =
|
|
68
69
|
plugins.flexharness.IFlexTurnReversionApplyInspection;
|
|
69
70
|
|
|
70
|
-
export const flexIpcProtocolVersion =
|
|
71
|
+
export const flexIpcProtocolVersion = 39 as const;
|
|
71
72
|
/**
|
|
72
73
|
* The installation's own Flex runs, which belong to no project: the configuration projection's adapting run, and
|
|
73
74
|
* Session Intelligence over a session outside every project.
|
|
@@ -307,6 +308,8 @@ export interface IFlexDriverRequest {
|
|
|
307
308
|
export interface IFlexDriverResolution {
|
|
308
309
|
instructions: string;
|
|
309
310
|
notes: string;
|
|
311
|
+
/** The revision of the session scratchpad that holds the notes; names them as the driver's pinned context. */
|
|
312
|
+
notesRevision: number;
|
|
310
313
|
}
|
|
311
314
|
export const flexDriverToolNamePattern = /^[a-z][a-z0-9_]{0,63}$/u;
|
|
312
315
|
export const flexDriverToolInputMaxBytes = 2 * 1024 * 1024;
|
|
@@ -363,7 +366,7 @@ export interface IFlexHostRequestMap {
|
|
|
363
366
|
request: IFlexTurnReversionReleaseRequest;
|
|
364
367
|
response: { released: true };
|
|
365
368
|
};
|
|
366
|
-
/** The controller-owned driver session's instructions and notes; null for every other session. */
|
|
369
|
+
/** The controller-owned driver session's instructions and notes with their revision; null for every other session. */
|
|
367
370
|
'driver.resolve': {
|
|
368
371
|
request: IFlexDriverRequest;
|
|
369
372
|
response: { driver: IFlexDriverResolution | null };
|
|
@@ -836,6 +839,14 @@ export interface IFlexRequestMap {
|
|
|
836
839
|
request: { scopeId: string; sessionId: string; limit?: number; before?: string; includeUndone?: boolean };
|
|
837
840
|
response: TFlexMessagePage;
|
|
838
841
|
};
|
|
842
|
+
/**
|
|
843
|
+
* The same pages with each message's identity and state only (no parts, model, usage or error):
|
|
844
|
+
* a whole transcript is scanned without reading its text or tool output.
|
|
845
|
+
*/
|
|
846
|
+
'message.metadata.page': {
|
|
847
|
+
request: { scopeId: string; sessionId: string; limit?: number; before?: string; includeUndone?: boolean };
|
|
848
|
+
response: TFlexMessageMetadataPage;
|
|
849
|
+
};
|
|
839
850
|
'message.get': {
|
|
840
851
|
request: { scopeId: string; sessionId: string; messageId: string; includeUndone?: boolean };
|
|
841
852
|
response: TFlexMessage;
|
|
@@ -1358,6 +1369,7 @@ export const readOnlyFlexRequestMethods: ReadonlySet<TFlexRequestMethod> = new S
|
|
|
1358
1369
|
'session.reversion.info',
|
|
1359
1370
|
'slash.list',
|
|
1360
1371
|
'message.page',
|
|
1372
|
+
'message.metadata.page',
|
|
1361
1373
|
'message.get',
|
|
1362
1374
|
'prompt.get',
|
|
1363
1375
|
'prompt.list',
|
|
@@ -1387,6 +1399,7 @@ const requestMethods: ReadonlySet<string> = new Set<TFlexRequestMethod>([
|
|
|
1387
1399
|
'slash.list',
|
|
1388
1400
|
'slash.execute',
|
|
1389
1401
|
'message.page',
|
|
1402
|
+
'message.metadata.page',
|
|
1390
1403
|
'message.get',
|
|
1391
1404
|
'prompt.start',
|
|
1392
1405
|
'prompt.continue',
|
|
@@ -1919,28 +1932,22 @@ const isFlexModelLimit = (valueArg: unknown): valueArg is TFlexModelLimit => (
|
|
|
1919
1932
|
isFlexPlainObject(valueArg) && plugins.flexModels.isModelLimitInfo(valueArg)
|
|
1920
1933
|
);
|
|
1921
1934
|
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1935
|
+
const flexMessageIdentityKeys = ['messageId', 'sessionId', 'runId', 'role', 'status', 'createdAt'] as const;
|
|
1936
|
+
const flexMessageOptionalIdentityKeys = ['completedAt', 'steerId', 'promptId', 'keptInContext', 'undone', 'cacheWarm'] as const;
|
|
1937
|
+
const flexMessageKeys = [...flexMessageIdentityKeys, 'parts'] as const;
|
|
1938
|
+
const flexMessageOptionalKeys = [
|
|
1939
|
+
...flexMessageOptionalIdentityKeys, 'model', 'usage', 'error', 'limit', 'rejectedPermissionId',
|
|
1940
|
+
] as const;
|
|
1941
|
+
|
|
1942
|
+
/** The identity and state a transcript message and its metadata share (`TFlexMessageMetadata`). */
|
|
1943
|
+
const hasFlexMessageIdentity = (valueArg: Record<string, unknown>): boolean =>
|
|
1944
|
+
isIdentifier(valueArg.messageId)
|
|
1930
1945
|
&& isIdentifier(valueArg.sessionId)
|
|
1931
1946
|
&& isIdentifier(valueArg.runId)
|
|
1932
1947
|
&& ['user', 'assistant'].includes(String(valueArg.role))
|
|
1933
1948
|
&& ['streaming', 'completed', 'failed', 'cancelled'].includes(String(valueArg.status))
|
|
1934
1949
|
&& isIsoDate(valueArg.createdAt)
|
|
1935
1950
|
&& (valueArg.completedAt === undefined || isIsoDate(valueArg.completedAt))
|
|
1936
|
-
&& Array.isArray(valueArg.parts)
|
|
1937
|
-
&& valueArg.parts.length <= 2000
|
|
1938
|
-
&& valueArg.parts.every((partArg) => isMessagePart(partArg))
|
|
1939
|
-
&& (valueArg.model === undefined || isModelIdentity(valueArg.model))
|
|
1940
|
-
&& (valueArg.usage === undefined || isUsage(valueArg.usage))
|
|
1941
|
-
&& (valueArg.error === undefined || isBoundedString(valueArg.error, 2048))
|
|
1942
|
-
&& (valueArg.limit === undefined || isFlexModelLimit(valueArg.limit))
|
|
1943
|
-
&& (valueArg.rejectedPermissionId === undefined || isIdentifier(valueArg.rejectedPermissionId))
|
|
1944
1951
|
&& (valueArg.steerId === undefined || (valueArg.role === 'user' && isIdentifier(valueArg.steerId)))
|
|
1945
1952
|
&& (valueArg.promptId === undefined || (valueArg.role === 'user' && isBoundedString(valueArg.promptId, 512)))
|
|
1946
1953
|
&& !(valueArg.promptId !== undefined && valueArg.steerId !== undefined)
|
|
@@ -1949,6 +1956,25 @@ export const isFlexPublicMessage = (valueArg: unknown): valueArg is TFlexMessage
|
|
|
1949
1956
|
&& (valueArg.undone === undefined || valueArg.undone === true)
|
|
1950
1957
|
&& (valueArg.cacheWarm === undefined || valueArg.cacheWarm === true);
|
|
1951
1958
|
|
|
1959
|
+
export const isFlexPublicMessage = (valueArg: unknown): valueArg is TFlexMessage =>
|
|
1960
|
+
isFlexPlainObject(valueArg)
|
|
1961
|
+
&& hasFlexExactKeys(valueArg, flexMessageKeys, flexMessageOptionalKeys)
|
|
1962
|
+
&& hasFlexMessageIdentity(valueArg)
|
|
1963
|
+
&& Array.isArray(valueArg.parts)
|
|
1964
|
+
&& valueArg.parts.length <= 2000
|
|
1965
|
+
&& valueArg.parts.every((partArg) => isMessagePart(partArg))
|
|
1966
|
+
&& (valueArg.model === undefined || isModelIdentity(valueArg.model))
|
|
1967
|
+
&& (valueArg.usage === undefined || isUsage(valueArg.usage))
|
|
1968
|
+
&& (valueArg.error === undefined || isBoundedString(valueArg.error, 2048))
|
|
1969
|
+
&& (valueArg.limit === undefined || isFlexModelLimit(valueArg.limit))
|
|
1970
|
+
&& (valueArg.rejectedPermissionId === undefined || isIdentifier(valueArg.rejectedPermissionId));
|
|
1971
|
+
|
|
1972
|
+
/** A transcript message's identity and state as `message.metadata.page` carries it: no content. */
|
|
1973
|
+
export const isFlexMessageMetadata = (valueArg: unknown): valueArg is plugins.flexharness.TFlexMessageMetadata =>
|
|
1974
|
+
isFlexPlainObject(valueArg)
|
|
1975
|
+
&& hasFlexExactKeys(valueArg, flexMessageIdentityKeys, flexMessageOptionalIdentityKeys)
|
|
1976
|
+
&& hasFlexMessageIdentity(valueArg);
|
|
1977
|
+
|
|
1952
1978
|
export const isFlexPublicSession = (valueArg: unknown): valueArg is TFlexSession => {
|
|
1953
1979
|
if (
|
|
1954
1980
|
!isFlexPlainObject(valueArg)
|
|
@@ -2573,6 +2599,7 @@ export const isFlexRequestPayload = <TMethod extends TFlexRequestMethod>(
|
|
|
2573
2599
|
&& isScopeAndSession(valueArg)
|
|
2574
2600
|
&& isUpdateSessionOptions(valueArg.options);
|
|
2575
2601
|
case 'message.page':
|
|
2602
|
+
case 'message.metadata.page':
|
|
2576
2603
|
return hasFlexExactKeys(valueArg, ['scopeId', 'sessionId'], ['limit', 'before', 'includeUndone'])
|
|
2577
2604
|
&& isScopeAndSession(valueArg)
|
|
2578
2605
|
&& (valueArg.limit === undefined
|
|
@@ -3118,10 +3145,11 @@ export const isFlexHostResponseResult = <TMethod extends TFlexHostRequestMethod>
|
|
|
3118
3145
|
if (!hasFlexExactKeys(valueArg, ['driver'])) return false;
|
|
3119
3146
|
const driver = valueArg.driver;
|
|
3120
3147
|
return driver === null || (isFlexPlainObject(driver)
|
|
3121
|
-
&& hasFlexExactKeys(driver, ['instructions', 'notes'])
|
|
3148
|
+
&& hasFlexExactKeys(driver, ['instructions', 'notes', 'notesRevision'])
|
|
3122
3149
|
&& isBoundedString(driver.instructions, 64 * 1024)
|
|
3123
3150
|
&& isBoundedString(driver.notes, 128 * 1024, true)
|
|
3124
|
-
&& driver.notes.length <= 32_768
|
|
3151
|
+
&& driver.notes.length <= 32_768
|
|
3152
|
+
&& isNonNegativeInteger(driver.notesRevision));
|
|
3125
3153
|
}
|
|
3126
3154
|
if (methodArg === 'driver.tool') {
|
|
3127
3155
|
if (Object.hasOwn(valueArg, 'error')) {
|
|
@@ -3441,11 +3469,12 @@ export const isFlexResponseResult = <TMethod extends TFlexRequestMethod>(
|
|
|
3441
3469
|
case 'slash.execute':
|
|
3442
3470
|
return isSlashExecutionResult(valueArg);
|
|
3443
3471
|
case 'message.page':
|
|
3472
|
+
case 'message.metadata.page':
|
|
3444
3473
|
return isFlexPlainObject(valueArg)
|
|
3445
3474
|
&& hasFlexExactKeys(valueArg, ['messages', 'startIndex'], ['nextCursor'])
|
|
3446
3475
|
&& Array.isArray(valueArg.messages) && valueArg.messages.length <= 50
|
|
3447
3476
|
&& Number.isSafeInteger(valueArg.startIndex) && (valueArg.startIndex as number) >= 0
|
|
3448
|
-
&& valueArg.messages.every(isFlexPublicMessage)
|
|
3477
|
+
&& valueArg.messages.every(methodArg === 'message.page' ? isFlexPublicMessage : isFlexMessageMetadata)
|
|
3449
3478
|
&& (valueArg.nextCursor === undefined || isBoundedString(valueArg.nextCursor, 4096));
|
|
3450
3479
|
case 'message.get':
|
|
3451
3480
|
return isFlexPublicMessage(valueArg);
|
|
@@ -228,6 +228,12 @@ export interface IFleetEvent {
|
|
|
228
228
|
/** The native session (`harness:nativeId`) a ticket notice addressed. */
|
|
229
229
|
sessionId?: string;
|
|
230
230
|
reportId?: string;
|
|
231
|
+
/**
|
|
232
|
+
* The comment on `ticketKey` that holds this event's whole text, by the id the ticket board returned: a worker's report
|
|
233
|
+
* the fleet commented (`worker.report.mid_turn`, `worker.turn.ended`), the board pump's stall notice, a commented
|
|
234
|
+
* `ticket.changed`. Read it with `ticket_get` and `commentId`. Absent when no comment holds the text.
|
|
235
|
+
*/
|
|
236
|
+
commentId?: string;
|
|
231
237
|
turnId?: string;
|
|
232
238
|
status?: string;
|
|
233
239
|
text?: string;
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
TControllerBrowserPaintAvailability,
|
|
6
6
|
TControllerBrowserPaintTermination,
|
|
7
7
|
} from './browserpaint.js';
|
|
8
|
-
export const controllerProtocolVersion =
|
|
8
|
+
export const controllerProtocolVersion = 46 as const;
|
|
9
9
|
export const controllerUpgradeManagementVersion = 2 as const;
|
|
10
10
|
export const controllerPackageName = 'agl' as const;
|
|
11
11
|
/** The systemd user unit `agl service install` writes; a controller it runs reports it in its status. */
|