Skip to main content

Module web

Module web 

Source
Expand description

The web UI: magi’s queue and run history, readable from a phone.

The terminal is the wrong surface for the two things an operator actually does between runs — file a task and check whether the last competition landed. Both happen away from the desk, so they get an HTTP surface: a handful of JSON routes and three embedded files.

§One binary

index.html, app.css and app.js are compiled in with include_str!. There is no --assets-dir and no filesystem fallback, because a UI that reads its own front end from disk breaks the moment the binary is copied somewhere else — which is exactly what cargo install magi-cli does. No JS toolchain, no CDN, no remote font: everything the phone needs arrives from this process.

§No authentication

There is none, deliberately, and the startup log says so. The tailnet is the security boundary: --bind auto resolves to this machine’s Tailscale address, so the UI is reachable from the operator’s own devices and from nothing else. Anyone who can open the URL can file and hold tasks, which is why binding to 0.0.0.0 is not offered and why the fallback when Tailscale is missing is loopback rather than every interface.

§Change notification

A phone must not poll a full run list on a mobile link. GET /api/events is a server-sent stream carrying nothing but two revision numbers — the newest modification time in the queue and under the runs directory — so the client refetches only what moved. The browser’s own SSE reconnection covers a sleeping phone; there is no session to lose.

§Reading state must never take the server down

A corrupt run.json is skipped in the list and explained with a 500 on the detail route. No handler unwraps a filesystem or parse result: a single bad file left by a killed run would otherwise turn the whole history into a blank page.

§Agent-authored HTML, rendered anyway

Everything else here refuses to put API data into the document: app.js builds nodes and sets textContent, and even an href from a run record is laundered first. A confirmation panel breaks that rule on purpose - an agent asking the owner to approve a merge needs a diff and a table, not one line of prose - and the only reason it is acceptable is that the panel is never part of this document.

It is served by [question_panel] and [question_asset] and rendered in an <iframe sandbox> carrying exactly allow-popups and allow-popups-to-escape-sandbox: no allow-scripts, no allow-same-origin. So no script in a panel runs, and the frame cannot reach the parent document, the cookie jar or localStorage. On top of that both routes send [PANEL_CSP], which denies every network destination, so a panel cannot phone home through a remote image or a beacon either - the two things it may load, images and inline CSS, are the two things free formatting actually needs. Assets come from the question’s own directory and never from the network, and their content types come from a closed whitelist, so an agent cannot get markup rendered outside the frame by naming a file .html.

§A conversation turn is not a filesystem read

Every other route here is disk work, which is why [blocking] exists. POST /api/talks/{id}/say is the exception: it spawns an agent CLI and waits tens of seconds for a sentence. It is a plain await holding no lock and no executor thread, and concurrent turns on one talk are refused rather than queued - see Ui::begin_talk_turn.

§Independent scheduler

/api/loop starts and controls a detached scheduler. Replacing or closing the web process does not stop that scheduler or its task/chat workers. Stop ends scheduling; explicit park additionally asks task workers to stop at their next node boundary. Updating never requests a park.

Structs§

Opts
How to serve.
Ui
Everything the handlers touch.

Enums§

Bind
Which address to listen on.

Constants§

DEFAULT_PORT
Default port. Chosen high and memorable; nothing else in the fleet uses it.

Functions§

resolve_bind
Resolve --bind to an address, plus a warning when the answer is not what the operator asked for.
runs_unreadable
Runs on disk under runs whose state this build cannot parse - almost always a schema bump, occasionally a run killed mid-write.
serve
Serve the UI independently from scheduler and execution workers. A restart releases its listener before starting the successor.