Files
magpie/docs/subsystems/agent-wiring.md
T
yetone 588e51ea4c agent, gui: an agent connected by its config to an address where nothing answers is told, not shown connected (#1013, cjyrainbow: Codex Desktop's WSL environment reads Windows' ~/.codex/config.toml, whose openai_base_url and [model_providers.magpie].base_url the reporter pointed at Windows as WSL sees it, forwarded to 127.0.0.1:3425 by a portproxy of their own; after a reboot the portproxy listened no more and the Agents page still said Codex was connected) — Drift looked only at the config, which was right. Now an agent pointed at an address off loopback (this machine's Codex at an address the user kept, #816, through Agent.reach; every WSL twin under NAT, at Windows as the distro sees it, through Agent.Gateway) has that address tried from here (reach.go: GET /v1/models, any HTTP status an answer, 2 s timeout, in the background, at most every 20 s per address, never waited for), and one that doesn't answer while the gateway answers on loopback is a new drift, "unreachable", with Addr and the advice in Detail: under WSL (or on Windows) magpie with Settings › Share on local network on, or the user's netsh interface portproxy, which may need setting up again after a restart; Windows' firewall allowing the port for the WSL network; on Windows 11, networkingMode=mirrored; elsewhere, the forward to start again or gateway.URL() to point at. An answer not had yet, and a gateway down on loopback too, are no drift. The config is never changed for it, except where the address is one a WSL distro reached Windows at before (distro.Was, the last 4 kept in wsl.json as a re-probe finds Gateway changed) and the distro reaches Windows at another now: Drift's Move names that one, and Reapply moves the config there (Agent.move; Codex's codexMoveGateway rewrites openai_base_url and magpie's own provider's base_url, only where they begin with the old address). An address no distro was seen at is the user's own and is never offered a move. In the GUI the row's line says "{agent} is set up, but nothing answers at the address it reaches magpie by" in red with the advice as its tooltip, and its pill is "How to fix" (an i, not the reapply arrow), which opens the advice in a dialog (too long for the status line; it wraps and can be copied) and sets nothing — or "Use <address>" (the existing "Use {url}" string) when there is a Move, which reapplies; two new t() strings with zh, ja and de. The probe runs from Windows, not from inside WSL, so a firewall keeping WSL out isn't seen; the advice names it. TestKeptAddressNotAnswering (the reporter's case: Codex at 172.28.96.1, unreachable with the address and loopback in the detail; no drift once it answers, none with the gateway down too), TestKeptAddressProbeNeverWaitedFor, TestKeptAddressMovedWithWSL (Move from a Was address, Reapply rewrites both URLs, Check empty and no drift after; a 10.0.0.7 address is told but never moved) and TestWSLAgentAddressNotAnswering (Codex in a NAT distro told, with Share on local network, portproxy and firewall; a mirrored one never probed) fail without the Drift change and pass; TestLoopbackNotProbed, TestReachProbe (httptest: a 401 is an answer, a closed port none) and TestWSLWas pass. TestMain stubs reachProbe so no test dials out. -race -count=10 on them; go test -tags nogui ./internal/... under a temp HOME, go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. agent-unreachable.test.cjs (the window at 560px and the tray panel at 440px: the red line and tooltip, How to fix's dialog posting nothing, Use <address> posting the reapply, no sideways scroll) fails on main and passes on Chromium and WebKit in en, zh, ja and de; agent-connect, agent-layout, agent-field-names, aside-native, gui-de, gui-ja and i18n-keys pass on both. Not tried on a real WSL NAT setup (the Windows test box didn't answer ssh). docs/subsystems/agent-wiring.md says it.
2026-10-06 19:17:55 +08:00

10 KiB

Agent wiring

magpie connects a coding agent to its gateway by editing the agent's own config. That covers the endpoint, the key magpie-<agent> and the model, in the agent's own format. When the agent is disconnected, magpie puts back what the agent had before. Package internal/agent describes every supported agent: where its config lives, which fields matter and which values to offer. It also records what magpie set, so a later change made by something else shows up as drift.

Responsibilities and sources of truth

Part Responsibility Source
Agent description Agent has an id, name, icon, Bin and Dir for detection, Path, Fields and Native. It also has optional hooks: Notice, UA, ListsModels, Sync, Unwire, Join/Joined, Routed, Follow, RenameRefs, Check, LastUsed, Reached, Import/Added, Launch, SplitSuffix and Spelled. Each hook's comment says which agents need it. agent.go
Fields and options A Field has Get, Set and Options. Set("") returns the field to the agent's default and takes magpie's wiring out. Quiet fields follow another field until set (Follows, such as Claude Code's tiers). An Option carries the picker's facts: Direct, Via, Same, Alias, Rate and Context. agent.go, route.go
The list of agents All lists every agent magpie knows, then the WSL agents (wslAgents). Clients lists every agent whose requests the gateway knows by name, which usage.Agents reads. One file per agent: claude.go, codex.go, opencode in agents.go, and so on. agents.go
Connect ConnectHow picks the model an agent starts on and reports how. How is one of: kept, again, joined, magpie, same, alike, default or first (#726). With no models to give, it returns NoModelsError (code no_models). applied.go
What magpie set Apply sets a field and records it in applied.json. Wired says whether magpie is in the config. Drift reports unwired, replaced, unreachable or bypassed. Reapply sets the record again. Keep accepts the config as it is now. applied.go
What came before The stash (stash.json) remembers what the config said before magpie pointed it at the gateway, so Disconnect restores it rather than guessing. stash.go
Disconnect and preview Disconnect runs Unwire, then returns each field still on magpie to its default. DisconnectPreview runs the real Disconnect on a copy under a temporary home, in a subprocess (DryRunArg), and diffs the files. The preview is never a second account of what Disconnect does. applied.go, preview.go
Keeping configs current Reseat moves agents off a model magpie no longer serves (#200). OnGateway and Rewire write the new gateway address into every connected agent when the port changes. Source and Stale say what an unconnected agent runs on, and whether running copies still use the old list. reseat.go, rewire.go, connectinfo.go
WSL twins On Windows each running distro is probed once, without starting a stopped one. The wslKinds agents found there become agents of their own, <id>@wsl:<distro>, edited through \\wsl.localhost\<distro>. The gateway address and key follow the distro's networking: mirrored or NAT (place, keyAt). ListsFor keeps a twin's model picks under the Windows twin's id (#927). wsl.go, gatewaykey.go
Environment-only agents Agents that read the gateway only from the environment get a Launch command. Cursor Private Inference instead gets user-level variables (cursorLocalOn, KeepCursorLocalEnv), and magpie writes none of Cursor's own files. Its effort field is kept in magpie (provider.AgentEffort) and asked by the gateway, its levels those of the models it lists (cursorLocalEfforts, #1003). cursorlocal.go

Runtime path

  1. Connect. The Agents page's switch calls ConnectHow (POST /api/agents/{action}/{id} in internal/gui/api.go). It stashes the current values (<id>.connect.was). It brings back the models used before a switch-off (reconnect), or calls Join where the agent keeps its own model (Codex signed in with ChatGPT). Otherwise it picks a model by the order ConnectHow documents and sets it through Apply.
  2. Pick. Pick sets a field from the model picker. Picking one of magpie's models on an agent that isn't connected connects it first.
  3. Notice. After a change the page shows Notice when the agent reads its config only at start-up and needs a restart.
  4. Drift. As the Agents page is drawn, Drift compares the config with applied.json. It runs Check first, then each field. When the config is right and points at an address off loopback (a WSL twin under NAT, or this machine's Codex at an address the user kept, #816), it tries that address (reach.go): in the background, at most every 20 s, never waited for. When nothing answers there while the gateway answers on loopback, the drift is unreachable (#1013), with what has to listen there: Share on local network, the user's portproxy, Windows' firewall. Then it checks whether the agent's latest use (LastUsed, Reached) reached the gateway.
  5. Disconnect. Disconnect puts back what the stash kept and forgets the record.

Constraints and failure behavior

  • magpie only removes what it wrote. Anything else in an agent's config stays as it was.
  • Codex signed in to ChatGPT stays beside its sign-in (openai_base_url) unless the user picks magpie API. magpie becomes its provider only while the Codex app holds the active account, which it sends nothing for: /wham/usage says it isn't allowed, and it has no credits and isn't a workspace still within its overage (codexHeld, read by provider.CodexUsedUp). Where the app goes by what usage can't show (an experiment's gate, a reserve, a Team plan's overage under the reserve experiment), it counts as held: a wrong move costs ChatGPT extras, a missed one every turn. A saved reading kept through a failed read stops saying held once its used-up window has started again; a hold with no window used up stays till a fresh read. The app then loses its ChatGPT state (durable threads, remote control), so a window at 100% alone, which credits get past, doesn't move it (codex.out marks the move, and Sync undoes it once the account has room).
  • Moving a field from one of magpie's models to another (the agent's own picker) is not drift. Moving it off magpie is.
  • A value an agent spells as its own provider's model is never taken for one of magpie's, even when magpie has a provider of the same name (Spelled, #835).
  • unreachable never sets anything: the config is right, and the page's pill only says what has to listen. The exception is an address a WSL distro reached Windows at before (distro.Was, kept in wsl.json) while it reaches Windows at another now: the pill offers that one, and Reapply moves the config there (Agent.move, Codex's codexMoveGateway). An address no distro was seen at is the user's own and is never moved. An unknown answer (no probe yet) and a gateway down on loopback too are not drift. Tests stub reachProbe in TestMain.
  • Listing never starts a stopped WSL distro. A stopped distro's agent shows what was last seen there (asleep, wsl.json); setting one of its fields writes its files, which starts the distro.
  • Codex's sign-in (login) is ChatGPT ("", the default), Always ChatGPT (chatgpt) or magpie API (api). With ChatGPT, magpie's models join Codex's own beside the sign-in (openai_base_url), and while the Codex app holds the ChatGPT account Codex is signed in to (codexUsedUp, provider.CodexUsedUp: OpenAI no longer allows the account and it has no credits left, a workspace still within its overage excepted), magpie is made Codex's provider (codex.out): the Codex app sends nothing at all on an account it holds, a magpie model's turn included (#540), and as a provider of its own magpie is past that. A window at 100% with credits left doesn't count, and Codex stays signed in. Sync puts it back beside the sign-in once the account has room. Always ChatGPT never does that: Codex stays in its ChatGPT state, and the Codex app may send nothing till the account has room, while Codex CLI's turns still reach magpie by the base URL, and its routing and other accounts serve them.
  • An agent's config is written in the form the installed version reads, taken from what that version declares, not the latest one. VSCodium has no Copilot Chat of its own, and the Marketplace's last one (0.48.1) has no Custom Endpoint provider, only OpenAI Compatible (customoai, #1014). magpie reads the provider list in the package.json of the github.copilot-chat installed under ~/.vscode-oss/extensions (vscodeChatOf). It writes its group for customendpoint when that is declared, for customoai when only that is, with each model's window as maxInputTokens + maxOutputTokens and the token as x-api-key (that version drops an Authorization in requestHeaders). A missing or unreadable extension keeps customendpoint. Sync rewrites a group written for the other provider, and Check reports it, as it does a Copilot Chat declaring neither. That Copilot Chat registers its providers only while signed in to GitHub with a personal Copilot plan, which the Notice says.
  • Tests never touch the real machine. TestMain uses testenv, which points HOME, USERPROFILE, the XDG folders and APPDATA into a temporary folder, clears agentenv.Vars and puts failing stand-ins for keychain tools and agent CLIs on PATH. Agent-specific side effects (Aside, the Zed keychain, Codex's usage check) are also stubbed in TestMain.

Verification

go test -tags nogui ./internal/agent -run 'TestConnect|TestDisconnect|Drift|TestKeptAddress|TestWSLAgentAddress|TestReachProbe|TestDryRunKnowsTheAskersProviders|TestReseat|TestPort|TestWSL'
go test -tags nogui ./internal/agent
node --test internal/gui/tests/agent-connect.test.cjs internal/gui/tests/agent-disconnect-preview.test.cjs