Files
vacuum-wall/AGENTS.md
T
mteehan 3de82e3b9b Remove query-string cache-busting from static assets
Drop ?v=N version pins from all JS imports and HTML <link>/<script> tags.
Cache invalidation is now handled solely by server-side cache-control headers.
Update docs and AGENTS.md accordingly.
2026-07-28 13:50:22 +00:00

8.5 KiB

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 <cmd> ──→ 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:<group> with mode 0660.

Code Layout

  • webui/server.py — Flask app entry point. Only file that creates the app. SPA root route (/) renders index.html with server-side __WS_URL_PLACEHOLDER__ substitution (no Jinja). All other paths return 404.
  • webui/api/*.py — Flask blueprints, one per subsystem. Routes prefix /api/<subsystem>/. 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.pySingle 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/<subsystem>/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

# 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 <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>}_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

firewalldavahi-daemondnsmasqvacuum-walldvacuum-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.

.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