**Phase 10 final pass — repo green; live criteria blocked off-repo.**
- Verified: `gateway` runs `dashboards-agent` (active/enabled, `10.1.1.27:9120` → `/health` 200, `/snapshot` 401 no-token / 200 bearer on-host, token matches `/etc/dashboards-agent.token`); `driveripper`/`deskwork` inactive, `sudo -n` → "a password is required", `:9120` empty.
- New measurement (transient runtime rule on gateway `firewall-cmd --add-port=9120/tcp`, **reverted** — runtime+permanent port lists verified back to the original set, no rich rules): laptop still gets no SYN through, while `:22`/`:80` on the same address answer in ms ⇒ the block is the lab gateway `10.2.0.1` port filter, not this app/host firewall/agent. No other live change made (no sudoers, no upsd, no UPS command).
- Tests/lint (exact): `bash .agents/validate.sh` → **EXIT 0** — `uv run pytest --cov --cov-fail-under=94` **793 passed, 11 skipped, coverage 98.33 %**; `ruff check src tests agent` clean; `pyright` 0 errors; `node --check` clean; hygiene clean; `uv pip list --outdated` empty (bumped fastapi 0.143.3→0.143.5, `uv.lock` only).
- `uv run pytest -m live --live` (`.env.dev` sourced): **4 passed, 1 skipped, 6 failed** — all six are `:9120`/`:3051` connect timeouts; zero parse/contract failures.
- Dev server restarted on current code: engines `ok`; 3×`host` + 3×`disks` + `nut` `DOWN` with real reasons (designed degradation, stub rows intact, no credential leakage).
- Completion criteria: screenshots ✓ (`data/logs/phase10-{index,system,disks,llm,ups}-live.png`); tunnel-vocabulary grep ✓ outside `.agents/` history (PLAN.md locked line, phase overviews, past reports; fixtures/`uv.lock` hits are digit substrings — `tests/test_deploy_scripts.py` guards operational files); no page-payload change ✓ (contract tests). **Not met, operator-side:** agents on all three hosts, `LIST UPS` from the laptop, `dev-server.sh status` all-ok, live suite green.
- Fixed in this pass: `sources/nut.py` unreachable-upsd message no longer prints a bare `(TimeoutError: )` (now `(timed out after 4s)` / `(ConnectionRefusedError: …)`), pinned by a new test + extended refused-port test.
- Docs: README verified consistent (every env var in `config.py` documented, routes/flags/`--with-llm`/pager sizes checked) — no changes needed. AGENTS.md fixed 4 inconsistencies: per-host sudo reality (gateway grants `NOPASSWD ALL`, which is why it installed without `agent-sudoers`; driveripper scoped; deskwork unrelated), the now-proven firewall finding, `/system` table typo ("agent per host"), gap-4 DB/WAL sizes + test count. `.env.example` `upsd.users` sample now one `instcmds` per line (it contradicted README and the in-app copy). `.env.dev` (gitignored) comment corrected to match the evidence.
- Flagged, untouched (locked): `.agents/PLAN.md` L92–95 still says upsd is "forwarded to `127.0.0.1:3494` in dev" — false after this phase; evidence + exact operator commands appended to `data/logs/NOTES-verify.md` §11.
- Next pending phase: `11_admin_login`.
6.3 KiB
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
opsuser (not root). The repo documents the simplest possible working sudoers addition indeploy/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.devso 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
01_sudoers_grant_and_installer_hint.md—deploy/agent-sudoers(the minimal working grant- install instructions, in the
nut-sudoershouse style);install-agent.shrun without root prints the exact line to add.
- install instructions, in the
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.03_upsd_acl_for_laptop.md— driveripper: append ACL lines for the host range and the laptop range toupsd.conf(read-first,cmp-after, never blind-write), restartnut-server, verify the NUT wire from the laptop.04_env_dev_direct_dial.md— stop the tunnels; rewrite the local.env.devto direct targets (userinfo tokens,NUT_HOST/NUT_PORT=3051); restart the dev server; sources ok.05_retire_tunnels_update_scripts.md— deletescripts/dev-tunnels.sh; rewrite.env.dev.exampledirect-shaped; fixcapture-fixtures.sh, the live suite's docstring and skip messages,ups_contract.py, thealerts.pydocstring,test_config.py,.env.example's NUT note, and the dev-workflow sections ofREADME.md/AGENTS.md.06_verify_direct_dial_live.md— full live verification:/api/healthok over direct dial,uv run pytest -m live --livegreen, browser screenshots of/+/system+/ups+/llmon 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 -nondeploy/install-agent.shandscripts/capture-fixtures.shafter their edits.- The real proof of this phase is the live path:
uv run pytest -m live --liveagainst the directly-dialed fleet (the same production code paths over the prod-shaped transport) plus a browser screenshot perAGENTS.mdrule 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.shholds.
Documentation & Consistency
- The final pass reviews
README.mdandAGENTS.mdin 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 ofdev-tunnels.sh,DEV_TUNNELS,DEV_FORWARDS,9121/9122/9123or3494). - Task 05 updates
README.md/AGENTS.mdimmediately for the user-visible dev-workflow change. PLAN.mdis locked and must not be edited: its Data-Model note ("forwarded to127.0.0.1:3494in 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-agentis 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 UPSfrom 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 statusshows every sourceokover direct dialuv run pytest -m live --live(with.env.devsourced) is greendata/logs/holdsphase10-*.pngscreenshots of/,/system,/ups,/llmon live data over direct dial- full test suite 100% passing, coverage gate (≥94%) holds,
ruff+pyrightclean README.mdandAGENTS.mddescribe 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