Wheelhouse docs

Config as code with agent.yaml#

You will end up with a file that says what the router should look like, and a command that tells you the difference between the file and reality. Keep the file in version control and you have config as code, with the same review and history as anything else you keep there.

Before you start#

  • Shell access to the router, or an API token with the operator role.
  • A licence if you are going to apply anything. Planning is a read.

The format: a header, and the configuration tree#

The file has two parts. wheelhouse: is a header — the schema it is written to, and the router commit it was written against — and config: is the router's own tree, nested the way show configuration nests it.

/config/wheelhouse/agent.yaml
wheelhouse:
  schema: 1
  base: "2026-08-30 22:19:41"   # the config commit this file was written against
config:
  service:
    dns:
      forwarding:
        cache-size: 10000
    ssh:
  qos:
    interface:
      eth1:
        egress: WAN-OUT

The format is sniffed, so agent.yaml is YAML and an inline document posted to the API is JSON. Both describe the same tree.

A file written before the header still reads, with one warning saying how to add one. A file that declares a schema this agent does not know is refused, and so is a header field it does not have. plan, plan --json and GET /api/drift carry the schema and base back, so a diff says what it was measured against. GET /api/reconcile/file reports the operations and the warnings, not the header.

Paths are checked against the router's own nodes#

Before anything is diffed, every path in the file is checked against the router's own configuration templates. A node the router does not have stops the plan and is named:

service.dnss: the router has no such configuration node (did you mean "dns"?). Nothing
in this document was planned: one unknown node stops the whole file, rather than staging
the half of it that is right — fix service.dnss and run it again

That is the difference between finding a typo in the file and finding it at commit, in a message that names neither the file nor the line. Two things worth knowing:

  • It is all or nothing per file, and it says so. One unknown node stops the whole document — the reconcile loop adds nothing to the pending changes rather than adding the good half — and the refusal itself says nothing was planned, so "no changes" and "refused" are never confused.
  • It is honest about not knowing. Where the template tree is not readable — the CLI run off the router, say — the check is skipped and the plan says so in a warning rather than refusing paths it cannot judge.

plan, apply, the reconcile loop and GET /api/reconcile/file all run it.

A key with no value#

ssh: in YAML, or {} in JSON, is how the file says this node exists. What that means depends on the router, so the engine reads it against live state:

Live stateWhat the engine does
The node is missingPlans a bare set, and the plan says so in a warning
The node already has childrenSatisfied; nothing to do
The node holds a valueA contradiction; the plan refuses to run, naming the node and the value the router holds

An explicit empty string (host-name: "") is refused where the document is read, before any diffing.

Step 1 — Plan#

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

The exit code is the interface. plan exits 2 when it found a difference, the way diff -q does, and 0 when the router already matches. That drops into a shell test or a CI gate without parsing any text.

bash
wheelhouse-agent plan --file agent.yaml || echo "the router does not match the file"

Two more flags:

bash
wheelhouse-agent plan --file agent.yaml --json      # for scripts
wheelhouse-agent plan --file agent.yaml --full      # also delete what the file omits

--full is the dangerous one. Without it the file is a set of assertions: what it names must be so, and everything else is left alone. With it the file is the whole truth, and anything on the router the file does not mention is deleted. Read a --full plan very carefully the first time.

Step 2 — Apply#

apply adds the difference to the pending changes by default, and applies it only when told to: the same rule the web interface obeys.

bash
# Add to a running agent's pending changes; a person applies them from the Commit Bar.
wheelhouse-agent apply --file agent.yaml --agent-url https://127.0.0.1:8443

# Or apply, with the router's own auto-revert armed.
wheelhouse-agent apply --file agent.yaml --commit --confirm-minutes 2

Two refusals:

  • apply refuses to add changes without --agent-url. The pending changes belong to the running agent, not to a process that is about to exit.
  • apply refuses --commit without a confirm window. A committing automation without a rollback is how a fleet goes dark.

Adding changes through the agent authenticates with the break-glass token (--admin-token or --admin-token-file), and nothing creates that file by default — see the break-glass token.

Step 3 — Or do it over the API#

POST /api/reconcile takes a desired document and returns the operations. apply: true adds the difference to the pending changes; applying them is still the Commit Bar's job.

bash
R=https://<router>:8443
T=wh_...
curl -sk -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
  -d '{"desired_state":{"service":{"dns":{"forwarding":{"cache-size":"10000"}}}},"apply":false}' \
  "$R/api/reconcile"

The Reconcile page (System → Reconcile, listed when Advanced mode is on) does the same thing with a text box: paste a document into Wanted configuration or name a file under From the agent’s file, press Work out changes, read the plan, and press Add to pending changes. Working out the changes applies nothing.

GET /api/reconcile/file diffs the agent's own file against live. Its ?file= parameter takes a .yaml, .yml or .json name inside the data directory, needs the operator role, and answers 400 if the file plans an empty value.

Step 4 — Keep the file somewhere#

Version control, and the same review as your other infrastructure. The file is also something a backup has to contain — Take a backup that is actually complete names it.

Check it worked#

bash
wheelhouse-agent plan --file /config/wheelhouse/agent.yaml
echo $?     # 0 means the router matches the file

Then confirm the change is really on the router:

bash
show configuration commands | match cache-size

Undoing it#

An applied change is archived as a revision like any other; restore the revision before it. The file itself is a file; edit it, or the router will drift back the next time somebody applies it.

See also#

Updated 2026-09-18 desired-state config-as-code automation