7.5 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.
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 |
webui/api/network |
/api/network/ |
lib.network |
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. Before reasoning about any subsystem, read the relevant doc(s) below to ground your understanding in the project's documented behavior rather than inference from code alone.
| 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.