agent update

This commit is contained in:
2026-06-17 03:59:37 +00:00
parent 687fa8f52f
commit b8f20e99d9
+46 -25
View File
@@ -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 <cmd> ──→ 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:<group>` 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:<group>` 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/<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, 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/<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/`.
- `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.