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 both the endpoint reference and the OpenAPI document are generated from that one table, so neither can drift from the code:
wheelhouse-agent openapi # OpenAPI 3.1, from the agent's own route table
wheelhouse-agent endpoints # the same table, as the Markdown endpoint referenceBoth answer before a router, a key or a licence is read, so a build machine that has never
seen a router can generate them; CI does, and fails when the checked-in reference and the
code disagree. A running agent serves the same document at GET /api/openapi.json.
Authenticate 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.
Committing has to be asked for twice
Everything stages by default. apply refuses to stage without --agent-url, because the
staging area belongs to the running agent rather than to a process that is about to exit,
and it refuses --commit without a confirm window.
Automation that reaches a router through this engine leaves a working set behind for a person to read, unless it is told twice to do otherwise.
# 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 rather than 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.
Each router has its own pending changes, in a queue of its own on the agent. Changes reach
it through the API, or through the page's command entry in Advanced mode; on the Fleet
page they are reviewed, removed one at a time, discarded, or applied to that router.
Apply with auto-revert arms that router's own commit-confirm, and Keep changes
confirms it on that router and no other, at POST /api/fleet/{id}/commit/confirm — the
local router's confirm route would leave the far one to restart.
Read on#
- The wiki: the endpoint index and the desired-state guide.
Call the same API the interface calls
An API token, a curl, and the four steps of a change. There is no private endpoint set
and no second protocol.