7.9 KiB
Vacuum Wall — Agent Instructions
What This Is
SSL proxy / firewall appliance. Python 3 Flask WebUI behind nginx reverse proxy. Deploys on Debian 13 (trixie). Serves from repo root by default.
Architecture
Client ──→ nginx (SSL + basic auth) ──→ Flask (127.0.0.1:9090)
Flask ──→ daemon/client.py (Unix socket) ──→ vacuum-walld (aiohttp, daemon.sock)
vacuum-walld ──→ daemon/handlers/*.py ──→ sudo <cmd> ──→ system service
Two-User Model with Shared Group
vacuum-walld(daemon user): runs the privileged background daemon withNOPASSWD sudowhitelist (/etc/sudoers.d/vacuum-walld). Owns project directory and socket. Primary group is the WebUI user's primary group.- WebUI user (default: repo owner in
--devmode): runs the Flask process with zero sudo access. Communicates with the daemon via Unix socket. - Shared group: both users share the WebUI user's primary group. Socket is
vacuum-walld:<group>with mode0660. Project dir is owned by the WebUI user with group-read+execute.
Code Layout
webui/server.py— Flask app entry point. Only file that creates theapp.webui/api/*.py— Flask blueprints, one per subsystem. Routes prefix/api/<subsystem>/. All calldaemon.clientinstead oflib/directly.webui/api/common.py— Shared_ok()/_error()response helpers used by all blueprints.daemon/server.py— aiohttp server, cache engine, batch routing, handler registry.daemon/client.py— Sync HTTP client over Unix socket usingrequests_unixsocket.Session.daemon/handlers/*.py— Privileged operation handlers (allsudocalls live here).lib/common.py— Shared utilities:run(),run_proc(),load_json(),save_json(),deep_merge(),ensure_dirs(). Alllib/modules use these instead of defining local helpers.lib/*.py— Backend modules (parsing, config, shared logic). All have full type hints and__all__exports. No sudo calls — privilege escalation is handled bydaemon/handlers/*.py.data/— Runtime artifacts (generated .confs,.htpasswd, ACME certs, firewall backup, dnsmasq fragments).config/<subsystem>/config.json— Declarative JSON configs (source of truth). Generated.confindata/nginx/sites-enabled/. Certs indata/acme/.system/— System file templates.systemd/(service units installed to/etc/systemd/system/),sudoers.d/,nginx/.webui/static/— Vendored frontend libraries (JS + CSS). Flask auto-serves at/static/.
Project uses .venv. Install deps with pip install -e . (from pyproject.toml). __init__.py files in webui/ and lib/ are intentionally empty — no sys.path boilerplate needed.
No CDN packages. All frontend libraries (JS and CSS) must be vendored in webui/static/. Never reference unpkg.com, cdn.jsdelivr.net, or similar. To add/update a library, edit the version in scripts/update-vendor.sh and run it.
| Library | Version | Vendor file | Symlink (active) | CDN source |
|---|---|---|---|---|
| htmx | 2.0.4 | vendor/htmx-2.0.4.min.js |
webui/static/htmx.min.js |
npm:htmx.org@2.0.4 |
| htmx-ext-json-enc | 2.0.0 | vendor/json-enc-2.0.0.js |
webui/static/json-enc.js |
npm:htmx-ext-json-enc@2.0.0 |
Deployment
install.sh installs only system components and configures them; the project serves from the repo root by default. All options can be set via env vars or CLI flags (CLI takes precedence). Set INSTALL_DIR or --path to override install directory. Use --dev to auto-detect repo owner as service user (non-dev mode requires --user).
All Python modules use Path(__file__).resolve().parent.parent for PROJECT_DIR — no hardcoded paths. ACME certs live at PROJECT_DIR/data/acme/.
Local Dev
.venv/bin/python webui/server.py # binds 127.0.0.1:9090
In production the systemd unit runs as the configured service user (NoNewPrivileges, ProtectSystem=strict, loopback-only networking).
When install.sh --dev is used, the repo owner gets NOPASSWD sudo for system service commands (nginx -t, nginx -s reload, firewall-cmd, wg, systemctl reload dnsmasq, etc.). This allows invoking those commands directly in bash to inspect or test live system state during debugging, without relying on the mocked test suite.
Blueprint ↔ lib Mapping (Naming Is Not 1:1)
| Blueprint | URL prefix | Backend module |
|---|---|---|
webui/api/firewall |
/api/firewall/ |
lib.firewall |
webui/api/dhcp |
/api/dhcp/ |
lib.dnsmasq |
webui/api/proxy |
/api/proxy/ |
lib.nginx |
webui/api/certs |
/api/certs/ |
lib.acme |
webui/api/wireguard |
/api/wireguard/ |
lib.wireguard |
Privileged Operations
daemon/handlers/*.py call sudo for everything that touches system services. Whitelist is system/sudoers.d/vacuum-walld.
acme.sh must never run as root — always as the service user via sudo -u.
Pattern for mutations: write JSON → render native config → sudo <cmd> to apply.
Adding a new privileged command requires a sudoers entry and the daemon/handlers/ code.
API Response Contract
- Success:
{"ok": true, "data": <value>}— helper_ok(data)fromwebui.api.common - Error:
{"ok": false, "error": "msg"}— helper_error(msg, code=400)fromwebui.api.common acme.issue()/acme.renew()raiseRuntimeErroron failure — API layer wraps in try/except- HTTP codes:
400bad request,404not found,500internal failure
Page Routes vs API
server.py serves HTML pages with Jinja templates. All data is wrapped in _safely(fn, default) so page routes never 500 — they render with fallback values instead.
Deploy
install.sh is the single deploy script. Run as root, requires MGMT_DOMAIN, MGMT_PASS, ACME_EMAIL env vars.
Lint and Tests
Linter / formatter: Ruff (ruff check + ruff format). Config in pyproject.toml under [tool.ruff].
Tests: pytest in tests/. Run with python -m pytest. Tests mock out subprocess calls (firewalld, acme.sh, WireGuard, nginx, dnsmasq) — no system services required.
.venv/bin/ruff check lib/ webui/ tests/ # lint
.venv/bin/ruff format lib/ webui/ tests/ # format
.venv/bin/python -m pytest tests/ -v # test (212 tests)
Install dev tooling with pip install -e ".[dev]".
Docs
docs/ contains the authoritative reference for each subsystem.
| Doc | Contents |
|---|---|
docs/architecture.md |
Request flow, subsystem communication, two-user model, zone model, state management |
docs/security.md |
Privilege model, sudo whitelist, systemd hardening, TLS config, zone trust levels |
docs/deployment.md |
Install script options, what install.sh does, post-install setup, troubleshooting |
docs/config.md |
JSON schema for each subsystem config (dnsmasq, nginx, wireguard, cert types) |
docs/api.md |
REST API endpoint reference, request/response contracts, route patterns |
docs/overview.md |
Subsystem summaries, tech stack, complete project directory tree |
Important Rules
- Ask, don't assume. If something is unclear, ask before writing a single line. Never make silent assumptions about intent, architecture, or requirements.
- Simplest solution first. Always implement the simplest thing that could work. Do not add abstractions or flexibility that weren't explicitly requested.
- Don't touch unrelated code. If a file or function is not directly part of the current task, do not modify it, even if you think it could be improved.
- Flag uncertainty explicitly. If you are not confident about an approach or technical detail, say so before proceeding. Confidence without certainty causes more damage than admitting a gap.