re-add privileged actions to todo so we can re-plan the phase

This commit is contained in:
2026-10-19 10:17:09 -04:00
parent 28139df6d6
commit 29524b3ebc
6 changed files with 2 additions and 418 deletions
@@ -1,90 +0,0 @@
# Phase 15 — The upsd wire: a managed root CA, STARTTLS, and the account gate
**Source:** `TODO.md` L6 — "For privileged actions (UPS commands) the exporter should be installed with password protection and that password must be used to register it with the collector. Effectively the exporters become protected web servers. Additionally, the collector should generate and manage a root certificate that needs to be installed with each exporter. In fact, just use the certs as auth rather than a password."
**Story:** n/a
**Context:** `src/dashboards/sources/nut.py` (the ~10-line `NutClient` wire protocol, `login()`'s `USERNAME`/`PASSWORD` verbs pinned by `test_login_uses_the_verbs_upsd_2_8_5_answers_to`, `assess()`/`COMMANDS` risk tiers), `src/dashboards/collectors.py` (`NutMonitor`, the redacted-URL error doctrine), `src/dashboards/api.py` (the one `POST /api/ups/command` and its four gates), `src/dashboards/config.py` (env-only settings), `tests/test_nut.py` (the fake upsd), `tests/fixtures/nut_apc.vars` / `nut_cyberpower.vars` (the wire-verb ground truth, incl. `HELP`'s advertised verbs: `GET LIST SET INSTCMD LOGIN LOGOUT USERNAME PASSWORD STARTTLS`), `scripts/`, `README.md` → *NUT*
## Objective
The privileged action in this app is `POST /api/ups/command`, and its security today
rests on the `upsd.users` account (the password the operator installed on the
exporter, carried in the collector's config and used at `LOGIN` before every
`INSTCMD` — the "register with the collector" half is already true) plus the four
gates. What it does **not** have is transport security: the account password and the
command travel in cleartext on the wire. This phase gives the wire what the TODO asks
for, in the form NUT 2.8.5 can express: the collector **generates and manages a root
CA**, issues a server certificate for the exporter (installed on it by the operator)
and a client certificate for itself; the `NutClient` upgrades the connection with
`STARTTLS`, verifies the exporter's certificate against the local CA (the collector
dials the real upsd, not a lookalike) and presents its own client certificate.
- ASSUMPTION (the load-bearing one): "just use the certs as auth rather than a
password" is implemented as **certs authenticate the transport, the account
password still gates the command** — the deployed upsd 2.8.5's command gate *is* the
`upsd.users` account (`SET`/`INSTCMD` still need it, per AGENTS.md's verified fleet
facts; NUT has no wire-level client-certificate check for commands, and its own
`HELP` verb list shows `USERNAME`/`PASSWORD` as the auth pair). A client
certificate presented to a server that does not check it is a trust slot for the
next NUT, not the gate today; the gate that exists today is made *usable only over
TLS* in the documented deployment, which is the protection the TODO is after
(password + commands off the cleartext wire, both endpoints authenticated). The
`available_commands()` limitation (the wire cannot tell what the account is
granted, AGENTS.md known gap 1) is unchanged — the risk tiers and the typed
`DANGER` stay load-bearing.
## Dependencies
- `05_ups_hero_battery_glyph` (complete) — the `NutClient`/`NutMonitor`/`COMMANDS`
surface this phase extends.
- `01…04` (complete) — `collectors.py`'s redacted-URL error doctrine (invariant 6)
that the TLS error messages must follow.
- `11_admin_login` (todo) — **shared file `config.py`**: 11 appends the four `DASH_*`
auth settings, this phase appends the `NUT_TLS*` block; whichever runs second
applies on top of the file state at that time, never restoring the other's lines.
`api.py` is also shared (11's gate vs this phase's none — this phase adds no route
and changes no route; the one POST stays the one POST).
## Tasks
1. `01_ca_and_certificate_tooling.md` — the collector's root CA: generation script, the 0600 material layout, and the upsd-side install steps verified against the deployed upsd 2.8.5 (rule 7: no guessed `upsd.conf` line).
2. `02_nutclient_starttls.md` — `STARTTLS` in the `NutClient`: stdlib `ssl`, server verified against the local CA, client certificate presented, the rest of the protocol byte-identical, and the cleartext path unchanged when TLS is off.
3. `03_config_gates_and_redaction.md` — the `NUT_TLS*` settings (off by default), the one-POST-four-gates story made explicit (the account password *is* the registration credential, unchanged), and invariant 6: no key material in anything that leaves the process.
4. `04_hermetic_tls_tests.md` — the loopback-TLS fake upsd, the `STARTTLS` framing pin (a server without the branch fails exactly as the `USER`/`PASS` regression does), wrong-CA rejection, the gate over TLS, the no-leak test, and the read-only `live` test.
## Testing & Quality
- Unit: `tests/test_nut.py` — `STARTTLS` framing (the fake upsd's `STARTTLS` branch),
server-cert verification (a self-signed server the local CA did not issue →
refused, with a redacted message), client-certificate presentation (the loopback
server with `CERT_REQUIRED` sees the dashboard's client cert), the
`USERNAME`/`PASSWORD` login **over TLS** (the gate still works through the
upgrade), and the cleartext path unmodified (`NUT_TLS` off → no `STARTTLS` sent,
every pre-existing test passes unmodified in intent).
- Unit: the no-leak test — a TLS failure's message, the health record it becomes and
the `event` row that carries it contain no key material and no `NUT_TLS_DIR`
contents (the URL redaction doctrine, extended to key material).
- Live (`-m live --live`, read-only per the AGENTS.md live rule): a
`STARTTLS`-capable probe of the real upsd (does the deployed 2.8.5 answer the
verb — `HELP` says it does; the probe records the answer), **no `INSTCMD`** sent
from the test (a battery test on hardware something is plugged into is not a
test).
- Coverage: **>90%** on new/modified code; the 94% gate must hold.
## Completion Criteria
- [ ] `scripts/nut-tls-certs.sh` (or the Python equivalent in `scripts/`) generates
the CA + both certificates on a clean machine; the material is 0600, outside
git, and the README's *NUT* section walks the operator through installing the
server certificate on the exporter (verified against the deployed upsd 2.8.5 —
the exact config in `data/logs/`, not a memory of a man page)
- [ ] with `NUT_TLS=1` and the material in place, `NutClient` completes `STARTTLS`,
verifies the server, and the `LOGIN` + a `GET` reading succeed over the
upgraded socket (the hermetic loopback; the `live` probe for the real verb
answer)
- [ ] with `NUT_TLS` unset (the default), every pre-existing `tests/test_nut.py`
behaviour is byte-identical — no `STARTTLS` on the wire, cleartext reads
unchanged
- [ ] no key material in any health record, event row, error message or `/api/config`
answer (the no-leak test + the redaction suite)
- [ ] `uv run pytest` green, `bash .agents/validate.sh` green
- [ ] `README.md`'s NUT section documents the CA, the install steps, the
password-still-gates-the-command reality, and why (the NUT 2.8.5 constraint)
@@ -1,77 +0,0 @@
# Task 01 — The collector's root CA: tooling and the verified upsd-side install
**Phase:** `15_nut_tls_credentials` · **Source:** `TODO.md:6` — "the collector should generate and manage a root certificate that needs to be installed with each exporter"
## Objective
The certificate side of "the collector manages the root certificate": a script that
generates the local CA, the exporter's server certificate and the collector's client
certificate with sane defaults, the material layout on disk, and — per rule 7
(verify against the deployed upstream, never guess) — the **actual** upsd 2.8.5
server-side configuration, verified on the host and recorded, not a man-page memory.
No key material ever enters a tracked file, and the generator is re-runnable (a
rotation is a re-run, not a migration).
## Work
1. `scripts/nut-tls-certs.sh` (bash + `openssl`, stdlib tooling — no new dependency,
the locked-decisions doctrine): generate, under `NUT_TLS_DIR` (default
`~/.config/dashboards/nut-tls/`, mode `0700`, files `0600`):
- `ca.key`/`ca.crt` — a local root CA (RSA 4096 or Ed25519, 20 y, `CA:TRUE`);
- `upsd.key`/`upsd.crt` — the exporter's server certificate, signed by the CA
(`CN` + `subjectAltName` for the address the collector dials —
`driveripper.reeselink.com` and `10.1.x.x` per the fleet facts; the SAN list is
a script argument, defaulting to the `NUT_HOST` value);
- `dashboard.key`/`dashboard.crt` — the collector's client certificate (the one the
`NutClient` will present);
- re-runnable: existing material is verified (the certs chain to the CA) and
left alone; `--rotate` regenerates (a rotation re-issues all three — the old
server cert stops verifying, which is the intended eviction, like the
session-secret rotation in phase 11).
2. Verify the upsd-side configuration **on the deployed upsd** (from
`driveripper`, where `sudo` grants the `/etc/ups` reads per the fleet facts):
read the running `upsd`'s TLS capability — `upsd -V`, its conf's current `LISTEN`
lines, and the 2.8.5 documentation *as installed on the host* (`man upsd` / the
package's docs) — and record the exact configuration that serves `STARTTLS` on
3051 with the server certificate, in `data/logs/NOTES-upsd-tls.md` (the exact
directive name(s), the cert/key paths upsd expects, and whether the 2.8.5 build
verifies *client* certificates at all — the phase-15 ASSUMPTION says it does not;
the note records the evidence either way). If the installed 2.8.5 **cannot**
serve `STARTTLS` (a `--without-tls` build — check `upsd -V`'s feature list),
record that and the phase's task 02/04 narrow to "the client is ready, the
server-side is the operator's upgrade call" — do **not** install a different NUT
(a fleet change is the operator's action, the AGENTS.md fleet-facts doctrine).
- ASSUMPTION: the script uses `openssl` (present on every fleet host, the
`deploy/install-agent.sh` doctrine — stdlib/tooling only, no new package) and
the default `NUT_TLS_DIR` lives in the operator's home config tree, not the
repo (invariant 6: the material is a credential, never tracked).
3. `README.md` → *NUT*: the operator walkthrough — run the script on the machine that
dials upsd, copy `upsd.crt`/`upsd.key` to the exporter, apply the task-1-recorded
upsd configuration (the `sudo` grant for the `/etc/ups` writes exists per the
fleet facts; the write itself stays the operator's action, like every other
`upsd.users` edit in this repo — the known-gap-1 doctrine: never grant `actions =
-`, never write it blind), restart `nut-server`, and confirm the collector
connects (task 04's `live` probe).
## Testing & Quality
- Script test (the repo's `tests/test_deploy_scripts.py` device — it already fails
the day a script regresses): the script's output layout (file names, modes
0700/0600, the certs chain to the CA via `openssl verify`), re-run idempotency,
and `--rotate` changing the key fingerprints; no tracked file receives key material
(the test asserts the generated dir is the only place the material is).
- Coverage: **>90%** on new/modified code (script + its test; the 94% gate must
hold).
## Completion Criteria
- [ ] a clean run of the script produces the CA + both certificates with the pinned
layout/modes; `openssl verify -CAfile ca.crt upsd.crt` passes; a re-run is a
no-op; `--rotate` changes fingerprints
- [ ] `data/logs/NOTES-upsd-tls.md` records the deployed upsd 2.8.5's actual TLS
capability and the exact server-side configuration (or the finding that this
build cannot serve `STARTTLS` — the note, not the assumption, decides task
02's scope)
- [ ] `tests/test_deploy_scripts.py` pin green; `bash .agents/validate.sh` green
- [ ] the README walkthrough exists and names the operator actions that stay the
operator's (the upsd config write, the restart)
@@ -1,82 +0,0 @@
# Task 02 — `STARTTLS` in the NutClient: verified server, presented client
**Phase:** `15_nut_tls_credentials` · **Source:** `TODO.md:6` — "just use the certs as auth rather than a password" (implemented per the phase ASSUMPTION: certs authenticate the transport; the `upsd.users` account still gates the command)
## Objective
The `NutClient` upgrades the connection with the verb the server's own `HELP`
advertises (`STARTTLS` — pinned in `tests/test_nut.py`'s verb-list test): after the
upgrade the socket is wrapped in stdlib `ssl` (no new dependency), the exporter's
certificate is verified against the local CA (the collector dials the real upsd, not
a lookalike), the collector's client certificate is presented (the trust slot for a
server that will check it), and **every other byte of the ~10-line protocol is
unchanged** — the `USERNAME`/`PASSWORD` login, the `LIST`/`GET` reads, the
`INSTCMD` write. When `NUT_TLS` is off (the default, and the only state the current
fleet can use — the laptop cannot reach 3051 at all, per the fleet facts), the
client behaves exactly as today: no `STARTTLS` sent, cleartext, every pre-existing
test passing unmodified in intent.
## Work
1. `src/dashboards/sources/nut.py` — `NutClient` (the socket half, where the
existing `connect`/`_read_line`/`login`/`instcmd` live):
- a `tls: bool` + `ca_path`/`cert_path`/`key_path` constructor path (built by
`NutMonitor` from the task-03 settings; the client itself stays testable with
explicit paths);
- on connect, when `tls`: send `STARTTLS\n`, read the OK line (the framing the
fake upsd in `tests/test_nut.py` already models for the other verbs — the same
one-line-per-command discipline), then wrap the **existing** socket with
`ssl.SSLContext(verify_mode=CERT_REQUIRED, purpose=server_auth)`:
`load_verify_locations(ca)`, `load_cert_chain(cert, key)` (the client
certificate), `server_hostname` from the dialled host — the wrap happens on the
connected socket, so `_read_line`/the writers are unchanged after the upgrade
(they already read/write `self._sock`);
- when `tls` is off: the constructor path is not even built (no `SSLContext`
object, no file reads — the cleartext path touches no TLS code at runtime);
- the `LOGIN`/`LOGOUT`/`GET`/`LIST`/`INSTCMD` sequence is unchanged in both modes
(the login happens after the upgrade — the password is the first thing that
travels over the encrypted socket, which is the point).
- The scope guard from task 01: if `data/logs/NOTES-upsd-tls.md` recorded that
the deployed 2.8.5 build **cannot** serve `STARTTLS`, this task still ships
the client (the hermetic loopback proves it; the README says the server side is
the operator's upgrade call) — the client capability is independent of one
host's build.
2. `src/dashboards/collectors.py` — `NutMonitor` takes the task-03 settings and
builds the TLS half (one place, the `from_env()` single-source doctrine `nut_sim`
already follows: the assembly and the model cannot disagree about what is
enabled). A TLS failure (refused upgrade, unverified server, expired cert) is an
`UpstreamError` like any other dial failure — the source goes `DOWN` with the
real reason (invariant 4), and the message follows the existing redacted-URL
doctrine (the dialled address redacted, and **no key material** — task 04's
no-leak test pins it).
3. `src/dashboards/sources/nut_sim.py` — no change expected (the simulator is a
labelled fake, invariant 3/its own doctrine); the `SimUps` tests keep passing —
TLS is a transport property the simulator does not model, and that is correct.
## Testing & Quality
- Unit: `tests/test_nut.py` — the fake upsd's `STARTTLS` branch (a scripted
loopback `ssl` server, the task-01-generated certificates in a temp dir — the
test generates its own material with the task-01 script, never a tracked
fixture): the upgrade framing (`STARTTLS` → OK → the wrapped socket answers the
same verbs), the `LOGIN` over TLS, a `GET` reading over TLS, and the
`CERT_REQUIRED` rejection: a loopback server with a certificate the local CA did
not issue → the connection is refused with an `UpstreamError` whose message
names the redacted host and **no** key material.
- Regression: `NUT_TLS` off (the default in every pre-existing test) → every
pre-existing `test_nut.py` / `test_collectors.py` / `test_api.py` behaviour
passes unmodified in intent; no `STARTTLS` string ever appears on the cleartext
wire (the fake upsd's scripted log asserts it was never sent).
- Coverage: **>90%** on this task's new/modified code; the 94% gate must hold.
## Completion Criteria
- [ ] the loopback-TLS fake upsd: `STARTTLS` framing, verified server, presented
client certificate (the server with `CERT_REQUIRED` sees it), login + read
over the upgraded socket — all green
- [ ] a wrong-CA server is refused with a redacted, key-free `UpstreamError`; the
source state path (task 04's poller test) shows `DOWN` with the reason, last
good payload kept (invariant 4)
- [ ] the cleartext path is byte-identical: the pre-existing suite passes and the
fake upsd records no `STARTTLS` verb in cleartext mode
- [ ] `bash .agents/validate.sh` green
@@ -1,84 +0,0 @@
# Task 03 — `NUT_TLS*` settings, the registration story, and the no-leak boundary
**Phase:** `15_nut_tls_credentials` · **Source:** `TODO.md:6` — "the exporter should be installed with password protection and that password must be used to register it with the collector"
## Objective
The config surface: `NUT_TLS` (off by default — the current fleet cannot reach upsd
from the laptop at all, and the deployed dashboard reads cleartext today; enabling
is the operator's action per the README walkthrough) and `NUT_TLS_DIR` (the
0600 material tree task 01 lays out). Plus the "registration" half made explicit in
code and docs: the `upsd.users` account password the operator installed on the
exporter is carried in the collector's env (the existing `NUT_USER`/`NUT_PASSWORD`
settings — the `.env.dev` already holds the credentials per known gap 1c) and is
used at `LOGIN` before every `INSTCMD`; TLS changes *how* it travels, never *that*
it is the gate. And invariant 6 extended: a key is a credential, so no key material
may appear in anything that leaves the process (health records, event rows, error
messages, `/api/config`).
## Work
1. `src/dashboards/config.py` — append the block (the phase-11 `DASH_*` block is the
pattern; whichever phase runs second appends on top, never restoring the
other's lines): `NUT_TLS` (the `_env()` truthy rule, same as `NUT_SIMULATE`'s
`from_env()` doctrine — one place the rule lives, the settings object carries
the parsed flag) and `NUT_TLS_DIR` (path, default `~/.config/dashboards/nut-tls`,
expanded). The `Target`/redaction half is unchanged: the TLS settings are not
URLs and carry no userinfo.
- ASSUMPTION: `NUT_TLS_DIR` is a *directory* (the script's layout:
`ca.crt`/`dashboard.crt`/`dashboard.key` inside it), not three path variables —
one knob, one layout, the rotation is a re-run of the script (task 01), and a
mistyped path is a clear `UpstreamError` ("TLS material not found in …"), not
three silent defaults.
2. `src/dashboards/sources/nut.py` — the no-leak boundary (invariant 6, the existing
doctrine's exact pattern: `strip_userinfo`/`Target.display_url` for URL tokens):
- every `UpstreamError` message built in the TLS path is checked against a
**key-free list** — the message may name the redacted dial address, the verb,
and a fixed failure kind (`tls: server certificate not signed by the local
CA`, `tls: client refused the certificate`, `tls: material not found in <dir>`)
— nothing interpolated from the key files themselves (they are opened for the
`SSLContext` and never read into a string; the one place a path may appear is
`NUT_TLS_DIR`'s *path*, which is not secret material);
- the `tests/test_auth.py` device for fixed-text raise/log sites (the structural
pin that fails the day someone interpolates a credential) is extended to the
`nut.py` TLS error-construction sites — the same structural test shape,
applied to the new code.
3. `.env.example` / `.env.dev.example` — the `NUT_TLS*` block (commented, off, the
walkthrough pointer to the README's NUT section); the existing
`NUT_USER`/`NUT_PASSWORD` comments gain the one line that names the registration
story: "this is the account the exporter was installed with — it is used at
`LOGIN` before every `INSTCMD`; with `NUT_TLS=1` it travels over the encrypted
socket, without it the wire is cleartext (the firewall, not the password, is
what keeps strangers off 3051 today — the fleet-facts doctrine: the gate is the
account + the four gates on `POST /api/ups/command`, and `NUT_ALLOW_ACTIONS`
stays the first of them)."
4. `README.md` → *NUT*: the settings table rows + the one-paragraph "how the
privileged action is protected" that states the whole story in operator words:
the account gates the command (the upsd side), the four gates gate the app side
(`NUT_ALLOW_ACTIONS`, the account actually having the command, the UI
confirmation, the on-battery refusal), TLS (when enabled) keeps the password and
the command off the cleartext wire and authenticates both ends by certificate —
and says *why the password remains the gate* (the NUT 2.8.5 constraint, the
phase ASSUMPTION, with the task-01 note as the evidence).
## Testing & Quality
- Unit: `tests/test_config.py` — the `NUT_TLS` truthy rule (the `NUT_SIMULATE`
test's shape), the `NUT_TLS_DIR` default/expansion, and the "unset means
cleartext" default (an app built with no TLS settings never opens a key file —
a `monkeypatch` on the file reads asserts zero opens in the default suite).
- Unit: the structural no-leak pin (the `test_auth.py` device, task-02's error
sites) — a regression that interpolates a key file's *contents* into a message
fails the test, not a log line.
- Coverage: **>90%** on new/modified code; the 94% gate must hold.
## Completion Criteria
- [ ] `NUT_TLS` unset (the default in every pre-existing test and in both env
templates): no key file is opened, no `STARTTLS` is sent — the pre-existing
suite is byte-identical in behaviour
- [ ] `NUT_TLS=1` + the task-01 layout: the settings resolve to the three files;
a missing file is a clear `UpstreamError` naming the directory, nothing more
- [ ] the env templates + README carry the registration story and the NUT 2.8.5
"why the password remains the gate" paragraph
- [ ] `bash .agents/validate.sh` green
@@ -1,85 +0,0 @@
# Task 04 — The loopback-TLS suite, the no-leak pin, and the read-only live probe
**Phase:** `15_nut_tls_credentials` · **Source:** `TODO.md:6` — "For privileged actions (UPS commands) the exporter should be installed with password protection…" (the whole phase, proven)
## Objective
The proof layer: a hermetic loopback TLS fake upsd (stdlib `ssl`, the task-01
script's certificates in a temp dir — never tracked), the `STARTTLS` framing pin
that fails the day the server-side verb stops being answered (the exact device
`test_login_uses_the_verbs_upsd_2_8_5_answers_to` uses for the `USER`/`PASS`
regression), the wrong-CA rejection, the client-certificate presentation, the
`LOGIN` gate **over TLS**, the no-leak pin for key material, and the read-only
`live` probe of the deployed upsd's `STARTTLS` answer. No `INSTCMD` from any test
(a battery test on hardware something is plugged into is not a test — the
AGENTS.md live rule).
## Work
1. `tests/test_nut.py` — the fake upsd's TLS half (the existing fake's scripted-
reply doctrine, one socket upgrade away):
- a loopback `ssl.SSLContext` server (the task-01 `upsd.crt`/`upsd.key`,
`CERT_REQUIRED` + `load_verify_locations(dashboard.crt)` so it **demands** the
client certificate) answering the same one-line-per-command script: `STARTTLS`
→ OK, then the post-upgrade verbs;
- `test_starttls_upgrade_framing` — the client sends `STARTTLS` exactly once,
before the `LOGIN`, and the post-upgrade `LIST`/`GET` answers are parsed
identically to the cleartext suite (the `UpsReading` contract,
`tests/test_ups_contract.py`, holds over TLS);
- `test_client_certificate_is_presented` — the server context's
`CERT_REQUIRED` passes only because the client presented `dashboard.crt`
(the test asserts the server saw a verified peer cert, not just a completed
handshake);
- `test_unverified_server_is_refused` — a second loopback server with a
self-signed certificate the local CA did not issue → `UpstreamError`, the
message redacted and key-free (asserted against the no-leak list), the
connection closed, no `LOGIN` sent (the password never travelled);
- `test_login_gate_over_tls` — a wrong `PASSWORD` over the upgraded socket
still answers the upsd's `ERR` exactly as the cleartext suite pins it (the
account gate is TLS-agnostic — the phase ASSUMPTION's core, proven).
2. `tests/test_collectors.py` — the poller half (invariant 4's exact doctrine, the
existing `store-error`/stale device): a `NutMonitor` whose loopback server
dies mid-session (the TLS handshake succeeds, then the socket closes) → the
source is `DOWN` with the real reason, the last good payload kept, one
transition event, no exception ends the task; a TLS-missing-material start
(task 03's `NUT_TLS_DIR` with no files) → the same `DOWN` path with the clear
reason, **without dialling** (invariant 3's shape: a source that cannot even
build its material does not pretend to be configured-and-dead — the event says
the material is missing, once, not per poll).
3. `tests/test_api.py` — the no-leak pin at the boundary that leaves the process:
with a TLS failure injected, `/api/health`'s job record, the `event` table row
and `/api/events`' answer contain none of the key material (the temp-dir
`dashboard.key`'s contents, the `ca.key`'s contents, and any PEM marker are all
absent) — the same test shape that pins the URL-token redaction, applied to
key material.
4. `tests/test_live_fleet.py` — the `live` probe (skipped unless `--live` and
`NUT_TLS`-capable material exists; **read-only**): the deployed upsd's
`STARTTLS` answer — `HELLO`-style: open a connection, send `STARTTLS`, record
the one-line answer (OK or the 2.8.5 build's refusal — both are findings for
`data/logs/NOTES-upsd-tls.md`'s "what the live server does" line, the task-01
note's running update), **no `LOGIN` and no `INSTCMD`** (the account password
is not a test input; the known-gap-1 doctrine: a live test never writes).
## Testing & Quality
- Unit: per the work list — framing, presentation, refusal, the gate over TLS,
the poller's `DOWN` path, the no-leak pin.
- Live: the read-only probe, its answer recorded (the tally in the completion
note, the known transport gap acknowledged: from the laptop the probe skips on
the 3051 unreachability, per the fleet facts).
- Coverage: **>90%** on new/modified code; the 94% gate must hold.
## Completion Criteria
- [ ] `uv run pytest tests/test_nut.py tests/test_collectors.py tests/test_api.py`
green — the whole task-02 matrix (framing/presentation/refusal/gate) plus
the poller `DOWN` path plus the no-leak pin
- [ ] no `INSTCMD` string in any `live` test (the suite's read-only rule, greppable
and pinned by the `test_live_fleet.py` convention the existing live tests
follow)
- [ ] `uv run pytest -m live --live` run (skips recorded for the unreachable
paths); the `STARTTLS` live answer recorded in `data/logs/NOTES-upsd-tls.md`
when reachable
- [ ] `bash .agents/validate.sh` green — the phase's final pass: the whole suite
100% (the phase-11 completion doctrine: "Complete" only when the entire
project test suite passes, including the opt-in live run)
+2
View File
@@ -1 +1,3 @@
# TODO
- [ ] For privileged actions (UPS commands) the exporter should be installed with password protection and that password must be used to register it with the collector. Effectively the exporters become protected web servers. Additionally, the collector should generate and manage a root certificate that needs to be installed with each exporter. In fact, just use the certs as auth rather than a password.