phase: 17_installer_device_filter

**Phase 17 — final verification pass: complete, all green.**

Verified (criteria):
- `grep -n "DevicePolicy\|DeviceAllow" deploy/install-agent.sh` → no match; `PrivateDevices=false` retained (line 472) ✓
- `install-agent.sh --help` names `--as-root`/`--caps` with the kernel-gate sentence; `--force` documented ✓
- Fresh fake-root install = pre-phase unit minus the filter lines: `test_a_default_install_unit_is_the_pre_phase_unit_minus_the_filter_lines` + `test_the_template_unit_has_no_device_filter_lines` pass ✓
- Re-run that differs → exit 1 with diff on stdout, `--force` applies, identical re-run silent: `test_a_re_run_that_differs_refuses_without_force_and_applies_with_it` (+ 3 sibling guard pins) pass ✓
- driveripper reproduction — verified **read-only**, no live change: `systemctl cat` shows `User=root`, `CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN`, no filter, same `ExecStart`; `active`, `CapEff 0000000000220000`; `/api/disks/snapshot` → 16/16 drives with temperatures ✓
- `uv run pytest` → **1451 passed, 15 skipped**; `bash .agents/validate.sh` → **exit 0** (coverage **98.94%** ≥ 94, `ruff` clean, `pyright` 0 errors, `node --check` + hygiene clean); `uv run pytest -m live --live` → **11 passed**; dev server 9/9 sources `ok` (one initial live failure was a transient gufo timeout, clean on re-run)

Docs (inconsistencies found + fixed, incl. pre-existing):
- `AGENTS.md` fleet record said driveripper's unit runs `User=dashboards-agent` while the same bullet (and the live host) says `User=root` — rewritten to separate the installed user from the running one.
- `AGENTS.md` gap 10 header still claimed "the installer template is what is left" → now records phase 17 as the fix.
- `README.md` "with no capabilities" → "with an empty `CapabilityBoundingSet` (unless `--caps`…)", since this phase added the capability flag.
- `README.md` battery-test "gated four times" now notes the two session gates in front, matching AGENTS invariant 1.
- `.env.example` was missing three README-documented settings (`DASH_ENV_FILE`, `DASH_RELOAD`, `NUT_SIMULATE`) and `AGENT_CERT_BUNDLE` in the agent-variables note — added, annotated; all template values re-checked against `config.py` defaults.

Deviations: no live `--force` run on driveripper (needs the Oct-18 sudo-staged installer re-staged — it lacks the flags — and would push agent 1.1.0 mid phase-15 rollout); command + expected diff + post-checks recorded in AGENTS/gap 10. `uv pip list --outdated` still advisory (fastapi 0.153→0.156), untouched as in tasks 02/04 — this phase moved no dependency.

Next pending phase: `.agents/phases/todo/18_nav_hamburger/`.
This commit is contained in:
2026-10-20 13:48:40 -04:00
parent 7e367a9014
commit b02c13f7d3
30 changed files with 1502 additions and 81 deletions
@@ -0,0 +1,20 @@
**Phase 17 — final verification pass: complete, all green.**
Verified (criteria):
- `grep -n "DevicePolicy\|DeviceAllow" deploy/install-agent.sh` → no match; `PrivateDevices=false` retained (line 472) ✓
- `install-agent.sh --help` names `--as-root`/`--caps` with the kernel-gate sentence; `--force` documented ✓
- Fresh fake-root install = pre-phase unit minus the filter lines: `test_a_default_install_unit_is_the_pre_phase_unit_minus_the_filter_lines` + `test_the_template_unit_has_no_device_filter_lines` pass ✓
- Re-run that differs → exit 1 with diff on stdout, `--force` applies, identical re-run silent: `test_a_re_run_that_differs_refuses_without_force_and_applies_with_it` (+ 3 sibling guard pins) pass ✓
- driveripper reproduction — verified **read-only**, no live change: `systemctl cat` shows `User=root`, `CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN`, no filter, same `ExecStart`; `active`, `CapEff 0000000000220000`; `/api/disks/snapshot` → 16/16 drives with temperatures ✓
- `uv run pytest` → **1451 passed, 15 skipped**; `bash .agents/validate.sh` → **exit 0** (coverage **98.94%** ≥ 94, `ruff` clean, `pyright` 0 errors, `node --check` + hygiene clean); `uv run pytest -m live --live` → **11 passed**; dev server 9/9 sources `ok` (one initial live failure was a transient gufo timeout, clean on re-run)
Docs (inconsistencies found + fixed, incl. pre-existing):
- `AGENTS.md` fleet record said driveripper's unit runs `User=dashboards-agent` while the same bullet (and the live host) says `User=root` — rewritten to separate the installed user from the running one.
- `AGENTS.md` gap 10 header still claimed "the installer template is what is left" → now records phase 17 as the fix.
- `README.md` "with no capabilities" → "with an empty `CapabilityBoundingSet` (unless `--caps`…)", since this phase added the capability flag.
- `README.md` battery-test "gated four times" now notes the two session gates in front, matching AGENTS invariant 1.
- `.env.example` was missing three README-documented settings (`DASH_ENV_FILE`, `DASH_RELOAD`, `NUT_SIMULATE`) and `AGENT_CERT_BUNDLE` in the agent-variables note — added, annotated; all template values re-checked against `config.py` defaults.
Deviations: no live `--force` run on driveripper (needs the Oct-18 sudo-staged installer re-staged — it lacks the flags — and would push agent 1.1.0 mid phase-15 rollout); command + expected diff + post-checks recorded in AGENTS/gap 10. `uv pip list --outdated` still advisory (fastapi 0.153→0.156), untouched as in tasks 02/04 — this phase moved no dependency.
Next pending phase: `.agents/phases/todo/18_nav_hamburger/`.
@@ -0,0 +1,65 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 14%]
........................................................................ [ 19%]
........................................................................ [ 24%]
...........................s............................................ [ 29%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 44%]
........................................................................ [ 49%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 63%]
................................................................ssssssss [ 68%]
sss..................................................................... [ 73%]
........................................................................ [ 78%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 93%]
........................................sss............................. [ 98%]
.......................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 436 0 100%
src/dashboards/app.py 185 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 3490 37 99%
Required test coverage of 94% reached. Total coverage: 98.94%
1451 passed, 15 skipped, 1 warning in 153.26s (0:02:33)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
fastapi 0.153.0 0.156.0 wheel
@@ -0,0 +1,16 @@
**Implemented (phase 17 task 01 — `deploy/install-agent.sh`)**
- Deleted `DevicePolicy=strict` + all nine `DeviceAllow=…` lines from the `do_install` unit heredoc; `PrivateDevices=false` stays explicit.
- Rewrote the rationale comment block (and the top-of-file/help paragraph) to the new truth: no device filter on purpose — systemd matches a device-filter glob by *name* against `/proc/devices`, kernel 7.2 lists NVMe as `blkext` (major 259), so `/dev/nvme*` matched nothing and a strict policy denied the whole namespace (driveripper, root included); device access is DAC via the `disk` group + SELinux. Kept the `status=208/STDIN` warning as the reason `PrivateDevices=false` is written out.
- `tests/test_deploy_scripts.py`: the sandbox-install test now pins the *absence* of the filter plus `PrivateDevices=false`; added `test_the_written_unit_is_only_comments_and_directives` — a defect found while verifying this task: the unit heredoc is unquoted (so phase 15's `$TLS_UNIT_LINES` expands), which **executes backticked prose** (`disk`, `blkext`, `/dev/nvme*` became `command not found` + a smartctl banner pasted into the unit). Mutation-checked: the pin catches it.
- Docs: README install paragraph and four AGENTS.md places (fleet divergence bullet, driveripper sudoers bullet, Deployment paragraph, gap 10) now say the template applies no device filter; the obsolete "missing `/dev/ng*n*`/`/dev/bsg/*` in the allow-list" warning is retired; gap 10 records `--as-root`/`--caps` + the re-run guard as the phase's remaining tasks.
**Tests / lint / coverage**
- `uv run pytest` → **1428 passed, 15 skipped** (was 1427; +1 new test), exit 0.
- `bash .agents/validate.sh` → exit 0: `pytest --cov --cov-fail-under=94` **98.94%** total, `ruff check src tests agent` clean, `pyright` 0 errors, `node --check` clean, repo hygiene clean. `uv pip list --outdated` empty (no deps moved).
- Task criteria: `grep -n "DevicePolicy\|DeviceAllow" deploy/install-agent.sh` → no match; `bash -n` clean; manual fake-root sandbox install (`--bind 127.0.0.1 --port 9199`) diffed against a HEAD-version sandbox install: directive lines differ **only** by the ten removed filter lines (comments rewritten per the task).
**README/AGENTS.md**: updated as above (docs stayed in sync mid-phase).
**Decisions/deviations**: the phase's grep criterion forbids the literal directive names in the script, so the comment explains them in prose ("device-filter glob", "strict device policy") instead of naming `DevicePolicy`/`DeviceAllow`. Added the heredoc-expansion pin (not in the task text) because it is a real pre-existing hazard this task's own comment nearly shipped. **No live-infrastructure changes** — the driveripper reproduction (`--as-root --caps … --force`, `systemctl diff`, 16/16 drives) is task 04's operator-gated step and needs the task 02 flags first.
**Next pending task**: `.agents/phases/todo/17_installer_device_filter/02_root_and_caps_flags.md`
@@ -0,0 +1,63 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 14%]
........................................................................ [ 19%]
........................................................................ [ 24%]
...........................s............................................ [ 29%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 44%]
........................................................................ [ 49%]
........................................................................ [ 54%]
........................................................................ [ 59%]
........................................................................ [ 64%]
.........................................sssssssssss.................... [ 69%]
........................................................................ [ 74%]
........................................................................ [ 79%]
........................................................................ [ 84%]
........................................................................ [ 89%]
........................................................................ [ 94%]
.................sss.................................................... [ 99%]
... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 436 0 100%
src/dashboards/app.py 185 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 3490 37 99%
Required test coverage of 94% reached. Total coverage: 98.94%
1428 passed, 15 skipped, 1 warning in 127.90s (0:02:07)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
@@ -0,0 +1,11 @@
**Task 02 — `--as-root` / `--caps` (phase 17) — done**
- `deploy/install-agent.sh`: added `--as-root` (`User=root`) and `--caps LIST` (`CapabilityBoundingSet=<space-separated>`), both off by default → default unit byte-identical to before; comma→space normalisation drops empty entries and runs with globbing off (`--caps '*'` stays literal).
- Validation: `--caps` without `--as-root` → stderr refusal + exit 1 **before** `need_root` and any write (same reasoning as the bundle validator); `--caps` with no argument / empty / punctuation-only → exit 1 with its own sentence; `--as-root` alone proceeds with the exact CapEff-0 note on stdout.
- Unit heredoc gained the one `--caps` comment above `CapabilityBoundingSet=`; `--status` and the token-file `0640 root:dashboards-agent` logic deliberately untouched; `--help` (the header comment) now names both flags with the kernel-gate sentence and the copy-pasteable recipe line.
- Tests (`tests/test_deploy_scripts.py`, 8 new / 12 new cases): exact `User=`/`CapabilityBoundingSet=` lines for default, `--as-root --caps …`, `--as-root` alone; parametrized refusals asserting nothing was written and no privileged stub ran; missing-argument refusal; normalisation + no-glob; `--help` recipe; `status()` blind to `AS_ROOT`/`CAPS`.
- `bash .agents/validate.sh` → **1439 passed, 15 skipped**, coverage **98.94%** (floor 94), `ruff check src tests agent` clean, `pyright` 0 errors, `node --check` clean, `bash -n deploy/install-agent.sh` clean; `uv run pytest -m live --live` → **11 passed** (unchanged).
- Docs: README (install flag block + a new kernel-gate paragraph) and AGENTS.md (fleet record, sudoers/grant bullet, deployment flag list, gap 10 tail) now state the flags exist, the `driveripper` recipe `--as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN`, and that the unconditional unit rewrite is task 03's remaining guard.
- **No live changes made**: the driveripper re-run (`systemctl diff`, `CapEff`, 16/16 drives) is task 04's operator-verified criterion, and gap 10 warns the sudo-granted Oct-18 *staged* snapshot would clobber the working unit — it must be re-staged from the repo first (now noted in AGENTS.md).
- Advisory only (not touched, deps not in scope): `uv pip list --outdated` reports `fastapi 0.153.0 → 0.154.0`.
- Next pending task: `.agents/phases/todo/17_installer_device_filter/03_no_silent_clobber.md`.
@@ -0,0 +1,65 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 14%]
........................................................................ [ 19%]
........................................................................ [ 24%]
...........................s............................................ [ 29%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 44%]
........................................................................ [ 49%]
........................................................................ [ 54%]
........................................................................ [ 59%]
........................................................................ [ 64%]
....................................................sssssssssss......... [ 69%]
........................................................................ [ 74%]
........................................................................ [ 79%]
........................................................................ [ 84%]
........................................................................ [ 89%]
........................................................................ [ 94%]
............................sss......................................... [ 99%]
.............. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 436 0 100%
src/dashboards/app.py 185 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 3490 37 99%
Required test coverage of 94% reached. Total coverage: 98.94%
1439 passed, 15 skipped, 1 warning in 135.34s (0:02:15)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
fastapi 0.153.0 0.154.0 wheel
@@ -0,0 +1,18 @@
All green. Report:
**Implemented**
- `deploy/install-agent.sh`: new `--force` flag (`FORCE=0` default) + `--help` text (`--force overwrite an existing unit even if the generated unit differs (prints the diff first)`, aligned to the `--as-root`/`--caps` column).
- `do_install` now renders the unit heredoc to `mktemp "${TMPDIR:-/tmp}/dashboards-agent-unit.XXXXXX"`; if `$UNIT` exists and `! cmp -s`, it prints `refusing to overwrite $UNIT (generated unit differs); run with --force to apply:` + `diff -u` and `exit 1` **before** the unit write, `daemon-reload` and `enable --now`; with `--force` it prints the same diff then overwrites; identical unit → silent proceed. `--status`/`--uninstall` untouched.
- 8 new pins in `tests/test_deploy_scripts.py`: refuse (exit 1, diff on stdout, unit byte-identical to the hand-edit, token value+`0640` unchanged, **zero new `systemctl` calls**), force (diff then write + re-enable), silent idempotent re-run, fresh install unaffected (plain vs `--force` render identical), the `driveripper` kernel-gate line refuses once → applies with `--force` → silent afterwards, uninstall never asks, source-ordering pin (render < cmp < write < reload < enable, no `cat > "$UNIT" <<` remains), and a `--help` alignment pin.
- Docs: README install block + a re-run-guard paragraph, and TLS-rollout step 2 now runs `--cert-bundle … --force` with the reason (installing a bundle *is* a unit change); AGENTS.md fleet bullet, the Commands flag list + guard paragraph, gap 10's task-03 status, and gap 11's staged-snapshot warnings (the Oct-18 copy predates the guard, so it still clobbers silently).
**Tests / lint / coverage**
- `uv run pytest` → **1447 passed, 15 skipped**; `bash .agents/validate.sh` → green (coverage **98.94%**, floor 94%), `ruff check src tests agent` clean, `pyright` 0 errors, `node --check` ok.
- `uv run pytest -m live --live` → 11 passed (read-only; dev server still 9/9 `ok`). **No live infra change** — the `driveripper` `--force` reproduction is task 04's operator-verified step.
**Decisions / deviations**
- Guard sits immediately before the unit write (the task's own parenthetical), so the earlier idempotent writes still run; the refusal path never re-writes the unit nor touches the service, and the reused token's value/mode are pinned unchanged (mtime isn't — noted in the test and script comment).
- Refusal sentence *and* diff both go to stdout (the diff is the answer's payload; task-04 pins "diff on stdout"). Temp file gets a readable prefix so the diff's `+++` names the generated unit.
- Wrote the formal pins now rather than leaving the guard untested until task 04; task 04's remaining doc/live items are untouched except where my change made prose stale.
**Next pending task:** `.agents/phases/todo/17_installer_device_filter/04_tests_docs_gate.md`
@@ -0,0 +1,65 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 14%]
........................................................................ [ 19%]
........................................................................ [ 24%]
...........................s............................................ [ 29%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 44%]
........................................................................ [ 49%]
........................................................................ [ 54%]
........................................................................ [ 59%]
........................................................................ [ 64%]
............................................................sssssssssss. [ 68%]
........................................................................ [ 73%]
........................................................................ [ 78%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 93%]
....................................sss................................. [ 98%]
...................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 436 0 100%
src/dashboards/app.py 185 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 3490 37 99%
Required test coverage of 94% reached. Total coverage: 98.94%
1447 passed, 15 skipped, 1 warning in 146.51s (0:02:26)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
fastapi 0.153.0 0.154.0 wheel
@@ -0,0 +1,15 @@
**Implemented**
- `tests/test_deploy_scripts.py`: added the four task-named pins — `test_the_template_unit_has_no_device_filter_lines` (heredoc region + the whole-file `DevicePolicy`/`DeviceAllow` absence, `PrivateDevices=false` kept, rationale must name `/proc/devices`/`blkext`/status 208), `test_a_default_install_unit_is_the_pre_phase_unit_minus_the_filter_lines` (whole pre-phase unit recorded verbatim as `PRE_PHASE_DEFAULT_UNIT` + `DEVICE_FILTER_LINES`, unified-diff failure output), `test_as_root_and_caps_write_the_kernel_gate_unit_lines` (recipe changes **exactly** two directives vs a default install; `--caps` alone refused before any privileged call; `--as-root` alone proceeds with the CapEff note), `test_a_re_run_that_differs_refuses_without_force_and_applies_with_it` (refuse→force→silent on one host).
- Mutation-checked both new filter pins (re-adding `DevicePolicy=strict`+`DeviceAllow=/dev/nvme*`, dropping `RestrictRealtime`, flipping `ProtectHome`) fail; script restored and `bash -n` clean.
- Docs: README's TLS rollout now restarts the service after `--force` and says why (installer uses `systemctl enable --now`, which only *starts*; `--status` picks http/https by reading the unit file); AGENTS.md gap 10 + the Deployment paragraph record task 04's read-only reproduction, the Oct-18 staged snapshot refusing the recipe with `unknown argument: --as-root`/exit 2, and the test count 1427→1451.
**Tests / lint** (`uv run`, exact): `bash .agents/validate.sh` → **exit 0** — `pytest --cov --cov-fail-under=94` **1451 passed, 15 skipped, coverage 98.94%**; `ruff check src tests agent` clean; `pyright` 0 errors; `node --check` + repo hygiene clean. `uv run pytest tests/test_deploy_scripts.py` → 63 passed, 0 failed. `uv run pytest -m live --live` → **11 passed, 0 skipped**; dev server all 9 sources `ok`.
**README/AGENTS changes:** as above (plus `data/logs/NOTES-17-installer-repro.md`, local/gitignored evidence).
**Decisions / deviations**
- "Byte-identical minus the ten filter lines" is compared on **directives** (comments are the one thing task 01 rewrote deliberately); the new prose is pinned separately for its mechanism sentences. Documented in the test.
- **No live change made.** `driveripper`'s installed unit was diffed (read-only, `systemctl cat`) against the sandbox render of `--bind 10.1.0.126 --port 9120 --as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN`: **every directive identical**, only comments differ; host `active`, `CapEff 0000000000220000`, 16/16 drives with temps. Running the real `--force` needs re-staging the sudo-granted Oct-18 snapshot (which lacks the flags, so it can't run them) and would push agent 1.1.0 to a prod host mid phase-15 rollout — left to the operator with the exact command, expected diff and post-checks in the note.
- `uv pip list --outdated` advisory (fastapi 0.153→0.155) left untouched, same handling as task 02; deps are not in this task's scope.
**Next pending task:** `.agents/phases/todo/18_nav_hamburger/01_hamburger_markup.md` (phase 17 has no other todo task).
@@ -0,0 +1,65 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 14%]
........................................................................ [ 19%]
........................................................................ [ 24%]
...........................s............................................ [ 29%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 44%]
........................................................................ [ 49%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 63%]
................................................................ssssssss [ 68%]
sss..................................................................... [ 73%]
........................................................................ [ 78%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 93%]
........................................sss............................. [ 98%]
.......................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 436 0 100%
src/dashboards/app.py 185 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/certs.py 292 0 100%
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
src/dashboards/rates.py 73 0 100%
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 134 0 100%
src/dashboards/sources/host.py 209 0 100%
src/dashboards/sources/llm.py 178 1 99% 285
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 313 2 99% 553-554
------------------------------------------------------------------
TOTAL 3490 37 99%
Required test coverage of 94% reached. Total coverage: 98.94%
1451 passed, 15 skipped, 1 warning in 154.14s (0:02:34)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
fastapi 0.153.0 0.155.0 wheel
+15 -4
View File
@@ -10,6 +10,13 @@ DASH_PORT=8090
DASH_DB=data/dashboards.sqlite3 DASH_DB=data/dashboards.sqlite3
# DASH_READ_ONLY=1 kills every mutating endpoint regardless of other flags # DASH_READ_ONLY=1 kills every mutating endpoint regardless of other flags
DASH_READ_ONLY=0 DASH_READ_ONLY=0
# Where the seed file lives (default: .env next to the checkout). Set it when several installs
# share one box. A real environment variable always beats anything the file declares — the file
# seeds, it never overrides.
# DASH_ENV_FILE=/etc/dashboards/.env
# DASH_RELOAD=1 makes `uv run dashboards` watch the source and restart on a change — development
# only: every poll loop restarts with the process, so the charts stop for a second on each save.
# DASH_RELOAD=1
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Admin login (phase 11) # Admin login (phase 11)
@@ -110,6 +117,10 @@ NUT_TIMEOUT_SECONDS=4
# Instant commands (battery test, beeper, calibration) are the only write path in the # Instant commands (battery test, beeper, calibration) are the only write path in the
# app. Requires NUT_USER plus this flag; the UI still confirms every command. # app. Requires NUT_USER plus this flag; the UI still confirms every command.
NUT_ALLOW_ACTIONS=0 NUT_ALLOW_ACTIONS=0
# NUT_SIMULATE=1 replaces upsd with the labelled simulator (sources/nut_sim.py) so the UPS page
# is developable with no battery in the house. Every panel it produces says SIMULATED, and it is
# never the default: leave these two lines alone against a real upsd.
# NUT_SIMULATE=1
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# History retention (raw -> 1m rollup -> 1h rollup) # History retention (raw -> 1m rollup -> 1h rollup)
@@ -129,7 +140,7 @@ POLL_MS_UPS=5000
DEBUGPY=0 DEBUGPY=0
DEBUGPY_PORT=5678 DEBUGPY_PORT=5678
# The host agent reads its OWN variables (AGENT_HOST, AGENT_PORT, AGENT_TOKEN, # The host agent reads its OWN variables (AGENT_HOST, AGENT_PORT, AGENT_TOKEN, AGENT_TOKEN_FILE,
# AGENT_TOKEN_FILE). They belong on the monitored host — in the root-owned env/token files # and AGENT_CERT_BUNDLE for the TLS bundle `--cert-bundle` installs). They belong on the monitored
# that deploy/install-agent.sh writes — not in this file: a dashboard config that everyone # host — in the root-owned env/token/bundle files that deploy/install-agent.sh writes — not in this
# copies tends to end up with the fleet's tokens in it. # file: a dashboard config that everyone copies tends to end up with the fleet's tokens in it.
+92 -35
View File
@@ -782,13 +782,18 @@ That scheme is the history API's key — treat it as a public interface.
**`ok`** and the `live` suite is **11 passed / 0 failed** (it was 9/2 on the missing token that **`ok`** and the `live` suite is **11 passed / 0 failed** (it was 9/2 on the missing token that
morning, and 8/3 on the ConnectError before that). The unit reads morning, and 8/3 on the ConnectError before that). The unit reads
`--host 10.1.0.126 --port 9120 --token-file /etc/dashboards-agent.token` (`640 `--host 10.1.0.126 --port 9120 --token-file /etc/dashboards-agent.token` (`640
root:dashboards-agent`, the mode `deploy/install-agent.sh` writes) with `User=dashboards-agent`, root:dashboards-agent`, the mode `deploy/install-agent.sh` writes) and is still in the `disk`
and `getent group disk` → `disk:x:6:dashboards-agent`. group (`getent group disk` → `disk:x:6:dashboards-agent`) — but it does **not** run as that
**The deployed unit has since diverged from the template in the three ways the SMART user: gap 10's hand-fix set `User=root`, and re-read from the host on 2026-10-20 the installed
block below records** (the `DevicePolicy`/`DeviceAllow` lines removed, `User=root`, unit is `User=root` + `CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN` with no device filter.
`CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN`) — re-running `deploy/install-agent.sh` **Those two extra lines are now template output, and the unit's rewrite is no longer
on driveripper would clobber the working unit, because it rewrites the unit unconditionally silent**: phase 17 took the
(gap 10). `DevicePolicy`/`DeviceAllow` lines out of the template (task 01), gave it `--as-root` and
`--caps` (task 02), and made a re-run whose generated unit differs from the installed one print
the diff and exit 1 unless `--force` was passed (task 03), so
`sudo deploy/install-agent.sh --bind 10.1.0.126 --port 9120 --as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN --force`
reproduces it and a re-run that would quietly replace a hand-fixed unit is now a refusal with a
diff attached rather than a clobber (gap 10).
`/api/health` today reads **all nine sources `ok`**: `host:driveripper`, `host:deskwork`, `/api/health` today reads **all nine sources `ok`**: `host:driveripper`, `host:deskwork`,
`host:gateway`, `disks:driveripper`, `disks:deskwork`, `disks:gateway`, `nut`, `llm:gufo`, `host:gateway`, `disks:driveripper`, `disks:deskwork`, `disks:gateway`, `nut`, `llm:gufo`,
`llm:vllm`. `llm:vllm`.
@@ -857,17 +862,19 @@ That scheme is the history API's key — treat it as a public interface.
`deskwork` grants `ops` neither the installer nor `visudo`, so its install went through root by `deskwork` grants `ops` neither the installer nor `visudo`, so its install went through root by
the operator. **`driveripper` does grant the installer** — `(root) NOPASSWD: the operator. **`driveripper` does grant the installer** — `(root) NOPASSWD:
/home/ops/agent-install/deploy/install-agent.sh`, an Oct-18 snapshot of the script staged under /home/ops/agent-install/deploy/install-agent.sh`, an Oct-18 snapshot of the script staged under
the operator's home (`sudo -n -l`, re-checked 2026-10-20) — and that grant is the trap, not the the operator's home (`sudo -n -l`, re-checked 2026-10-20) — and that grant is still the trap,
shortcut: the installer rewrites the unit unconditionally, so running it there today replaces the not the shortcut: it points at that Oct-18 **snapshot**, which carries neither this phase's
`User=root` + `CapabilityBoundingSet` unit that gap 10 resolved with this repo's flags, nor the filter removal, nor the re-run guard, so running the granted path replaces the
`User=dashboards-agent` + `DevicePolicy=strict` template and re-breaks all 16 SMART reads. The `User=root` + `CapabilityBoundingSet` unit that gap 10 resolved with the snapshot's own
live unit is not the one this repo writes (no `DevicePolicy`/`DeviceAllow` lines at all — `User=dashboards-agent`, capability-less unit and re-breaks all 16 SMART reads — **silently**,
systemd's default `auto` — `PrivateDevices=false`, and the two named capabilities). because the guard that refuses a differing re-run (task 03) lives only in this repo's script,
Worth knowing before the next install: the allow-list `install-agent.sh` which therefore has to be re-staged before any of it means anything there. The template now writes the live unit's shape: no device filter at all
does write (`/dev/nvme*`, `/dev/sd*`, `/dev/vd*`, `/dev/sg*`) has **no `/dev/ng*n*` and no (phase 17 task 01 — systemd's default `auto`, with `PrivateDevices=false`) and `--as-root`
`/dev/bsg/*`**, the nodes smartctl 7.5 reaches for NVMe admin and SATA pass-through, so a plus `--caps CAP_SYS_RAWIO,CAP_SYS_ADMIN` for the `User=root` and
`strict` unit there would deny exactly what a SMART read needs — whatever phase takes `CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN` lines (task 02). The old allow-list's
driveripper's SMART on has to settle the node list and the grant together. missing nodes are no longer a concern for the next install: with no filter, `/dev/ng*n*` and
`/dev/bsg/*` are reachable the same way `/dev/nvme*` and `/dev/sd*` are, subject to DAC and
SELinux.
- **SMART: real captures, and one live host finally reads them.** `smartctl` 7.5 is installed on - **SMART: real captures, and one live host finally reads them.** `smartctl` 7.5 is installed on
deskwork and driveripper. The `ops` account (which is who runs a hand-started dev agent) is in deskwork and driveripper. The `ops` account (which is who runs a hand-started dev agent) is in
no `disk` group, driveripper grants it `(root) NOPASSWD: /usr/sbin/smartctl*` and deskwork no `disk` group, driveripper grants it `(root) NOPASSWD: /usr/sbin/smartctl*` and deskwork
@@ -953,7 +960,7 @@ set -a; . ./.env.dev; set +a # dev dials the fleet the prod way, no
./scripts/dev-server.sh status # per-source ok/DOWN — unauthenticated: /api/health ./scripts/dev-server.sh status # per-source ok/DOWN — unauthenticated: /api/health
./scripts/dev-server.sh logs ./scripts/dev-server.sh logs
uv run pytest # hermetic: 1427 passed + 15 skipped (11 live, uv run pytest # hermetic: 1451 passed + 15 skipped (11 live,
# 4 store_perf), no network # 4 store_perf), no network
uv run pytest --cov # coverage gate (98.94%; floor 94%) uv run pytest --cov # coverage gate (98.94%; floor 94%)
uv run pytest -m live --live # opt-in: real fleet / engines / upsd uv run pytest -m live --live # opt-in: real fleet / engines / upsd
@@ -993,23 +1000,42 @@ bash .agents/validate.sh # the phase gate: the four checks abov
Deployment: `Containerfile` (two-stage, `uv sync --frozen`, runs as uid 10001 on `0.0.0.0:8090`, Deployment: `Containerfile` (two-stage, `uv sync --frozen`, runs as uid 10001 on `0.0.0.0:8090`,
SQLite under `/var/lib/dashboards` — put a volume there) and `deploy/install-agent.sh` SQLite under `/var/lib/dashboards` — put a volume there) and `deploy/install-agent.sh`
per monitored host (systemd unit, own user, `--bind`/`--port`/`--token`/`--rotate-token`/ per monitored host (systemd unit, own user, `--bind`/`--port`/`--token`/`--rotate-token`/
`--cert-bundle`/`--status`/`--uninstall`; it stores the token `0640 root:dashboards-agent` — the `--cert-bundle`/`--as-root`/`--caps`/`--force`/`--status`/`--uninstall`; it stores the token
`0640 root:dashboards-agent` — the
unit hands the agent the *path* and the agent opens it as its own user, so that user must be able unit hands the agent the *path* and the agent opens it as its own user, so that user must be able
to read it and no other account may, and a `--cert-bundle` file is installed to to read it and no other account may, and a `--cert-bundle` file is installed to
`/etc/dashboards-agent.certs` at that same mode after a shape check that runs before anything is `/etc/dashboards-agent.certs` at that same mode after a shape check that runs before anything is
touched and never dials the dashboard (the agent re-reads the bundle live for the denylist, so touched and never dials the dashboard (the agent re-reads the bundle live for the denylist, so
0600 would fail the unit exactly like it fails the token file; the task file's `0600` is the 0600 would fail the unit exactly like it fails the token file; the task file's `0600` is the
intent — *root and the agent's own user only* — and 0640 is the mode that achieves it) — never intent — *root and the agent's own user only* — and 0640 is the mode that achieves it) — never
puts a token on argv, and runs under `ProtectSystem=strict` with puts a token on argv, and runs under `ProtectSystem=strict` with **no device
`DevicePolicy=strict` reopening only the disk nodes plus the `/dev/null` family, because strict filter** — SMART device access is DAC via the `disk` group + SELinux, because kernel 7.2 matches
without `/dev/null` kills the unit at spawn (`status=208/STDIN`); a bundle-installed unit also a device-filter glob by name against `/proc/devices` (where NVMe is `blkext`), so the old
gets `--cert-bundle` plus `RuntimeDirectory=dashboards-agent` (mode 0700 — the bundle unpacks its allow-list denied the whole namespace; `PrivateDevices=false` stays explicit because a private
`/dev` with no device list kills the unit at spawn (`status=208/STDIN`). Where DAC is not enough,
`--as-root` writes `User=root` and `--caps CAP1,CAP2` writes that `CapabilityBoundingSet` line
(space-separated, as systemd spells it): kernel 7.2's unprivileged-passthrough gate checks
capabilities rather than uid, so `--as-root` alone is capability-less (`CapEff 0`) and prints a
note saying so, while `--caps` without `--as-root` is refused with exit 1 before `need_root` and
before any write. Both default off, and neither touches the token file's mode or the `--status`
probe. A bundle-installed
unit also gets `--cert-bundle` plus `RuntimeDirectory=dashboards-agent` (mode 0700 — the bundle unpacks its
PEM blocks there and the agent deletes the key once the SSL context holds it), keeps the PEM blocks there and the agent deletes the key once the SSL context holds it), keeps the
`--token-file` line for a host-by-host rollout, and `--status` then probes `https` on the `--token-file` line for a host-by-host rollout, and `--status` then probes `https` on the
bundle's CA with **no** client certificate via a `python3` `ssl` one-liner — task 02's bundle's CA with **no** client certificate via a `python3` `ssl` one-liner — task 02's
`CERT_OPTIONAL` + `/health` exemption is what makes that probeable, and there is no external TLS `CERT_OPTIONAL` + `/health` exemption is what makes that probeable, and there is no external TLS
binary. A re-run without the flag leaves an installed bundle in place, and `--uninstall` removes binary. A re-run without the flag leaves an installed bundle in place, and `--uninstall` removes
it with the token. No root shell? The one sudoers it with the token — and neither is guarded, because both are deliberate. What is guarded is the
unit itself (phase 17 task 03): it is rendered to a temp file first, and if an installed `$UNIT`
differs the run prints `refusing to overwrite …` plus the `diff -u` on stdout and exits 1 before
the unit write, the `daemon-reload` and the `enable`, so a redeploy cannot undo a hand-fix and an
identical re-run stays the silent zero-noise path. `--force` prints the same diff and applies it —
which is what the TLS rollout's `--cert-bundle` step needs on a token-mode host, because that step
*is* a unit change. What it applies is the **file**: `systemctl enable --now` only *starts* a unit,
so a run that rewrote `ExecStart` leaves the already-running agent on its old settings until
`systemctl restart dashboards-agent` — and `--status` chooses http from https by reading the unit
file, so probing before that restart reports "no answer" for a change that landed correctly (the
rollout in README restarts for exactly this reason). No root shell? The one sudoers
line that lets `ops` run the installer is `deploy/agent-sudoers`; run the script without root and line that lets `ops` run the installer is `deploy/agent-sudoers`; run the script without root and
it prints that line with its own resolved path.) it prints that line with its own resolved path.)
The quadlet unit + env template + nginx/SNI live in the deployment repo, like every other The quadlet unit + env template + nginx/SNI live in the deployment repo, like every other
@@ -1712,7 +1738,8 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
would be a re-decision of that phase, not a bug fix. Do not "fix" the residual by hiding the would be a re-decision of that phase, not a bug fix. Do not "fix" the residual by hiding the
short window or its label. short window or its label.
10. **`driveripper`'s 16 drives read `n/a`: kernel 7.2's unprivileged-passthrough gate 10. **`driveripper`'s 16 drives read `n/a`: kernel 7.2's unprivileged-passthrough gate
(2026-10-19, resolved the same day — the installer template is what is left).** (2026-10-19, resolved the same day; the installer template that shipped the broken device
filter was cleared in phase 17).**
The agent is installed, the `disk` group grant is in place, the block nodes open, and the The agent is installed, the `disk` group grant is in place, the block nodes open, and the
page renders all 16 real drive rows — but kernel 7.2.11 blocks the *agent user's* `smartctl` page renders all 16 real drive rows — but kernel 7.2.11 blocks the *agent user's* `smartctl`
at the command level (not the open; the probe matrix is in at the command level (not the open; the probe matrix is in
@@ -1745,11 +1772,39 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
`test_the_pass_budget_floor_is_twenty_seconds`, `test_the_pass_budget_floor_is_twenty_seconds`,
`test_the_pass_budget_error_line_names_the_scaled_figure` — which demands the line read `80s` `test_the_pass_budget_error_line_names_the_scaled_figure` — which demands the line read `80s`
for 16 disks, not the floor; the override test stays). Re-read live on 2026-10-20: the installed for 16 disks, not the floor; the override test stays). Re-read live on 2026-10-20: the installed
agent answers 16/16 drives with temperatures and no `pass budget … exhausted` line. The agent answers 16/16 drives with temperatures and no `pass budget … exhausted` line.
`deploy/install-agent.sh` template still ships the `DevicePolicy`/`DeviceAllow` lines that The `deploy/install-agent.sh` template no longer ships the device-filter lines that
misbehaved on this host misbehaved on this host — phase 17 task 01 removed them (the `nvme*` glob matches nothing
(a deny-all filter; the `nvme*` glob matches nothing on this kernel because on this kernel because `/proc/devices` lists NVMe as `blkext`, and a strict device policy
`/proc/devices` lists NVMe as `blkext`) — the template change is its own phase. denies everything a list fails to match), and the unit pins in `tests/test_deploy_scripts.py`
now assert their absence. Task 02 added the two flags that reproduce this host's remaining
lines — `--as-root` → `User=root`, `--caps CAP_SYS_RAWIO,CAP_SYS_ADMIN` →
`CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN`, both off by default, `--caps` refused
without `--as-root`, `--as-root` alone accepted with the CapEff-0 note — pinned by
`tests/test_deploy_scripts.py` (exact `User=` / `CapabilityBoundingSet=` lines in a fake-root
sandbox, the refusal before any write, the `--help` recipe). Task 03 removed the unconditional
rewrite: the unit is rendered to a temp file, and a re-run whose render differs from the
installed unit prints the diff on stdout and exits 1 unless `--force` is passed, so the flag
line above now needs `--force` to land and a routine re-run can no longer clobber this host's
unit (pinned by the refuse/force/silent sequence in `tests/test_deploy_scripts.py`, including
the `--force` leg of this host's own recipe — the same
`--bind 10.1.0.126 --port 9120 --as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN --force` line the
fleet record above prints). Task 04 verified the reproduction **read-only** rather
than by running it: the unit installed on `driveripper` (`systemctl cat`, 2026-10-20) differs
from what this template renders with that exact flag line in **comment lines only** — every
directive matches, `User=root`, `CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN`, no device
filter, `PrivateDevices=false`, the same `ExecStart` on the token file — and the host still
answers `active` with `CapEff: 0000000000220000` and 16/16 drives with temperatures in
`/snapshot`. So the operator's `--force` run lands the new rationale prose and changes nothing
systemd acts on (the three captures and the exact probe list are in
`data/logs/NOTES-17-installer-repro.md`; no live change was made while pinning this phase). Two facts that came out of reading the live host, both load-bearing for the
next install: the sudo-granted `/home/ops/agent-install` snapshot is the Oct-18 file (no
`--as-root`, no `--caps`, no `--force`, no `--cert-bundle`, still nine `DeviceAllow` lines) and
it **refuses** the recipe with `unknown argument: --as-root` and exit 2 before `need_root`, so
that path cannot apply this recipe at all until the repo's script is re-staged — while a
re-run of it *without* the flags is still the silent clobber the guard exists for. (The other
fact from that read — an applied change reaches the file but not the running process until
something restarts it — is in the Deployment paragraph, and README's rollout now restarts.)
`tests/fixtures/smartctl_driveripper_*.txt` (captured via `sudo`) remain the ground truth `tests/fixtures/smartctl_driveripper_*.txt` (captured via `sudo`) remain the ground truth
for the drives' shape. for the drives' shape.
11. **The CA is durable, the collector dials with it, and no bundle is installed with one** 11. **The CA is durable, the collector dials with it, and no bundle is installed with one**
@@ -1814,8 +1869,9 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
place (`data/logs/verify-15-tls-e2e.py`, transcript `verify-15-tls-e2e.out`). What is left of place (`data/logs/verify-15-tls-e2e.py`, transcript `verify-15-tls-e2e.out`). What is left of
the phase is the live rollout (07): the fleet's agents still run token mode, so `HOST_TARGETS` the phase is the live rollout (07): the fleet's agents still run token mode, so `HOST_TARGETS`
stays `http://` here until somebody installs a bundle on a host — which needs root on that host stays `http://` here until somebody installs a bundle on a host — which needs root on that host
(`ops` has no installer grant on `deskwork`, and `driveripper`'s unit must not be rewritten, gap (`ops` has no installer grant on `deskwork`, and `driveripper`'s unit is reproducible only with
10), and flipping the URL before the bundle is installed turns that host red by construction. phase 17's `--as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN --force` line, gap 10), and flipping
the URL before the bundle is installed turns that host red by construction.
Re-checked from the dev box the same day, and worth knowing before that rollout: all three Re-checked from the dev box the same day, and worth knowing before that rollout: all three
agents answer `/health` with `version` **1.0.0** (`agent_version` is the `/snapshot` spelling), agents answer `/health` with `version` **1.0.0** (`agent_version` is the `/snapshot` spelling),
so none of them carries task 02's TLS code either — the rollout ships the repo's so none of them carries task 02's TLS code either — the rollout ships the repo's
@@ -1824,8 +1880,9 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
exactly that TLS block; its `_smart_pass` and the phase-16 budget scale-up are already there. exactly that TLS block; its `_smart_pass` and the phase-16 budget scale-up are already there.
Copying 1.1.0 into place is not an `install-agent.sh` re-run on that host (gap 10) — even Copying 1.1.0 into place is not an `install-agent.sh` re-run on that host (gap 10) — even
though `ops` there is sudo-granted a staged copy of the installer (see the fleet record above), though `ops` there is sudo-granted a staged copy of the installer (see the fleet record above),
which is the one route that must not be taken until phase 17 fixes the unit template — and no which is the one route that must not be taken until that snapshot is re-staged from this repo:
live change was made while pinning phase 16. the Oct-18 copy predates phase 17's template *and* its re-run guard, so it would rewrite that
unit silently — and no live change was made while pinning phase 16.
## Locked decisions (and why) ## Locked decisions (and why)
+50 -7
View File
@@ -81,10 +81,21 @@ python3 host_agent.py --host 0.0.0.0 --port 9120 --token-file /etc/dashboards-ag
python3 host_agent.py --host 0.0.0.0 --port 9120 --cert-bundle /etc/dashboards-agent.certs python3 host_agent.py --host 0.0.0.0 --port 9120 --cert-bundle /etc/dashboards-agent.certs
sudo deploy/install-agent.sh # systemd unit + user + token sudo deploy/install-agent.sh # systemd unit + user + token
sudo deploy/install-agent.sh --cert-bundle agent.bundle # ...or the TLS install (bundle from /certs) sudo deploy/install-agent.sh --cert-bundle agent.bundle # ...or the TLS install (bundle from /certs)
sudo deploy/install-agent.sh --as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN
# ...or a kernel-gated host (SMART passthrough, below)
sudo deploy/install-agent.sh --force # ...only when it must overwrite a unit it would change
deploy/install-agent.sh --status # unit state and /health (https when TLS) deploy/install-agent.sh --status # unit state and /health (https when TLS)
sudo deploy/install-agent.sh --uninstall sudo deploy/install-agent.sh --uninstall
``` ```
Re-running the installer is the silent, idempotent path it has always been **as long as the unit
it would write is the unit already on disk**. When the two differ the run stops: it prints
`refusing to overwrite …` with the unified diff on stdout and exits 1 before writing the unit,
before `daemon-reload` and before the service is re-enabled, so a routine redeploy can never undo
a fix somebody made by hand on the host (`--as-root`/`--caps` on a host already running the
dedicated user is exactly such a change, and installing a TLS bundle onto a token-mode host is
another). `--force` prints the same diff and then applies it.
No root shell on the monitored host? `deploy/agent-sudoers` is the single sudoers line that No root shell on the monitored host? `deploy/agent-sudoers` is the single sudoers line that
lets the fleet's unprivileged `ops` account run the installer; run it without root and it lets the fleet's unprivileged `ops` account run the installer; run it without root and it
prints that exact line with the script's own resolved path instead of saying "run as root". prints that exact line with the script's own resolved path instead of saying "run as root".
@@ -149,9 +160,30 @@ hostname, mounts, disk and GPU models, and process names. `deploy/install-agent.
a system user, generates a token once (re-runs reuse it; `--rotate-token` for a new one), a system user, generates a token once (re-runs reuse it; `--rotate-token` for a new one),
stores it `0640 root:dashboards-agent` — the unit hands the agent the *path*, and the agent stores it `0640 root:dashboards-agent` — the unit hands the agent the *path*, and the agent
opens it as its own user, so the file must be readable by that user and by nobody else — opens it as its own user, so the file must be readable by that user and by nobody else —
and runs the agent under `ProtectSystem=strict`, `DevicePolicy=strict` with only the disk and runs the agent under `ProtectSystem=strict` with an empty `CapabilityBoundingSet` (unless
nodes and the `/dev/null` family reopened (SMART needs the former; without `/dev/null` in `--caps` is asked for, below) and **no device filter**.
the allow list systemd cannot start the service at all), with no capabilities. SMART device access is DAC: the installer adds the service user to the `disk` group and SELinux
stays in front of it. The strict device policy this script used to write is what locked
`driveripper`'s whole device namespace — systemd matches those globs by name against
`/proc/devices`, where kernel 7.2 lists NVMe as `blkext`, so `/dev/nvme*` matched nothing and a
strict policy denies whatever a list fails to match. `PrivateDevices=false` is still written out
explicitly, because a private `/dev` with no device list is what dies at spawn
(`status=208/STDIN`).
When DAC is not enough — a **kernel gate** — `--as-root` and `--caps` write the two unit lines
that gate checks accept. Kernel 7.2 gates smartctl's raw passthrough behind *capabilities*, not
uid: an NVMe SMART-log read through the block node needs `CAP_SYS_ADMIN` unless the drive's
firmware advertises the page, and SATA SMART behind a SAT layer is an ATA pass-through the
unprivileged `SG_IO` allow-list does not include (`CAP_SYS_RAWIO` is what the kernel names for
it). So `--as-root` writes `User=root` and `--caps CAP_SYS_RAWIO,CAP_SYS_ADMIN` writes
`CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN` (comma-separated in, space-separated out);
both default off, because a host whose drives answer through the `disk` group needs neither.
`--caps` without `--as-root` is refused with exit 1 — a capability grant on the dedicated
service user is not the kernel-gate fix — and `--as-root` alone proceeds with a note, because a
root process under the default empty bounding set is capability-less (`CapEff 0`) and reads no
more than the unprivileged service user did. The token file, its `0640 root:dashboards-agent`
mode and the `--status` probe are deliberately unchanged by these flags: root reads the token
either way, and a probe of a port cannot see who owns the process behind it.
### Managing the certificates ### Managing the certificates
@@ -197,9 +229,12 @@ curl -s -H "Authorization: Bearer $SESSION" -o agent-deskwork.bundle \
# 2. install it beside the token — as root on the host, 0640 root:dashboards-agent. The same run # 2. install it beside the token — as root on the host, 0640 root:dashboards-agent. The same run
# copies the repo's `agent/host_agent.py`, which is how a pre-1.1.0 agent gains TLS mode at all # copies the repo's `agent/host_agent.py`, which is how a pre-1.1.0 agent gains TLS mode at all
# (an older file does not know the `--cert-bundle` flag). The unit gains `--cert-bundle` and its # (an older file does not know the `--cert-bundle` flag). The unit gains `--cert-bundle` and its
# RuntimeDirectory, and restarts into TLS mode. # RuntimeDirectory — and then restart the service as root, because the installer's
# `systemctl enable --now` only *starts* a unit, so an already-running agent keeps serving its
# old mode until something restarts it.
scp agent-deskwork.bundle <host>-ops:/tmp/ scp agent-deskwork.bundle <host>-ops:/tmp/
ssh <host>-ops sudo /tmp/install-agent.sh --cert-bundle /tmp/agent-deskwork.bundle ssh <host>-ops sudo /tmp/install-agent.sh --cert-bundle /tmp/agent-deskwork.bundle --force
ssh <host>-ops sudo systemctl restart dashboards-agent
ssh <host>-ops sudo /tmp/install-agent.sh --status # https, with the CA and no client cert ssh <host>-ops sudo /tmp/install-agent.sh --status # https, with the CA and no client cert
# 3. point that host's HOST_TARGETS entry at https with NO userinfo token — the certificate is # 3. point that host's HOST_TARGETS entry at https with NO userinfo token — the certificate is
@@ -207,6 +242,12 @@ ssh <host>-ops sudo /tmp/install-agent.sh --status # https, with the CA and no
export HOST_TARGETS="deskwork=https://<addr>:9120 …" export HOST_TARGETS="deskwork=https://<addr>:9120 …"
``` ```
`--force` in step 2 is not a shortcut: installing a bundle *is* a change to that host's unit (the
`--cert-bundle` flag and its `RuntimeDirectory`), so without it the run prints the diff and stops
by design — read it, then re-run with the flag. The same rule covers every change the installer
applies, which is why the `--status` line above comes *after* the restart: the probe reads the
unit file to decide between http and https, so pointing it at a process that has not been
restarted yet reports "no answer" for a bundle that installed perfectly.
`install-agent.sh --bind … --port 9120` prints the `https://<addr>:9120` line for a bundle install, `install-agent.sh --bind … --port 9120` prints the `https://<addr>:9120` line for a bundle install,
so step 3 is a copy rather than a guess. A host left on `http://` keeps working over its bearer so step 3 is a copy rather than a guess. A host left on `http://` keeps working over its bearer
token — the rollout is per host, and one revoked or expired certificate never blinks the others. token — the rollout is per host, and one revoked or expired certificate never blinks the others.
@@ -854,8 +895,10 @@ Instant commands are the only endpoint in the application that changes anything
process** (`POST /api/ups/command`; the session and certificate-authority writes change only this process** (`POST /api/ups/command`; the session and certificate-authority writes change only this
app's own rows), and they are gated four times: `NUT_ALLOW_ACTIONS=1`, not `DASH_READ_ONLY`, an `upsd.users` app's own rows), and they are gated four times: `NUT_ALLOW_ACTIONS=1`, not `DASH_READ_ONLY`, an `upsd.users`
account that actually has the command, 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 (deep test, calibration, `load.off`) require typing `DANGER`. Two more sit in front of those
gate; hiding a button is not a safety control. Commands that would cut power are refused (since the login landed): a session, and this session's CSRF token when the write arrived on the
cookie — additive, never a replacement, so the typed `DANGER` is still what cuts power. The server
re-checks every gate on every request; hiding a button is not a safety control. Commands that would cut power are refused
outright while the UPS is already on battery. 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`, Give the app a narrowly scoped NUT user instead of a god-mode one — per-command `instcmds`,
+162 -30
View File
@@ -4,8 +4,11 @@
# sudo ./deploy/install-agent.sh # loopback only (read it locally) # sudo ./deploy/install-agent.sh # loopback only (read it locally)
# sudo ./deploy/install-agent.sh --bind 10.0.0.5 # serve the LAN, token required # sudo ./deploy/install-agent.sh --bind 10.0.0.5 # serve the LAN, token required
# sudo ./deploy/install-agent.sh --cert-bundle agent.bundle # serve TLS (phase 15) # sudo ./deploy/install-agent.sh --cert-bundle agent.bundle # serve TLS (phase 15)
# sudo ./deploy/install-agent.sh --as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN
# # a kernel-gated host (see below)
# ./deploy/install-agent.sh --status # unit state + /health, no root # ./deploy/install-agent.sh --status # unit state + /health, no root
# sudo ./deploy/install-agent.sh --uninstall # sudo ./deploy/install-agent.sh --uninstall
# sudo ./deploy/install-agent.sh --force # re-apply over a unit this would change
# #
# No root shell? `deploy/agent-sudoers` is the single sudoers line that lets the fleet's # No root shell? `deploy/agent-sudoers` is the single sudoers line that lets the fleet's
# unprivileged `ops` account run this script as root. Run it without root and it prints that # unprivileged `ops` account run this script as root. Run it without root and it prints that
@@ -32,17 +35,56 @@
# the agent exempts for probes. Re-running without the flag leaves an installed bundle in # the agent exempts for probes. Re-running without the flag leaves an installed bundle in
# place, the token-reuse idiom. # place, the token-reuse idiom.
# #
# The agent is read-only by construction (it answers GET and writes nothing), runs as its # The agent is read-only by construction (it answers GET and writes nothing), runs as its own
# own system user, and gets the usual systemd lockdown. It still needs to read /proc, /sys # system user (unless --as-root says otherwise, below), and gets the usual systemd lockdown. It
# and statvfs(), which the hardening below leaves alone. # still needs to read /proc, /sys and statvfs(), which the hardening below leaves alone.
# #
# Disk health (`smartctl -i -A -H`, the `/disks` page) is the one add-on that needs more # Disk health (`smartctl -i -A -H`, the `/disks` page) is the one add-on that needs more
# than /proc and /sys: smartctl must open the disk device nodes. The installer therefore # than /proc and /sys: smartctl must open the disk device nodes. The installer therefore
# adds the agent user to the `disk` group when one exists, and the unit device-allows the # adds the agent user to the `disk` group when one exists, and the unit applies no device
# disk nodes instead of PrivateDevices. This is raw *read* access to the disks — the # filter at all: kernel 7.2 matches a device-filter glob by name against /proc/devices, where
# standard arrangement for SMART monitoring, granted deliberately — and its absence is a # NVMe is listed as `blkext`, so the old `/dev/nvme*` pattern denied the whole device
# documented degradation, never an install failure: no smartmontools (or no group) simply # namespace instead of opening four nodes. Device access is DAC via the `disk` group, with
# means every disk reports `smart: null` with an explanatory error entry. # SELinux still in front of it — the standard arrangement for SMART monitoring, granted
# deliberately. Its absence is a documented degradation, never an install failure: no
# smartmontools (or no group) simply means every disk reports `smart: null` with an
# explanatory error entry.
#
# A kernel gate is a different grant, and --as-root / --caps are how the unit gets it. Kernel
# 7.2 gates smartctl's raw passthrough behind *capabilities*, not uid: an NVMe SMART-log read
# through the block node needs CAP_SYS_ADMIN unless the drive's firmware advertises the page,
# and SATA SMART behind a SAT layer is an ATA pass-through the unprivileged SG_IO allow-list
# does not include (CAP_SYS_RAWIO is what the kernel names for that). Those checks run against
# the process's capabilities, so a root service under the default empty CapabilityBoundingSet
# is capability-less (CapEff 0) and reads nothing — which is why the two flags belong together.
#
# --as-root run the service as root (for a kernel-gated host: kernel 7.2 gates
# unprivileged smartctl passthrough behind capabilities, not uid — a root
# process under the default empty bounding set is capability-less, so this
# flag needs --caps to mean anything)
# --caps LIST CapabilityBoundingSet line for the unit (comma-separated, e.g.
# CAP_SYS_RAWIO,CAP_SYS_ADMIN); default: empty (capability-less service)
#
# Both default off, because a host whose drives answer through the disk group needs neither.
# --caps without --as-root is refused (exit 1): a capability grant on the dedicated service
# user is not the kernel-gate fix. --as-root alone proceeds with a note, since a
# capability-less root is a legitimate if unusual choice. The recipe for the host this exists
# for — 16 drives, NVMe firmware without the SMART-log page and SATA behind SAT — is:
#
# sudo ./deploy/install-agent.sh --bind <addr> --port 9120 \
# --as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN
#
# A re-run is the zero-noise path it has always been as long as the unit this script would
# write is the unit already on disk. When the two differ the run stops: the diff between the
# installed unit and the generated one is printed and the exit code is 1, before the unit is
# written, before daemon-reload and before the service is re-enabled — so a routine redeploy
# cannot reset a unit somebody fixed by hand. driveripper's root + CapabilityBoundingSet unit
# is exactly that hand-fixed case (and the flags above are what reproduce it), which is why
# the refusal, not the overwrite, is the default. --force applies the change, with the same
# diff printed first so the operator sees what they are committing to.
#
# --force overwrite an existing unit even if the generated unit differs (prints the
# diff first)
set -euo pipefail set -euo pipefail
SERVICE="dashboards-agent" SERVICE="dashboards-agent"
@@ -69,6 +111,14 @@ PORT="9120"
TOKEN="${AGENT_TOKEN:-}" TOKEN="${AGENT_TOKEN:-}"
ROTATE=0 ROTATE=0
BUNDLE_SRC="" BUNDLE_SRC=""
# --as-root / --caps: the kernel-gate variant of the unit (phase 17 task 02). Both off means
# the default install — dedicated user, empty CapabilityBoundingSet — byte-for-byte what this
# script wrote before the flags existed.
AS_ROOT=0
CAPS=""
# --force: the re-run guard's opt-in (phase 17 task 03). 0 means a re-run whose generated unit
# differs from the installed one refuses after printing the diff.
FORCE=0
MODE="install" MODE="install"
usage() { usage() {
@@ -84,6 +134,22 @@ while [ $# -gt 0 ]; do
--token) TOKEN="$2"; shift 2 ;; --token) TOKEN="$2"; shift 2 ;;
--rotate-token) ROTATE=1; shift ;; --rotate-token) ROTATE=1; shift ;;
--cert-bundle) BUNDLE_SRC="$2"; shift 2 ;; --cert-bundle) BUNDLE_SRC="$2"; shift 2 ;;
--as-root) AS_ROOT=1; shift ;;
--caps)
[ $# -ge 2 ] || {
echo "--caps needs a capability list (e.g. --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN)" >&2
exit 1
}
# An empty list is a typo, not "the default": the operator typed the flag, so refusing
# is kinder than writing the capability-less unit they did not ask for.
if [ -z "$(printf '%s' "$2" | tr -d '[:space:],')" ]; then
echo "--caps needs at least one capability name (e.g. --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN)" >&2
exit 1
fi
CAPS="$2"
shift 2
;;
--force) FORCE=1; shift ;;
--status) MODE="status"; shift ;; --status) MODE="status"; shift ;;
--uninstall) MODE="uninstall"; shift ;; --uninstall) MODE="uninstall"; shift ;;
-h|--help) usage 0 ;; -h|--help) usage 0 ;;
@@ -91,6 +157,16 @@ while [ $# -gt 0 ]; do
esac esac
done done
# Refused here, before need_root and before any write, for the bundle validator's reason: a
# wrong flag combination is answered with its own sentence rather than sending the operator to
# add a sudoers line for a mistake in the flags. --caps on the dedicated service user is not
# the kernel-gate fix — the gate checks the process's capabilities, and a non-root user cannot
# hold them across exec — so the grant only means anything on a root service.
if [ -n "$CAPS" ] && [ "$AS_ROOT" -ne 1 ]; then
echo "--caps needs --as-root: a capability grant on the service user is not the kernel-gate fix" >&2
exit 1
fi
need_root() { need_root() {
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
# The dev workflow deploys over ssh as the unprivileged `ops` account, so "run as root" # The dev workflow deploys over ssh as the unprivileged `ops` account, so "run as root"
@@ -327,7 +403,38 @@ RuntimeDirectory=dashboards-agent
RuntimeDirectoryMode=0700" RuntimeDirectoryMode=0700"
fi fi
cat > "$UNIT" <<UNIT_FILE # The kernel-gate variant (--as-root / --caps). Default: the dedicated user and an empty
# CapabilityBoundingSet, i.e. the capability-less unit this script has always written.
UNIT_USER="$AGENT_USER"
if [ "$AS_ROOT" -eq 1 ]; then
UNIT_USER="root"
fi
# systemd wants the capabilities space-separated; the flag takes the comma-separated form
# that reads like a list on a command line. Empty entries are dropped so `A,,B` cannot put
# a double space in the unit file.
CAPS_LINE=""
set -f # a capability name is a word, never a glob: keep `*` out of the unit file
for _cap in $(printf '%s' "$CAPS" | tr ',' ' '); do
CAPS_LINE="${CAPS_LINE:+$CAPS_LINE }$_cap"
done
set +f
if [ "$AS_ROOT" -eq 1 ] && [ -z "$CAPS_LINE" ]; then
# Not a refusal: a capability-less root is a legitimate if unusual choice, and the operator
# may have a reason this script cannot see. The note is the reminder gap 10 paid for — the
# first root-user attempt on the kernel-7.2 host still read n/a on all 16 drives, and this
# empty bounding set was the only thing that had changed.
echo "note: --as-root with the default empty CapabilityBoundingSet leaves the process" \
"capability-less (the kernel gates check capabilities, not uid); pair it with --caps" \
"for a kernel-gated host"
fi
# The unit is rendered to a temp file rather than straight to $UNIT so the guard below can
# compare the guard's input with what would actually be written — byte for byte, with every
# flag, bundle and capability decision already applied, not a model of them. The prefix is so
# the +++ header of the diff the guard prints reads as the generated unit and not as an
# anonymous /tmp file.
TMP_UNIT="$(mktemp "${TMPDIR:-/tmp}/dashboards-agent-unit.XXXXXX")"
cat > "$TMP_UNIT" <<UNIT_FILE
[Unit] [Unit]
Description=Homelab host agent (read-only system metrics) Description=Homelab host agent (read-only system metrics)
After=network-online.target After=network-online.target
@@ -335,7 +442,7 @@ Wants=network-online.target
[Service] [Service]
Type=simple Type=simple
User=$AGENT_USER User=$UNIT_USER
ExecStart=/usr/bin/env python3 $AGENT_TARGET --host $BIND --port $PORT --token-file ${TOKEN_FILE}${TLS_EXEC_ARGS} ExecStart=/usr/bin/env python3 $AGENT_TARGET --host $BIND --port $PORT --token-file ${TOKEN_FILE}${TLS_EXEC_ARGS}
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5
@@ -343,29 +450,26 @@ Environment=PYTHONUNBUFFERED=1
SyslogIdentifier=$SERVICE SyslogIdentifier=$SERVICE
$TLS_UNIT_LINES $TLS_UNIT_LINES
# The agent reads /proc, /sys and statvfs() and writes nothing, so everything writable can # The agent reads /proc, /sys and statvfs() and writes nothing, so everything writable can
# be taken away. smartctl is the one thing that needs a real /dev node, so instead of # be taken away. smartctl is the one thing that needs a real /dev node, and it gets one the
# PrivateDevices the unit closes all devices and reopens only the disk nodes, read/write # way every SMART monitor on Linux does: DAC — the installer adds the service user to the
# at the cgroup layer while the code (and smartctl -i -A -H) stays read-only by design. # disk group — with SELinux still in front of it. The unit therefore carries no device
# filter of its own, and the code (and smartctl -i -A -H) stays read-only by design.
# #
# The pseudo nodes are not decoration: DevicePolicy=strict denies every node that is not # No filter is the fix, not an omission. systemd resolves a device-filter glob by name
# listed, and systemd needs /dev/null for the service's stdin before it ever execs python — # against /proc/devices (fnmatch in bpf_devices_allow_list_major), and kernel 7.2 lists NVMe
# without it the unit dies at spawn with "Failed at step STDIN" (status 208), which is how # there as blkext (major 259) rather than nvme, so the old /dev/nvme* glob matched nothing —
# the first phase-10 install failed on a live host running systemd 257. /dev/zero, full, # and a strict device policy denies every node the list failed to match, so on driveripper
# random and urandom are the rest of the set a PrivateDevices= sandbox would have provided, # that unit locked the whole device namespace, root included
# and none of them is a disk. # (data/logs/NOTES-driveripper-agent.md). A glob that silently matches nothing is the failure
# mode; having no filter has none, and it is what the live hosts run today.
#
# PrivateDevices=false is the systemd default and is written out for one reason: a private
# /dev with no device list is what dies at spawn with "Failed at step STDIN" (status 208),
# because systemd opens /dev/null for the service stdin before it ever execs python — that is
# how the first phase-10 install failed on a live host running systemd 257.
NoNewPrivileges=true NoNewPrivileges=true
PrivateTmp=true PrivateTmp=true
PrivateDevices=false PrivateDevices=false
DevicePolicy=strict
DeviceAllow=/dev/null rw
DeviceAllow=/dev/zero rw
DeviceAllow=/dev/full rw
DeviceAllow=/dev/random rw
DeviceAllow=/dev/urandom rw
DeviceAllow=/dev/nvme* rw
DeviceAllow=/dev/sd* rw
DeviceAllow=/dev/vd* rw
DeviceAllow=/dev/sg* rw
ProtectSystem=strict ProtectSystem=strict
ProtectHome=read-only ProtectHome=read-only
ProtectKernelTunables=true ProtectKernelTunables=true
@@ -376,13 +480,41 @@ RestrictNamespaces=true
RestrictRealtime=true RestrictRealtime=true
LockPersonality=true LockPersonality=true
MemoryDenyWriteExecute=true MemoryDenyWriteExecute=true
CapabilityBoundingSet= # --caps: kernel 7.2 gates unprivileged smartctl passthrough (NVMe admin log: CAP_SYS_ADMIN;
# SG_IO allow-list: CAP_SYS_RAWIO) and those checks look at capabilities, not uid — so a root
# service needs them named here, because an empty bounding set bounds even root to CapEff 0.
CapabilityBoundingSet=$CAPS_LINE
SystemCallFilter=@system-service SystemCallFilter=@system-service
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
UNIT_FILE UNIT_FILE
# No silent clobber (phase 17 task 03). $UNIT is the operator's file as much as this
# template's output: it gets edited on the host to fix something the template did not know
# (driveripper: User=root plus the two capabilities the kernel-7.2 passthrough gate needs),
# and an unconditional rewrite would undo that fix while looking like a routine redeploy. So
# a re-run whose unit differs stops, with the diff on stdout so the difference is readable
# rather than guessed, and --force is the opt-in to apply it anyway. The diff is printed on
# both paths and on stdout, not stderr: it is the payload of the answer, and splitting it
# across two streams interleaves it with the sentence above it. The writes before this point
# are the idempotent ones — the service user, its disk-group membership, the agent file, and
# the token/bundle reuse, which re-reads the existing secret instead of replacing it — and a
# refusal does nothing after it: no unit write, no daemon-reload, no enable, so the unit on
# disk and the service running against it stay exactly as they were.
if [ -f "$UNIT" ] && ! cmp -s "$UNIT" "$TMP_UNIT"; then
if [ "$FORCE" -eq 1 ]; then
diff -u "$UNIT" "$TMP_UNIT" || true
else
echo "refusing to overwrite $UNIT (generated unit differs); run with --force to apply:"
diff -u "$UNIT" "$TMP_UNIT" || true
rm -f "$TMP_UNIT"
exit 1
fi
fi
cat "$TMP_UNIT" > "$UNIT"
rm -f "$TMP_UNIT"
systemctl daemon-reload systemctl daemon-reload
systemctl enable --now "$SERVICE" systemctl enable --now "$SERVICE"
sleep 1 sleep 1
+775 -5
View File
@@ -12,6 +12,7 @@ apart from the path the script was invoked through.
from __future__ import annotations from __future__ import annotations
import difflib
import json import json
import os import os
import re import re
@@ -372,11 +373,22 @@ def test_install_path_installs_into_its_own_paths_and_prints_the_token_once(tmp_
# the bundle unpacks into (the agent falls back to a private mkdtemp without one). # the bundle unpacks into (the agent falls back to a private mkdtemp without one).
assert "--cert-bundle" not in unit, "no bundle installed means no TLS flag on a token install" assert "--cert-bundle" not in unit, "no bundle installed means no TLS flag on a token install"
assert "RuntimeDirectory=dashboards-agent" not in unit assert "RuntimeDirectory=dashboards-agent" not in unit
# DevicePolicy=strict denies every node not listed, and systemd opens /dev/null for stdin # No device filter, on purpose (phase 17): a strict device policy denies every node the
# before it execs python: without this line the unit dies at spawn (status 208/STDIN). # allow-list fails to match, and systemd matches those globs by *name* against
assert "DevicePolicy=strict" in unit # /proc/devices — where kernel 7.2 lists NVMe as `blkext`, so the old `/dev/nvme*` entry
assert "DeviceAllow=/dev/null" in unit, "strict device policy needs /dev/null or the unit cannot start" # locked the whole device namespace on driveripper. Device access is DAC (the `disk` group
assert "DeviceAllow=/dev/nvme*" in unit, "SMART reads need the disk nodes back" # the installer grants) plus SELinux, the standard SMART-monitoring arrangement.
assert "DevicePolicy" not in unit, (
"a strict device policy with a name-matched allow-list is the deny-all device filter "
"that locked driveripper's SMART reads — the unit must carry none"
)
assert "DeviceAllow" not in unit, "no device filter means no allow-list either"
# PrivateDevices=false survives as the explicit statement of the systemd default: a
# private /dev with no device list dies at spawn (status 208/STDIN), because systemd
# opens /dev/null for the service stdin before it execs python.
assert "PrivateDevices=false" in unit, (
"the explicit default is what keeps a private-/dev sandbox from dying at STDIN"
)
assert token and len(token) >= 32, "a generated token is expected" assert token and len(token) >= 32, "a generated token is expected"
assert token in result.stdout, "first install prints the token, and never again" assert token in result.stdout, "first install prints the token, and never again"
assert f'HOST_TARGETS="hostname=http://{token}@10.0.0.5:9120"' in result.stdout assert f'HOST_TARGETS="hostname=http://{token}@10.0.0.5:9120"' in result.stdout
@@ -387,6 +399,473 @@ def test_install_path_installs_into_its_own_paths_and_prints_the_token_once(tmp_
assert "chown root:dashboards-agent" in calls, "the token file must become group-readable" assert "chown root:dashboards-agent" in calls, "the token file must become group-readable"
#: A line the unit heredoc may legitimately write: a comment, a `[Section]` header, or a
#: systemd directive assignment. Anything else is the unquoted heredoc having *expanded*
#: something — see `test_the_written_unit_is_only_comments_and_directives`.
_UNIT_LINE_RE = re.compile(r"^(?:#|\[[A-Za-z]+\]|[A-Za-z]+=)")
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_the_written_unit_is_only_comments_and_directives(tmp_path: Path) -> None:
"""The unit heredoc is unquoted so `$TLS_UNIT_LINES` and `$TLS_EXEC_ARGS` can expand.
That is also what executes its **comments**: writing the phase-17 rationale with backticks
around `disk`, `blkext` and `/dev/nvme*` made the install run them (`disk: command not
found`, smartctl's version banner) and paste the output into the unit file, with `bash -n`
and every text assertion still happy. Quote the heredoc and phase 15's interpolated lines
stop working, so the guard is on the *result*: a unit line is a comment, a section header,
or a directive assignment — never the output of something the prose mentioned.
"""
script, _call_log, env = _sandbox_installer(tmp_path)
result = subprocess.run(
[BASH, str(script), "--bind", "127.0.0.1", "--port", "9199"],
capture_output=True,
text=True,
env=env,
timeout=60,
check=False,
)
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
offenders = [line for line in unit.splitlines() if line.strip() and not _UNIT_LINE_RE.match(line)]
assert not offenders, "the unit heredoc expanded its own text into the unit file:\n" + "\n".join(
offenders
)
assert result.stderr == "", "installing must not execute anything the unit's comments mention"
# --- the template's unit: no device filter, and the whole unit pinned (phase 17 task 04) ------
#: The ten lines the pre-phase template wrote between ``PrivateDevices=false`` and
#: ``ProtectSystem=strict`` — the device filter phase 17 removes. Pinned as the *old* text, in
#: order, because the fix is the absence of exactly these lines: re-adding
#: ``DeviceAllow=/dev/nvme* rw`` with ``DevicePolicy=strict`` is the deny-all returning, and a
#: test that only asserted "`DevicePolicy` is absent today" would pass while somebody added
#: the allow-list back under a default `auto` policy that ignores it.
DEVICE_FILTER_LINES = (
"DevicePolicy=strict",
"DeviceAllow=/dev/null rw",
"DeviceAllow=/dev/zero rw",
"DeviceAllow=/dev/full rw",
"DeviceAllow=/dev/random rw",
"DeviceAllow=/dev/urandom rw",
"DeviceAllow=/dev/nvme* rw",
"DeviceAllow=/dev/sd* rw",
"DeviceAllow=/dev/vd* rw",
"DeviceAllow=/dev/sg* rw",
)
#: The unit the template wrote **before** phase 17, rendered for a default token install
#: (bind `10.0.0.5`, port `9120`, no bundle), with the sandbox root spelled `<ROOT>`.
#:
#: This is the whole unit and not a highlight reel: it is the reference the default install is
#: compared against, so a template line added, dropped, reordered or reworded *outside* this
#: phase's remit fails a test instead of landing in the next host's service file. The comment
#: block is included verbatim (it is what the operator reads in `systemctl cat`) even though
#: phase 17 task 01 rewrote that prose on purpose — which is why the comparison below reads
#: directives, and the new rationale is pinned separately for the two facts it must carry.
PRE_PHASE_DEFAULT_UNIT = """[Unit]
Description=Homelab host agent (read-only system metrics)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=dashboards-agent
ExecStart=/usr/bin/env python3 <ROOT>/target/lib/host_agent.py --host 10.0.0.5 --port 9120 --token-file <ROOT>/target/token
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
SyslogIdentifier=dashboards-agent
# The agent reads /proc, /sys and statvfs() and writes nothing, so everything writable can
# be taken away. smartctl is the one thing that needs a real /dev node, so instead of
# PrivateDevices the unit closes all devices and reopens only the disk nodes, read/write
# at the cgroup layer while the code (and smartctl -i -A -H) stays read-only by design.
#
# The pseudo nodes are not decoration: DevicePolicy=strict denies every node that is not
# listed, and systemd needs /dev/null for the service's stdin before it ever execs python —
# without it the unit dies at spawn with "Failed at step STDIN" (status 208), which is how
# the first phase-10 install failed on a live host running systemd 257. /dev/zero, full,
# random and urandom are the rest of the set a PrivateDevices= sandbox would have provided,
# and none of them is a disk.
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=false
DevicePolicy=strict
DeviceAllow=/dev/null rw
DeviceAllow=/dev/zero rw
DeviceAllow=/dev/full rw
DeviceAllow=/dev/random rw
DeviceAllow=/dev/urandom rw
DeviceAllow=/dev/nvme* rw
DeviceAllow=/dev/sd* rw
DeviceAllow=/dev/vd* rw
DeviceAllow=/dev/sg* rw
ProtectSystem=strict
ProtectHome=read-only
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
CapabilityBoundingSet=
SystemCallFilter=@system-service
[Install]
WantedBy=multi-user.target
"""
def _unit_heredoc() -> str:
"""The template's unit heredoc, exactly as the script holds it.
Scoped on purpose: the rest of the script is prose about flags, and a pin that read the
whole file for `DeviceAllow` would be satisfied by deleting a sentence rather than by the
absence of a *unit line*.
"""
text = INSTALLER.read_text()
assert "<<UNIT_FILE" in text, "the unit is still written from a heredoc"
heredoc = text.split("<<UNIT_FILE\n", 1)[1].split("\nUNIT_FILE\n", 1)[0]
assert "[Service]" in heredoc and "CapabilityBoundingSet" in heredoc, (
"the heredoc region extracted from the script is not the unit"
)
return heredoc
def _unit_directives(unit: str) -> list[str]:
"""The lines systemd acts on — every line that is neither a comment nor blank.
Comments carry the phase's reasoning and *are* part of the file, but they are also the one
thing phase 17 task 01 rewrote deliberately (the old block justified the filter; the new
one explains its absence). Directives are what a host runs, so those are compared exactly.
"""
return [line for line in unit.splitlines() if line.strip() and not line.startswith("#")]
def test_the_template_unit_has_no_device_filter_lines() -> None:
"""The fix is the absence, so the absence is what is pinned.
Three layers, because the regression has three shapes: a `DevicePolicy=strict` or
`DeviceAllow=` line back in the heredoc (the deny-all returning on kernel 7.2, where
systemd matches those globs by *name* against `/proc/devices` and lists NVMe as `blkext`,
so `/dev/nvme*` matched nothing and strict denied the whole namespace);
`PrivateDevices=false` removed as "redundant" because it is the systemd default (a private
`/dev` with no device list dies at spawn with `status=208/STDIN`); and a rationale comment
that stops naming the mechanism, which is how the next operator re-adds the filter while
believing they are hardening the unit.
"""
text = INSTALLER.read_text()
# The phase's completion criterion is this exact grep, so it is pinned as one: neither
# directive may appear anywhere in the script, prose included.
assert "DevicePolicy" not in text, "no device policy line anywhere in the installer"
assert "DeviceAllow" not in text, "no device allow-list anywhere in the installer"
heredoc = _unit_heredoc()
offenders = [line for line in heredoc.splitlines() if re.match(r"^\s*(DevicePolicy|DeviceAllow)\s*=", line)]
assert offenders == [], "the unit heredoc grew a device filter line again:\n" + "\n".join(offenders)
assert "PrivateDevices=false" in heredoc, (
"the explicit default is the line that keeps the unit from dying at STDIN; the filter "
"going away does not make it redundant"
)
# The rewritten rationale has to be an explanation, not a shrug (task 01's own criterion).
comment = "\n".join(line for line in heredoc.splitlines() if line.startswith("#"))
assert "/proc/devices" in comment and "blkext" in comment, (
"the comment must name the mechanism, or the next reader cannot tell an omitted filter "
"from a forgotten one"
)
assert "208" in comment, "and the STDIN failure that PrivateDevices=false still prevents"
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_a_default_install_unit_is_the_pre_phase_unit_minus_the_filter_lines(tmp_path: Path) -> None:
"""No flags writes the unit this script has always written, minus exactly ten lines.
`AGENTS.md` invariant 3 in deployment clothes: a host that needs nothing new must not get a
new unit from an unrelated phase. The reference is the whole pre-phase unit recorded above,
so every directive is compared (not a keyword spot-check) and the only allowed difference is
the device filter's absence — asserted as the ordered list comparison *and* as each of the ten
lines being absent by name, so a filter line silently renamed rather than removed fails too.
"""
script, _call_log, env = _sandbox_installer(tmp_path)
result = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
installed = (tmp_path / "target" / "dashboards-agent.service").read_text()
reference = PRE_PHASE_DEFAULT_UNIT.replace("<ROOT>", str(tmp_path))
expected = [line for line in _unit_directives(reference) if line not in DEVICE_FILTER_LINES]
assert len(expected) == len(_unit_directives(reference)) - len(DEVICE_FILTER_LINES), (
"DEVICE_FILTER_LINES must match ten real lines of the reference unit — a filter line "
"this list no longer names is a filter that would pass this test"
)
got = _unit_directives(installed)
drift = "\n".join(
difflib.unified_diff(
expected, got, fromfile="pre-phase unit minus the filter", tofile="installed", lineterm=""
)
)
assert got == expected, f"the default unit drifted from the pre-phase unit by more than the filter:\n{drift}"
# And the ten lines are gone rather than reworded into something equivalent.
assert not [line for line in installed.splitlines() if line.startswith(DEVICE_FILTER_LINES)]
# --- the kernel-gate unit: --as-root / --caps (phase 17 task 02) ---------------------------
#: The exact sentence `--as-root` alone prints. It is not a refusal — a capability-less root is
#: a legitimate if unusual choice — but it is the fact gap 10 paid for: the first root-user
#: attempt on the kernel-7.2 host still read `n/a` on all 16 drives, because the bounding set
#: bounds even root (`CapEff 0`). Pinned verbatim so a paraphrase that drops "not uid" is a
#: test failure rather than an operator rediscovering it.
AS_ROOT_NOTE = (
"note: --as-root with the default empty CapabilityBoundingSet leaves the process "
"capability-less (the kernel gates check capabilities, not uid); pair it with --caps "
"for a kernel-gated host"
)
#: `--caps` on the dedicated service user is not the kernel-gate fix (a non-root process cannot
#: hold these capabilities across exec), so the combination is refused rather than written.
CAPS_REFUSAL = "--caps needs --as-root: a capability grant on the service user is not the kernel-gate fix"
#: The two capabilities the kernel names for the two gates (NVMe SMART log / SG_IO), and the
#: `driveripper` recipe as `--help` must teach it (phase 17 completion criterion).
KERNEL_CAPS = "CAP_SYS_RAWIO,CAP_SYS_ADMIN"
KERNEL_GATE_RECIPE = f"--as-root --caps {KERNEL_CAPS}"
def _install(script: Path, env: dict[str, str], *flags: str) -> subprocess.CompletedProcess[str]:
"""One sandboxed installer run: fake root boundary, real script body."""
try:
return subprocess.run(
[BASH, str(script), *flags],
capture_output=True,
text=True,
env=env,
timeout=60,
check=False,
)
except subprocess.TimeoutExpired as exc: # pragma: no cover - the recursion regression
pytest.fail(f"the installer did not return (flags {flags}, partial output: {exc.stdout!r})")
def _directive(unit: str, key: str) -> list[str]:
"""Every line of the unit assigning `key` — a duplicate assignment is a heredoc bug."""
return [line for line in unit.splitlines() if line.split("=", 1)[0] == key]
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_the_default_unit_keeps_the_service_user_and_an_empty_bounding_set(tmp_path: Path) -> None:
"""No flags = the unit this script wrote before the flags existed, byte-for-byte.
`CapabilityBoundingSet=` with nothing after the `=` is the capability-less default and what
every host whose drives answer through the `disk` group runs; it is also the line
`--caps` replaces, so its exact spelling is pinned here rather than as `in unit`.
"""
script, _call_log, env = _sandbox_installer(tmp_path)
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199")
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
assert _directive(unit, "User") == ["User=dashboards-agent"]
assert _directive(unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet="]
assert AS_ROOT_NOTE not in result.stdout, "the note belongs to --as-root alone"
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_as_root_with_caps_writes_the_kernel_gate_unit(tmp_path: Path) -> None:
"""The two lines that reproduce the working `driveripper` unit, exactly.
Comma on the command line, space in the unit: that is systemd's own spelling for
`CapabilityBoundingSet`, and it is the whole reason the flag takes a list rather than the
line. Everything else in the unit — the token file, its `0640 root:dashboards-agent`, the
hardening block — is unchanged by these flags.
"""
script, call_log, env = _sandbox_installer(tmp_path)
kernel_gate = ["--bind", "10.0.0.5", "--port", "9120", "--as-root", "--caps", KERNEL_CAPS]
result = _install(script, env, *kernel_gate)
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
assert _directive(unit, "User") == ["User=root"]
assert _directive(unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN"]
assert AS_ROOT_NOTE not in result.stdout, "--caps answers the note: the grant is there"
# The token file's logic is deliberately untouched by --as-root: root reads it either way,
# and re-deriving the mode per service user would be a second rule for no new safety.
token = tmp_path / "target" / "token"
assert stat.S_IMODE(token.stat().st_mode) == 0o640
assert "chown root:dashboards-agent" in call_log.read_text()
assert "--host 10.0.0.5 --port 9120" in unit
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_as_root_and_caps_write_the_kernel_gate_unit_lines(tmp_path: Path) -> None:
"""The whole flag contract in one place, plus the part only a comparison can pin.
The three flag shapes (`--as-root --caps LIST`, `--caps` alone, `--as-root` alone) each have
their own detailed pin nearby; what none of them shows is **how much** the recipe moves. So:
two installs in two sandboxes at the same bind and port, one default and one kernel-gated, and
their directive lists must differ by exactly the two lines this phase promises and nothing
else — the hardening block, the token file's path, the `ExecStart` and the `[Install]` section
are not these flags' business, and an `--as-root` that also slipped a `ProtectSystem` change in
would be a privilege change nobody asked for. The two half-recipes are then run in their own
sandboxes (one host re-run with different flags is the guard's decision, not the parser's).
"""
plain_script, _plain_log, plain_env = _sandbox_installer(tmp_path / "plain")
plain = _install(plain_script, plain_env, "--bind", "10.1.0.126", "--port", "9120")
assert plain.returncode == 0, f"default install failed:\n{plain.stdout}\n{plain.stderr}"
gate_script, gate_log, gate_env = _sandbox_installer(tmp_path / "gate")
recipe = _install(
gate_script, gate_env, "--bind", "10.1.0.126", "--port", "9120", "--as-root", "--caps", KERNEL_CAPS
)
assert recipe.returncode == 0, f"the driveripper recipe failed:\n{recipe.stdout}\n{recipe.stderr}"
def directives(where: str) -> list[str]:
root = tmp_path / where
unit = (root / "target" / "dashboards-agent.service").read_text()
return _unit_directives(unit.replace(str(root), "<ROOT>"))
changed = [
f"{a} -> {b}"
for a, b in zip(directives("plain"), directives("gate"), strict=True)
if a != b
]
assert changed == [
"User=dashboards-agent -> User=root",
"CapabilityBoundingSet= -> CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN",
], "--as-root --caps may write these two lines and nothing else"
assert "chown root:dashboards-agent" in gate_log.read_text(), (
"--as-root leaves the token file's ownership alone: root reads it either way, and the "
"grant must not widen because of a flag that is about capabilities"
)
# The two half-recipes, in their own sandboxes (a differing re-run of one host is the
# guard's business, not the flag parser's).
caps_only_script, caps_only_log, caps_only_env = _sandbox_installer(tmp_path / "caps-only")
caps_only = _install(
caps_only_script, caps_only_env, "--bind", "127.0.0.1", "--port", "9199", "--caps", KERNEL_CAPS
)
assert caps_only.returncode == 1, "--caps without --as-root is refused"
assert CAPS_REFUSAL in caps_only.stderr
assert not caps_only_log.exists(), "and refused before a single privileged command"
root_only_script, _root_only_log, root_only_env = _sandbox_installer(tmp_path / "root-only")
root_only = _install(root_only_script, root_only_env, "--bind", "127.0.0.1", "--port", "9199", "--as-root")
assert root_only.returncode == 0, f"--as-root alone must proceed:\n{root_only.stderr}"
assert AS_ROOT_NOTE in root_only.stdout, "with the capability-less note"
root_unit = (tmp_path / "root-only" / "target" / "dashboards-agent.service").read_text()
assert _directive(root_unit, "User") == ["User=root"]
assert _directive(root_unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet="]
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_as_root_alone_installs_and_prints_the_capability_note(tmp_path: Path) -> None:
"""A capability-less root proceeds — with the reminder, on stdout, and no refusal."""
script, _call_log, env = _sandbox_installer(tmp_path)
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199", "--as-root")
assert result.returncode == 0, f"--as-root alone must proceed:\n{result.stdout}\n{result.stderr}"
assert AS_ROOT_NOTE in result.stdout, "the note is the operator's only warning here"
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
assert _directive(unit, "User") == ["User=root"]
assert _directive(unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet="], (
"--as-root alone writes no capability grant: the note is the whole effect"
)
@pytest.mark.parametrize(
("caps", "refusal"),
[
("CAP_SYS_RAWIO", CAPS_REFUSAL),
("CAP_SYS_RAWIO,CAP_SYS_ADMIN", CAPS_REFUSAL),
("", "--caps needs at least one capability name"),
(" , ", "--caps needs at least one capability name"),
],
ids=["one-cap", "two-caps", "empty", "punctuation-only"],
)
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_caps_without_as_root_is_refused_before_anything_is_written(
tmp_path: Path, caps: str, refusal: str
) -> None:
"""Exit 1, the reason on stderr, and no side effect at all.
The check sits *before* `need_root` (the `--cert-bundle` validator's reasoning): a wrong
flag combination gets its own sentence instead of a sudoers hint, and nothing after it —
user, group membership, token, unit — can have run.
"""
script, call_log, env = _sandbox_installer(tmp_path)
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199", "--caps", caps)
assert result.returncode == 1, "the refusal keeps exit 1"
assert result.stdout == "", "a refusal is a failure: nothing on stdout"
assert refusal in result.stderr
assert GRANT_PREFIX not in result.stderr, "a flag mistake is not a sudoers problem"
target = tmp_path / "target"
assert not (target / "dashboards-agent.service").exists(), "refused means nothing written"
assert not (target / "token").exists()
assert not call_log.exists(), "no privileged command ran either"
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_caps_without_an_argument_is_refused_rather_than_crashing(tmp_path: Path) -> None:
"""`--caps` last on the command line: exit 1 naming the flag, not an `unbound variable`."""
script, _call_log, env = _sandbox_installer(tmp_path)
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199", "--as-root", "--caps")
assert result.returncode == 1
assert result.stdout == ""
assert "--caps needs a capability list" in result.stderr
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_the_caps_list_is_normalised_to_systemd_spelling(tmp_path: Path) -> None:
"""Empty entries and stray whitespace never reach the unit as a double space…
…and a `*` stays a literal: the list is split with pathname expansion switched off, so an
operator typo cannot substitute the working directory's file names into `User=`'s partner.
"""
script, _call_log, env = _sandbox_installer(tmp_path)
messy = ["--bind", "127.0.0.1", "--port", "9199", "--as-root", "--caps", "CAP_SYS_RAWIO,, CAP_SYS_ADMIN "]
result = _install(script, env, *messy)
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
assert _directive(unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN"]
script_glob, _log, env_glob = _sandbox_installer(tmp_path / "glob")
star = ["--bind", "127.0.0.1", "--port", "9199", "--as-root", "--caps", "*"]
result_glob = _install(script_glob, env_glob, *star)
assert result_glob.returncode == 0, f"install failed:\n{result_glob.stdout}\n{result_glob.stderr}"
unit_glob = (tmp_path / "glob" / "target" / "dashboards-agent.service").read_text()
assert _directive(unit_glob, "CapabilityBoundingSet") == ["CapabilityBoundingSet=*"], (
"the capability list is words, not a glob"
)
def test_help_teaches_the_kernel_gate_recipe() -> None:
"""Completion criterion: the `driveripper` flags are learnable from `--help` alone.
`--help` *is* the header comment, so the sentence that explains why a root service needs
its capabilities named is pinned here too — it is the one thing an executor on a fresh
clone cannot infer from a unit file.
"""
result = _run([BASH, str(INSTALLER), "--help"])
assert result.returncode == 0
assert KERNEL_GATE_RECIPE in result.stdout, "the recipe line must be copy-pasteable"
assert "--as-root run the service as root" in result.stdout
assert "--caps LIST CapabilityBoundingSet line for the unit" in result.stdout
assert "capabilities, not uid" in result.stdout, "the kernel gate is the why of both flags"
assert "capability-less" in result.stdout, "--as-root without --caps has to say what it is"
def test_the_status_probe_is_blind_to_who_the_service_runs_as() -> None:
"""`--status` probes the agent over the wire; the unit's user is invisible to it, on purpose.
A status path that read `AS_ROOT`/`CAPS` would make a health probe depend on flags the
operator may not have passed on this invocation — the probe answers about the port.
"""
status_body = INSTALLER.read_text().split("status() {", 1)[1].split("\n}", 1)[0]
assert "AS_ROOT" not in status_body
assert "CAPS" not in status_body
def test_token_file_is_readable_by_the_service_user_and_nobody_else(tmp_path: Path) -> None: def test_token_file_is_readable_by_the_service_user_and_nobody_else(tmp_path: Path) -> None:
"""`--token-file` is a path the *agent* opens, as its own user. """`--token-file` is a path the *agent* opens, as its own user.
@@ -409,6 +888,297 @@ def test_token_file_is_readable_by_the_service_user_and_nobody_else(tmp_path: Pa
assert not mode & 0o007, "the token file must never be world-readable" assert not mode & 0o007, "the token file must never be world-readable"
# --- the re-run guard: no silent clobber (phase 17 task 03) ----------------------------------
def _refusal(unit: Path) -> str:
"""The sentence a refused re-run prints, with the unit path substituted.
Pinned verbatim because it is the operator's only explanation of why a routine re-run
stopped, and because the diff follows it on **stdout** rather than stderr: the diff is the
payload of the answer (an operator pipes it into a pager or a file), and splitting a
sentence from its diff across two streams only interleaves them.
"""
return f"refusing to overwrite {unit} (generated unit differs); run with --force to apply:"
#: One line the operator can plausibly have edited on the host — `RestartSec` is a real knob,
#: it is not a line any flag writes, and it sits far from every other assertion in these tests.
OPERATOR_EDIT = ("RestartSec=5", "RestartSec=7 # hand-tuned on the host after a flapping start")
def _hand_edit(unit_path: Path) -> str:
"""Edit the installed unit the way an operator does, and return what it now holds."""
text = unit_path.read_text()
assert OPERATOR_EDIT[0] in text, "the template unit lost the line the operator edits"
edited = text.replace(*OPERATOR_EDIT)
unit_path.write_text(edited, encoding="utf-8")
return edited
def _systemctl_calls(call_log: Path) -> list[str]:
"""Only the `systemctl` lines of the stub log — the ones that touch the running service.
The privileged calls before the guard (`id`, `getent`, `usermod`, `chown`) do run again on
a refused re-run: they are the idempotent ones the guard's reasoning accepts. `systemctl`
is what a refused run must not do — no `daemon-reload`, no `enable --now` — so that is the
half compared here.
"""
return [line for line in call_log.read_text().splitlines() if line.startswith("systemctl ")]
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_a_rerun_that_would_change_the_installed_unit_refuses(tmp_path: Path) -> None:
"""install → hand-edit → re-run: exit 1, the diff on stdout, nothing applied.
This is the incident the guard exists for: `driveripper`'s unit was fixed by hand, and the
installer rewrote it unconditionally. A refused run must leave the unit on disk as the
operator left it and must not reload or re-enable anything, or the refusal arrives after
the thing it is refusing.
"""
script, call_log, env = _sandbox_installer(tmp_path)
unit_path = tmp_path / "target" / "dashboards-agent.service"
token_path = tmp_path / "target" / "token"
first = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
assert first.returncode == 0, f"first install failed:\n{first.stdout}\n{first.stderr}"
edited = _hand_edit(unit_path)
token_before = token_path.read_bytes()
reloads_before = _systemctl_calls(call_log)
assert reloads_before, "the first install did reload and enable the service"
result = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
assert result.returncode == 1, "a differing re-run refuses with exit 1"
assert result.stderr == "", "the refusal and its diff are one answer: stdout, not split"
assert _refusal(unit_path) in result.stdout
assert "--- " in result.stdout and "+++ " in result.stdout, "the diff is unified"
assert f"-{OPERATOR_EDIT[1]}" in result.stdout, "the diff shows the operator's line going away"
assert "+RestartSec=5" in result.stdout, "and the template's line coming back"
assert unit_path.read_text() == edited, "a refused run leaves the installed unit byte-identical"
# The reuse path re-reads the existing secret and rewrites the same bytes with the same
# mode; its *value* — what invariant 6 and the HOST_TARGETS entry depend on — never moves,
# which is the guarantee here (mtime is not: the write happens before the guard).
assert token_path.read_bytes() == token_before, "the reused token must not change"
assert stat.S_IMODE(token_path.stat().st_mode) == 0o640
assert _systemctl_calls(call_log) == reloads_before, (
"a refused run must not daemon-reload or enable: that would restart the service the "
"refusal exists to leave alone"
)
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_force_prints_the_diff_then_writes_the_generated_unit(tmp_path: Path) -> None:
"""`--force` is the opt-in, and the diff still arrives: an operator approves what they saw."""
script, call_log, env = _sandbox_installer(tmp_path)
unit_path = tmp_path / "target" / "dashboards-agent.service"
_install(script, env, "--bind", "10.0.0.5", "--port", "9120")
_hand_edit(unit_path)
result = _install(script, env, "--bind", "10.0.0.5", "--port", "9120", "--force")
assert result.returncode == 0, f"--force must proceed:\n{result.stdout}\n{result.stderr}"
assert _refusal(unit_path) not in result.stdout, "--force is not a refusal"
assert f"-{OPERATOR_EDIT[1]}" in result.stdout and "+RestartSec=5" in result.stdout, (
"the diff is printed even when the answer is yes"
)
unit = unit_path.read_text()
assert "RestartSec=5" in unit and OPERATOR_EDIT[1] not in unit, "the generated unit is written"
assert _systemctl_calls(call_log).count("systemctl enable --now dashboards-agent") == 2, (
"the overwrite is followed by the usual reload/enable"
)
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_an_unchanged_rerun_is_silent_and_proceeds(tmp_path: Path) -> None:
"""Idempotence is the normal path: same flags, same unit, no diff, no complaint, exit 0."""
script, call_log, env = _sandbox_installer(tmp_path)
unit_path = tmp_path / "target" / "dashboards-agent.service"
first = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
assert first.returncode == 0, f"first install failed:\n{first.stdout}\n{first.stderr}"
installed = unit_path.read_text()
result = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
assert result.returncode == 0, f"an identical re-run must proceed:\n{result.stdout}\n{result.stderr}"
assert _refusal(unit_path) not in result.stdout
assert "+++ " not in result.stdout, "nothing differs, so nothing is printed about differences"
assert "reusing the existing token" in result.stdout, "the token idiom is unchanged"
assert unit_path.read_text() == installed
assert _systemctl_calls(call_log).count("systemctl enable --now dashboards-agent") == 2
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_a_re_run_that_differs_refuses_without_force_and_applies_with_it(tmp_path: Path) -> None:
"""The refuse → force → silent sequence on **one** host, in that order.
Each leg has its own detailed pin beside it; what those cannot show is the transition — a
guard that refuses correctly the first time and then never lets an identical re-run through
silently is the same bug seen from the other side (an operator who has to type `--force`
every deploy stops reading the diff). So: install, hand-edit, re-run (refused, unit left
exactly as edited, no new `systemctl` call), re-run with `--force` (diff first, generated
unit on disk, service re-enabled), then re-run plain once more (silent, unit unmoved).
"""
script, call_log, env = _sandbox_installer(tmp_path)
unit_path = tmp_path / "target" / "dashboards-agent.service"
flags = ["--bind", "10.1.0.126", "--port", "9120", "--as-root", "--caps", KERNEL_CAPS]
first = _install(script, env, *flags)
assert first.returncode == 0, f"first install failed:\n{first.stdout}\n{first.stderr}"
edited = _hand_edit(unit_path)
reloads = len(_systemctl_calls(call_log))
refused = _install(script, env, *flags)
assert refused.returncode == 1, "leg 1: a differing re-run refuses"
assert _refusal(unit_path) in refused.stdout and "+++ " in refused.stdout
assert unit_path.read_text() == edited, "leg 1: the hand edit survives"
assert len(_systemctl_calls(call_log)) == reloads, "leg 1: nothing was reloaded or re-enabled"
applied = _install(script, env, *flags, "--force")
assert applied.returncode == 0, f"leg 2: --force failed:\n{applied.stdout}\n{applied.stderr}"
assert "+++ " in applied.stdout, "leg 2: the diff is printed even on a yes"
assert unit_path.read_text() != edited, "leg 2: the generated unit replaced the hand edit"
assert "RestartSec=5" in unit_path.read_text()
generated = unit_path.read_text()
# three calls per applied install: daemon-reload, enable --now, and the is-active check
assert len(_systemctl_calls(call_log)) == reloads + 3, "leg 2: reload + enable, as a normal install"
settled = _install(script, env, *flags)
assert settled.returncode == 0, f"leg 3: a silent re-run failed:\n{settled.stdout}\n{settled.stderr}"
assert "refusing to overwrite" not in settled.stdout and "+++ " not in settled.stdout
assert "reusing the existing token" in settled.stdout
assert unit_path.read_text() == generated, "leg 3: the silent path leaves the applied unit in place"
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_a_fresh_install_never_meets_the_guard(tmp_path: Path) -> None:
"""No installed unit → no comparison, no output about one, and `--force` changes nothing.
The guard is about a *re*-run. A first install must stay exactly the run it was before this
task, with or without `--force` — hence the two sandboxes writing byte-identical units.
"""
plain_script, _plain_log, plain_env = _sandbox_installer(tmp_path / "plain")
forced_script, _forced_log, forced_env = _sandbox_installer(tmp_path / "forced")
plain = _install(plain_script, plain_env, "--bind", "10.0.0.5", "--port", "9120")
forced = _install(forced_script, forced_env, "--bind", "10.0.0.5", "--port", "9120", "--force")
assert plain.returncode == 0, f"a first install failed:\n{plain.stdout}\n{plain.stderr}"
assert forced.returncode == 0, f"--force broke a first install:\n{forced.stdout}\n{forced.stderr}"
for stdout in (plain.stdout, forced.stdout):
assert "refusing to overwrite" not in stdout
assert "+++ " not in stdout
def rendered(where: str) -> str:
"""The unit that sandbox wrote, with its own sandbox root name blanked out.
The two runs live in different directories, so the absolute paths in `ExecStart` and
`--token-file` differ while every *decision* in the unit is the same — and the decision
is what `--force` must not touch.
"""
root = tmp_path / where
return (
(root / "target" / "dashboards-agent.service")
.read_text()
.replace(str(root), "<sandbox>")
)
assert rendered("plain") == rendered("forced"), "--force writes the unit the default install writes"
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_the_kernel_gate_reproduction_refuses_once_and_then_holds(tmp_path: Path) -> None:
"""The `driveripper` case end to end: a token-mode unit is *not* the kernel-gate unit.
Switching a host to `--as-root --caps` is a real change to a real unit, so it asks first,
applies with `--force`, and is silent afterwards — the flag line in `--help` is a
reproduction command only if running it twice lands the same unit and says nothing twice.
"""
script, _call_log, env = _sandbox_installer(tmp_path)
unit_path = tmp_path / "target" / "dashboards-agent.service"
kernel_gate = ["--bind", "10.1.0.126", "--port", "9120", "--as-root", "--caps", KERNEL_CAPS]
_install(script, env, "--bind", "10.1.0.126", "--port", "9120")
token_mode = unit_path.read_text()
asked = _install(script, env, *kernel_gate)
assert asked.returncode == 1, "a flag change to an installed unit is a change, so it asks"
assert _refusal(unit_path) in asked.stdout
assert "-User=dashboards-agent" in asked.stdout and "+User=root" in asked.stdout
assert "+CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN" in asked.stdout
assert unit_path.read_text() == token_mode, "the question must not also be the answer"
applied = _install(script, env, *kernel_gate, "--force")
assert applied.returncode == 0, f"--force failed:\n{applied.stdout}\n{applied.stderr}"
applied_unit = unit_path.read_text()
assert _directive(applied_unit, "User") == ["User=root"]
assert _directive(applied_unit, "CapabilityBoundingSet") == [
"CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN"
]
again = _install(script, env, *kernel_gate, "--force")
assert again.returncode == 0, f"the same command twice failed:\n{again.stdout}\n{again.stderr}"
assert "+++ " not in again.stdout, "once applied, the same flags are the zero-noise path"
assert unit_path.read_text() == applied_unit
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
def test_uninstall_is_still_deliberate_and_never_asks(tmp_path: Path) -> None:
"""The guard belongs to `do_install`: `--uninstall` removes a hand-edited unit without one.
A guard on the teardown path would be a guard that keeps a unit nobody wants, and
`uninstall()` does not render a unit at all.
"""
script, call_log, env = _sandbox_installer(tmp_path)
unit_path = tmp_path / "target" / "dashboards-agent.service"
_install(script, env, "--bind", "10.0.0.5", "--port", "9120")
_hand_edit(unit_path)
result = _install(script, env, "--uninstall")
assert result.returncode == 0, f"uninstall refused:\n{result.stdout}\n{result.stderr}"
assert "refusing to overwrite" not in result.stdout
assert not unit_path.exists(), "uninstall still removes the unit, edited or not"
assert "systemctl disable --now dashboards-agent" in _systemctl_calls(call_log)[-3:]
def test_the_guard_sits_between_the_render_and_the_unit_write() -> None:
"""Text pin for the ordering the behaviour depends on.
The comparison must read the *rendered* unit (every flag, bundle and capability decision
applied) and must come before the write, the `daemon-reload` and the `enable`; a `cat >
"$UNIT" <<UNIT_FILE` would put the clobber back while still passing every behavioural test
above on a host with no installed unit.
"""
text = INSTALLER.read_text()
assert "do_install() {" in text, "the install body is still the function that writes the unit"
# Scoped to do_install: `uninstall()` also reloads the daemon, and an unscoped index would
# compare offsets across two different functions.
body = text.split("do_install() {", 1)[1]
render = body.index('cat > "$TMP_UNIT" <<UNIT_FILE')
guard = body.index('! cmp -s "$UNIT" "$TMP_UNIT"')
write = body.index('cat "$TMP_UNIT" > "$UNIT"')
reload = body.index("systemctl daemon-reload")
enable = body.index('systemctl enable --now "$SERVICE"')
assert render < guard < write < reload < enable
assert 'cat > "$UNIT" <<' not in text, "the template may only write $UNIT after comparing it"
assert 'if [ "$FORCE" -eq 1 ]; then' in text, "--force is the guard's own opt-in branch"
assert "rm -f \"$TMP_UNIT\"" in text, "the rendered temp file is cleaned up on every path"
assert 'mktemp "${TMPDIR:-/tmp}/dashboards-agent-unit.XXXXXX"' in text, (
"the diff's +++ side must name the generated unit, not an anonymous temp file"
)
#: The `--force` help line's sentence (phase 17 completion criterion: the flags are learnable
#: from `--help`, and the diff-first promise is the reason an operator trusts `--force`).
FORCE_HELP_SENTENCE = "overwrite an existing unit even if the generated unit differs"
def test_help_documents_force_and_keeps_the_flag_block_aligned() -> None:
stdout = _run([BASH, str(INSTALLER), "--help"]).stdout
lines = [line for line in stdout.splitlines() if FORCE_HELP_SENTENCE in line]
assert len(lines) == 1, f"exactly one --force flag line is expected, got {lines}"
assert lines[0].lstrip().startswith("--force")
assert "(prints the" in lines[0] and "diff first)" in stdout
# One column for every flag's description: the block is a table, and `--force` landing at
# its own column is how `--help` stays readable as a reference rather than a paragraph.
as_root = next(line for line in stdout.splitlines() if line.lstrip().startswith("--as-root"))
assert lines[0].index(FORCE_HELP_SENTENCE) == as_root.index("run the service as root")
# --- the certificate bundle (phase 15 task 06) -------------------------------------------- # --- the certificate bundle (phase 15 task 06) --------------------------------------------
#: Where the installed bundle lives — one constant in the script, pinned here so a rename #: Where the installed bundle lives — one constant in the script, pinned here so a rename