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#
| Tool | Version the tree expects | Where that is stated |
|---|---|---|
| Go | 1.22 or newer; CI runs the golang:1.26-bookworm image with GOTOOLCHAIN=auto | agent/go.mod, .forgejo/workflows/ci.yml |
| Node | 22.12.0 in CI, installed from the upstream tarball | .forgejo/workflows/ci.yml |
| Python | 3, standard library only | packaging/build-deb.py, site/build.py |
| Docker | Only to build the ISO, and only there | packaging/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.
cd agent
go build -o wheelhouse-agent .
./wheelhouse-agent --demo --addr 127.0.0.1:8090 --data-dir /tmp/wh-dataOn 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.1Drop 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:
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:
cd ui
npm install
WHEELHOUSE_AGENT=http://127.0.0.1:8090 npm run dev # http://localhost:3000WHEELHOUSE_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.
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 binaryAn 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#
# 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.pyThe 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 see | What it is |
|---|---|
either --demo or both --api-url and --api-key are required, exit 2 | Exactly that. The agent will not start pointing at nothing |
another agent is running against this data dir | The 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 404 | set 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 router | The 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 something | No 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:
| Variable | Flag 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#
- Building the agent — the build flags, and what
-X main.versiondoes. - Building the web UI — the dev server, the build, and the screenshot script.
- Architecture — what the agent is talking to when it works.
- The test suites — what to run before you push.
- Every flag · Environment variables — all 51 flags, with defaults, rather than the handful used here.
- The developer install — the same path written for someone setting up a lab rather than changing the code.
- What first boot does — the three configuration lines above, as the appliance runs them.
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