Add AGENTS.md and align the docs with the code
The brief asked for an AGENTS.md capturing the overall plan and it never landed, while two files already pointed readers at it. It now holds the invariants (read-only by construction, never invent a number, unconfigured is not broken, no CDN, no guessed metric names), the locked decisions with their reasons, the verified fleet facts, the commands, and the four known gaps. Also corrected the docs that had drifted: - upsd config lives in /etc/ups on driveripper, not /etc/nut, and the real blocker is that upsd binds nothing on 3493 — with the sudo rules still needed written down in deploy/nut-sudoers. - .env.example told operators to set `LISTEN 0.0.0.0` and `actions = -`; both are worse than needed. Loopback + ssh forward, and per-command instcmds instead of granting load.off. - nut.py claimed the command path is gated three times and that the confirmation asks for the UPS name; it is gated four times and asks for DANGER. - .gitignore described a tracked .agents/ roadmap that does not exist. LLM-Generated: true
This commit is contained in:
@@ -12,6 +12,10 @@ Three dashboards, one domain, one process, one SQLite file:
|
||||
No Prometheus, no Grafana, no CDN, no build step. Nothing is fetched from the internet at
|
||||
runtime; every asset is served by this app.
|
||||
|
||||
Working on this repo? **[`AGENTS.md`](AGENTS.md)** holds the invariants (read-only by
|
||||
construction, never invent a number, unconfigured ≠ broken), the locked decisions, the real
|
||||
fleet facts, and the known gaps.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
@@ -33,11 +37,17 @@ for host in ("driveripper-ops", "deskwork-ops", "gateway-ops"):
|
||||
subprocess.run(["ssh", host, "mkdir -p ~/lab-agent"], check=True)
|
||||
subprocess.run(["scp", "agent/host_agent.py", f"{host}:~/lab-agent/"], check=True)
|
||||
PY
|
||||
./scripts/dev-tunnels.sh start # 9121/9122/9123 -> each host's agent
|
||||
./scripts/dev-tunnels.sh start # 9121/9122/9123 -> each host's agent,
|
||||
# + DEV_FORWARDS -> upsd on 127.0.0.1:3494
|
||||
set -a; . ./.env.dev; set +a
|
||||
uv run dashboards
|
||||
uv run dashboards # foreground
|
||||
./scripts/dev-server.sh restart # or backgrounded, with a pidfile + logs
|
||||
```
|
||||
|
||||
`DEV_TUNNELS` starts the agent remotely and forwards it; `DEV_FORWARDS` forwards a service
|
||||
that already listens there (upsd) — that is how the UPS page reads a real battery without
|
||||
3493 being reachable from anywhere but this laptop.
|
||||
|
||||
## The host agent
|
||||
|
||||
`agent/host_agent.py` is one file, **stdlib only**, Python 3.10+. It reads `/proc`,
|
||||
@@ -115,9 +125,9 @@ store.py ── SQLite (WAL): sample(source,host,metric,res,ts) + event ──
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
uv run pytest # unit + API tests, fully mocked
|
||||
uv run pytest # unit + API tests, fully mocked, no network
|
||||
uv run pytest --cov # coverage gate
|
||||
uv run pytest -m live --live # optional: hit the real fleet/engines
|
||||
uv run pytest -m live --live # optional: real fleet / engines / upsd
|
||||
uv run ruff check src tests agent && uv run pyright # lint + types
|
||||
./scripts/capture-fixtures.sh # re-record ground-truth captures
|
||||
```
|
||||
@@ -125,18 +135,35 @@ uv run ruff check src tests agent && uv run pyright # lint + types
|
||||
Test philosophy: **real captures as fixtures, mocks for behaviour.** Anything that must
|
||||
match an upstream (`/proc` layouts, vLLM metric names, NUT protocol framing) is asserted
|
||||
against a verbatim capture in `tests/fixtures/`; behaviour that needs a failure — upsd
|
||||
refusing a command, an engine returning 500, a host rebooting — is a fake. `-m live`
|
||||
opt-in tests are the bridge: they run only when you point them at real hardware.
|
||||
refusing a command, an engine returning 500, a host rebooting — is a fake. The `live` tests
|
||||
(`tests/test_live_fleet.py`) are the bridge: they SKIP unless you pass `--live`, each one
|
||||
also skips when its source is unset, they drive the production code paths (`collectors.*`,
|
||||
`NutMonitor.collect`) against whatever hardware is reachable, and they never write — a
|
||||
battery test on live hardware is not a unit test.
|
||||
|
||||
## Battery-test buttons
|
||||
|
||||
Instant commands are the only mutating endpoint in the application (`POST /api/ups/command`)
|
||||
and they are gated four times: `NUT_ALLOW_ACTIONS=1`, not `DASH_READ_ONLY`, an `upsd.users`
|
||||
account that actually has `actions`, and a confirmation in the UI — destructive ones
|
||||
account that actually has the command, and a confirmation in the UI — destructive ones
|
||||
(deep test, calibration, `load.off`) require typing `DANGER`. The server re-checks every
|
||||
gate; hiding a button is not a safety control. Commands that would cut power are refused
|
||||
outright while the UPS is already on battery.
|
||||
|
||||
Give the app a narrowly scoped NUT user instead of a god-mode one — per-command `instcmds`,
|
||||
not `actions = -`:
|
||||
|
||||
```ini
|
||||
# /etc/ups/upsd.users
|
||||
[dashboards]
|
||||
password = "..."
|
||||
instcmds = test.battery.start.quick test.battery.start.deep test.battery.stop
|
||||
```
|
||||
|
||||
Bind upsd to loopback (`LISTEN 127.0.0.1` in `upsd.conf`) and reach it with an ssh forward
|
||||
(`DEV_FORWARDS`) or a tailnet address. `NUT_SIMULATE=1` stands in for upsd during
|
||||
development; everything it produces is badged `SIMULATED`, and it is never the default.
|
||||
|
||||
## Deployment
|
||||
|
||||
`Containerfile` is a two-stage build: `uv.lock` is resolved in the builder (exactly the
|
||||
|
||||
Reference in New Issue
Block a user