The live viewer¶
The viewer is a web front end for place-and-route experiments. A running experiment writes telemetry into a live directory; the viewer replays it into lanes (candidates, trials, rounds) and shows each lane’s board as it changes: native copper, provisional routes, checkpoints, placement costs and the search’s alternatives. Optional services add a schematic view, an atopile source browser, design notes, a 3D view and an assistant. The viewer never changes the experiment; the only thing it writes into the live directory is what you ask for (runtime control requests, pins, annotation drafts and snapshots).
Code: yapnr/viewer/; its README describes the layout and the tests.
Running it¶
bazel run //:viewer -- --root runs/example/live # http://127.0.0.1:8766
bazel run //:viewer -- --config viewer.toml --port 8795
Relative paths are relative to the directory bazel run was started from. The server listens on
127.0.0.1 only, unless you add listeners (see Network access).
From a plain checkout, build the static files once and run the package with Python 3.11 or newer, with the engine on the import path:
bazel build //yapnr/viewer:dist
PYTHONPATH=.:hardware/pnr python3 -m yapnr.viewer --root runs/example/live \
--dist bazel-bin/yapnr/viewer/dist
Without the assembled dist the viewer serves its bare static files: everything works except the schematic layout (elkjs) and the 3D view (three.js), which say so in the page.
The live directory¶
Path |
Written by |
What |
|---|---|---|
|
the experiment |
immutable telemetry events ( |
|
the experiment |
native board checkpoints, named by content hash |
|
the viewer |
board geometry extracted with KiCad’s Python (cached) |
|
the viewer |
runtime control requests the experiment reads |
|
the viewer |
pinned states and annotated snapshots |
|
the viewer |
caches ( |
The experiment folder is the root’s parent unless --experiment-dir says otherwise. Trial run
directories must lie under it; the viewers of one experiment share its 3D export lock, and an
optional restart-status.json there is shown in the page. Design notes default to
<experiment>/notes, control preferences to <experiment>/viewer-preferences.json.
Configuration¶
Every setting has a flag; a TOML file (--config, schema yapnr-viewer-v1) holds the same
settings, with paths relative to the file. A flag wins over the file, the file over the default,
and every default is derived from the root: nothing points at a machine path.
bazel run //:viewer -- --help lists the flags; the file format is in the docstring of
yapnr/viewer/config.py. A typical file:
schema = "yapnr-viewer-v1"
root = "runs/example/live"
[server]
port = 8766
title = "Example board"
[design]
graph = "inputs/graph.json" # netlist: schematic view, notes, source index
constraints = "inputs/constraints.yaml"
[design.atopile]
root = "hardware/example" # the folder with ato.yaml
build = "default" # its build target: the entry module
Machine settings never go in that file. KiCad and the claude CLI come from flags, the
environment or the user’s machine config, ~/.config/yapnr/config.toml (or
$XDG_CONFIG_HOME/yapnr/config.toml; YAPNR_USER_CONFIG names another file, empty disables it):
[kicad]
cli = "~/Applications/KiCad-headless.app/Contents/MacOS/kicad-cli"
python = "~/Applications/KiCad-headless.app/Contents/Frameworks/Python.framework/Versions/Current/bin/python3"
[agent]
claude = "/opt/claude/bin/claude"
Design inputs and optional services¶
Feature |
Needs |
Without it |
|---|---|---|
Board geometry of checkpoints |
KiCad’s Python ( |
events without a layout show no board |
Placement cost inspection |
the engine’s cost model ( |
the cost panel says unavailable |
Schematic view |
|
the schematic tab says unavailable |
Source browser (Inspect) |
atopile sources ( |
the Source tab says why; notes use the graph |
3D view |
a headless |
the 3D pane says why |
Design notes |
nothing (on by default) |
|
Ask agent, AI net labels |
|
off (the default): no Ask buttons; the Ask tab says how to turn it on |
The atopile source browser reads the project’s ato.yaml: builds.<build>.entry names the entry
module, and the source folder is that file’s folder (--atopile-src overrides it; --parts
overrides <src>/parts). It joins the instance tree to the netlist by component address and pad
sets, so its net titles and summaries are mechanical. Without atopile sources the viewer still
runs: the schematic draws generic symbols, notes resolve targets against the netlist alone and
3D model paths are used as the board has them.
--engine-runtime points the subprocess services (cost replay, schematic builder) at a frozen
copy of the engine, the one a run was made with; the directory is the one that contains pnr.
KiCad¶
The viewer runs KiCad only headlessly: KiCad’s Python extracts board geometry (and the graph of
a routed checkpoint for the cost replay), and kicad-cli exports the 3D view’s GLB. The order
is: the flag, the environment (YAPNR_KICAD_CLI, alias PNR_KICAD_CLI; YAPNR_KICAD_PYTHON),
the machine config, then discovery: on macOS only the headless copy in
~/Applications/KiCad-headless.app (make it as in DEVELOPERS.md), on
Linux kicad-cli on PATH and a python3 that imports pcbnew. The GUI application is never
used: a path inside KiCad.app or /Applications/KiCad, or inside any application bundle that is
not background-only, is refused, because every call would put an icon in the Dock.
Network access¶
By default the viewer listens on 127.0.0.1. To reach it from another machine, add a listener and
the origin the browser will use, for example:
bazel run //:viewer -- --root runs/example/live --listen 127.0.0.1 --listen 192.0.2.10 \
--allow-origin http://192.0.2.10:8766 --allow-origin http://viewer.example.com:8766
Host guard (DNS rebinding): a request must carry a
Hostthat is a loopback name, a--listenaddress, an--allow-originhost (or its first label) or an--allow-hostname; anything else gets 421.Origin check: writes need an allowlisted
Origin(http://127.0.0.1:<port>,http://localhost:<port>,--allow-origin). The Ask agent and every notes write reject a missingOrigintoo.Everyone who reaches the port can see the experiment, the design sources you configured and the notes and stored conversations, and can write notes as “the user” (recorded with their address). The Origin check stops other web pages, not a client that forges the header. Expose the viewer only on networks you trust, and never to the internet.
The Ask agent and AI net labels (off by default)¶
Both are off unless you turn them on, also on a loopback listener:
--agent onenables Ask: a read-only assistant that answers questions about the selection (parts, nets, pads, regions, source lines, lanes, events). Each question is one headlessclaude -prun.--agent-web onalso lets it use WebSearch and WebFetch.--net-summaries onadds AI net labels: one tool-lessclaudecall per changed netlist dossier (per ~60k characters of it; about $0.20 for a hundred nets with sonnet), labelled as AI output in Inspect.
What that means:
Cost. Every turn and every labelling call is paid on the operator’s Claude account (the
claudeCLI’s login). Each call has a budget that the CLI enforces (--max-budget-usd): an Ask turn--agent-budget-usd(default $2), a net-label call--net-summary-budget-usd(default $1). Both kinds count against one cap per server process,--agent-total-usd(default $20;0turns the cap off). A call starts only while the cap still covers its whole budget on top of what was spent and what the running calls hold, so concurrent turns cannot pass it together; a call that ends without a cost report from the CLI (timeout, cancel, a stopped unsafe turn) is counted at its whole budget. When the cap cannot cover another call, Ask says so and the remaining net labels stay missing until a restart, which resets the count (it is kept in memory). The CLI checks its budget as the turn runs, so one call can end slightly above its budget. The default model is opus (--agent-model sonnetis cheaper).Reads. The agent can read the working directory (
--agent-cwd, default the experiment folder) and the folders you allow (--agent-read-dir, default the atopile sources and the experiment folder). It has no shell and no write tools; its only write path is the design-notes tools of a per-turn MCP server, and it can only propose, never accept, a change. A turn whose CLI reports any other tool or server is killed.Web. With
--agent-web onfetched pages are untrusted input: a page can try to steer the model (prompt injection). WebFetch goes through a guard hook that allows public hosts only (loopback, private, link-local, tailnet and.localnames are refused, also after DNS resolution), and the prompt forbids putting design data into URLs or queries, but a model can still be misled. Leave web off unless you need datasheets.Exposure. On a non-loopback listener anyone who reaches the port can run paid turns and read what the agent can read; the server warns at startup. Do not enable the agent there unless you understand and accept that.
Design notes¶
Notes (<experiment>/notes, shared by all viewers of an experiment) record observations,
questions, requirements, decisions and proposals, with targets on the board or in the source.
People create, edit, accept, reject, apply and delete them in the Notes tab; the Ask agent can
only add notes, comment and edit its own open notes. design-notes.md in the same folder is the
feed for the next design pass, and python -m yapnr.viewer.notes.store report --dir <notes>
prints it (--status accepted, --json).
HTTP interface¶
Route |
What |
|---|---|
|
lanes, events, errors ( |
|
a checkpoint’s geometry |
|
runtime controls (requested, active, limits) |
|
immutable pins, drafts, annotated snapshots |
|
placement cost replay |
|
the schematic model |
|
the atopile source index and files |
|
design notes |
|
the Ask agent (server-sent events) |
|
the 3D view |
|
version, source repository and revision |
Source code and license¶
The page header links to the viewer’s source (AGPL-3.0-or-later, section 13): the source tree of
the revision the server runs (YAPNR_SOURCE_REVISION, or the git checkout’s HEAD), marked
“modified” when the checkout has uncommitted changes to tracked files. If you serve a modified
viewer to other people, publish your changes and point --source-url at them. The revision is the
viewer’s own; runs do not record the engine commit that produced them yet. The front end loads two
third-party libraries, served unmodified next to their licenses: elkjs (EPL-2.0, with an
Apache-2.0 web-worker shim; both texts are served) and three.js (MIT); see
THIRD_PARTY.md.