Repository navigation
/dispatch console: open, sign in to and check the web console from pi, and run it as a service #708
Description
Goal
An operator in pi types /dispatch console and is in the web console in one step: the address, whether it is running,
and a sign-in link, with the browser opened when the session is local. The console also runs as a service, so nobody
has to keep a terminal open for it.
Part of #650.
Today
- Serving. The console is its own program,
pi-dispatch-console serve, which runs in the foreground. There is
no service unit and nopi-dispatch serviceoption for it (docs/console.md:56-62). - What
/dispatchoffers./dispatch insightsand the panel'sikey print the spend screen's address, or thessh -Lline for a socket,
plus "start it withpi-dispatch-console serve" (admin/src/index.ts:1912-1935,admin/src/console/link.mjs).- Nothing says whether the console is running.
link.mjs:6-7: "a probe would be a network call from the panel for
a line of text".
- Signing in needs a terminal:
pi-dispatch-console token add(paste the token) orpi-dispatch-console link --name <token>(a 60-second, single-use code after the#). - First visit, step count. A first-time operator goes from pi to a signed-in console in five steps across two
terminals:- token add;
- serve, in a second terminal kept open;
- find the URL;
- open it;
- paste the token.
Proposal
/dispatch console [page]
An operator-typed subcommand (registerCommand), never a model tool. Its output goes to ctx.ui.notify only,
never into model context, and it refuses when ctx.hasUI is false.
- Address. The same
consolePageLinesrules asinsights: TCP loopback, the socket'sssh -Lline,<this host>for an any-address bind, port 0, an unparseablePI_CONSOLE_LISTEN. An optionalpage(runs,fleet,
insights, a run id) deep-links. - Running or not. One
GET /healthzto the configured loopback address or socket, never to any other address,
with a short timeout. It says "running" or "not running: start it with … orpi-dispatch service install --console". This reverses the "no probe" line inlink.mjswith a stated reason: the panel prints a URL that
does not answer today, which is the silent failure this project refuses. - Sign in, with
--link. It mints a one-time login code in-process for a named token, exactly as
pi-dispatch-console linkdoes (the panel runs as the deployment account, which is the same trust).- It is audited as actor
panelbefore the mint, and refused if the audit row cannot be appended. - It is shown in the notice only. It never prints the token, never reads it from or writes it to
.env, and never
puts the code in a query string. - With one operator token it is used; with several, the command asks which.
- It is audited as actor
- Open, with
--openor by default when local. It usesworker/src/open-browser.mjsonly when the session is
local (noSSH_CONNECTION, a display, a TCP listener). The URL is always printed as well, because the printed URL
stays the contract.
It must not bind a port or start serve itself. serve holds the single-instance lock and lives as long as its
process, so a console started from inside pi would be an unmanaged child.
The console as a service
pi-dispatch service install --console (systemd user unit, launchd, nssm, the Quadlet path), with pi-dispatch up
offering it.
- It binds the default loopback address, or the socket when
PI_CONSOLE_LISTENnames one. - Doctor reports it running or not.
- The
deploy/andworker/deploy/mirrors change together.
Spec work (same PRs)
REQ-ADMIN-VIA-PI-EXTENSIONandDES-ADMIN-VIA-PI-EXTENSION: the new subcommand. "Spawns no browser" becomes
"spawns one only on--openor a local session". "Binds no network port" is UNCHANGED, checked. The model-callable
tool list is UNCHANGED, and its pin stays.OQ-024: reopened by the opener ("What would reopen it: a command that spawns a browser"), and closed again with
the local-only rule.DES-OPERATOR-CONSOLE: the panel as a second minter of login codes, as threat model text.INT-CONSOLE-AUDIT-LOG:
thepanelactor or kind for a minted link.REQ-CONSOLE-SECURITY: UNCHANGED, checked (the code still sits after#, sessions and guards are untouched).- The service units: the deploy contract and
DES-entry for service install.
Done when
/dispatch consoleprints the address and "running" or "not running" against a live and a stopped console, and
--linksigns in a fresh browser in one paste.- A test shows nothing from the command reaches model context, no tool exposes it, the probe never leaves loopback or
the socket, and the minted link is audited before it exists. --openopens a browser on a local session, and only prints over SSH.pi-dispatch service install --consolekeeps the console running across a reboot on Linux and macOS, and doctor
says so.
Activity
feat(worker): run the operator console as a service (#708)
feat(admin): /dispatch console opens, checks and signs in to the web …
edgehero commentedon Oct 21, 2026
Done in two PRs:
pi-dispatch service install --consoleas a third service component (a systemd user unit or staged system unit, a launchd agent, or an nssm service).serve --deploymentand never loads the whole.env.@edgehero/pi-dispatch/console-probe) is used by doctor,upand the command./dispatch console [page] [--link [name]] [--no-open]: the address, whether the console is running, and an audited one-time sign-in link that can return to a page.Residuals, named in DES-OPERATOR-CONSOLE:
/tmp, so the printed link is the fallback;rundll32is verified only by reading.Owed at release: admin (the command and the guard), worker (the service component, the probe and doctor), and the deploy templates.
edgehero commentedon Oct 21, 2026
Security advisory published: GHSA-4qvc-f352-mm29 (console login codes could be planted through pi's write tools). #729 fixed it on
mainbefore any release contained the console. No released version is affected; 5.0.0 is the first release with the console, and it includes the fix.