Files
vacuum-wall/AGENTS.md
T
mteehan 5ac69dfa7e refactor: move JS vendors to vendor/ with versioned filenames
Vendor JS files to vendor/ with versioned names, symlinks in
webui/static/ select active version. Update update-vendor.sh and
AGENTS.md.
2026-05-25 01:20:37 +00:00

6.0 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 ──→ lib/*.py ──→ sudo <cmd> ──→ system service
  • webui/server.py — Flask app entry point. Only file that creates the app.
  • webui/api/*.py — Flask blueprints, one per subsystem. Routes prefix /api/<subsystem>/.
  • webui/api/common.py — Shared _ok() / _error() response helpers used by all blueprints.
  • lib/common.py — Shared utilities: run(), run_proc(), load_json(), save_json(), deep_merge(), ensure_dirs(). All lib/ modules use these instead of defining local helpers.
  • lib/*.py — Backend modules. All have full type hints and __all__ exports.
  • 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/ (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. The vacuum-wall system user has HOME=$INSTALL_DIR but no actual home directory (--no-create-home).

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 vacuum-wall system user (NoNewPrivileges, ProtectSystem=strict, loopback-only networking).

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

lib/ modules call sudo for everything that touches system services. Whitelist is system/sudoers.d/vacuum-wall.

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 lib/ code.

API Response Contract

  • Success: {"ok": true, "data": <value>} — helper _ok(data) from webui.api.common
  • Error: {"ok": false, "error": "msg"} — helper _error(msg, code=400) from webui.api.common
  • 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
  • Full spec: docs/api.md

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 (192 tests)

Install dev tooling with pip install -e ".[dev]".

Docs

docs/ contains the authoritative reference. docs/architecture.md covers request flow, zone model, data directory layout, and shared utility patterns in detail.

Important Rules

  1. Ask, don't assume. If something is unclear, ask before writing a single line. Never make silent assumptions about intent, architecture, or requirements.
  2. Simplest solution first. Always implement the simplest thing that could work. Do not add abstractions or flexibility that weren't explicitly requested.
  3. 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.
  4. 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.