agl 41.0.1 → 41.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/changelog.md +24 -0
  2. package/dist_serve/bundle.js +1448 -1448
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/aglapi/aglapisurface.document.js +7 -3
  6. package/dist_ts/classes.authmodels.d.ts +15 -0
  7. package/dist_ts/classes.authmodels.js +78 -7
  8. package/dist_ts/classes.authstore.js +23 -6
  9. package/dist_ts/classes.cli.js +8 -3
  10. package/dist_ts/classes.controller.d.ts +13 -0
  11. package/dist_ts/classes.controller.js +51 -14
  12. package/dist_ts/classes.controllermanagement.d.ts +53 -6
  13. package/dist_ts/classes.controllermanagement.js +180 -72
  14. package/dist_ts/classes.inferencegateway.d.ts +6 -6
  15. package/dist_ts/classes.inferencegateway.js +28 -15
  16. package/dist_ts/classes.inferenceusage.d.ts +19 -7
  17. package/dist_ts/classes.inferenceusage.js +49 -13
  18. package/dist_ts/classes.inferenceusagetap.d.ts +15 -3
  19. package/dist_ts/classes.inferenceusagetap.js +16 -9
  20. package/dist_ts/classes.processinspection.d.ts +3 -1
  21. package/dist_ts/classes.processinspection.js +4 -2
  22. package/dist_ts/classes.proxycontrol.d.ts +22 -7
  23. package/dist_ts/classes.proxycontrol.js +5 -3
  24. package/dist_ts/classes.proxycontrolclient.d.ts +4 -3
  25. package/dist_ts/classes.proxycontrolclient.js +7 -5
  26. package/dist_ts/classes.proxydaemon.d.ts +6 -4
  27. package/dist_ts/classes.proxydaemon.js +42 -17
  28. package/dist_ts/classes.proxyleaseholder.d.ts +11 -2
  29. package/dist_ts/classes.proxyleaseholder.js +25 -2
  30. package/dist_ts/classes.proxystore.d.ts +3 -2
  31. package/dist_ts/classes.proxystore.js +5 -2
  32. package/dist_ts/classes.upgradecoordinator.d.ts +2 -1
  33. package/dist_ts/classes.upgradecoordinator.js +13 -10
  34. package/dist_ts/classes.upgradetransaction.d.ts +6 -0
  35. package/dist_ts/classes.upgradetransaction.js +38 -13
  36. package/dist_ts/classes.usagefeed.js +3 -2
  37. package/dist_ts/functions.controllermanagementsurface.d.ts +37 -0
  38. package/dist_ts/functions.controllermanagementsurface.js +110 -0
  39. package/dist_ts/functions.inferencecli.d.ts +6 -3
  40. package/dist_ts/functions.inferencecli.js +9 -5
  41. package/dist_ts/functions.inferencecliarguments.d.ts +1 -1
  42. package/dist_ts/functions.inferencecliarguments.js +19 -7
  43. package/dist_ts/functions.inferencelaunch.d.ts +30 -9
  44. package/dist_ts/functions.inferencelaunch.js +49 -23
  45. package/dist_ts/interfaces.auth.d.ts +14 -3
  46. package/dist_ts/interfaces.auth.js +4 -3
  47. package/dist_ts_interfaces/index.d.ts +1 -0
  48. package/dist_ts_interfaces/index.js +2 -1
  49. package/dist_ts_interfaces/interfaces.d.ts +6 -1
  50. package/dist_ts_interfaces/interfaces.js +2 -2
  51. package/dist_ts_interfaces/management.d.ts +33 -0
  52. package/dist_ts_interfaces/management.js +35 -0
  53. package/dist_ts_interfaces/metrics.d.ts +29 -7
  54. package/dist_ts_interfaces/metrics.js +14 -1
  55. package/package.json +6 -5
  56. package/readme.md +123 -28
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/aglapi/aglapisurface.document.ts +6 -2
  59. package/ts/classes.authmodels.ts +74 -5
  60. package/ts/classes.authstore.ts +24 -4
  61. package/ts/classes.cli.ts +9 -2
  62. package/ts/classes.controller.ts +98 -59
  63. package/ts/classes.controllermanagement.ts +284 -107
  64. package/ts/classes.inferencegateway.ts +31 -19
  65. package/ts/classes.inferenceusage.ts +58 -19
  66. package/ts/classes.inferenceusagetap.ts +27 -8
  67. package/ts/classes.processinspection.ts +2 -1
  68. package/ts/classes.proxycontrol.ts +25 -9
  69. package/ts/classes.proxycontrolclient.ts +7 -4
  70. package/ts/classes.proxydaemon.ts +43 -17
  71. package/ts/classes.proxyleaseholder.ts +36 -7
  72. package/ts/classes.proxystore.ts +8 -3
  73. package/ts/classes.upgradecoordinator.ts +20 -11
  74. package/ts/classes.upgradetransaction.ts +53 -2
  75. package/ts/classes.usagefeed.ts +2 -1
  76. package/ts/functions.controllermanagementsurface.ts +149 -0
  77. package/ts/functions.inferencecli.ts +15 -7
  78. package/ts/functions.inferencecliarguments.ts +16 -6
  79. package/ts/functions.inferencelaunch.ts +57 -23
  80. package/ts/interfaces.auth.ts +14 -3
  81. package/ts_interfaces/index.ts +1 -0
  82. package/ts_interfaces/interfaces.ts +6 -1
  83. package/ts_interfaces/management.ts +52 -0
  84. package/ts_interfaces/metrics.ts +34 -7
  85. package/ts_web/00_commitinfo_data.ts +1 -1
  86. package/ts_web/classes.controllersocketclient.ts +5 -2
  87. package/ts_web/elements.metricsboard.ts +68 -18
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. The controller reads these only while a browser watches a usage window:
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; below them a line chart shows the tokens per minute by model, each
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). The listener serves the Claude pool's three endpoints Claude Code uses --
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 a Claude client, and refuses every other path, method and pool with 404 before anything is
716
- read or routed; nothing else of AGL is reachable through it. It accepts confined runs' credentials alone: a request
717
- without a credential, with one the proxy does not know, or with a credential of the loopback listener (any client's or
718
- lease's) is refused with 401, and a confined run's credential is refused on the loopback listener alike.
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 status # names the confined listener and lists the runs by id
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 Claude pool (the Claude accounts first, then the Anthropic API keys) whose sessions are those of the client
729
- `confined`, so its requests are placed, moved and counted (token usage under `confined`) like any other Claude
730
- client's. Right after `docker run` it adds the listener's directory as a read-only mount at `/run/agl-proxy` and what
731
- Claude Code inside needs to reach it, each variable passed by name so the credential appears on no command line:
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 `agl proxy run
739
- claude` sets them.
740
-
741
- It prints the run's id on standard error (`AGL proxy confined run <id>: …`), passes its standard input and output
742
- through, and exits with the container's status. The container never receives an account token, the control socket or
743
- any other part of the AGL home: the mount holds the socket alone, and the variables that would give Claude Code another
744
- login or route (`CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, …) are removed from the environment `docker` runs with,
745
- so a `-e NAME` of one of them passes nothing. What the launcher names itself after `docker run` is its own: it must not
746
- set the variables above. The command takes `docker run` and `docker container run` only, with the image and its
747
- command after the options.
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, which its check of the
764
- kept leases does not know. Let every confined run end before downgrading; if an earlier proxy's start already failed,
765
- start this release once with no confined run going and wait out the 60-second reclaim window, which ends the kept
766
- credential, then downgrade.
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 the Claude pool's three Claude Code endpoints.
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
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '41.0.1',
6
+ version: '41.2.0',
7
7
  description: 'Agent Gateway Layer for OpenCode, FlexHarness, and Codex sessions'
8
8
  }
@@ -3765,11 +3765,15 @@ export const AGL_API_SURFACE: plugins.flexTypedRequest.IFlexTypedRequestSurface
3765
3765
  },
3766
3766
  "protocolVersion": {
3767
3767
  "type": "number",
3768
- "const": 46
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: 46;\n upgradeManagementVersion: 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}"
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",
@@ -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, valueArg.type === 'resource.browser.operation' ? 18 : 24)
402
+ && !isBase64Url(valueArg.operationId, type === 'resource.browser.operation' ? 18 : 24)
338
403
  )
339
404
  || (
340
405
  Object.hasOwn(valueArg, 'senderLabel')
341
- && (valueArg.type !== 'session.message' || !isControllerAgentMessageSender(valueArg.senderLabel))
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 (!(valueArg.type as string).startsWith('configuration.')) throw new Error('Invalid local configuration audit event.');
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
  }
@@ -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 ControllerAuthModel.exact.findStoredOne({ id: this.controllerId });
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 ControllerAuthModel.exact.findStoredOne({ id: controllerId });
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 ControllerAuthModel.exact.findStoredOne({ id: controllerId });
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 ControllerAuthModel.exact.findStoredOne({ id: this.controllerId });
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
- const currentStatusExpectation = {
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,