Skip to content

/dispatch console: open, sign in to and check the web console from pi, and run it as a service #708

Closed

Description

@edgehero

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 no pi-dispatch service option for it (docs/console.md:56-62).
  • What /dispatch offers.
    • /dispatch insights and the panel's i key print the spend screen's address, or the ssh -L line for a socket,
      plus "start it with pi-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) or pi-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:
    1. token add;
    2. serve, in a second terminal kept open;
    3. find the URL;
    4. open it;
    5. 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.

  1. Address. The same consolePageLines rules as insights: TCP loopback, the socket's ssh -L line, <this host> for an any-address bind, port 0, an unparseable PI_CONSOLE_LISTEN. An optional page (runs, fleet,
    insights, a run id) deep-links.
  2. Running or not. One GET /healthz to the configured loopback address or socket, never to any other address,
    with a short timeout. It says "running" or "not running: start it with … or pi-dispatch service install --console". This reverses the "no probe" line in link.mjs with a stated reason: the panel prints a URL that
    does not answer today, which is the silent failure this project refuses.
  3. Sign in, with --link. It mints a one-time login code in-process for a named token, exactly as
    pi-dispatch-console link does (the panel runs as the deployment account, which is the same trust).
    • It is audited as actor panel before 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.
  4. Open, with --open or by default when local. It uses worker/src/open-browser.mjs only when the session is
    local (no SSH_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_LISTEN names one.
  • Doctor reports it running or not.
  • The deploy/ and worker/deploy/ mirrors change together.

Spec work (same PRs)

  • REQ-ADMIN-VIA-PI-EXTENSION and DES-ADMIN-VIA-PI-EXTENSION: the new subcommand. "Spawns no browser" becomes
    "spawns one only on --open or 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:
    the panel actor 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 console prints the address and "running" or "not running" against a live and a stopped console, and
    --link signs 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.
  • --open opens a browser on a local session, and only prints over SSH.
  • pi-dispatch service install --console keeps the console running across a reboot on Linux and macOS, and doctor
    says so.

Activity

  1. edgehero commented on Oct 21, 2026

    @edgehero
    OwnerAuthor

    Done in two PRs:

    • feat(worker): run the operator console as a service (#708) #728, the console as a service: pi-dispatch service install --console as a third service component (a systemd user unit or staged system unit, a launchd agent, or an nssm service).
      • The unit runs serve --deployment and never loads the whole .env.
      • The folder is quoted correctly in the unit.
      • Install refuses when there is no operator token or the admin package is missing.
      • One shared healthz probe (@edgehero/pi-dispatch/console-probe) is used by doctor, up and the command.
    • feat(admin): /dispatch console opens, checks and signs in to the web console (#708) #729, /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.
      • The browser opens only on a local session, through a private page, so the code never appears in a process's arguments.
      • The login-codes directory joins the guarded files.

    Residuals, named in DES-OPERATOR-CONSOLE:

    • a stale console lock after a crash keeps the unit stopped until someone clears it;
    • snap and Flatpak browsers cannot read the page in /tmp, so the printed link is the fallback;
    • tmux attached over SSH opens on the unattended desktop;
    • one-time links do not work on Windows hosts;
    • rundll32 is 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.

  2. edgehero commented on Oct 21, 2026

    @edgehero
    OwnerAuthor

    Security advisory published: GHSA-4qvc-f352-mm29 (console login codes could be planted through pi's write tools). #729 fixed it on main before 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions