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#
| Path | What 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:
| File | Subject |
|---|---|
main.go | Flags, the subcommand split, routes(), runDaemon(), and most read handlers |
vyos.go | The router client: the five endpoints, the timeout tiers, error handling |
opmode.go | The op-mode show reads the product depends on |
parse.go | Turning VyOS' fixed-width text tables into structures |
staging.go | The working set |
store.go | state.json (accounts, sessions, tokens, settings) and audit.jsonl beside it |
auth.go, authhttp.go | Argon2id, TOTP, sessions, the role wrappers |
admin.go | User and token administration, with the rules that stop an admin locking the product out |
security.go | Body limits, redaction, rate limiting, headers, request metrics |
oidc.go | Single sign-on as a relying party |
license.go | Key verification, refresh, and the write-plane gate |
apps.go, catalog.go, catalog.json | The app store: lifecycle, and the catalogue itself |
desired.go, reconcile.go | Desired state, the plan engine and the optional loop |
wan.go | Uplinks, failover, load balancing, policy routing |
ids.go | Suricata through service suricata |
gaps.go, parity.go | VRRP and conntrack sync; power, images and the other operations that are not config changes |
fleet.go | The router list and its concurrent reads |
cache.go, primer.go | The read cache and what keeps it warm |
metrics.go | A hand-written Prometheus exposition |
tls.go | Certificates, including the self-signed path |
lock_unix.go, lock_other.go | An advisory lock on the data directory, so a one-shot apply cannot rewrite a running daemon's state |
*_test.go | Over 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 pageui/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.
| Path | What it does |
|---|---|
build-deb.py | Writes wheelhouse-agent_<version>_<arch>.deb with the Python standard library alone |
wheelhouse-agent.service | The shipped unit: flags, hardening, the 200 MB ceiling |
wheelhouse-firstboot.service, firstboot.sh | Mints the router API key, enables REST, pins the API to loopback |
wheelhouse-console.service, .timer, console-banner.sh | The banner above the login prompt, redrawn on a timer |
wait-for-vyos.sh | Both units wait for VyOS to finish loading its configuration, which happens seconds after vyos-router.service reports active |
wheelhouse-install | The disk installer: the dialog walk-through and the unattended --answers path |
wheelhouse-live-installer | A systemd generator that makes getty@tty1 run the installer on a live boot |
wheelhouse-autoinstall | The seed finder |
install-driver.py | Drives VyOS' own install image through a pseudo-terminal, answering its prompts |
seed-to-answers.py | Cloud-config in, installer answers out, with and without PyYAML |
dialogrc | The installer's colours |
copyright | The .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/#
| Path | What it does |
|---|---|
opnsense-import.py | Turns 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.py | Builds 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.mdcarries an explicit empty slot with the command that would produce them. - No signing key. The release pipeline signs
SHA256SUMSwhenRELEASE_SIGNING_KEYis 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#
- Architecture — how these directories relate at run time.
- Building the agent · Building the web UI · Building the package · Building the image
- Working agreements — what a change to any of these owes the next person.
- Files and directories — the same question from the other end: every path the product creates on a running router.
- The developer install —
install/from the operator's side, and why it is not the product.
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