Wheelhouse docs

The repository, directory by directory#

One repository holds the whole product: the Go control plane, the React web UI, the installer, the packaging that turns both into a .deb and an ISO, the migration and seed-building tools, and the documents that record what was decided. This page is the map. It names every top-level directory, says what lives there and what it depends on, and points at the one file in each that is worth opening first.

Top level#

PathWhat it is
agent/The control plane: Go, one main package, no sub-packages
ui/The web UI: React, TypeScript, Vite, Tailwind
packaging/The .deb builder, the systemd units, first boot, the console banner, the installer, and iso/ for the image
install/The developer install path and the artefact signing script — not the product
tools/The OPNsense importer, the seed builder, and their tests
scripts/check-images.py, which validates and pins the catalogue's container images
docs/Operator and contributor documentation, including adr/
site/This documentation site and the product site, and the generator that builds both
.forgejo/workflows/Continuous integration: ci.yml, iso.yml, and the publishing helpers beside them

And at the root: PLAN.md (the product thesis, the architecture and the milestones), README.md (including the status section that this documentation is not allowed to contradict), AGENTS.md (the working agreements), CHANGELOG.md, PRICING.md, SECURITY.md and SUPPORT.md.

agent/#

Flat. Every file is package main, and the split is by subject rather than by layer:

FileSubject
main.goFlags, the subcommand split, routes(), runDaemon(), and most read handlers
vyos.goThe router client: the five endpoints, the timeout tiers, error handling
opmode.goThe op-mode show reads the product depends on
parse.goTurning VyOS' fixed-width text tables into structures
staging.goThe working set
store.gostate.json (accounts, sessions, tokens, settings) and audit.jsonl beside it
auth.go, authhttp.goArgon2id, TOTP, sessions, the role wrappers
admin.goUser and token administration, with the rules that stop an admin locking the product out
security.goBody limits, redaction, rate limiting, headers, request metrics
oidc.goSingle sign-on as a relying party
license.goKey verification, refresh, and the write-plane gate
apps.go, catalog.go, catalog.jsonThe app store: lifecycle, and the catalogue itself
desired.go, reconcile.goDesired state, the plan engine and the optional loop
wan.goUplinks, failover, load balancing, policy routing
ids.goSuricata through service suricata
gaps.go, parity.goVRRP and conntrack sync; power, images and the other operations that are not config changes
fleet.goThe router list and its concurrent reads
cache.go, primer.goThe read cache and what keeps it warm
metrics.goA hand-written Prometheus exposition
tls.goCertificates, including the self-signed path
lock_unix.go, lock_other.goAn advisory lock on the data directory, so a one-shot apply cannot rewrite a running daemon's state
*_test.goOver two hundred test functions, run race-clean in CI. Count them: grep -h '^func Test' agent/*_test.go \| wc -l

There is no internal/, no pkg/ and no interface layer between the handlers and the router client. That is deliberate: the agent is one program with one job, and the file name is the index.

ui/#

ui/
├── index.html          the Vite entry point
├── package.json        three runtime dependencies: react, react-dom, react-router-dom
├── vite.config.ts      the dev server and its proxy to the agent
├── tailwind.config.js  semantic names over the CSS custom properties in index.css
├── public/fonts/       Inter and JetBrains Mono, self-hosted, with their OFL texts
├── scripts/            screenshot.mjs, driven by `npm run shots`
└── src/
    ├── lib/            api.ts, session.tsx, hooks.ts, staging.tsx, toast.tsx, theme.tsx, format.ts
    ├── components/     ui.tsx, Layout.tsx, nav.ts, CommitBar.tsx, CommandPalette.tsx, Brand.tsx, …
    └── pages/          one component per route, plus the tab bodies that share a page

ui/dist/ is the build output and is what the .deb installs. ui/node_modules/ is npm ci's work and belongs to nobody.

packaging/#

Everything that turns two build outputs into something installable.

PathWhat it does
build-deb.pyWrites wheelhouse-agent_<version>_<arch>.deb with the Python standard library alone
wheelhouse-agent.serviceThe shipped unit: flags, hardening, the 200 MB ceiling
wheelhouse-firstboot.service, firstboot.shMints the router API key, enables REST, pins the API to loopback
wheelhouse-console.service, .timer, console-banner.shThe banner above the login prompt, redrawn on a timer
wait-for-vyos.shBoth units wait for VyOS to finish loading its configuration, which happens seconds after vyos-router.service reports active
wheelhouse-installThe disk installer: the dialog walk-through and the unattended --answers path
wheelhouse-live-installerA systemd generator that makes getty@tty1 run the installer on a live boot
wheelhouse-autoinstallThe seed finder
install-driver.pyDrives VyOS' own install image through a pseudo-terminal, answering its prompts
seed-to-answers.pyCloud-config in, installer answers out, with and without PyYAML
dialogrcThe installer's colours
copyrightThe .deb's licence file
iso/The image: build-iso.sh, wheelhouse.toml and branding/

install/ — the developer path, not the product#

install/install.sh puts an agent on a host that can reach a VyOS router's API. Its own header says what it is:

THIS IS THE DEVELOPMENT AND LAB PATH, NOT THE PRODUCT.

It installs into /opt/wheelhouse, keeps secrets in /etc/wheelhouse and binds 127.0.0.1:8090. An appliance uses /usr/bin/wheelhouse-agent, /config/wheelhouse and 0.0.0.0:8443, and reads nothing from /etc/wheelhouse — so a secret written there by this script is a file the shipped agent will never open. Keeping the two apart matters more than it sounds: most "the agent cannot see my key" reports are this confusion.

install/sign.sh builds and verifies checksums and detached signatures; install/wheelhouse-agent.service is the developer unit, deliberately a subset of the shipped one.

tools/#

PathWhat it does
opnsense-import.pyTurns an OPNsense config.xml into VyOS commands plus a report of what could not come across. --check-against compares a running router with the configuration it should have. Ships in the .deb as wheelhouse-opnsense-import
wheelhouse-seed.pyBuilds an unattended-install seed, as an image or a directory
tests/Four unittest suites, standard library only

docs/#

The operator and contributor documentation that predates this site, and which this site restates rather than replaces. docs/README.md is its index; docs/adr/ holds the decision records; docs/ui.md is the UI contributor's document; docs/api-cookbook.md holds worked API calls.

What is not in the tree#

  • No screenshots. There are none in the repository, and none may be fabricated for this site. README.md carries an explicit empty slot with the command that would produce them.
  • No signing key. The release pipeline signs SHA256SUMS when RELEASE_SIGNING_KEY is configured, and says loudly that it did not when it is missing. No key has been generated.
  • No generated API client and no OpenAPI document.

See also#

Checked against#

agent/main.go · agent/catalog.json · ui/package.json · ui/vite.config.ts · packaging/README.md · packaging/build-deb.py · install/install.sh · install/sign.sh · tools/opnsense-import.py · docs/README.md · README.md · AGENTS.md

Updated 2026-09-02 development repository layout