API-first, and configuration as code#
Every route the web interface uses is a route you can call. There is no private API and no second, richer protocol underneath.
One API#
The agent registers every route in one place, and the operator reference lists all of them. Re-derive the list at any time:
grep -o 'mux.HandleFunc("[A-Z]* [^"]*"' agent/main.goAuthenticate with an API token instead of a session cookie. Tokens carry a role, may have
an expiry, are stored only as a SHA-256, and are shown once. They begin wh_, so a
leaked one is findable by a secret scanner and greppable in a log. Token requests skip the
CSRF header that browser sessions must send, which is why a token is the right credential
for a script and a copied cookie is not.
R=https://<router>:8443
T=wh_...
curl -sk -H "Authorization: Bearer $T" "$R/api/system"
curl -sk -H "Authorization: Bearer $T" "$R/api/wan"
curl -sk -H "Authorization: Bearer $T" "$R/api/audit?limit=300"Configuration as code#
One file says what the router should look like; the agent works out the set and
delete commands that close the gap. The format mirrors the VyOS configuration tree,
so there is nothing new to learn:
service:
dns:
forwarding:
cache-size: 10000
ssh:
qos:
interface:
eth1:
egress: WAN-OUTThe format is sniffed, so the same engine reads YAML on disk and JSON over the API.
wheelhouse-agent plan --file /config/wheelhouse/agent.yaml
# set service dns forwarding cache-size 10000
# set service ssh
# warning: service.ssh: planned as a bare set (a node with no value)
#
# 2 change(s) needed … exit 2 (0 = in sync, like diff -q)plan exits 0 when the router already matches and 2 when it does not, the way diff -q
does, so it drops into a pipeline without parsing. --json is there for scripts and
--full also deletes what the file omits.
A key with no value is how the file says this node exists. What that means depends on
the router, so the engine reads it against live state: over a missing node it plans a
bare set and warns; over a node that already has children it is satisfied; over a node
holding a value it is a contradiction and the plan refuses to run rather than guessing.
Four surfaces, one engine#
The same diff logic sits behind the Reconcile page in the interface,
POST /api/reconcile, the plan and apply subcommands, and an opt-in loop
(agent/desired.go,
ADR-001).
Everything stages by default. apply refuses to stage without --agent-url, because
the staging area belongs to the running agent and not to a process that is about to exit,
and it refuses --commit without a confirm window. Automation that commits silently is
how a fleet gets an outage at 03:00, so committing has to be asked for twice.
# Stage into a running agent's Commit Bar — a person still commits
wheelhouse-agent apply --file … --agent-url https://127.0.0.1:8443
# Or commit from cron, protected by the router's own commit-confirm
wheelhouse-agent apply --file … --commit --confirm-minutes 2The loop, if you want one#
--reconcile-file turns on a loop that re-diffs on an interval (default 60 s, 10 s
floor) after a settling delay. It is off unless asked for. In the default stage
mode drift lands in the Commit Bar and a person commits it; in commit mode the loop
commits with a confirm window and then confirms only once the router still answers, so a
change that cut the agent off is left to roll back on its own.
The loop never stages the same operation twice: if it already filled the Commit Bar and
nobody committed, the next pass says so in the log instead of duplicating the work. Its
writes are audited as actor: reconcile-loop, role: system.
Drift is an endpoint#
curl -sk -H "Authorization: Bearer $T" "$R/api/drift"{"managed": true, "file": "/config/wheelhouse/agent.yaml", "mode": "stage",
"drifted": true, "ops": 2, "checked_at": "2026-08-30T22:27:07Z"}Three wheelhouse_desired_* metrics carry the same facts to Prometheus.
Fleet#
--fleet-config loads a JSON file of routers and serves concurrent per-router health,
plus per-router configuration and version reads and a configure endpoint. The Fleet page
shows a live status list.
Read on#
- The wiki: the endpoint index and the desired-state guide.