Running the server
Bring up the web viewer on your own machine with Docker, and bring your designs with you.
The viewer is a server you run, not a hosted service. This page is about bringing it up on your own machine so a team can open a browser and work, rather than each engineer installing a toolchain.
If you only want the CLI, Getting started is the shorter path.
The one-liner ¶
docker run -p 8080:8080 -v ~/boards:/workspace/boards ghcr.io/panyam/agni:v0.1.1
Open http://localhost:8080. Your boards are in the file tree on the left, under a mount named
boards.
With no -v at all it still runs, serving the two demo boards baked into the image, so you can
confirm the thing works before pointing it at your own designs.
The examples on this page pin a version, as
Getting started does, because a report is only reproducible if you can say
which build produced it. Every published version is on
the package page, and :latest tracks the
newest if you would rather not pin. agni version inside the container always tells you what you
actually have.
Bringing your designs ¶
Every subdirectory of /workspace becomes a mount named after itself. One -v per folder, no
flags:
docker run -p 8080:8080 \
-v ~/boards:/workspace/boards \
-v ~/datasheets:/workspace/datasheets \
ghcr.io/panyam/agni:v0.1.1
That gives two mounts, boards and datasheets. Each mount is a containment boundary, because the
browser addresses files as a mount name plus a mount-relative path, never an absolute one, and the
server rejects any path that escapes its mount.
Dotfile directories are skipped, so a .git sitting in a bind-mounted parent does not show up as a
design folder.
Without Docker ¶
The image carries the viewer’s built assets and the symbol libraries, and a server needs both beyond the engine. If you run the binary directly, you supply the viewer’s built assets yourself.
agni serve --addr :8080 --mount boards=~/boards --web-dir /path/to/agni/web
--web-dir is the directory holding the viewer’s own templates/ and its built static/*.js, not a
folder of designs. It defaults to web. A repo checkout keeps them there, so from a checkout you can
leave it off.
With no web dir at all, a release binary finds its own viewer. A go install of a release tag
downloads the bundle its release attached. A build that is not a release, such as make agni or
go install ...@main, has no bundle, so agni serve serves the API alone. The run says so on
stderr, and / answers with a short text page listing the services and how to get the viewer. The
Connect API, /healthz, and the rule and relation doc images all work. A script or the
Python client needs nothing more.
That only happens when NOTHING named a web dir and there is no ./web. If you name a directory
through the flag, an agni.yaml or the environment, it still has to exist and hold a built viewer.
A ./web that holds templates without its bundle also stops the run. A checkout that has not run
make ui is in that state. Either one is a mistake you want to hear about.
The datasheets workbench is a separate service, agnids, built from the datasheet/ module. It has
its own mounts, writes into them, and holds the docling environment, so it deploys apart from the
viewer:
agnids serve --addr :8090 --mount ds=~/datasheets --web-dir /path/to/agni/datasheet/web --viewer-url http://host:8080
Its web assets are its own, datasheet/web, built by make ui, so a workbench deployment needs no
part of the viewer’s web/. From a checkout, make dsserve DATASHEET_DIR=~/datasheets runs it. The
two link to each other only when told where the other is: agni serve --datasheets-url http://host:8090 shows the landing page’s Datasheets card, and agnids serve --viewer-url http://host:8080 makes the workbench’s heading link home. The same service can also serve a
published corpus of PartSpecs with --corpus <dir>. agni serve --params-url http://host:8090
reads that corpus in place of a --params directory (see
Datasheets).
Two ways to avoid typing it every time:
| where you put it | what it suits |
|---|---|
web_dir: in an agni.yaml (machine configuration) |
per-directory, and it travels with a checkout |
AGNI_WEB_DIR in the environment |
an installed binary whose assets sit at a fixed path |
A run says on stderr when the value came from the environment, because an AGNI_WEB_DIR exported
months ago outlives the memory of exporting it.
For one design, reach for agni open instead. It picks a free
port and prints the URL of the board rather than of a file browser. It finds the viewer the same way
serve does, and unlike serve it needs one, since showing a page is its whole job. agni serve is for several designs at once, or for a server other people reach.
Symbol libraries are already there ¶
Some schematics name their symbols rather than embedding them. Such a schematic needs the symbol
library before it can resolve to a pin-level netlist. The image ships the
KiCad, xschem, and gEDA symbol libraries and points --symbol-path at all three, so external
symbols resolve with nothing to install.
Without the libraries the design fails quietly. It still reads, but it reads short, with fewer components, fewer nets, and therefore fewer findings, and no error tells you so. If your component or net counts look low, check this first, whether you are running in Docker or not.
The libraries are most of the image’s ~340MB. The size is deliberate, because a smaller image that renders your board wrong is the worse default.
Running one-shot commands ¶
The image’s entrypoint is the agni binary and the server is only its default command, so any
subcommand works with the same environment the server has:
docker run -v ~/boards:/workspace/boards ghcr.io/panyam/agni:v0.1.1 \
check /workspace/boards/board.kicad_pro --format json
This is mostly for CI, where a container is easier than provisioning a Go toolchain, and where it matters that the gate and the viewer run identical engine and symbol data. At your own desk, installing the CLI is nicer. See the CLI reference.
Asking a running server from the CLI ¶
Every agni command reads the design itself, so a second command on the same board reads and
projects it again. A running server keeps what it read (see Kept reads), and
--remote on query, check, trace and review sends the command’s request there instead:
agni query designs/board --remote http://localhost:8080 'component.mpn(?r, ?m) => count(?r)'
The output matches the local command’s, because the CLI already builds a request for the server and renders the response it gets back. On the Jetson board a warm query takes 0.02 s end to end against 0.33 s run locally, and a check 0.18 s against 1.39 s.
The design’s files go with the request, so the server needs no copy of them (agni issue 1061). The
CLI sends a manifest of the files its analysis reads. Those are the design’s folder, its project’s
descriptor and the config directories the project names, and ListDesignFiles names the same set
for a browser. The server asks for the contents it does not already hold, so a second run of an
unchanged design sends the manifest alone. A server started with --upload-max-bytes 0 takes no
files. Otherwise it keeps what it was sent up to that limit, 1 GB by default.
The server answers with its own datasheet corpus and profiles, the ones it was started with. So
--params and --profile-path are refused with --remote rather than ignored. So is --timing,
since it would time this process. The server’s own --slow-request and the Agni-Timing header time
the request where it runs. The server scores and returns a review without storing it, so the review
leaves nothing in the server’s list. stats, diff, render and the
other commands that read a design themselves still run locally.
Health ¶
GET /healthz returns 200 while the server is up. The image declares a HEALTHCHECK that calls
agni healthcheck against it, so an orchestrator gets liveness with no extra tooling in the image.
The probe reports that the process is serving HTTP and nothing more. Mounts, the rule catalog, and
the parameter set are all validated at startup, so a misconfigured server fails before it ever
listens. A 200 is not evidence that your --profile-path was right.
House rules for the whole server ¶
The catalog overlay is deployment configuration, applied once at startup to every rule-running surface:
docker run -p 8080:8080 \
-v ~/boards:/workspace/boards \
-v ~/houserules:/etc/agni \
ghcr.io/panyam/agni:v0.1.1 \
serve --addr :8080 --mount-root /workspace \
--profile-path /etc/agni/profiles \
--conventions /etc/agni/conventions.yaml
Overriding the command like this replaces the default arguments, so --mount-root has to be
repeated. The symbol paths do not, because they come from the environment.
A profile named after a built-in supersedes the built-in for every design this server reads, so point it at rules that suit the whole mounted set rather than one project. See interface profiles and naming conventions.
Keeping review runs ¶
serve --review-store <dir> makes the server keep review runs. Without it the server
still serves everything else, and the review endpoints answer that they store no reviews rather than
storing a run and dropping it. The viewer’s Review panel then scores the checklist without storing it
(CreateReview with validate_only) and holds the run in the page until you leave it, as it does on
the browser engine, which keeps no store at all.
The server writes the store into a directory, so in a container give it its own volume:
docker run -p 8080:8080 \
-v ~/boards:/workspace/boards \
-v agni-reviews:/var/lib/agni/reviews \
ghcr.io/panyam/agni:v0.1.1 \
serve --addr :8080 --mount-root /workspace \
--review-store /var/lib/agni/reviews
It is deliberately a different volume from your board folders.
Why the store sits outside your design mounts, and what one stored run is
Design mounts are read-only. Keeping runs somewhere else preserves that, because the server never
saves a file beside your schematics. A named volume also survives docker rm. Without that, storing
runs would be pointless.
Each run is one file, in the same format as agni review --results-out. Ordinary tools can read the
volume, and a run can be copied out and rendered anywhere. A run records
the checklist it scored, not a pointer to it, so editing your project’s checklist afterwards never
rewrites what an older run says it asked.
Every client of the server can see the runs, because agni serve has no authentication yet. Treat
the store the way you treat the mounts. It is fine for one team on a trusted network, and it is no
boundary between teams. Nothing prunes it either, so a CI job creating a run per commit will grow the
volume until you delete runs yourself.
The datasheet service image ¶
The datasheets workbench has an image of its own, ghcr.io/panyam/agnids, published by every release
from the same tag as agni’s. It carries agnids, the workbench, and the docling environment behind
the “Extract (first pass)” action, with docling’s models already inside, so Extract works on a host
with no outbound network. The docling environment makes the image about 3.7GB, where agni’s is
about 340MB, so the two are separate images.
docker run -p 8090:8090 --user $(id -u):$(id -g) -v ~/datasheets/ti:/datasheets/ti -v ~/corpus:/corpus ghcr.io/panyam/agnids:latest
Every subdirectory of /datasheets becomes a mount named after itself, as /workspace does in the
agni image. Mount them read-write, since the workbench saves annotations and extracted doc-IR into
them. /corpus is the corpus store, holding the workbench’s drafts and the published specs that
agni serve --params-url reads, so mount a directory there to keep them. To link the workbench’s
heading back to a viewer, pass the whole command, because arguments after the image replace its
default rather than adding to it:
docker run -p 8090:8090 -v ~/datasheets/ti:/datasheets/ti ghcr.io/panyam/agnids:latest \
serve --addr :8090 --mount-root /datasheets --web-dir /srv/agnids/web --corpus /corpus \
--pdf2doc "/opt/docling/bin/python /opt/agnids/pdf2doc.py" --viewer-url http://host:8080
Writes and file ownership ¶
The datasheets workbench (agnids) writes back into what you mount. It writes an annotation or an
extracted doc-IR beside its datasheet, and a draft or a published spec into the corpus store. The
agni image writes only to a --review-store. Both containers run as a non-root user
(uid 10001), so those files are never written as root.
On Docker Desktop (macOS, Windows) that is the whole story. Its file-sharing layer maps ownership to you, so a file the container wrote as uid 10001 appears on your host owned by your own account, and there is nothing to configure.
On Linux, bind mounts pass ownership through unchanged, so those files arrive owned by uid 10001 and you may not be able to edit them afterwards. Run as yourself instead:
docker run --user $(id -u):$(id -g) -p 8090:8090 -v ~/datasheets/ti:/datasheets/ti ghcr.io/panyam/agnids:latest
What is not in the image ¶
Two capabilities shell out to external programs and are deliberately left out of the agni image,
because both are large and neither is needed to read, check, diff, or query a design.
agni native render/openmakes native golden renders by driving the format’s own tool (kicad-cli, xschem, Lepton). See native verification.- Datasheet extraction, the workbench’s “Extract (first pass)” action, belongs to the separate datasheet service, which has its own image.
A shared deployment ¶
Everything above assumes one engineer, one machine. There is no authentication, no per-user
scoping, and no session isolation, so anyone who can reach the port sees every mount, and anyone who
can reach agnids can write through the datasheets workbench. That is the right trade for localhost and the wrong one for a
shared host. Put it behind something that authenticates before you expose it.
Analysing in the visitor’s browser ¶
The viewer’s analysis can run in the visitor’s browser instead of on the server. The engine is the
same Go code compiled to WebAssembly, and it answers the same calls the server does, so the panels
cannot tell which one answered. make wasm builds it, and then either the server or a single page
picks it:
agni serve --engine wasm --mount tut=examples/tutorial-project
A page’s ?engine=wasm or ?engine=server overrides the server’s choice for that page. On the
wasm engine the page fetches the design’s files from the server once and analyses them in a worker,
and after that only listings and file reads reach the server. Files a visitor drops on the page stay
in the browser whichever engine is chosen.
A design whose files add up to more than --wasm-max-bytes (128 MB by default) goes to the server
engine instead, and the top bar says so. The default comes from measuring a 97 MB KiCad board, which
the browser read in 2.4 s and checked in 8.8 s.
With no server at all, agni site <outdir> --base <path> --seed <name>=<folder> writes the viewer,
the engine and the seeded designs as static files for any web host to serve.
The public demo runs this way, since the project hosts no server for it.
Kept reads ¶
The server keeps what it read between requests, so a second query, check or drawing on a design costs its own work rather than another parse of every file. On a 1,834-component KiCad board, a repeated net count went from 656 ms to 2.5 ms and a one-rule check from 1.35 s to 18 ms.
A kept read is checked on every use against each file it opened, walked or looked for and did not
find: the entry, every sub-sheet, a declared companion, the board and the symbol libraries. Edit any
of them and the next request reads the design again. A read whose project configuration cannot be
identified is never kept. A check also keeps each rule’s findings on the kept model, so a later
check of the same design runs only the rules it has not run yet. --design-cache <n> sets how many
reads are kept (default 12), and 0 keeps none.
A work budget for served queries ¶
A query that joins two large relations can run for minutes on a big board, and a shared server
answers everyone’s queries from one process. --query-budget <work> stops any query, and any
query-backed rule a check or review runs, once it has done that much work. A query stopped this way
returns resource_exhausted naming the budget, and a rule stopped this way reports one inconclusive
finding saying so, since it never decided. A request can ask for a smaller budget with
work_budget and never for a larger one.
No budget is enforced by default, because the right number depends on the boards a deployment
serves. Instead the server logs every query costing more than --query-budget-warn (10,000,000
units by default) with the query and a budget ten times its cost, so a deployment can watch what
its users ask before choosing one.
This line is from the tutorial project’s board with --query-budget-warn 100:
query cost: /agni.v1.webapi.QueryService/RunQuery spent 394 work units, over the 100 warning threshold; no budget is enforced, and --query-budget 3940 would allow ten times this cost: component.net(?a, ?n), component.net(?b, ?n) => ?a, ?b
One unit is one tuple a rule derives or one fact a lookup reads, and a circuit walk such as
net.reaches adds one for each connection it examines, so the number grows with the board. On a public 1,123-part sample board the heaviest shipped example query cost about one
million units. agni query --format json and the RunQuery response report what an answer cost
as work. A served request also stops when its caller goes away, which returns canceled rather
than an error about the request.
Where a request’s time goes ¶
A slow request can say where its time went. --slow-request <duration> logs one line for every
request over it, naming the stage that took the most, the tier that answered each cached read, and
the slowest rules. These two lines are the tutorial project’s board, asked twice with
--slow-request 1ms:
slow request: /agni.v1.webapi.CheckService/CheckDesign took 87ms, over 1ms; slowest stage resolve.overlay 44ms; cache model=built design=built board=built geometry=built; slowest rules profile/emmc-signal-missing 3ms, profile/spi_nor-signal-missing 3ms, dl/power-pin-mistyped 2ms, profile/pcie-signal-missing 2ms, profile/emmc-host-incomplete 2ms
slow request: /agni.v1.webapi.CheckService/CheckDesign took 29ms, over 1ms; slowest stage resolve.overlay 17ms; cache model=memory geometry=memory; slowest rules profile/emmc-signal-missing 2ms, profile/emmc-signal-dangling 1ms, profile/emmc-host-incomplete 1ms, gateway-profiles/can-host-incomplete 1ms, gateway-profiles/can-esd-missing 1ms
The second request found the model in memory, so it read nothing, and the cache column says so. If
a cache layer reads built on every request, the server is failing to keep that design. When
evaluating a query was the slowest stage, the line carries that query’s plan as well.
Any client can ask for the same breakdown on a single request by sending an Agni-Timing: 1 header
(Agni-Timing: explain adds each query’s plan). The response then carries it in an Agni-Timing
header, as the RequestTiming message in protojson, and as a Server-Timing header. A browser’s
network panel draws that header as a bar per stage. The breakdown travels in headers rather than in
the response, because a number that varied on every run would make every saved answer differ from
the last. The viewer asks for it when its address carries ?timing (or ?timing=explain) and logs
each request’s breakdown to the console, and the browser engine answers it just as the server does.
The CLI’s --timing on check, query, trace and review prints it from the same record, and
query --explain prints the plans alone.
To find which stage raised the memory peak, send Agni-Timing: memory instead, or open the page with
?timing=memory, or pass --timing-memory on the CLI. Each stage then carries the process’s memory
as it ended, and each rule what it allocated.
Stages nest, so build.model includes the reads beneath it (read.netlist, read.board) and the
model it builds, and the stage list says when each started as well as how long it took.