Wheelhouse

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:

bash
grep -o 'mux.HandleFunc("[A-Z]* [^"]*"' agent/main.go

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.

bash
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:

/config/wheelhouse/agent.yaml
service:
  dns:
    forwarding:
      cache-size: 10000
  ssh:
qos:
  interface:
    eth1:
      egress: WAN-OUT

The format is sniffed, so the same engine reads YAML on disk and JSON over the API.

bash
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.

bash
# 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 2

The 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#

bash
curl -sk -H "Authorization: Bearer $T" "$R/api/drift"
json
{"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#

Updated 2026-09-02