# Vacuum Wall — Agent Instructions ## What This Is SSL proxy / firewall appliance. Python 3.13+ Flask SPA 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 ──→ system service ``` Blueprints are thin proxies — they never call `lib/` directly. All operations flow through the daemon client over a Unix socket. ### Two-User Model with Shared Group - **`vacuum-walld`** (daemon user): runs the privileged background daemon with `NOPASSWD sudo` whitelist (`/etc/sudoers.d/vacuum-walld`). Owns socket. Primary group is the WebUI user's primary group. Daemon user name is derived: `USER_NAME` + `d`. - **WebUI user** (default: repo owner in `--dev` mode): 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:` with mode `0660`. ### Code Layout - `webui/server.py` — Flask app entry point. **Only** file that creates the `app`. SPA root route (`/`) serves `index.html` (no templating). All other paths return 404. - `webui/api/*.py` — Flask blueprints, one per subsystem. Routes prefix `/api//`. All call `daemon.client` instead of `lib/` directly. - `webui/api/common.py` — Shared `_ok()` / `_error()` response helpers used by all blueprints. - `daemon/server.py` — aiohttp server, route registry, batch routing, WebSocket broadcast, state refresh, periodic polling. - `daemon/client.py` — Sync HTTP client over Unix socket using `requests_unixsocket.Session`. - `daemon/iface.py` — **Single source of truth** for all daemon API endpoints. Every endpoint is a frozen `(method, path)` tuple. Renaming here auto-updates both server registry and client calls. - `daemon/handlers/*.py` — Privileged operation handlers. All `sudo` calls live here. - `lib/state.py` — In-memory state store with per-subsystem collectors. Populated at daemon startup, refreshed on request. Backs WebSocket versioning/broadcast. - `lib/common.py` — Shared utilities: `run()`, `run_proc()`, `load_json()`, `save_json()`, `deep_merge()`, `ensure_dirs()`, `config_hash()`, `validate_interface_name()`. - `lib/logging.py` — Logging setup used by both webui and daemon. Reads `VACUUM_WALL_LOG_LEVEL`. - `lib/*.py` — Backend modules (parsing, config, shared logic). Full type hints and `__all__` exports. No sudo calls. - `vendor/` — Vendored scripts and JS libraries (`acme.sh`, `htm`). - `data/` — Runtime artifacts (generated .confs, `.htpasswd`, ACME certs, firewall backup, dnsmasq fragments). - `config//config.json` — Declarative JSON configs (source of truth). - `system/` — System file templates. `systemd/` (units installed to `/etc/systemd/system/`), `sudoers.d/`, `nginx/`. Project uses `.venv`. Install deps with `pip install -e .` (from `pyproject.toml`). `__init__.py` files in `webui/`, `lib/`, and `daemon/` are intentionally empty. ### Frontend (hoover) Custom reactive SPA framework at `webui/static/hoover/`. See `docs/hoover.md` for full API reference. Conventions: - All imports from `/static/hoover/index.js` (barrel export). - Pages in `webui/static/pages/` export `definePage({ init, subscribe, load, render })`. - Bootstrap: `webui/static/app.js` mounts two render roots (`#sidebar`, `#main`), then `connect()` for WS. - `h()` builds VNodes with `on:click` prefix. `html` tag (htm) templates use camelCase `onClick` (adapter translates). - State always has `loading`, `refreshing`, `error` plus data. `load()` receives `(state, abortController, entry)`. - `openModal` + `formModal` for dialogs; `apiSubmit()` for form submission. - No build step — ES modules served raw. Cache controlled via HTTP headers. ### Daemon Endpoints - Unix socket at `data/daemon.sock` (configurable via `VACUUM_WALLD_SOCKET`) - WebSocket at `127.0.0.1:9091` (configurable via `VACUUM_WALLD_WS_PORT`) for real-time state notifications - Periodic polling per subsystem via `lib.state._DEFAULT_POLL_INTERVALS`, overridable with `VACUUM_WALL_POLL_INTERVALS` env var (format `subsystem:seconds,subsystem:seconds`) - Start as `python -m daemon.server` or via the `vacuum-walld` console script ## Environment Variables - `VACUUM_WALL_DEV` — dev mode flag; disables aggressive static asset caching - `VACUUM_WALL_LOG_LEVEL` — log level (default `INFO`) - `VACUUM_WALLD_SOCKET` — override daemon socket path (default `data/daemon.sock`) - `VACUUM_WALLD_WS_PORT` — override WebSocket port (default `9091`) - `VACUUM_WALL_POLL_INTERVALS` — override poll intervals, e.g. `firewall:60,wireguard:5` - `VACUUM_WALL_EXTERNAL_IP_URL` — custom URL for external IP detection (acme handler) ## Local Dev ```bash # Setup python3 -m venv .venv && . .venv/bin/activate pip install -e ".[dev]" bash scripts/update-vendor.sh # fetches acme.sh + htm.js # Start (Flask only, binds 127.0.0.1:9090) .venv/bin/python webui/server.py ``` Reload running Flask via SIGHUP (auto-reloads `webui.*` and `lib.*` modules, then SIGTERM restart). ## Blueprint / Handler / lib Mapping | Blueprint | URL Prefix | Handler | lib Module | |-----------------------|---------------------|----------------------------|-----------------| | `webui/api/firewall` | `/api/firewall/` | `daemon/handlers/firewall` | `lib.firewall` | | `webui/api/dhcp` | `/api/dhcp/` | `daemon/handlers/dnsmasq` | `lib.dnsmasq` | | `webui/api/proxy` | `/api/proxy/` | `daemon/handlers/nginx` | `lib.nginx` | | `webui/api/certs` | `/api/certs/` | `daemon/handlers/acme` | `lib.acme` | | `webui/api/wireguard` | `/api/wireguard/` | `daemon/handlers/wireguard`| `lib.wireguard` | | `webui/api/network` | `/api/network/` | `daemon/handlers/network` | `lib.network` | | `webui/api/logs` | `/api/logs/` | `daemon/handlers/logs` | — | | `webui/api/status` | `/api/status/` | `daemon/handlers/status` | — | ## 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 ` to apply. Adding a new privileged command requires a sudoers entry **and** the `daemon/handlers/` code. ## API Response Contract - Success: `{"ok": true, "data": }` — `_ok(data)` (Flask) or `ok(data)` (aiohttp) - Error: `{"ok": false, "error": "msg"}` — `_error(msg, code)` (Flask) or `error(msg, code)` (aiohttp) - `acme.issue()` / `acme.renew()` raise `RuntimeError` on failure — API layer wraps in try/except - HTTP codes: `400` bad request, `404` not found, `409` conflict, `500` internal failure ## Deploy `scripts/install.sh` is the single deploy script. Run as root. CLI flags take precedence over env vars. `MGMT_PASS` is strictly required; `MGMT_DOMAIN` auto-detected from hostname. ### Service Start Order `firewalld` → `avahi-daemon` → `dnsmasq` → `vacuum-walld` → `vacuum-wall` ## Lint and Tests **Linter / formatter:** Ruff (`ruff check` + `ruff format`). Config in `pyproject.toml`. **Tests:** pytest in `tests/` (17 modules). All subprocess calls are mocked — no system services required. ```bash .venv/bin/ruff check lib/ webui/ tests/ # lint .venv/bin/ruff format lib/ webui/ tests/ # format .venv/bin/python -m pytest tests/ -v # test ``` ## Docs `docs/` contains the authoritative reference for each subsystem. Read the relevant doc before reasoning about a 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 scripts/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/hoover.md` | Custom frontend framework API reference | | `docs/overview.md` | Subsystem summaries, tech stack, complete project directory tree |