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
package/readme.md
CHANGED
|
@@ -331,6 +331,7 @@ agl proxy run claude -- # Claude Code on Claude accounts
|
|
|
331
331
|
agl proxy run claude --provider openai --model <catalog-model> --
|
|
332
332
|
agl proxy run opencode --model <catalog-model> -- run 'Explain this repository'
|
|
333
333
|
agl proxy confined run -- docker run --rm --user "$(id -u):$(id -g)" -i <image> claude -p # Claude Code in a container
|
|
334
|
+
agl proxy confined run --provider openai -- docker run --rm --user "$(id -u):$(id -g)" -i <image> claude -p
|
|
334
335
|
agl proxy confined end <run id>
|
|
335
336
|
```
|
|
336
337
|
|
|
@@ -444,7 +445,8 @@ listener, a Unix socket that serves confined runs alone (see [Confined container
|
|
|
444
445
|
- A ChatGPT pool serves `/openai/v1/responses` and `/openai/v1/models` (the pooled Codex catalog in Codex's
|
|
445
446
|
own shape when Codex names its `client_version`), and `/anthropic/v1/messages` and `/anthropic/v1/models`
|
|
446
447
|
for Claude Code on ChatGPT models. Raw Responses requests retain their storage and streaming choices.
|
|
447
|
-
Claude Code's Messages requests are translated by the shared provider package
|
|
448
|
+
Claude Code's Messages requests are translated by the shared provider package (see
|
|
449
|
+
[Claude Code on the ChatGPT pool](#claude-code-on-the-chatgpt-pool));
|
|
448
450
|
`/anthropic/v1/messages/count_tokens` answers 404 there by design, on which Claude Code estimates the count
|
|
449
451
|
itself. Other OpenAI endpoints, such as Chat Completions, and WebSocket inference are not served.
|
|
450
452
|
- A Claude pool serves Claude Code's own requests: `/anthropic/v1/messages`,
|
|
@@ -626,7 +628,15 @@ proxy then keeps the account out of rotation itself until it restarts, and says
|
|
|
626
628
|
The proxy also records the tokens every answer states -- input, output, cache reads and cache writes -- per minute and per
|
|
627
629
|
hour, model, client name, account and the session id the client sent, minutes for a day and hours for eight days, also
|
|
628
630
|
for an Anthropic API key; it keeps no prompt, answer or credential, and a write that fails is reported on its standard
|
|
629
|
-
error and retried with the next.
|
|
631
|
+
error and retried with the next. An answer that does not state its cache reads or writes (a ChatGPT answer without
|
|
632
|
+
`input_tokens_details.cache_write_tokens`, a Claude answer whose `cache_read_input_tokens` or
|
|
633
|
+
`cache_creation_input_tokens` is missing or null) has unknown cache reads or writes, not none: each bucket counts the
|
|
634
|
+
reads and writes its answers stated (`cacheReadTokens`, `cacheWriteTokens`) and, beside them, the answers that did not
|
|
635
|
+
state theirs (`cacheReadUnknownCalls`, `cacheWriteUnknownCalls`). An OpenAI account's unknown cache counts stay in its
|
|
636
|
+
input count, since OpenAI counts every input token there; a Claude account's input leaves the cache out, so a bucket of
|
|
637
|
+
one with unknown cache counts adds up to less than its answers used (`controllerUsageTotalIsLowerBound`). A bucket
|
|
638
|
+
stored before AGL counted them reads with all its answers unknown. The controller reads these only while a browser
|
|
639
|
+
watches a usage window:
|
|
630
640
|
`controller.metrics.usage.watch { window }` (`1h` by minute, `24h` or `7d` by hour) answers the window's buckets with
|
|
631
641
|
a `watchId`, the feed's `epoch` and a `revision`, and the changes follow on that connection alone as `usage.changed`
|
|
632
642
|
events, each continuing the revision before it, until `controller.metrics.usage.unwatch { watchId }` or the
|
|
@@ -640,7 +650,10 @@ is told to watch again and refused. An unreachable proxy refuses a watch with `p
|
|
|
640
650
|
|
|
641
651
|
The web app's header has a **Metrics** button. It shows the token usage in the main pane, as the Board button
|
|
642
652
|
shows the board; leaving it ends its watch. A toggle chooses the last hour, day or week. Totals for input, output,
|
|
643
|
-
cache reads, cache writes and calls head the view;
|
|
653
|
+
cache reads, cache writes and calls head the view; cache reads and writes read **Unknown** when no call stated any, and
|
|
654
|
+
**≥** the stated count when some calls did not, with the number of calls whose count is unknown under the tile (the
|
|
655
|
+
model and session tables say the same, and their **Total** reads **≥** when it leaves out a Claude answer's unknown
|
|
656
|
+
cache count); below them a line chart shows the tokens per minute by model, each
|
|
644
657
|
model's line drawn from zero (each completed minute for the hour, the hourly average for the day and week, the current
|
|
645
658
|
hour's over its minutes so far), and pointing at a moment lists every model's value there with the time. A table lists
|
|
646
659
|
every model the window counted, sorted by total tokens. A model's row opens a second table of its sessions by client and session id. The
|
|
@@ -706,45 +719,112 @@ proxy's binding with it, and the sessions it served move at their next request.
|
|
|
706
719
|
pool, 16 requests in flight per session, 256 per client credential, 512 in all, 32 MiB per request and 8 MiB
|
|
707
720
|
per inspected response event (or non-streaming JSON response).
|
|
708
721
|
|
|
722
|
+
#### Claude Code on the ChatGPT pool
|
|
723
|
+
|
|
724
|
+
Claude Code on the ChatGPT pool -- `agl proxy run claude --provider openai`, a Claude terminal on ChatGPT models, a client
|
|
725
|
+
moved there with `agl proxy credential <client> --provider openai`, or a confined run with `--provider openai` -- sends
|
|
726
|
+
Anthropic Messages requests. The proxy translates each for the ChatGPT backend (`https://chatgpt.com/backend-api/codex/responses`,
|
|
727
|
+
a Responses endpoint) with the Messages translation of `@modelprofile.com/flexharness-providers` and sends it on the
|
|
728
|
+
chosen ChatGPT account's own credential.
|
|
729
|
+
|
|
730
|
+
What the translation adds to a request is what the backend requires, and no text of AGL's own:
|
|
731
|
+
|
|
732
|
+
- Claude Code's system prompt becomes the request's `instructions`, which the backend requires at its top; a system
|
|
733
|
+
message later in the conversation stays in its place as a `developer` message.
|
|
734
|
+
- `store: false` (the backend keeps no response), `stream: true` (an answer is assembled whole for a client that did not
|
|
735
|
+
ask to stream) and, for a model that reasons, `include: ["reasoning.encrypted_content"]`: the model's reasoning comes
|
|
736
|
+
back encrypted and travels in the thinking block's signature, which the proxy signs with a key of its own for the
|
|
737
|
+
account it came from. A thinking block whose signature this proxy did not write for that account is not sent back;
|
|
738
|
+
the answer counts them in `x-flexharness-thinking-dropped`.
|
|
739
|
+
- `prompt_cache_key`, Claude Code's session id (`x-claude-code-session-id`), so the backend's prompt cache keys on the
|
|
740
|
+
conversation; parallel tool calls turned off when Claude Code turns them off; Claude Code's tools as function tools,
|
|
741
|
+
a name of more than 64 characters under a short alias.
|
|
742
|
+
- The account's credential and account id. None of Claude Code's own headers go on, and never the client's credential.
|
|
743
|
+
|
|
744
|
+
What it drops: from a request, `max_tokens` (the backend takes no output limit; the answer says
|
|
745
|
+
`x-flexharness-max-tokens: not-sent`), a thinking token budget (`budget_tokens`, which has no equivalent: the effort
|
|
746
|
+
sets the depth), and Anthropic's own hints -- prompt cache markers, `metadata`, `service_tier`, citations -- which are
|
|
747
|
+
accepted without effect. What cannot be translated faithfully is refused with a 400 that names it: server tools (Claude
|
|
748
|
+
Code then continues without them), server-side context management, code execution containers, remote MCP servers and
|
|
749
|
+
deferred tool loading, whose betas `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` keeps Claude Code from sending. Of an
|
|
750
|
+
answer Claude Code gets the model's text, its tool calls, its reasoning summary as thinking, the stop reason and the
|
|
751
|
+
token counts (input, output, cache reads and writes, a cache count `null` when the backend did not state it); the
|
|
752
|
+
backend's response id, its own headers and the rest of its response do not reach it.
|
|
753
|
+
|
|
754
|
+
Reasoning effort: Claude Code sends its effort as `output_config.effort` (`low` to `max`, chosen with `/effort`,
|
|
755
|
+
`--effort` or `CLAUDE_CODE_EFFORT_LEVEL`), together with adaptive thinking; Claude Code 2.1.288, the release of AGL's
|
|
756
|
+
Agent SDK, sends `high` when none is chosen, for a ChatGPT model's id as well. The translation sends it as the request's
|
|
757
|
+
`reasoning.effort`, fitted to the efforts the model's catalog lists: a model without the requested one gets the nearest
|
|
758
|
+
lower, and the answer names the fit in `x-flexharness-effort-fitted` (`max=>xhigh`). Thinking asks for the reasoning's
|
|
759
|
+
summary (`reasoning.summary: auto`). A request without an effort sends none, which leaves the depth to the backend.
|
|
760
|
+
|
|
761
|
+
The ChatGPT account's own side: OpenAI documents Codex's personalization as its client's -- personal instructions in
|
|
762
|
+
the Codex home's global `AGENTS.md`, memories in a local store under the Codex home (off by default) -- and ChatGPT's
|
|
763
|
+
own memory as ChatGPT on the web's (learn.chatgpt.com, *Personalize ChatGPT* and *Memories*, read 2026-10-26). On this
|
|
764
|
+
route no Codex client and no Codex home take part: a request carries Claude Code's own system prompt and messages, names
|
|
765
|
+
no personality and no stored response, and carries nothing of the account but its credential and id. Whether the
|
|
766
|
+
ChatGPT backend adds anything of the account's side -- custom instructions, memory or other personalization -- to a
|
|
767
|
+
request on its own side cannot be seen in the request or its answer, so AGL cannot know it beyond what OpenAI documents.
|
|
768
|
+
|
|
709
769
|
#### Confined containers
|
|
710
770
|
|
|
711
771
|
`agl proxy confined run` runs Claude Code in a Docker container that reaches AGL through one route only: the proxy's
|
|
712
772
|
confined listener, a Unix socket alone in a directory of its own (`agl-proxy-confined/proxy.sock` beside the proxy's
|
|
713
|
-
control socket; `agl proxy status` names it).
|
|
773
|
+
control socket; `agl proxy status` names it). Each run is served from one pool, chosen when it starts: the Claude pool,
|
|
774
|
+
or with `--provider openai` the ChatGPT pool. The listener serves the three endpoints Claude Code uses --
|
|
714
775
|
`GET /anthropic/v1/models`, `POST /anthropic/v1/messages` and `POST /anthropic/v1/messages/count_tokens` -- exactly as
|
|
715
|
-
the loopback listener serves
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
776
|
+
the loopback listener serves Claude Code on the run's pool: on the Claude pool sent on to Anthropic as Claude Code sent
|
|
777
|
+
them, on the ChatGPT pool translated (see [Claude Code on the ChatGPT pool](#claude-code-on-the-chatgpt-pool), where
|
|
778
|
+
`count_tokens` answers 404 and Claude Code estimates). It refuses every other path and method with 404 before anything
|
|
779
|
+
is read or routed, every `/openai/v1/` path of a ChatGPT-pool run among them; nothing else of AGL is reachable through
|
|
780
|
+
it. It accepts confined runs' credentials alone: a request without a credential, with one the proxy does not know, or
|
|
781
|
+
with a credential of the loopback listener (any client's or lease's) is refused with 401, and a confined run's
|
|
782
|
+
credential is refused on the loopback listener alike.
|
|
719
783
|
|
|
720
784
|
```sh
|
|
721
785
|
agl proxy confined run -- docker run --rm --user "$(id -u):$(id -g)" -i <image> \
|
|
722
786
|
claude -p --model claude-opus-5-5 --output-format json < prompt.txt
|
|
723
|
-
agl proxy
|
|
787
|
+
agl proxy confined run --provider openai -- docker run --rm --user "$(id -u):$(id -g)" -i <image> \
|
|
788
|
+
claude -p --output-format json < prompt.txt # on the ChatGPT pool's default model
|
|
789
|
+
agl proxy status # names the confined listener and lists the runs by id, with their pools
|
|
724
790
|
agl proxy confined end <run id> # ends a run's credential at once
|
|
725
791
|
```
|
|
726
792
|
|
|
727
793
|
Before the container starts, the command asks the proxy for the run's credential: one for this run alone, a lease of
|
|
728
|
-
the
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
794
|
+
the run's pool whose sessions are those of the client `confined` on that pool, so its requests are placed, moved and
|
|
795
|
+
counted (token usage under `confined`) like any other client's of that pool. Without `--provider`, or with
|
|
796
|
+
`--provider anthropic`, it is a lease of the Claude pool (the Claude accounts first, then the Anthropic API keys),
|
|
797
|
+
exactly as before ChatGPT-pool runs. With `--provider openai` it is a lease of the ChatGPT pool as
|
|
798
|
+
`agl proxy run claude --provider openai` takes one: its translated Messages requests go to the pool's ChatGPT accounts
|
|
799
|
+
alone, never to an OpenAI API key (see [ChatGPT accounts first, then OpenAI API keys](#chatgpt-accounts-first-then-openai-api-keys)),
|
|
800
|
+
and the proxy hands the run the pool's models with it. Right after `docker run` the command adds the listener's
|
|
801
|
+
directory as a read-only mount at `/run/agl-proxy` and what Claude Code inside needs to reach it, each variable passed
|
|
802
|
+
by name so the credential appears on no command line:
|
|
732
803
|
|
|
733
804
|
- `ANTHROPIC_UNIX_SOCKET=/run/agl-proxy/proxy.sock`: Claude Code sends its API requests through the socket, as plain
|
|
734
805
|
HTTP, so no relay runs in the container (Claude Code's own setting, which `claude ssh` uses; verified with Claude Code
|
|
735
806
|
2.1.288, the release of AGL's Agent SDK);
|
|
736
807
|
- `ANTHROPIC_BASE_URL=http://localhost/anthropic`: the path Claude Code puts on its requests; the host is not used;
|
|
737
808
|
- `ANTHROPIC_AUTH_TOKEN`: the run's credential;
|
|
738
|
-
- `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` and `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`, as
|
|
739
|
-
claude` sets them
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
809
|
+
- on the Claude pool, `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` and `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`, as
|
|
810
|
+
`agl proxy run claude` sets them;
|
|
811
|
+
- on the ChatGPT pool, the variables `agl proxy run claude --provider openai` sets: the pool's default model as
|
|
812
|
+
`ANTHROPIC_MODEL` and for every alias (`ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`,
|
|
813
|
+
`ANTHROPIC_DEFAULT_HAIKU_MODEL`), the context window the catalog states for it (`CLAUDE_CODE_MAX_CONTEXT_TOKENS`),
|
|
814
|
+
`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1`, `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` and
|
|
815
|
+
`CLAUDE_CODE_ATTRIBUTION_HEADER=0`. Two things of that command stay on the host: its Claude Code configuration
|
|
816
|
+
directory, which lies in the AGL home, and its `/model` picker of the pool's models, an option of Claude Code's
|
|
817
|
+
command line, which the launcher leaves as the container's command names it. Name another of the pool's models with
|
|
818
|
+
Claude Code's own `--model` in the container's command.
|
|
819
|
+
|
|
820
|
+
It prints the run's id on standard error (`AGL proxy confined run <id>: …`; a ChatGPT-pool run's line says
|
|
821
|
+
`served from the ChatGPT pool`), passes its standard input and output through, and exits with the container's status. The
|
|
822
|
+
container never receives an account token, the control socket or any other part of the AGL home: the mount holds the
|
|
823
|
+
socket alone, and the variables that would give Claude Code another login or route (`CLAUDE_CODE_OAUTH_TOKEN`,
|
|
824
|
+
`ANTHROPIC_API_KEY`, …), and on the ChatGPT pool every model variable above, are removed from the environment `docker`
|
|
825
|
+
runs with, so a `-e NAME` of one of them passes nothing. What the launcher names itself after `docker run` is its own:
|
|
826
|
+
it must not set the variables above. The command takes `docker run` and `docker container run` only, with the image and
|
|
827
|
+
its command after the options.
|
|
748
828
|
|
|
749
829
|
The socket and its directory are the proxy user's alone (0600 and 0700), so the container's processes connect as that
|
|
750
830
|
user: `--user` with its uid and gid under a Docker daemon that runs as root, or the container's own root under rootless
|
|
@@ -760,10 +840,12 @@ serves it again at once while the command takes it back. `agl proxy confined end
|
|
|
760
840
|
flight end, the next one is refused, and the command stops the container (`docker run` passes its SIGTERM on) and exits
|
|
761
841
|
with the reason; it does the same when the proxy does not hand the credential back after a restart.
|
|
762
842
|
|
|
763
|
-
Downgrade: an earlier proxy refuses to start while its store keeps a confined run's credential
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
843
|
+
Downgrade: an earlier proxy refuses to start while its store keeps a confined run's credential its check of the kept
|
|
844
|
+
leases does not know: any confined run's for a release before confined runs, a ChatGPT-pool run's for one before
|
|
845
|
+
ChatGPT-pool runs (41.0.1 and earlier). Let every confined run end before downgrading; if an earlier proxy's start
|
|
846
|
+
already failed, start this release once with no confined run going and wait out the 60-second reclaim window, which
|
|
847
|
+
ends the kept credential, then downgrade. A launcher older than ChatGPT-pool runs still runs Claude-pool runs on this
|
|
848
|
+
proxy; an older proxy refuses this release's `--provider openai`, as it issues no ChatGPT-pool run.
|
|
767
849
|
|
|
768
850
|
## Native Codex Harness
|
|
769
851
|
|
|
@@ -2836,6 +2918,7 @@ The upgrade worker:
|
|
|
2836
2918
|
- Restarts `agl-proxy.service` onto the new release once the target controller is up, when the unit is installed and running and runs the pnpm-global `agl` shim, and waits until the proxy answers as the new release and its credential launcher, run with an empty environment as a harness runs it, runs the new release too. A unit that is not installed or not running is left alone. A restart that fails does not fail the upgrade: it is logged as a warning and appended to the completion message (`message` in `--json` output).
|
|
2837
2919
|
- Moves the accounts service onto the authswitch the new AGL brings, with authswitch's own `authority service stop`, `install` and `start`: the service is stopped before the package changes and started again, on the new release, when it ran before. A release that declares no `authswitch` command is refused before anything is stopped. A host without the service's unit keeps it that way, and the service's store is never touched. Stopping the service also stops the managed Codex runtimes it runs; they start again when next asked for. The log names what was stopped and started; a failed upgrade restores the previous AGL first, then the running service, and each step is recorded in the upgrade transaction, so recovering a lost worker finishes or undoes it.
|
|
2838
2920
|
- Preserves its transaction across worker or controller failure.
|
|
2921
|
+
- Reads the controller's status, prepares it and finalizes it through the controller's loopback management endpoint (an HTTP `POST` of one TypedRequest to `/management/typedrequest` on the controller's port, addressed to `127.0.0.1:<port>`), never through the browser's WebSocket, so the worker of one release finalizes the controller of the next whatever transport that release's browser speaks. `agl status` and `agl upgrade` read their own release's controller the same way. The endpoint is used only for a controller that declares it: before the controller runs, by `controllerManagementProtocolVersion` in the `package.json` of the installed release that starts it, and once it runs, by `managementProtocolVersion` in its status. A controller that declares neither, as every release before the endpoint, such as a source an upgrade recovery restores, is reached over its controller socket, which keeps serving the status read and the upgrade methods for the workers of earlier releases. Nothing is tried over one transport and then the other.
|
|
2839
2922
|
|
|
2840
2923
|
The cohort is every conversation with an AGL prompt that is queued or still running when the upgrade starts. AGL seals prompt admission, so no new message is accepted and no queue starts a turn, and stops each of them, as **Stop** does:
|
|
2841
2924
|
|
|
@@ -2857,6 +2940,16 @@ Upgrade coordination lives under `$AGL_HOME/upgrade`. Private logs live under `$
|
|
|
2857
2940
|
|
|
2858
2941
|
A failure reference from the Errors dialog appears in those logs on the line the controller wrote when the failure happened, together with the operation and the cause. The journal the dialog reads it from is in memory only, so it is gone after a controller restart while the log line remains.
|
|
2859
2942
|
|
|
2943
|
+
### Rolling back from a later release
|
|
2944
|
+
|
|
2945
|
+
A later release keeps a port's trusted-proxy secret sealed in the controller's database and never exports it, and `agl remote set` there can store gateway sources, which 41.1.x cannot enforce. To roll back a port that a later release serves behind a TLS reverse proxy:
|
|
2946
|
+
|
|
2947
|
+
1. On the later release, stop the controller and run `agl remote unset --port <port>`. It moves the port back to `http://localhost:<port>` and removes its passkey, so the next start prints a setup code. A 41.1.x controller refuses to start on a configuration with gateway sources, and its refusal names this command.
|
|
2948
|
+
2. Roll back and start the controller.
|
|
2949
|
+
3. To serve 41.1.x behind the proxy again, generate a new secret and set it both on the proxy route (`X-Harness-Controller-Proxy-Secret`) and in `AGL_TRUSTED_PROXY_SECRET`, as in [Remote Browser Access](#remote-browser-access). 41.1.x fixes a port's public origin and TLS mode on its first start, so that is a port that has not been started yet.
|
|
2950
|
+
|
|
2951
|
+
Audit events of types a later release wrote stay in the audit log and are pruned with the others.
|
|
2952
|
+
|
|
2860
2953
|
### Migrating From `hcon`
|
|
2861
2954
|
|
|
2862
2955
|
Starting from `@modelprofile.com/harness-controller@20.0.2`, run the active command twice:
|
|
@@ -2989,13 +3082,15 @@ Backend isolation is mandatory. Proxy mode binds the controller to all interface
|
|
|
2989
3082
|
- Local mode binds the controller to loopback only.
|
|
2990
3083
|
- The private OpenCode server always remains on loopback and uses a generated per-process password.
|
|
2991
3084
|
- Application RPC starts only after the exact TypedSocket package-major handshake.
|
|
3085
|
+
- The management endpoint (`/management/typedrequest`, used by the CLI and the upgrade worker) answers only an HTTP `POST` of `application/json` from a loopback peer (`127.0.0.1` or `::1`), addressed to `127.0.0.1:<port>`, with no `Origin` and no forwarding header (`Forwarded`, `X-Forwarded-*`, `X-Real-IP`, `X-Harness-Controller-Proxy-Secret`), so a browser or a request a proxy forwards is refused, also in proxy mode. It serves only the status read and the upgrade methods; each upgrade method authorises itself against the upgrade token and its one-time grant, as it does over the socket, and every upgrade request counts against the controller's global unauthenticated preflight limit.
|
|
2992
3086
|
- Client-managed connection tags are disabled; authentication and routing use server-owned peer state.
|
|
2993
3087
|
- Setup attempts are globally rate limited before enrollment.
|
|
2994
3088
|
- Permission auto-accept is an explicit server policy, never an assumption based on an open browser.
|
|
2995
3089
|
- Questions are never auto-answered.
|
|
2996
3090
|
- Unknown, malformed, concurrent, or unverifiable lifecycle state fails closed.
|
|
2997
3091
|
- The inference proxy's confined listener is a Unix socket of the proxy's user alone; it accepts only confined runs'
|
|
2998
|
-
credentials and serves them only
|
|
3092
|
+
credentials and serves them only Claude Code's three endpoints of the run's pool, the Claude pool or, translated, the
|
|
3093
|
+
ChatGPT pool's ChatGPT accounts.
|
|
2999
3094
|
|
|
3000
3095
|
The global **auto-accept permissions (yolo)** setting replies once to pending permissions across registered projects and writes those replies to the audit log. Each conversation also has a run-local Ask/Yolo switch. For an active managed OpenCode root, that switch covers its currently active direct children under the same exact family authority. For an active managed Flex root, it covers admitted descendants through their exact active parent-run authority chain; a relationship alone grants nothing. Terminal children are never auto-accepted. Disabling either policy seals new automatic replies and drains already admitted replies before returning. Neither mode creates persistent broad provider grants.
|
|
3001
3096
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3765,11 +3765,15 @@ export const AGL_API_SURFACE: plugins.flexTypedRequest.IFlexTypedRequestSurface
|
|
|
3765
3765
|
},
|
|
3766
3766
|
"protocolVersion": {
|
|
3767
3767
|
"type": "number",
|
|
3768
|
-
"const":
|
|
3768
|
+
"const": 47
|
|
3769
3769
|
},
|
|
3770
3770
|
"upgradeManagementVersion": {
|
|
3771
3771
|
"type": "number"
|
|
3772
3772
|
},
|
|
3773
|
+
"managementProtocolVersion": {
|
|
3774
|
+
"type": "number",
|
|
3775
|
+
"description": "The protocol of the controller's loopback management endpoint (`controllerManagementProtocolVersion`); absent from a controller older than the endpoint, which is reached over its socket."
|
|
3776
|
+
},
|
|
3773
3777
|
"controllerPid": {
|
|
3774
3778
|
"type": "number"
|
|
3775
3779
|
},
|
|
@@ -3887,7 +3891,7 @@ export const AGL_API_SURFACE: plugins.flexTypedRequest.IFlexTypedRequestSurface
|
|
|
3887
3891
|
],
|
|
3888
3892
|
"additionalProperties": false
|
|
3889
3893
|
},
|
|
3890
|
-
"declaration": "interface IControllerStatus {\n packageName: string;\n packageVersion: string;\n protocolVersion:
|
|
3894
|
+
"declaration": "interface IControllerStatus {\n packageName: string;\n packageVersion: string;\n protocolVersion: 47;\n upgradeManagementVersion: number;\n /**\n * The protocol of the controller's loopback management endpoint (`controllerManagementProtocolVersion`); absent\n * from a controller older than the endpoint, which is reached over its socket.\n */\n managementProtocolVersion?: number;\n controllerPid: number;\n processGroupId: number;\n processFingerprint: string;\n processMode: TControllerProcessMode;\n lifecycleState: TControllerLifecycleState;\n startedAt: number;\n setupRequired: boolean;\n /** Present when the controller runs as the systemd user unit (`agl service install`); absent for a detached or foreground one. */\n serviceUnit?: \"agl-controller.service\";\n /** The TLS mode of the runtime configuration the controller runs with; absent until it is resolved. */\n tlsMode?: TControllerTlsMode;\n upgrade?: IControllerUpgradeStatus;\n /** AGL's inference proxy, by state only; details need a signed-in owner (`controller.proxy.status.get`). */\n proxy?: { state: TControllerProxyState };\n harnesses: Array<{\n harnessId: TControllerSessionHarnessId;\n pid?: number;\n state: TControllerHarnessLifecycleState;\n healthy: boolean;\n version?: string;\n supportsLocalAttachments?: boolean;\n connectionMode?: 'local' | 'shared' | 'remote';\n diagnostic?: IControllerCodexDiagnostic;\n /** Codex only: automatic reconnection of this entry's profile to the local shared app-server. */\n rejoin?: IControllerCodexRejoinStatus;\n /**\n * Codex only, on the controller status: every connected or required profile. The entry's\n * `healthy` is then true only when each of them is, apart from a profile on an account Codex\n * cannot run on now, listed `stopped` with an `account_unavailable` diagnostic that says why;\n * its other members describe the default profile.\n */\n profiles?: IControllerCodexProfileStatus[];\n }>;\n}"
|
|
3891
3895
|
},
|
|
3892
3896
|
{
|
|
3893
3897
|
"name": "IControllerTerminalResource",
|
package/ts/classes.authmodels.ts
CHANGED
|
@@ -69,6 +69,41 @@ const credentialTransports = new Set([
|
|
|
69
69
|
'usb',
|
|
70
70
|
]);
|
|
71
71
|
const auditEventTypes: ReadonlySet<string> = new Set(controllerAuditEventTypes);
|
|
72
|
+
/**
|
|
73
|
+
* Any release's audit type, as the known ones are spelled: two or more dot-separated segments of lowercase letters,
|
|
74
|
+
* digits and inner hyphens, each starting with a letter (`session.pendingprompt.edit`,
|
|
75
|
+
* `provider.connection.activate-opencode`), at most 128 characters.
|
|
76
|
+
*/
|
|
77
|
+
const auditEventTypePattern = /^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z](?:[a-z0-9-]*[a-z0-9])?)+$/;
|
|
78
|
+
const auditEventTypeMaxLength = 128;
|
|
79
|
+
const auditDetailsMaxDepth = 8;
|
|
80
|
+
const auditDetailsMaxBytes = 4096;
|
|
81
|
+
|
|
82
|
+
/** Plain JSON: plain objects, dense arrays, strings, finite numbers, booleans and null, nested at most `depthArg` deep. */
|
|
83
|
+
const isBoundedPlainJson = (valueArg: unknown, depthArg: number): boolean => {
|
|
84
|
+
if (valueArg === null || typeof valueArg === 'string' || typeof valueArg === 'boolean') return true;
|
|
85
|
+
if (typeof valueArg === 'number') return Number.isFinite(valueArg);
|
|
86
|
+
if (depthArg < 1) return false;
|
|
87
|
+
if (Array.isArray(valueArg)) {
|
|
88
|
+
if (Object.getPrototypeOf(valueArg) !== Array.prototype) return false;
|
|
89
|
+
for (let index = 0; index < valueArg.length; index++) {
|
|
90
|
+
if (!Object.hasOwn(valueArg, index) || !isBoundedPlainJson(valueArg[index], depthArg - 1)) return false;
|
|
91
|
+
}
|
|
92
|
+
return Object.keys(valueArg).length === valueArg.length;
|
|
93
|
+
}
|
|
94
|
+
return isPlainObject(valueArg)
|
|
95
|
+
&& Object.getOwnPropertySymbols(valueArg).length === 0
|
|
96
|
+
&& Object.values(valueArg).every((entryArg) => isBoundedPlainJson(entryArg, depthArg - 1));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* An audit event's `details`: this release never writes or reads them; a later release records what its own types
|
|
101
|
+
* add there. Bounded plain JSON: an object at most eight levels deep and 4 KiB serialized.
|
|
102
|
+
*/
|
|
103
|
+
const isAuditDetails = (valueArg: unknown): valueArg is Record<string, unknown> =>
|
|
104
|
+
isPlainObject(valueArg)
|
|
105
|
+
&& isBoundedPlainJson(valueArg, auditDetailsMaxDepth)
|
|
106
|
+
&& Buffer.byteLength(JSON.stringify(valueArg), 'utf8') <= auditDetailsMaxBytes;
|
|
72
107
|
const auditOutcomes = new Set(['attempted', 'succeeded', 'denied', 'failed']);
|
|
73
108
|
const auditRetentionSeconds = 90 * 24 * 60 * 60;
|
|
74
109
|
|
|
@@ -206,10 +241,29 @@ const isStoredCredential = (valueArg: unknown): valueArg is IStoredPasskeyCreden
|
|
|
206
241
|
&& isBase64Url(valueArg.counterUpdateId, 32);
|
|
207
242
|
};
|
|
208
243
|
|
|
244
|
+
/**
|
|
245
|
+
* The stored runtime configuration names `gatewaySources`, which only a later release writes (`agl remote set` with
|
|
246
|
+
* gateway sources). This release cannot enforce them, so it refuses the document; the message names the one remedy.
|
|
247
|
+
*/
|
|
248
|
+
export class ControllerRuntimeConfigFromLaterReleaseError extends Error {
|
|
249
|
+
constructor(controllerPortArg: unknown) {
|
|
250
|
+
const port = isValidPort(controllerPortArg) ? String(controllerPortArg) : '<port>';
|
|
251
|
+
super(
|
|
252
|
+
`The stored runtime configuration of controller:${port} names gatewaySources, which a later AGL release set `
|
|
253
|
+
+ 'and this release cannot enforce, so it does not start. Run '
|
|
254
|
+
+ `\`agl remote unset --port ${port}\` on the later release before rolling back.`,
|
|
255
|
+
);
|
|
256
|
+
this.name = 'ControllerRuntimeConfigFromLaterReleaseError';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
209
260
|
export const assertControllerAuthDocument = (
|
|
210
261
|
valueArg: unknown,
|
|
211
262
|
): asserts valueArg is IControllerAuthDocument => {
|
|
212
263
|
if (!isPlainObject(valueArg)) throw new Error('Invalid controller authentication document.');
|
|
264
|
+
if (isPlainObject(valueArg.config) && Object.hasOwn(valueArg.config, 'gatewaySources')) {
|
|
265
|
+
throw new ControllerRuntimeConfigFromLaterReleaseError(valueArg.config.controllerPort);
|
|
266
|
+
}
|
|
213
267
|
if (
|
|
214
268
|
!hasExactKeys(valueArg, [
|
|
215
269
|
'id',
|
|
@@ -315,36 +369,48 @@ export const assertWebAuthnCeremonyDocument = (
|
|
|
315
369
|
}
|
|
316
370
|
};
|
|
317
371
|
|
|
372
|
+
/**
|
|
373
|
+
* A stored audit event. The log is shared with later releases, so a type this release does not know (a later
|
|
374
|
+
* release's, read back after a rollback) is accepted when it is spelled as an audit type: its envelope is checked as
|
|
375
|
+
* strictly as a known type's, and only the rules that tie a field to particular types (`senderLabel` to
|
|
376
|
+
* `session.message`, `localCli` to `configuration.*`) are left to the release that knows the type. A known type keeps
|
|
377
|
+
* every check. `details` is bounded plain JSON on any type.
|
|
378
|
+
*/
|
|
318
379
|
export const assertControllerAuditEventDocument = (
|
|
319
380
|
valueArg: unknown,
|
|
320
381
|
): asserts valueArg is IControllerAuditEvent => {
|
|
321
382
|
if (!isPlainObject(valueArg)) throw new Error('Invalid controller audit event.');
|
|
383
|
+
const type = valueArg.type;
|
|
384
|
+
if (typeof type !== 'string' || type.length > auditEventTypeMaxLength || !auditEventTypePattern.test(type)) {
|
|
385
|
+
throw new Error('Invalid controller audit event.');
|
|
386
|
+
}
|
|
387
|
+
const known = auditEventTypes.has(type);
|
|
322
388
|
if (
|
|
323
389
|
!hasExactKeys(
|
|
324
390
|
valueArg,
|
|
325
391
|
['id', 'controllerId', 'timestamp', 'type', 'outcome'],
|
|
326
|
-
['operationId', 'peerId', 'credentialId', 'sessionId', 'requestId', 'senderLabel', 'localCli'],
|
|
392
|
+
['operationId', 'peerId', 'credentialId', 'sessionId', 'requestId', 'senderLabel', 'localCli', 'details'],
|
|
327
393
|
)
|
|
328
394
|
|| !isBase64Url(valueArg.id, 32)
|
|
329
395
|
|| !isNonEmptyString(valueArg.controllerId)
|
|
330
396
|
|| !/^controller:[1-9][0-9]{0,4}$/.test(valueArg.controllerId)
|
|
331
397
|
|| Number(valueArg.controllerId.slice('controller:'.length)) > 65535
|
|
332
398
|
|| !isValidDate(valueArg.timestamp)
|
|
333
|
-
|| !auditEventTypes.has(valueArg.type as string)
|
|
334
399
|
|| !auditOutcomes.has(valueArg.outcome as string)
|
|
335
400
|
|| (
|
|
336
401
|
Object.hasOwn(valueArg, 'operationId')
|
|
337
|
-
&& !isBase64Url(valueArg.operationId,
|
|
402
|
+
&& !isBase64Url(valueArg.operationId, type === 'resource.browser.operation' ? 18 : 24)
|
|
338
403
|
)
|
|
339
404
|
|| (
|
|
340
405
|
Object.hasOwn(valueArg, 'senderLabel')
|
|
341
|
-
&& (
|
|
406
|
+
&& ((known && type !== 'session.message') || !isControllerAgentMessageSender(valueArg.senderLabel))
|
|
342
407
|
)
|
|
408
|
+
|| (Object.hasOwn(valueArg, 'details') && !isAuditDetails(valueArg.details))
|
|
343
409
|
) {
|
|
344
410
|
throw new Error('Invalid controller audit event.');
|
|
345
411
|
}
|
|
346
412
|
if (Object.hasOwn(valueArg, 'localCli')) {
|
|
347
|
-
if (!
|
|
413
|
+
if (known && !type.startsWith('configuration.')) throw new Error('Invalid local configuration audit event.');
|
|
348
414
|
assertConfigurationLocalCliActor(valueArg.localCli);
|
|
349
415
|
}
|
|
350
416
|
for (const optionalKey of [
|
|
@@ -1919,4 +1985,7 @@ export class ControllerAuditEventModel
|
|
|
1919
1985
|
|
|
1920
1986
|
@plugins.nosqldb.svDb()
|
|
1921
1987
|
public localCli?: IControllerAuditEvent['localCli'];
|
|
1988
|
+
|
|
1989
|
+
@plugins.nosqldb.svDb()
|
|
1990
|
+
public details?: IControllerAuditEvent['details'];
|
|
1922
1991
|
}
|
package/ts/classes.authstore.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
import {
|
|
45
45
|
ControllerAuditEventModel,
|
|
46
46
|
ControllerAuthModel,
|
|
47
|
+
ControllerRuntimeConfigFromLaterReleaseError,
|
|
47
48
|
ControllerPendingPromptModel,
|
|
48
49
|
ControllerProjectModel,
|
|
49
50
|
ControllerResourceModel,
|
|
@@ -202,6 +203,24 @@ const auditPruneMaxBatchesPerSweep = 50;
|
|
|
202
203
|
const auditCountPruneMaxBatchesPerSweep = 250;
|
|
203
204
|
const auditPruneIntervalMs = 30 * 60 * 1000;
|
|
204
205
|
|
|
206
|
+
/**
|
|
207
|
+
* The stored authentication document of `controllerIdArg`. A runtime configuration only a later release writes is
|
|
208
|
+
* refused with its own reason (`ControllerRuntimeConfigFromLaterReleaseError`), not the client's generic assertion
|
|
209
|
+
* failure, so the operator reads the remedy.
|
|
210
|
+
*/
|
|
211
|
+
const findStoredAuthDocument = async (
|
|
212
|
+
controllerIdArg: string,
|
|
213
|
+
): Promise<plugins.nosqldb.TStoredDocument<IControllerAuthDocument> | null> => {
|
|
214
|
+
try {
|
|
215
|
+
return await ControllerAuthModel.exact.findStoredOne({ id: controllerIdArg });
|
|
216
|
+
} catch (errorArg) {
|
|
217
|
+
if (errorArg instanceof Error && errorArg.cause instanceof ControllerRuntimeConfigFromLaterReleaseError) {
|
|
218
|
+
throw errorArg.cause;
|
|
219
|
+
}
|
|
220
|
+
throw errorArg;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
205
224
|
/** A concurrent prune that removed an event first leaves nothing to do for it. */
|
|
206
225
|
const deleteStoredAuditEvents = async (
|
|
207
226
|
eventsArg: Array<plugins.nosqldb.TStoredDocument<IControllerAuditEvent>>,
|
|
@@ -1306,7 +1325,7 @@ export class SmartDataAuthStore implements IAuthStore, IAuthStoreSessionIdentity
|
|
|
1306
1325
|
throw new Error('controllerPort must be an integer between 1 and 65535.');
|
|
1307
1326
|
}
|
|
1308
1327
|
this.controllerId = `controller:${controllerPortArg}`;
|
|
1309
|
-
const stored = await
|
|
1328
|
+
const stored = await findStoredAuthDocument(this.controllerId);
|
|
1310
1329
|
if (!stored) return undefined;
|
|
1311
1330
|
return cloneRuntimeConfig(ControllerAuthModel.exact.toPersisted(stored).config);
|
|
1312
1331
|
}
|
|
@@ -1336,7 +1355,7 @@ export class SmartDataAuthStore implements IAuthStore, IAuthStoreSessionIdentity
|
|
|
1336
1355
|
}
|
|
1337
1356
|
const controllerId = `controller:${runtimeConfigArg.controllerPort}`;
|
|
1338
1357
|
this.controllerId = controllerId;
|
|
1339
|
-
const existing = await
|
|
1358
|
+
const existing = await findStoredAuthDocument(controllerId);
|
|
1340
1359
|
if (existing) {
|
|
1341
1360
|
const document = ControllerAuthModel.exact.toPersisted(existing);
|
|
1342
1361
|
return this.finalizeRuntimeConfigDocument(document, runtimeConfigArg);
|
|
@@ -1356,7 +1375,7 @@ export class SmartDataAuthStore implements IAuthStore, IAuthStoreSessionIdentity
|
|
|
1356
1375
|
return this.finalizeRuntimeConfigDocument(document, runtimeConfigArg);
|
|
1357
1376
|
} catch (errorArg) {
|
|
1358
1377
|
if (!isAmbiguousWrite(errorArg)) throw errorArg;
|
|
1359
|
-
const reconciled = await
|
|
1378
|
+
const reconciled = await findStoredAuthDocument(controllerId);
|
|
1360
1379
|
if (!reconciled) {
|
|
1361
1380
|
throw new AuthError(
|
|
1362
1381
|
'ambiguous_write',
|
|
@@ -2326,7 +2345,7 @@ export class SmartDataAuthStore implements IAuthStore, IAuthStoreSessionIdentity
|
|
|
2326
2345
|
if (!this.controllerId) {
|
|
2327
2346
|
throw new AuthError('not_initialized', 'The controller runtime configuration is unresolved.');
|
|
2328
2347
|
}
|
|
2329
|
-
const stored = await
|
|
2348
|
+
const stored = await findStoredAuthDocument(this.controllerId);
|
|
2330
2349
|
if (!stored) {
|
|
2331
2350
|
throw new AuthError('not_initialized', 'The controller authentication document is missing.');
|
|
2332
2351
|
}
|
|
@@ -5297,6 +5316,7 @@ export class SmartDataAuthStore implements IAuthStore, IAuthStoreSessionIdentity
|
|
|
5297
5316
|
&& leftArg.credentialId === rightArg.credentialId
|
|
5298
5317
|
&& leftArg.senderLabel === rightArg.senderLabel
|
|
5299
5318
|
&& JSON.stringify(leftArg.localCli) === JSON.stringify(rightArg.localCli)
|
|
5319
|
+
&& JSON.stringify(leftArg.details) === JSON.stringify(rightArg.details)
|
|
5300
5320
|
&& (
|
|
5301
5321
|
leftArg.sessionId === undefined
|
|
5302
5322
|
? rightArg.sessionId === undefined
|
package/ts/classes.cli.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
controllerProtocolVersion,
|
|
7
7
|
controllerServiceUnitName,
|
|
8
8
|
controllerUpgradeManagementVersion,
|
|
9
|
+
controllerManagementProtocolVersion,
|
|
9
10
|
configurationOwnerTokenEnvironmentVariable,
|
|
10
11
|
describeBrowserPaintAvailability,
|
|
11
12
|
type IControllerStatus,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
stopRunningController,
|
|
40
41
|
tryQueryControllerStatus as tryQueryManagedControllerStatus,
|
|
41
42
|
waitUntilControllerExited,
|
|
43
|
+
type IControllerStatusQuery,
|
|
42
44
|
} from './classes.controllermanagement.js';
|
|
43
45
|
import {
|
|
44
46
|
inspectControllerProcess,
|
|
@@ -409,11 +411,16 @@ export const parseRuntimeOverrides = (
|
|
|
409
411
|
};
|
|
410
412
|
};
|
|
411
413
|
|
|
412
|
-
|
|
414
|
+
/**
|
|
415
|
+
* The CLI manages the controller of its own package, so it reads that controller's status over the transport its own
|
|
416
|
+
* `package.json` declares (`controllerManagementProtocolVersion`, which this build's constant equals).
|
|
417
|
+
*/
|
|
418
|
+
const currentStatusExpectation: IControllerStatusQuery = {
|
|
413
419
|
packageName: controllerPackageName,
|
|
414
420
|
packageVersion: commitinfo.version,
|
|
415
421
|
protocolVersion: controllerProtocolVersion,
|
|
416
422
|
upgradeManagementVersion: controllerUpgradeManagementVersion,
|
|
423
|
+
managementProtocolVersion: controllerManagementProtocolVersion,
|
|
417
424
|
};
|
|
418
425
|
|
|
419
426
|
const assertStatus = (statusArg: unknown): IControllerStatus => assertControllerStatus(
|
|
@@ -1780,7 +1787,7 @@ const upgradeController = async (argvArg: TParsedArguments): Promise<void> => {
|
|
|
1780
1787
|
controller: payload.expectedController!,
|
|
1781
1788
|
});
|
|
1782
1789
|
try {
|
|
1783
|
-
const launched = await requestControllerUpgradeLaunch(port, payload.token);
|
|
1790
|
+
const launched = await requestControllerUpgradeLaunch(port, status, payload.token);
|
|
1784
1791
|
worker = {
|
|
1785
1792
|
pid: launched.workerPid,
|
|
1786
1793
|
cliPath: installation.cliPath,
|