From b8f20e99d98004ae552a4ef6196afceb949a95ab Mon Sep 17 00:00:00 2001 From: Mike Teehan Date: Wed, 17 Jun 2026 03:59:37 +0000 Subject: [PATCH] agent update --- AGENTS.md | 71 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8c4b0a8..47b4015 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## What This Is -SSL proxy / firewall appliance. Python 3 Flask WebUI behind nginx reverse proxy. +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 @@ -15,32 +15,55 @@ vacuum-walld ──→ daemon/handlers/*.py ──→ sudo ──→ syste ### 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 project directory and socket. Primary group is the WebUI user's primary 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`. Project dir is owned by the WebUI user with group-read+execute. +- **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`. +- `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//`. 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, cache engine, batch routing, handler registry. +- `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.py` — **Single 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/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 (parsing, config, shared logic). All have full type hints and `__all__` exports. No sudo calls — privilege escalation is handled by `daemon/handlers/*.py`. +- `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//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/`. +- `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/` and `lib/` are intentionally empty — no `sys.path` boilerplate needed. +Project uses `.venv`. Install deps with `pip install -e .` (from `pyproject.toml`). `__init__.py` files in `webui/` and `lib/` are intentionally empty. + +### Frontend (hoover) + +Custom reactive SPA framework at `webui/static/hoover/`. Provides VDOM rendering, reactivity, router, WebSocket bindings, API helpers, and shared UI components. Exported via `hoover/index.js`. Pages in `webui/static/pages/` each define a route using `definePage()`. Bootstrap is `webui/static/app.js`. No build step — served raw. + +### 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 also be started as `python -m daemon` 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. 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`). +`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 live at `PROJECT_DIR/data/acme/`. +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 + +`firewalld` → `avahi-daemon` → `dnsmasq` → `vacuum-walld` → `vacuum-wall` ## Local Dev @@ -48,11 +71,11 @@ All Python modules use `Path(__file__).resolve().parent.parent` for `PROJECT_DIR .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). +Reload running Flask via SIGHUP (auto-reloads `webui.*` and `lib.*` modules, then SIGTERM restart). -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. +In production: systemd units run with `NoNewPrivileges`, `ProtectSystem=strict`, loopback-only networking. -## Blueprint ↔ lib Mapping (Naming Is Not 1:1) +## Blueprint ↔ lib Mapping | Blueprint | URL prefix | Backend module | |-----------------------|-------------------|------------------| @@ -62,6 +85,7 @@ When `install.sh --dev` is used, the repo owner gets NOPASSWD sudo for system se | `webui/api/certs` | `/api/certs/` | `lib.acme` | | `webui/api/wireguard` | `/api/wireguard/` | `lib.wireguard` | | `webui/api/network` | `/api/network/` | `lib.network` | +| `webui/api/logs` | `/api/logs/` | `lib.logging` | ## Privileged Operations @@ -78,13 +102,10 @@ Adding a new privileged command requires a sudoers entry **and** the `daemon/han - 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 -## 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. +`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 @@ -95,14 +116,14 @@ Adding a new privileged command requires a sudoers entry **and** the `daemon/han ```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 (212 tests) +.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 to ground your understanding in the project's documented behavior rather than inference from code alone. +`docs/` contains the authoritative reference for each subsystem. **Before reasoning about any subsystem**, read the relevant doc(s) below. | Doc | Contents | |-----|----------| @@ -114,7 +135,7 @@ Install dev tooling with `pip install -e ".[dev]"`. | `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. 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. +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.