GL-4: uninstaller gap-closure + operator-key day-0 fold (host-install v1.11.0)

Uninstall: NEW 4b4 removes the self-update artifacts (guarded wrapper,
.prev/.new.* A/B slots, rollback unit, limits drop-in — derived from
configs/felhom-selfupdate-guarded); enrolled drives under /mnt/felhom-drives
unmounted plain-umount-only (busy = warn+guidance, never -l/-f; root bind
guarded); both modes end with a KEPT-vs-WIPED statement (drives/PBS/hub
record/escrow/vaulted recovery credential live on); guest-only mode prints
the vmid's bind-store drives.

Key-pin: OPERATOR_KEY_* constants (empty until the operator ceremony) +
--operator-pubkey-file (argv-validated; comment=key_id required; file
overrides constants) -> authz.signers per the agent SignerKey schema;
no-keys-resolved KEEPS preserved signers; verify reports armed/dormant WARN.

Harness: +13 static cases + GL4-D parity + GL4-INV + PVE-tier GL4 H-U
(full-uninstall dry transcript vs live 9201). 28/28 on felhom-pve;
red-proofs RP-1..3 run->fail->revert. GO-LIVE-PACKAGE.md ABSENT AGAIN ->
status in CONTEXT.md. Live teardown/armed-pin = GL-6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-08 08:14:07 +02:00
parent a63cc7151b
commit f7cc6a720e
7 changed files with 484 additions and 52 deletions
+64 -44
View File
@@ -2,59 +2,79 @@
> **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md); the scripts history lives in [scripts/CHANGELOG.md](scripts/CHANGELOG.md).
## RUNBOOK GL-1publish agent 0.74.0 + bake/publish golden 0.103.0 (2026-07-07)
## TASK GL-4uninstaller gap-closure + operator-key day-0 fold — host-install v1.11.0 (2026-07-08)
Full execution record with per-gate evidence:
`documentation/pilot/RUNBOOK-GL1-publish-2026-07-07.md`. Docs-only commit; no code changed, no
production host mutated (felhom-pve read-only; all root work inside the disposable drill VM).
**Baseline confirmed:** felhom.eu @ `a63cc715`, `SCRIPT_VERSION="1.10.0"`**`1.11.0`**;
felhom-agent reference @ `4c408467` (read-only — authz schema from `internal/config/config.go`
SignerKey, self-update paths from `configs/felhom-selfupdate-guarded`, drive root from
`internal/storage/{claim,netmount}.go`, bind store `/var/lib/felhom-agent/guest-binds.json` from
`cmd/felhom-agent/main.go:514`). §12 STOP honored: zero live install/uninstall runs anywhere.
### The four operator values (hub Day-0 manifest — Configs → Day-0 artifacts)
**Part 0 — GO-LIVE-PACKAGE.md was ABSENT AGAIN.** The spec said the operator attaches it; it is
not in the repo, not anywhere under `E:\git`. Per the spec's own fallback: G6/G1 status recorded
in CONTEXT.md, no Commit 1. Third task in a row without the doc — flagging it loudly.
```
AGENT_VERSION=0.74.0
AGENT_SHA256=1ec3f58842edce1e9e32d022eaef15b7ff599c658c7a36bfe7833c708076af05
GOLDEN_VERSION=0.103.0
GOLDEN_SHA256=8481e8a14e2aa0abe3831cc40e5aea4c32f2017e7561c15dca8a674df6a6026e
```
### Files modified
### Gate outcomes (all PASS)
- `scripts/felhom-host-install.sh` → v1.11.0 (Parts 13; feature detail in scripts/CHANGELOG.md)
- `scripts/hostinstall-mode-harness.sh` — +13 static cases + PVE-tier GL4 H-U (extended, not forked)
- `documentation/runbooks/day0-install.md` — §C.5b key-pin section; Part E teardown additions
- `scripts/CHANGELOG.md`, `REUSE.md` (parity-pattern row), `CONTEXT.md`, this file
- **0a0d**: baselines exact (`ced60ddf` / `59eb3bea` / `02d63ed0`; agent 0.63.0 + golden 0.98.3
the last published, targets 404); drill VM + `virgin` snapshot intact; controller image
0.103.0 resolvable; headroom ample.
- **1a/1b**: felhom-pve's RUNNING binary (`readlink /proc/…/exe`, no pending A/B marker) is
0.74.0; those exact bytes (hash-stable across both copy hops) published via
`publish-agent.sh` from `main`@`ced60ddf` — PUT 201, round-trip GET sha match.
- **2a/2b**: clean-room bake on the virgin-restored drill VM (build-golden.sh v2.0.0):
overlay2 + both split-volumes as separate mounts, rootfs+mp0+mp1 ALL included in the vzdump
(no exclusions, no FATAL), archive 583MB, publish HTTP 201; transcript token-leak grep = 0.
Deviations: debian-13 template pveam-downloaded inside (virgin never has it, checksum
verified); bake launched via `systemd-run` (bare nohup didn't survive ssh session close).
- **3a/3b**: both artifacts fetched **ANONYMOUSLY** and hashed to exactly the published sha
(golden 611 696 321 bytes, streamed+hashed+deleted); off-LAN HEAD 200 from the CC machine too.
### Per-scenario results
### Gate 3c finding (feeds G3) — read-credential sufficiency
| Scenario | Result | How |
|---|---|---|
| A — full uninstall dry covers everything | **PASS (live dry)** | GL4 H-U on felhom-pve vs the real guest 9201: selfupdate-artifact removals + kept-vs-wiped statement present, no `umount -l/-f`, no destructive op on any `/mnt/felhom-drives/` path (no drives currently mounted there → per-mount umount lines correctly conditional) |
| B — guest-only | **static** | branch adds `_guest_drive_note` (bind-store best-effort, generic fallback) + guest-scoped statement; not transcript-runnable on felhom-pve (no second Felhom guest) — GL-6 |
| C1 dormant WARN | grep-shape PASS (GL4-C1) + verify code path | |
| C2 keys written | **runtime PASS** (GL4-C2: valid file passes resolution, dies later at preflight, never at key parse) + write-shape in GL4-C4 | |
| C3 malformed file | **runtime PASS ×5** (unknown role / non-key line / missing key_id comment / empty file / missing file — each dies at argv naming the line) | |
| C4 preserve rule | grep-shape PASS (`if signers:` guard + pin-rotation notice) + red-proof RP-2 | |
| C5 file overrides constants | grep-shape PASS (notice + constant reset) — not runtime-testable while the shipped constants are empty (deliberate); the notice fires only when both sources are set | |
| D parity | **PASS** (curated token list over the uninstall section; every disclosure artifact covered by a removal or an explicit KEPT line) | |
The specified reviewer read-only token (`8417…4140`) was not locatable by CC anywhere in reach —
the gates ran with **no credential at all and passed**: the generic packages are anonymously
world-readable. Therefore rotating the customer `git.token` to read-only **cannot break** the
artifact fetch path (G3 unblocked from this side). Nuance: `resolve_git_creds` in host-install
**dies on an empty token** even though Gitea would serve the fetch anonymously — the requirement
is script-side; customer configs still need a (read-scoped) token until a small installer
follow-up makes it optional. This also reframes GL-2's finding that demo-felhom's git credentials
are EMPTY: the fetch would work, the installer's precondition is what fails.
**Red-proofs (run→fail→revert on scratch copies; repo file never mutated):**
RP-1 dropped the 4b4 block → GL4-D FAILED (24/28). RP-2 made the signers write unconditional →
GL4-C4 FAILED. RP-3 dropped the unknown-role die → GL4-C3a FAILED. All reverted (scratch deleted).
### Drill-VM end state
**Gates:** `bash -n` clean; shellcheck 0.10.0 `--severity=warning` clean on both scripts (the two
pre-existing SC2015 infos on untouched v1.9.1 lines remain triaged); harness **25/25 static
locally, 28/28 on felhom-pve** (H-A/H-B still cred-gated by demo-felhom's empty git credentials —
unchanged since GL-2). GL-2's Scenario-A contract re-verified: all v1.10.0 cases still pass.
Guest 9100 destroyed (`--purge`), in-VM cred file shredded, VM powered off, qcow2 **restored to
`virgin`** (verified) — environment exactly as found. Evidence kept: `~/drill/bake-0.103.0.log`
on the build server.
### Implementation notes / judgment calls
1. **4b4 inventory** (from the guarded script, not spec memory): wrapper +
`felhom-agent.prev` + `felhom-agent.new.*` + `felhom-agent-rollback.service` + the
`felhom-agent-limits.conf` drop-in (+dir). `pending.json` + the staging dir live under
`$AGENT_STATE_DIR` — already removed in step 4 (noted in the block comment).
2. **Root-bind guard**: with a busy child mount, v1.10.0's `run umount /mnt/felhom-drives` would
have DIED mid-teardown (set -e); the root umount is now skipped with a warn when children stayed
busy — the statement lists them as "retry".
3. The statement's conditional lines: PBS (any `pbs`-type storage present), recovery credential
(`_state_has break_glass`, snapshotted BEFORE the state file is deleted); hub record + escrow
always printed (escrow phrased "if one exists" — there is no cheap local detector).
4. Key-file validation happens at argv time (before the passphrase prompt) so all C3 cases run on
any machine; options-prefixed authorized_keys lines (e.g. `command="…"`) are rejected as
"bad key type" — the pin format is deliberately plain `<type> <b64> <key_id>`.
5. H-U initially FAILED on felhom-pve because the naive assertion flagged the legitimate
`rm -f /usr/local/sbin/felhom-mkfs-guarded` line as "contains mkfs" — fixed to Scenario A's
real invariant (destructive ops on `/mnt/felhom-drives/` paths only).
### NOT live-validated — awaiting supervised GL-6
- The real (non-dry) full uninstall: drive umounts incl. a genuinely busy mount, the statement on
a real teardown, residue re-diff at v1.11.0.
- Guest-only mode on a multi-guest host (Scenario B transcript).
- An armed key-pin install end-to-end (needs the operator's real keys — the pin CEREMONY, incl.
pinning felhom-pve, is the operator's; the constants ship empty).
- C7-class verify-drift firing.
### Follow-ups
1. **OPERATOR — hub Day-0 manifest bump** to the four values above (until then fresh installs
land agent 0.63.0 + golden 0.98.3).
2. **Operator-key pin** (out of GL-1 scope): day-0 ships no operator pubkey → self-update dormant
on fresh boxes; pin the real key + a small host-install follow-up (candidate: fold into GL-4).
3. **G3 rotation** unblocked per Gate 3c; also fixes demo-felhom's empty git credentials.
4. **GL-6** consumes the bumped manifest end-to-end (deliberately not run here).
- **OPERATOR:** the key ceremony (offline keypairs → fill `OPERATOR_KEY_*` or keep a pubkey file);
the GL-1 manifest bump is still pending too (agent 0.74.0 / golden 0.103.0).
- **OPERATOR:** actually attach/commit GO-LIVE-PACKAGE.md (absent for the third task running).
- Observation: `install-v191.sh` (v1.9.1) is what `~/drill` still carries; GL-6 should fetch the
served v1.11.0 from felhom.eu (git-sync auto-deploys this push in ~12 min).