Wheelhouse docs

The configuration tree#

A Wheelhouse router keeps its entire configuration in one tree. Every setting is a path through that tree ending in a value, and every change is a set or a delete against such a path. There is no second place where configuration lives: no per-page table, no XML document, no database rows that a form writes on save. The web UI is a set of editors over that one tree, and each screen states the subtree it owns beside its title, so you always know which part of the tree you are looking at.

Paths, nodes, values#

Read a path as words separated by spaces, the way the router's own CLI spells it:

set interfaces ethernet eth1 address 203.0.113.2/24
set interfaces ethernet eth1 description 'primary uplink'
set protocols static route 0.0.0.0/0 next-hop 203.0.113.1

Three kinds of thing appear in a path.

KindExampleWhat it is
A nodeinterfaces, protocols staticA branch with a fixed name. Its children are fixed too.
A tag nodeethernet eth1, route 0.0.0.0/0, name adguardA branch whose children are named by you. ethernet is the tag node; eth1 is one instance of it.
A value203.0.113.2/24, 'primary uplink'The leaf. It is the last word of a set command.

A node can also exist with no value at all — set service ssh switches SSH on and has nothing after it. That shape matters later, because a desired-state file has to be able to express it.

Reading a subtree#

GET /api/config returns the tree, or the subtree named by path, as JSON.

bash
R=https://<router>:8443
T=wh_...                                  # an API token with the viewer role

curl -sk -H "Authorization: Bearer $T" "$R/api/config?path=%5B%22service%22,%22dns%22%5D"
json
{"forwarding": {"cache-size": "10000", "listen-address": {"192.0.2.1": {}}}}

Two details in that request are worth knowing, because both have cost somebody time.

path is a JSON array, not a slash-joined string. The agent accepts both, but a slash-joined string cannot name a node whose key contains a slash — and plenty do: route 0.0.0.0/0, subnet 192.0.2.0/24. The web UI always sends the array form (agent/main.go parsePathParam).

A tag-node read comes back wrapped in its own name. The router answers a read ending at an ordinary node with the bare subtree, but a read ending at a tag node with the subtree inside a single key named after the node — container name answers {"name": {"adguard": …}}, not {"adguard": …}. The agent unwraps that in one place, agent/opmode.go configTag, and the comment there records what reading the wrapped form as an instance map cost: every instance reported as absent.

An unset subtree is not an error worth showing. The router answers a read of an empty path with HTTP 400 and the words Configuration under specified path is empty. That is the normal state of a router with no high-availability configuration, so handlers test for it rather than reporting the router as unreachable — agent/gaps.go handleVRRP.

Reading a subtree as commands#

Any subtree can be rendered back as the set lines that would recreate it. That is what the Config tree page's All commands view shows, and what every CLI panel on every editor is built from:

bash
curl -sk -H "Authorization: Bearer $T" "$R/api/config/commands?path=%5B%22nat%22%5D"
set nat destination rule 10 description 'web'
set nat destination rule 10 destination port 443
set nat destination rule 10 inbound-interface name eth1
set nat destination rule 10 translation address 192.0.2.10

A third form exists for backups: GET /api/config/raw returns the configuration in the router's native curly-brace format. It is the only configuration read that is not redacted, so it requires the admin role — agent/main.go, routes().

What the tree is not#

  • It is not the agent's data. The router owns it. The agent persists only what the router cannot: accounts, API token hashes, sessions, settings, the licence key and the audit log, in /config/wheelhouse/agent/store.go. Every other thing the UI shows is read from the router on demand.
  • It is not everything the router is doing. The tree is intent. What the kernel has actually installed is a separate question, and where the two disagree the UI says so — see State, intent and drift.
  • It does not cover the whole of Linux. Some things the kernel can do have no node in the tree. Where that matters, the page in question says so with a Platform gap callout rather than inventing a node.

Why every screen names its subtree#

Because it makes a screen falsifiable. A page that says it owns nat can be checked: read nat as commands, compare it with what the page shows, and any difference is a bug in the page rather than a mystery. The rule is in docs/ui.mdevery page names its config path next to the title — and it is the same rule that makes the Config tree page useful as a cross-check on every other page.

The Config tree page renders the whole tree, filters it by path or value, and renders any node you select as the commands that would recreate it (ui/src/pages/ConfigTree.tsx). The command palette indexes config objects out of the running configuration as well as pages, so a rule number or a peer name is a search away (ui/src/components/CommandPalette.tsx).

See also#

Checked against#

agent/vyos.go · agent/opmode.go · agent/main.go · agent/gaps.go · agent/staging.go · agent/store.go · ui/src/pages/ConfigTree.tsx · ui/src/lib/format.ts · docs/ui.md

Updated 2026-09-02 concepts config vyos