Files
vacuum-wall/AGENTS.md
T
mteehan 5ba0f31767 Add state management, WebSocket polling, html.js templating, and refactor pages
- lib/state.py: per-subsystem collectors with versioned state store
- daemon/server.py: state refresh on request, batch routing updates
- webui/static/hoover/html.js: new html tag template helper via htm.js
- webui/static/hoover/websocket.js: real-time state change notifications
- webui/static/hoover/vdom.js: VDOM improvements for keyed diff
- All frontend pages refactored to use html templates
- Add tests for state management and polling
- Update docs and AGENTS.md
2026-06-23 21:12:56 +00:00

9.3 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 catch-all renders index.html with server-side __WS_URL_PLACEHOLDER__ substitution (no Jinja).
  • 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.
  • 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 an endpoint here auto-updates both server registry and client calls. All blueprints and handlers import from here.
  • 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(), validate_interface_name(). All lib/ modules use these instead of defining local helpers.
  • lib/logging.py — Logging setup used by both webui and daemon.
  • 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, htmx, json-enc).
  • data/ — Runtime artifacts (generated .confs, .htpasswd, ACME certs, firewall backup, dnsmasq fragments).
  • config/<subsystem>/config.json — Declarative JSON configs (source of truth). Generated .conf in data/nginx/sites-enabled/. Certs in data/acme/.
  • 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 of reactivity, VDOM, router, API, components).
  • Pages in webui/static/pages/ export definePage({ init, subscribe, load, render }) as default.
  • Bootstrap: webui/static/app.js mounts two render roots (#sidebar, #main), then connect() for WS.
  • h() builds VNodes; html tag (from htm) enables JSX-like templates; #comp + hComp() for component lifecycle; key for keyed diff.
  • Events: h() uses on:click prefix. html templates use camelCase onClick (adapter translates to on:click).
  • State always has loading, refreshing, error plus data. load() receives (state, abortController, entry).
  • openModal + formModal for dialogs; apiSubmit() for form submission. ToastContainer() in main root.
  • No build step — ES modules served raw. Assets versioned via ?v=N query string.

Daemon Endpoints

  • Unix socket at data/daemon.sock (configurable via VACUUM_WALLD_SOCKET env var)
  • WebSocket at 127.0.0.1:9091 (configurable via VACUUM_WALLD_WS_PORT) for real-time state change notifications
  • Can be started as python -m daemon.server or via the vacuum-walld console script

Environment Variables

  • VACUUM_WALL_DEV — dev mode flag; when set, disables aggressive static asset caching
  • VACUUM_WALLD_SOCKET — override daemon socket path
  • VACUUM_WALLD_WS_PORT — override WebSocket port (default 9091)

Deployment

install.sh installs only system components and configures them; the project serves from the repo root by default. Options via env vars or CLI flags (CLI takes precedence). Set INSTALL_DIR or --path to override. Use --dev to auto-detect repo owner as service user; non-dev requires --user.

All Python modules use Path(__file__).resolve().parent.parent for PROJECT_DIR — no hardcoded paths. ACME certs at PROJECT_DIR/data/acme/.

Service Start Order

firewalldavahi-daemondnsmasqvacuum-walldvacuum-wall

Local Dev

.venv/bin/python webui/server.py          # binds 127.0.0.1:9090

Reload running Flask via SIGHUP (auto-reloads webui.* and lib.* modules, then SIGTERM restart).

In production: systemd units run with NoNewPrivileges, ProtectSystem=strict, loopback-only networking.

Blueprint ↔ Handler ↔ lib Mapping

Blueprint URL Prefix Handler Module 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

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) from webui.api.common (Flask) or ok(data) from daemon.server (aiohttp).
  • Error: {"ok": false, "error": "msg"} — helper _error(msg, code=400) from webui.api.common or error(msg, code) from daemon.server.
  • acme.issue() / acme.renew() raise RuntimeError on failure — API layer wraps in try/except.
  • HTTP codes: 400 bad request, 404 not found, 500 internal failure.

Deploy

install.sh is the single deploy script. Run as root. Only MGMT_PASS is strictly required; MGMT_DOMAIN is auto-detected from hostname.

Lint and Tests

Linter / formatter: Ruff (ruff check + ruff format). Config in pyproject.toml under [tool.ruff].

Tests: pytest in tests/. Tests mock out subprocess calls — 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

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.

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/hoover.md Custom frontend framework API reference
docs/overview.md Subsystem summaries, tech stack, complete project directory tree

Important Rules

  1. Ask, don't assume. If something is unclear, ask before writing a single line.
  2. Simplest solution first. Always implement the simplest thing that could work.
  3. Don't touch unrelated code. If a file or function is not directly part of the current task, do not modify it.
  4. Flag uncertainty explicitly. If you are not confident about an approach or technical detail, say so.