agl 41.0.1 → 41.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/changelog.md +11 -0
  2. package/dist_serve/bundle.js +881 -881
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/aglapi/aglapisurface.document.js +6 -2
  6. package/dist_ts/classes.authmodels.d.ts +15 -0
  7. package/dist_ts/classes.authmodels.js +78 -7
  8. package/dist_ts/classes.authstore.js +23 -6
  9. package/dist_ts/classes.cli.js +8 -3
  10. package/dist_ts/classes.controller.d.ts +13 -0
  11. package/dist_ts/classes.controller.js +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 +25 -13
  16. package/dist_ts/classes.processinspection.d.ts +3 -1
  17. package/dist_ts/classes.processinspection.js +4 -2
  18. package/dist_ts/classes.proxycontrol.d.ts +22 -7
  19. package/dist_ts/classes.proxycontrol.js +5 -3
  20. package/dist_ts/classes.proxycontrolclient.d.ts +4 -3
  21. package/dist_ts/classes.proxycontrolclient.js +7 -5
  22. package/dist_ts/classes.proxydaemon.d.ts +6 -4
  23. package/dist_ts/classes.proxydaemon.js +42 -17
  24. package/dist_ts/classes.proxyleaseholder.d.ts +11 -2
  25. package/dist_ts/classes.proxyleaseholder.js +25 -2
  26. package/dist_ts/classes.proxystore.d.ts +3 -2
  27. package/dist_ts/classes.proxystore.js +5 -2
  28. package/dist_ts/classes.upgradecoordinator.d.ts +2 -1
  29. package/dist_ts/classes.upgradecoordinator.js +13 -10
  30. package/dist_ts/classes.upgradetransaction.d.ts +6 -0
  31. package/dist_ts/classes.upgradetransaction.js +38 -13
  32. package/dist_ts/functions.controllermanagementsurface.d.ts +37 -0
  33. package/dist_ts/functions.controllermanagementsurface.js +110 -0
  34. package/dist_ts/functions.inferencecli.d.ts +6 -3
  35. package/dist_ts/functions.inferencecli.js +9 -5
  36. package/dist_ts/functions.inferencecliarguments.d.ts +1 -1
  37. package/dist_ts/functions.inferencecliarguments.js +19 -7
  38. package/dist_ts/functions.inferencelaunch.d.ts +30 -9
  39. package/dist_ts/functions.inferencelaunch.js +49 -23
  40. package/dist_ts/interfaces.auth.d.ts +14 -3
  41. package/dist_ts/interfaces.auth.js +4 -3
  42. package/dist_ts_interfaces/index.d.ts +1 -0
  43. package/dist_ts_interfaces/index.js +2 -1
  44. package/dist_ts_interfaces/interfaces.d.ts +5 -0
  45. package/dist_ts_interfaces/interfaces.js +1 -1
  46. package/dist_ts_interfaces/management.d.ts +33 -0
  47. package/dist_ts_interfaces/management.js +35 -0
  48. package/package.json +2 -1
  49. package/readme.md +110 -26
  50. package/ts/00_commitinfo_data.ts +1 -1
  51. package/ts/aglapi/aglapisurface.document.ts +5 -1
  52. package/ts/classes.authmodels.ts +74 -5
  53. package/ts/classes.authstore.ts +24 -4
  54. package/ts/classes.cli.ts +9 -2
  55. package/ts/classes.controller.ts +98 -59
  56. package/ts/classes.controllermanagement.ts +284 -107
  57. package/ts/classes.inferencegateway.ts +28 -17
  58. package/ts/classes.processinspection.ts +2 -1
  59. package/ts/classes.proxycontrol.ts +25 -9
  60. package/ts/classes.proxycontrolclient.ts +7 -4
  61. package/ts/classes.proxydaemon.ts +43 -17
  62. package/ts/classes.proxyleaseholder.ts +36 -7
  63. package/ts/classes.proxystore.ts +8 -3
  64. package/ts/classes.upgradecoordinator.ts +20 -11
  65. package/ts/classes.upgradetransaction.ts +53 -2
  66. package/ts/functions.controllermanagementsurface.ts +149 -0
  67. package/ts/functions.inferencecli.ts +15 -7
  68. package/ts/functions.inferencecliarguments.ts +16 -6
  69. package/ts/functions.inferencelaunch.ts +57 -23
  70. package/ts/interfaces.auth.ts +14 -3
  71. package/ts_interfaces/index.ts +1 -0
  72. package/ts_interfaces/interfaces.ts +5 -0
  73. package/ts_interfaces/management.ts +52 -0
  74. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -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;
@@ -37,10 +37,41 @@ const claudeNativeLaunch = (optionsArg: IInferenceLaunchOptions & { catalog: { p
37
37
  };
38
38
 
39
39
  /**
40
- * Claude Code on a ChatGPT pool: the proxy's Anthropic Messages endpoint answered by the pool's models, the default model
41
- * for every model alias, the context window the catalog states for it, and the pool's models as the whole `/model`
42
- * picker. Its configuration lives in its own directory, never the host's, so the host's `settings.json` cannot route it
43
- * elsewhere; it sends nothing beyond its model requests: no experimental betas, no nonessential traffic, no attribution.
40
+ * The variables that put Claude Code on a ChatGPT pool: the proxy's Anthropic Messages endpoint and credential, the
41
+ * pool's default model for every model alias, the context window the catalog states for it, and nothing beyond its model
42
+ * requests: no experimental betas, no nonessential traffic, no attribution. `agl proxy run claude --provider openai` and a
43
+ * ChatGPT-pool confined run give Claude Code these alike.
44
+ */
45
+ export const claudeChatGptEnvironment = (inputArg: {
46
+ baseUrl: string;
47
+ token: string;
48
+ models: readonly TInferenceModel[];
49
+ }): Record<string, string> => {
50
+ const model = inputArg.models.find((entryArg) => entryArg.isDefault) ?? inputArg.models[0];
51
+ if (!model) throw new Error('The ChatGPT pool offers no model Claude Code can run on.');
52
+ return {
53
+ ANTHROPIC_BASE_URL: inputArg.baseUrl,
54
+ ANTHROPIC_AUTH_TOKEN: inputArg.token,
55
+ ANTHROPIC_MODEL: model.modelId,
56
+ ANTHROPIC_DEFAULT_OPUS_MODEL: model.modelId,
57
+ ANTHROPIC_DEFAULT_SONNET_MODEL: model.modelId,
58
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: model.modelId,
59
+ ...(model.contextWindow === undefined ? {} : { CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(model.contextWindow) }),
60
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: '1',
61
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
62
+ CLAUDE_CODE_ATTRIBUTION_HEADER: '0',
63
+ };
64
+ };
65
+
66
+ /** Every variable `claudeChatGptEnvironment` may set, the context window's included. */
67
+ const claudeChatGptVariables: readonly string[] = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_MODEL',
68
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
69
+ 'CLAUDE_CODE_MAX_CONTEXT_TOKENS', 'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
70
+ 'CLAUDE_CODE_ATTRIBUTION_HEADER'];
71
+
72
+ /**
73
+ * Claude Code on a ChatGPT pool (`claudeChatGptEnvironment`), with the pool's models as the whole `/model` picker. Its
74
+ * configuration lives in its own directory, never the host's, so the host's `settings.json` cannot route it elsewhere.
44
75
  */
45
76
  export const claudeChatGptLaunch = (inputArg: {
46
77
  baseUrl: string;
@@ -48,21 +79,10 @@ export const claudeChatGptLaunch = (inputArg: {
48
79
  configDirectory: string;
49
80
  models: readonly TInferenceModel[];
50
81
  }): { environment: Record<string, string>; args: string[] } => {
51
- const model = inputArg.models.find((entryArg) => entryArg.isDefault) ?? inputArg.models[0];
52
- if (!model) throw new Error('The ChatGPT pool offers no model Claude Code can run on.');
53
82
  return {
54
83
  environment: {
55
84
  CLAUDE_CONFIG_DIR: inputArg.configDirectory,
56
- ANTHROPIC_BASE_URL: inputArg.baseUrl,
57
- ANTHROPIC_AUTH_TOKEN: inputArg.token,
58
- ANTHROPIC_MODEL: model.modelId,
59
- ANTHROPIC_DEFAULT_OPUS_MODEL: model.modelId,
60
- ANTHROPIC_DEFAULT_SONNET_MODEL: model.modelId,
61
- ANTHROPIC_DEFAULT_HAIKU_MODEL: model.modelId,
62
- ...(model.contextWindow === undefined ? {} : { CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(model.contextWindow) }),
63
- CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: '1',
64
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
65
- CLAUDE_CODE_ATTRIBUTION_HEADER: '0',
85
+ ...claudeChatGptEnvironment(inputArg),
66
86
  },
67
87
  args: ['--settings', JSON.stringify({
68
88
  modelPicker: {
@@ -140,16 +160,23 @@ export const inferenceHarnessLaunch = (optionsArg: IInferenceLaunchOptions): { a
140
160
  return { environment, args: args[0] !== undefined && openCodeCommands.has(args[0]) ? args : [...selection, ...args] };
141
161
  };
142
162
 
163
+ /** A confined run's pool, as its launcher configures Claude Code for it: a ChatGPT-pool run with the pool's models. */
164
+ export type TConfinedLaunchPool = { provider: 'anthropic' } | { provider: 'openai'; models: readonly TInferenceModel[] };
165
+
143
166
  /**
144
167
  * A confined run's `docker run`: the given command, with the confined listener's directory mounted read-only at
145
168
  * `aglProxyConfinedMountPoint` and Claude Code's settings for it added right after `run`, before the launcher's own
146
169
  * options and image. Claude Code inside reaches the proxy through the socket alone (`ANTHROPIC_UNIX_SOCKET`, which
147
170
  * 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.
171
+ * (`ANTHROPIC_AUTH_TOKEN`), and sends nothing beyond its model requests. On the Claude pool it reads the pool's models
172
+ * from the proxy; on the ChatGPT pool it gets the variables `agl proxy run claude --provider openai` gives it
173
+ * (`claudeChatGptEnvironment`: the pool's default model for every alias, its context window), but neither that command's
174
+ * configuration directory, which lies in the AGL home, nor its `/model` picker, an option of the container's command
175
+ * that the launcher does not touch. The container gets each variable by name from the docker command's environment, so
176
+ * the credential never appears on a command line; that environment drops every variable that would give Claude Code
177
+ * another login or route, and on the ChatGPT pool every model variable it sets, so `-e NAME` of one of them passes
178
+ * nothing. The mount is the socket's own directory, which holds nothing else and survives a proxy restart, unlike a
179
+ * mount of the socket file. Only `docker run` and `docker container run` are taken.
153
180
  */
154
181
  export const confinedDockerLaunch = (optionsArg: {
155
182
  args: readonly string[];
@@ -159,6 +186,7 @@ export const confinedDockerLaunch = (optionsArg: {
159
186
  baseUrl: string;
160
187
  token: string;
161
188
  environment: NodeJS.ProcessEnv;
189
+ pool: TConfinedLaunchPool;
162
190
  }): { command: string; args: string[]; environment: NodeJS.ProcessEnv } => {
163
191
  const [command, ...args] = optionsArg.args;
164
192
  const runAt = args[0] === 'run' ? 1 : args[0] === 'container' && args[1] === 'run' ? 2 : 0;
@@ -168,13 +196,19 @@ export const confinedDockerLaunch = (optionsArg: {
168
196
  const directory = path.dirname(optionsArg.socketPath);
169
197
  // `--mount` separates its fields with commas and reads quotes; a directory naming either cannot be passed safely.
170
198
  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> = {
199
+ const socket = `${aglProxyConfinedMountPoint}/${path.basename(optionsArg.socketPath)}`;
200
+ const variables: Record<string, string> = optionsArg.pool.provider === 'anthropic' ? {
172
201
  ...claudeProxyEnvironment(optionsArg.baseUrl),
173
- ANTHROPIC_UNIX_SOCKET: `${aglProxyConfinedMountPoint}/${path.basename(optionsArg.socketPath)}`,
202
+ ANTHROPIC_UNIX_SOCKET: socket,
174
203
  ANTHROPIC_AUTH_TOKEN: optionsArg.token,
204
+ } : {
205
+ ...claudeChatGptEnvironment({ baseUrl: `${optionsArg.baseUrl}/anthropic`, token: optionsArg.token,
206
+ models: optionsArg.pool.models }),
207
+ ANTHROPIC_UNIX_SOCKET: socket,
175
208
  };
176
209
  const environment = { ...optionsArg.environment };
177
210
  for (const name of claudeProxyReservedVariables) delete environment[name];
211
+ if (optionsArg.pool.provider === 'openai') for (const name of claudeChatGptVariables) delete environment[name];
178
212
  return {
179
213
  command,
180
214
  args: [...args.slice(0, runAt),
@@ -7,9 +7,10 @@ export type TControllerAuthLifecycleState = 'setupRequired' | 'ready';
7
7
  export type TWebAuthnCeremonyKind = 'setup' | 'authentication';
8
8
  export type TWebAuthnCeremonyState = 'pending' | 'consumed';
9
9
  /**
10
- * Single source of the audit event vocabulary: the persisted-document
10
+ * Single source of the audit types this release writes: the persisted-document
11
11
  * assertion consumes this list at runtime, so a type added here is
12
- * automatically storable — the two can no longer drift apart.
12
+ * automatically storable with its type-specific checks — the two can no longer
13
+ * drift apart. Other well-formed types are a later release's and still read.
13
14
  */
14
15
  export const controllerAuditEventTypes = [
15
16
  'controller.started',
@@ -249,11 +250,16 @@ export type TControllerAuditSessionId =
249
250
  | IControllerRuntimeId
250
251
  | plugins.browserRuntime.IBrowserClaudeSessionId;
251
252
 
253
+ /**
254
+ * A stored audit event. The audit log is shared with later releases, so a stored `type` is any audit type, also one
255
+ * this release does not know (a later release's, read back after a rollback). This release writes only its own types
256
+ * (`IRecordAuditEventInput`).
257
+ */
252
258
  export interface IControllerAuditEvent {
253
259
  id: string;
254
260
  controllerId: string;
255
261
  timestamp: Date;
256
- type: TControllerAuditEventType;
262
+ type: string;
257
263
  outcome: TControllerAuditOutcome;
258
264
  operationId?: string;
259
265
  peerId?: string;
@@ -263,6 +269,11 @@ export interface IControllerAuditEvent {
263
269
  /** The sender label an agent message declared (`session.message`). */
264
270
  senderLabel?: string;
265
271
  localCli?: IConfigurationLocalCliActor;
272
+ /**
273
+ * What a later release's type records beyond the envelope (bounded plain JSON). This release never writes or
274
+ * interprets it; it is declared so a rolled-back controller still reads and prunes such events.
275
+ */
276
+ details?: Record<string, unknown>;
266
277
  }
267
278
 
268
279
  export interface IControllerTempPasswordDocument {
@@ -9,6 +9,7 @@ export * from './functions.browserpaintcopy.js';
9
9
  export * from './devtools.js';
10
10
  export * from './requests.js';
11
11
  export * from './mcprequests.js';
12
+ export * from './management.js';
12
13
  export * from './codex.js';
13
14
  export * from './mcpworkspacerequests.js';
14
15
  export * from './mcpjobrequests.js';
@@ -325,6 +325,11 @@ export interface IControllerStatus {
325
325
  packageVersion: string;
326
326
  protocolVersion: typeof controllerProtocolVersion;
327
327
  upgradeManagementVersion: number;
328
+ /**
329
+ * The protocol of the controller's loopback management endpoint (`controllerManagementProtocolVersion`); absent
330
+ * from a controller older than the endpoint, which is reached over its socket.
331
+ */
332
+ managementProtocolVersion?: number;
328
333
  controllerPid: number;
329
334
  processGroupId: number;
330
335
  processFingerprint: string;
@@ -0,0 +1,52 @@
1
+ import type { IReq_ControllerStatus } from './requests.status.js';
2
+ import type {
3
+ IReq_ControllerUpgradeFinalize,
4
+ IReq_ControllerUpgradeLaunch,
5
+ IReq_ControllerUpgradePrepareBegin,
6
+ } from './requests.upgrade.js';
7
+
8
+ /**
9
+ * The controller's loopback management endpoint, on the controller's own port: one TypedRequest envelope
10
+ * (`method`, `request`, `response`, `correlation`, `requestInstanceId`) per HTTP POST of `application/json`, with
11
+ * `Host: 127.0.0.1:<port>`. The local CLI and the upgrade worker read the status and drive an upgrade through it,
12
+ * so an upgrade worker of one release can finalize the controller of the next whatever transport the browser
13
+ * speaks. Every later release keeps this path, these methods, their request and response shapes and the envelope
14
+ * (`test/test.controllermanagementwire.node.ts` pins the bytes an AGL 41 client sends and what it accepts).
15
+ */
16
+ export const controllerManagementTypedRequestPath = '/management/typedrequest' as const;
17
+
18
+ /**
19
+ * The protocol of this loopback management endpoint: its path, methods, envelope and their request and response
20
+ * shapes. 1 is the endpoint as its first release ships it; only an incompatible change to that contract raises it.
21
+ * A release declares it twice: as `controllerManagementProtocolVersion` in its `package.json`, which a client reads
22
+ * before the release's controller runs, and as `managementProtocolVersion` in that controller's status. A client
23
+ * reaches a controller over this endpoint only when it declares 1 or more, and over the controller socket otherwise
24
+ * (an older release has no endpoint).
25
+ *
26
+ * It is not `controllerUpgradeManagementVersion` (2), the upgrade contract: every upgrade worker requires exactly
27
+ * that number in its target's `package.json` and status, and upgrade transactions record it, so it cannot move
28
+ * without breaking the online upgrade of every earlier release. This number says only how the management methods
29
+ * are reached.
30
+ */
31
+ export const controllerManagementProtocolVersion = 1 as const;
32
+
33
+ /** The methods the endpoint answers; every other method is refused before it is routed. */
34
+ export const controllerManagementMethods = [
35
+ 'controller.status',
36
+ 'controller.upgrade.launch',
37
+ 'controller.upgrade.prepare.begin',
38
+ 'controller.upgrade.finalize',
39
+ ] as const;
40
+
41
+ export type TControllerManagementMethod = typeof controllerManagementMethods[number];
42
+
43
+ export type TControllerManagementTypedRequest =
44
+ | IReq_ControllerStatus
45
+ | IReq_ControllerUpgradeLaunch
46
+ | IReq_ControllerUpgradePrepareBegin
47
+ | IReq_ControllerUpgradeFinalize;
48
+
49
+ /** A management request carries a token, a version and a mode at most. */
50
+ export const controllerManagementRequestMaximumBodyBytes = 16 * 1024;
51
+ /** The status document with every harness profile and upgrade blocker it may report. */
52
+ export const controllerManagementResponseMaximumBytes = 1024 * 1024;
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '41.0.1',
6
+ version: '41.1.0',
7
7
  description: 'Agent Gateway Layer for OpenCode, FlexHarness, and Codex sessions'
8
8
  }