chore(agent): add phase 10 — dev dials the fleet directly
Six tasks that make dev the prod topology: the documented `ops`-installable agent sudoers grant, installing the agents as systemd services, the upsd ACL for the host and laptop ranges, `.env.dev` pointing at the real hosts (token in the URL userinfo, `NUT_PORT=3051`), retiring `scripts/dev-tunnels.sh`, and a live + browser verification pass. No dashboard Python changes — deployment, config, scripts and docs only. LLM-Generated: true
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# Phase 10 — Dev dials the fleet directly (no more SSH tunnels)
|
||||
|
||||
**Source:** user request (2026-10) — "I don't want this separation of architectures. My laptop can
|
||||
talk to these hosts directly via local network or vpn already. Just have dev work like prod."
|
||||
**Story:** n/a
|
||||
**Context:** `AGENTS.md` (dev workflow: `scripts/dev-tunnels.sh`, `DEV_TUNNELS`/`DEV_FORWARDS`),
|
||||
`deploy/install-agent.sh`, `deploy/nut-sudoers`, `scripts/dev-server.sh`,
|
||||
`scripts/capture-fixtures.sh`, `tests/test_live_fleet.py`, `.env.dev.example`, `.env.example`,
|
||||
`README.md`
|
||||
|
||||
## Objective
|
||||
|
||||
Dev becomes the prod topology: each host's agent runs as a **systemd service** (installed by
|
||||
`ops` through a documented minimal sudoers grant), bound to the address the dashboard dials,
|
||||
token in the URL userinfo; driveripper's upsd ACLs the host and laptop IP ranges on port 3051;
|
||||
`.env.dev` points at the real hosts with no `DEV_TUNNELS`/`DEV_FORWARDS`; `scripts/dev-tunnels.sh`
|
||||
is retired. The dashboard's Python is untouched — this phase changes deployment, config, scripts
|
||||
and docs, and proves parity with the live suite plus browser screenshots.
|
||||
|
||||
## Decisions (user, 2026-10)
|
||||
|
||||
- The agent is a **systemd service, installed by the `ops` user** (not root). The repo documents
|
||||
the **simplest possible working sudoers addition** in `deploy/agent-sudoers`, and the user adds
|
||||
it to each host.
|
||||
- The upsd ACL allows the **host's IP range and the laptop's IP range** — both real ranges,
|
||||
derived at install time and commented in `.env.dev` so they can be re-derived when the laptop's
|
||||
IP changes.
|
||||
|
||||
## Precondition (user action, before task 02)
|
||||
|
||||
The sudoers line needs root, and the command-scoped sudo on driveripper does not include
|
||||
`visudo`. The user adds the one line from `deploy/agent-sudoers` on each host. If it is missing
|
||||
when task 02 runs, that task must **stop cleanly and print the exact instructions** — never loop,
|
||||
never wait, never attempt to edit sudoers itself.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `05_ups_hero_battery_glyph` (complete) — the last UI phase; its pages are what this phase
|
||||
re-verifies against live data after the transport change.
|
||||
- `06_system_page_gpu_verdicts`, `07_llm_monitor_style_pages`, `08_events_paging_and_copy`,
|
||||
`09_disks_smartctl` (todo) — independent: this phase changes transport and ops, not payloads or
|
||||
page behaviour, and they do not depend on the transport.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. `01_sudoers_grant_and_installer_hint.md` — `deploy/agent-sudoers` (the minimal working grant
|
||||
+ install instructions, in the `nut-sudoers` house style); `install-agent.sh` run without
|
||||
root prints the exact line to add.
|
||||
2. `02_install_agents_on_fleet.md` — on all three hosts: kill the ad-hoc agent, stage the
|
||||
installer, install the systemd service bound to the dialed address, capture the printed
|
||||
tokens, verify from the laptop.
|
||||
3. `03_upsd_acl_for_laptop.md` — driveripper: append ACL lines for the host range and the laptop
|
||||
range to `upsd.conf` (read-first, `cmp`-after, never blind-write), restart `nut-server`, verify
|
||||
the NUT wire from the laptop.
|
||||
4. `04_env_dev_direct_dial.md` — stop the tunnels; rewrite the local `.env.dev` to direct targets
|
||||
(userinfo tokens, `NUT_HOST`/`NUT_PORT=3051`); restart the dev server; sources ok.
|
||||
5. `05_retire_tunnels_update_scripts.md` — delete `scripts/dev-tunnels.sh`; rewrite
|
||||
`.env.dev.example` direct-shaped; fix `capture-fixtures.sh`, the live suite's docstring and
|
||||
skip messages, `ups_contract.py`, the `alerts.py` docstring, `test_config.py`, `.env.example`'s
|
||||
NUT note, and the dev-workflow sections of `README.md`/`AGENTS.md`.
|
||||
6. `06_verify_direct_dial_live.md` — full live verification: `/api/health` ok over direct dial,
|
||||
`uv run pytest -m live --live` green, browser screenshots of `/` + `/system` + `/ups` + `/llm`
|
||||
on live data, no tunnel port needed on the laptop.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- The Python changes in this phase are test-file strings and one docstring — no new logic. The
|
||||
full suite must stay 100% green and overall coverage must not drop (gate: `--cov-fail-under=94`).
|
||||
- `bash -n` on `deploy/install-agent.sh` and `scripts/capture-fixtures.sh` after their edits.
|
||||
- The real proof of this phase is the live path: `uv run pytest -m live --live` against the
|
||||
directly-dialed fleet (the same production code paths over the prod-shaped transport) plus a
|
||||
browser screenshot per `AGENTS.md` rule 10.
|
||||
- The phase is "Complete" only when the **entire** project test suite passes 100% (every test in
|
||||
the project, not just the phase's — pre-existing failures included) and the coverage gate in
|
||||
`.agents/validate.sh` holds.
|
||||
|
||||
## Documentation & Consistency
|
||||
|
||||
- The final pass reviews `README.md` and `AGENTS.md` in full against the project as it stands
|
||||
after this phase — fixing every inconsistency found, **including pre-existing ones not caused
|
||||
by the phase** (including any remaining mention of `dev-tunnels.sh`, `DEV_TUNNELS`,
|
||||
`DEV_FORWARDS`, `9121`/`9122`/`9123` or `3494`).
|
||||
- Task 05 updates `README.md`/`AGENTS.md` immediately for the user-visible dev-workflow change.
|
||||
- `PLAN.md` is locked and must not be edited: its Data-Model note ("forwarded to
|
||||
`127.0.0.1:3494` in dev") goes stale after this phase — flag it in the final pass's report to
|
||||
the user, and leave the file untouched.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] `systemctl status dashboards-agent` is active on all three hosts, each bound to the address
|
||||
the dashboard dials, answering `/health` (unauthenticated) and `/snapshot` (bearer) from the
|
||||
laptop
|
||||
- [ ] driveripper's upsd answers `LIST UPS` from the laptop on 3051 — not via the host's loopback
|
||||
- [ ] `git grep -n "dev-tunnels\|DEV_TUNNELS\|DEV_FORWARDS\|9121\|9122\|9123\|3494"` matches
|
||||
nothing outside `.agents/phases/complete/`
|
||||
- [ ] `./scripts/dev-server.sh status` shows every source `ok` over direct dial
|
||||
- [ ] `uv run pytest -m live --live` (with `.env.dev` sourced) is green
|
||||
- [ ] `data/logs/` holds `phase10-*.png` screenshots of `/`, `/system`, `/ups`, `/llm` on live
|
||||
data over direct dial
|
||||
- [ ] full test suite **100% passing**, coverage gate (≥94%) holds, `ruff` + `pyright` clean
|
||||
- [ ] `README.md` and `AGENTS.md` describe the direct-dial dev workflow and mention no tunnels
|
||||
(verified in the final pass)
|
||||
- [ ] no behavior change in completed phases: the four pages render the same payloads as before
|
||||
the transport change
|
||||
Reference in New Issue
Block a user