Wheelhouse docs

A development environment#

Wheelhouse is built by three toolchains and no more: Go for the agent, Node for the web UI, and Python 3 for the packaging, the tools and this documentation site. None of the Python needs a pip install and none of it may grow one. To change anything you need a Go toolchain and a checkout; to change the UI you also need Node; to run the agent against something real you need a VyOS router with its HTTP API enabled, or --demo, which starts the agent with no router at all.

What to install#

ToolVersion the tree expectsWhere that is stated
Go1.22 or newer; CI runs the golang:1.26-bookworm image with GOTOOLCHAIN=autoagent/go.mod, .forgejo/workflows/ci.yml
Node22.12.0 in CI, installed from the upstream tarball.forgejo/workflows/ci.yml
Python3, standard library onlypackaging/build-deb.py, site/build.py
DockerOnly to build the ISO, and only therepackaging/iso/build-iso.sh

Nothing else is required. There is no Makefile, no task runner and no container for development.

Running the agent with no router#

--demo starts the agent without VyOS credentials. It is not a mock: every router read fails, honestly, until --api-url and --api-key are given.

bash
cd agent
go build -o wheelhouse-agent .
./wheelhouse-agent --demo --addr 127.0.0.1:8090 --data-dir /tmp/wh-data

On first start the agent creates the account named by --initial-admin (default admin), generates a password if none was given, and logs it once. Read it out of the process output or the journal and sign in.

Running the agent against a real router#

The agent needs a VyOS box with the API key configured and the REST endpoints enabled. That is exactly what first boot does on an appliance, and the same three lines do it by hand:

set service https api keys id wheelhouse key "<a long random string>"
set service https api rest
set service https listen-address 127.0.0.1

Drop listen-address 127.0.0.1 when the agent is running on another host, and give the router an address the agent can reach instead. Then:

bash
cd agent
go run . \
  --api-url https://192.0.2.10 \
  --api-key-file ~/.config/wheelhouse/api-key \
  --addr 127.0.0.1:8090 \
  --data-dir /tmp/wh-data \
  --log-level debug

--api-key exists but prefer --api-key-file: a flag value is visible to every user on the box through /proc, which is why the shipped unit uses only the file forms.

Running the UI against that agent#

The Vite dev server proxies /api, /health and the /api/stream WebSocket at the agent, so the application is same-origin in development exactly as it is in production — ui/vite.config.ts:

bash
cd ui
npm install
WHEELHOUSE_AGENT=http://127.0.0.1:8090 npm run dev    # http://localhost:3000

WHEELHOUSE_AGENT defaults to http://10.0.0.50:8090, which is the development bench, so set it. Point it at https://<router>:8443 to drive an appliance.

No credential is baked into the bundle. The browser authenticates against the agent and holds a session cookie; mutations carry the session's CSRF token in X-Wheelhouse-CSRF.

The developer install#

install/install.sh is the scripted version of the off-router setup: it puts a binary in /opt/wheelhouse, writes secrets to /etc/wheelhouse with mode 0600, installs a systemd unit and binds 127.0.0.1:8090.

bash
sudo ./install/install.sh --api-url https://192.0.2.10 --api-key "$KEY"
sudo ./install/install.sh verify      # health-check the running agent
sudo ./install/install.sh rollback    # restore the previous binary

An install over an existing one keeps the old binary at /opt/wheelhouse/wheelhouse-agent.old, restarts the service, health-checks it, and puts the old binary back automatically if the check fails. The health probe tries HTTPS first and falls back to plain HTTP, because the unit runs with a self-signed certificate and a probe that only spoke HTTP once rolled back a perfectly good binary.

The other things you can run locally#

bash
# The agent's tests, the way CI runs them.
(cd agent && go vet ./... && go test -race -count=1 ./...)

# The Python suites CI runs.
python3 tools/tests/test_opnsense_import.py
python3 tools/tests/test_seed.py

# The catalogue's reference shape, with no network.
python3 scripts/check-images.py agent/catalog.json --offline

# The UI, as CI checks it.
(cd ui && npm ci --no-audit --no-fund && npx tsc --noEmit -p . && npx vite build --logLevel warn)

# This documentation site.
python3 site/build.py --strict
python3 site/tests/test_build.py

The full picture of what each suite covers, and the two suites CI does not run, is on The test suites.

When it does not work#

The six failures that actually happen, and what each one is:

What you seeWhat it is
either --demo or both --api-url and --api-key are required, exit 2Exactly that. The agent will not start pointing at nothing
another agent is running against this data dirThe advisory lock. Another agent — or a wheelhouse-agent apply — holds that directory (agent/lock_unix.go)
… is mode 644; it must not be readable by group or other (chmod 600)A secret file's permissions. Every secret file's mode is checked, not assumed (agent/main.go loadSecretFiles)
Every router read fails, and the router answers 404set service https api rest was never committed. With only a key configured, the VyOS API answers 404 to everything
502 with a message from the routerThe agent reached VyOS and VyOS said no. The body carries the router's own words; read those rather than the status
402 on anything that changes somethingNo licence. Reads work; the write plane does not. This is the design, not a bug (agent/license.go)

A missing but optional secret file — the break-glass token, the licence key — logs a warning and the agent carries on. Only --api-key-file is required, because a unit file names all of them unconditionally and a missing optional one must not crash-loop the service.

Environment variables the agent reads#

All of them have a flag, and the flag wins where both are set. From agent/main.go registerFlags:

VariableFlag it backs
VYOS_API_KEY--api-key
WHEELHOUSE_ADMIN_TOKEN--admin-token
WHEELHOUSE_LICENSE--license-key
WHEELHOUSE_INITIAL_PASSWORD--initial-admin-password
WHEELHOUSE_OIDC_ISSUER--oidc-issuer
WHEELHOUSE_OIDC_CLIENT_ID--oidc-client-id
WHEELHOUSE_OIDC_SECRET--oidc-client-secret

The build also reads SOURCE_DATE_EPOCH in packaging/build-deb.py, and VITE_WHEELHOUSE_VERSION at UI build time (ui/src/components/product.ts).

See also#

Checked against#

agent/main.go · agent/go.mod · agent/lock_unix.go · ui/vite.config.ts · ui/package.json · install/install.sh · packaging/firstboot.sh · packaging/wheelhouse-agent.service · .forgejo/workflows/ci.yml · docs/ui.md · docs/deploy.md

Updated 2026-09-02 development environment setup