Wheelhouse docs

Desired state#

A desired-state file says what the router should look like. The agent reads it, reads the router, and works out the set and delete operations that would close the gap. Those operations travel the same road as anything typed into a form: they are staged, shown as commands, and committed by a person — unless you deliberately start a loop in commit mode, which commits behind the router's own commit-confirm window. This is not a second control plane. It is a different way of filling the same working set.

The file mirrors the configuration tree#

The document is the shape of the config tree, nested. Keys are node names, values are leaf values.

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

That plans exactly what it reads as:

set service dns forwarding cache-size 10000
set service ssh
set qos interface eth1 egress WAN-OUT

Both formats parse, chosen by content. A document starting with { or [ is read as JSON; anything else is read as YAML. So agent.yaml holds YAML, an inline document in an API body stays JSON, and a mislabelled file fails on its contents instead of being parsed into the wrong shape.

Values keep the spelling you wrote. The router stores text, so values are carried through unconverted: 0755 decoded normally becomes 493, 1.10 becomes 1.1, a date becomes a timestamp, and a mapping under a numeric key such as rule: 10: becomes something the engine would read as a leaf and plan as set … rule ''.

A key with no value#

ssh: in YAML, {} in JSON, means this node exists — the shape the config tree uses for a switch-on node. What it plans depends on what the router already holds, so the engine reads it against live state:

On the routerWhat the engine does
Nothing therePlans a bare set service ssh, and the plan carries a warning saying so.
A node that already has childrenSatisfied. Nothing planned.
A node holding a valueA contradiction, and the plan refuses to run: system.host-name: the router holds "edge" here but the file gives it no value.

An explicit empty string is different, and is refused where the document is read, before any diffing, with the hint an empty value is not something VyOS can set — give it a value, or drop the key. Warnings come back in the API's warnings field; the loop logs a set of warnings once, not once per pass.

Partial by default, full on request#

By default the engine emits set operations for differences only. It never deletes something the file does not mention, so a partial file — "these six things must be true" — is safe next to configuration that nothing manages.

--full (on the command line), full: true (in an API body) or --reconcile-full (on the loop) changes that: what the file does not declare is deleted. It is the right mode for a file that is meant to be the whole router, and the wrong one for a fragment.

Four ways to use it#

SurfaceDefault behaviourEscape hatch
The Reconcile pageStages into the Commit Bar—
POST /api/reconcileReturns the plan; apply: true stages it—
GET /api/reconcile/fileDiffs a file inside the agent's data directory against live—
wheelhouse-agent planPrints the commands, changes nothing, exits 2 when it found work--json, --full
wheelhouse-agent applyStages through a running agent (--agent-url)--commit --confirm-minutes N
--reconcile-file (the loop)Stages (--reconcile-mode stage)--reconcile-mode commit, window from --reconcile-confirm

plan exits 2 when the router differs from the file, the way diff -q does, so it drops into a shell test or a CI gate without anyone parsing text.

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

# Stage into a running agent's Commit Bar; a person still commits:
wheelhouse-agent apply --file /config/wheelhouse/agent.yaml \
  --agent-url https://127.0.0.1:8443

# Or commit from cron, behind the router's own commit-confirm:
wheelhouse-agent apply --file /config/wheelhouse/agent.yaml --commit --confirm-minutes 2

Two refusals:

  • apply without --agent-url refuses to stage. The working set is the running agent's memory. A one-shot process filling its own copy would report success and lose the work on exit, so staging is a POST /api/stage to the agent, authenticated with the break-glass token.
  • --commit without a confirm window refuses. Automation may commit, never silently.

GET /api/reconcile/file takes a name inside the agent's data directory, never a path. An absolute path is accepted only when it resolves inside the data directory, and the name must end in .yaml, .yml or .json. The default name is agent.yaml.

The loop#

Off unless asked for. --reconcile-file <path> starts it; it waits --reconcile-delay (15 seconds by default, so boot can settle) and then re-diffs every --reconcile-interval (60 seconds by default, clamped to a 10-second floor).

  • In stage mode it fills the Commit Bar and stops. The Commit Bar stays the thing that commits.
  • In commit mode it commits with a --reconcile-confirm minute window (2 by default) and confirms only once the router still answers, so a change that cut the agent off is left to roll back on its own.

It will not stage the same operation twice. Operations it has already put in the working set are not offered again until somebody commits or un-stages them. An operation deliberately un-staged is offered again on the next pass; the working set is the source of truth, not the loop's memory.

Its writes are attributed to it. Everything the loop does is audited as actor: reconcile-loop, role: system, so the record never blurs a machine write with a human one — see The audit log.

Reading the drift without a shell#

bash
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-09-02T10:27:07Z"}

managed: false and mode: "off" mean no file is being managed, which is the default. The metrics wheelhouse_desired_managed, wheelhouse_desired_drift and wheelhouse_desired_pending_ops exist only while a file is being managed.

What it is not#

  • It is not the agent's state file. state.json is the agent's own store — accounts, tokens, sessions, settings, the licence key.
  • It is not templated. One file, one router, no per-host substitution. Templating and file-watch triggers are not built.
  • It is not a second source of truth. The router's configuration tree is still the truth. The file is a claim about what the tree should contain.

See also#

Updated 2026-09-03 concepts automation reconcile