Files
felhom.eu/REPORT-r204-item4.md
T
admin 4faebe2926
gates / gates (push) Successful in 8s
docs: R-204 ALL FOUR items closed; R-193 credential half; R-192 by replacement; R-212 filed
- OPEN-ITEMS: R-204 all four CLOSED with both 2026-08-05 rulings recorded (the
  declared-state trigger and its four-meanings-of-absence reasoning; the recovery
  preview's dashboard-password exposure accepted as metadata, not content).
  R-193's credential half CLOSED, screen + deletion still open. R-192 CLOSED by
  REPLACEMENT. R-202 untouched.
- R-212 MINTED (R-211 was the highest, grepped): the orphaned-ciphertext deletion
  HALTED at its STOP because the measured paths do not match the register — three
  set-aside stores totalling ~1.45 GB, and the thing that is exactly 1.2 GB is
  demo-felhom's LIVE repo. Nothing was deleted.
- capability map: all four interventions closed; the row KEEPS a qualifier for a
  new reason — no step needs an operator, but there is no customer-facing recovery
  screen, and the journey has not been re-walked end to end.
- 07-backup-architecture 7.0: the four-step table updated; the declaration-vs-
  inference reasoning and the credential-automatic/key-customer-present split.
- CONTEXT: standing ruling S-33.
- STATUS: the headline change and the deletion STOP.
- REPORT-r204-item4.md rather than REPORT.md: a parallel session is active in this
  shared clone.
2026-08-05 11:06:41 +02:00

198 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REPORT — R-204 item 4 / R-193 credential half (hub v0.96.0), 2026-08-05
**A rebuilt box asks for its credential back, and the hub answers.** The last of the four manual
interventions the 2026-08-04 drill needed. Controller half: `felhom-controller` v0.199.0.
> **Written as `REPORT-r204-item4.md`, not `REPORT.md`.** A PARALLEL SESSION is active in this shared
> clone — it committed `c917251` (the R-205…R-211 disk containment) between my baseline read and my
> first commit. Per `CLAUDE.md`'s parallel-session rule the second session never touches the shared
> `REPORT.md`. Explicit per-file staging was used throughout; verified after the fact that none of my
> three commits carries a foreign file.
## 1. Baselines, re-read on arrival — with a drift
| Repo | Expected | Found |
|---|---|---|
| `felhom.eu` | `0dbd954fec90` / hub v0.95.0 | **DRIFTED to `ee9d9bf`** — one docs-only commit ahead (R-205…R-211 spike output). Hub code, `manifests/hub.yaml` and the deployed image were all still v0.95.0, so the drift did not affect the work. A second foreign docs commit (`c917251`) landed mid-session. |
| `felhom-controller` | `68f195676b91` / v0.198.0 | exact match, tree clean |
| `felhom-agent` | v0.125.0 | `3f5f61b`, clean (rider only, **no version bump**) |
| `app-catalog-felhom.eu` | n/a | `122bbee`, clean (rider only, **no version bump**) |
**The task's "highest register ID in use: R-204" was STALE.** Grepped before minting, as instructed:
the highest is **R-211**. This session mints **R-212** (§8 below).
## 2. Part 2.0 FIRST — does the stored value survive a consume? **YES.**
Established from the schema and the code, deliberately **not** from the PBS analogy:
- `one_time_secrets` declares `value TEXT NOT NULL` (store.go, the CREATE TABLE).
- `ConsumeOneTimeSecret` runs `UPDATE one_time_secrets SET consumed_at = datetime('now')` — **it
touches nothing else.** The value column is never cleared or overwritten.
**So restage-before-mint is possible**, and `RestageOneTimeSecret` mirrors `RestageHostPBSSecret`:
clear the consumed flag, never touch the value, never bump a generation, return false when no row
exists so the caller escalates.
This is asserted rather than assumed by `TestRestageOneTimeSecret_ReArmsTheSameValue`, which checks
the **same** value comes back — so a future hardening that cleared the column fails loudly instead of
silently turning every rebuild into an external mint. The two secrets are different objects with
different lifecycles, and assuming a shared shape is how two earlier sessions confused the credentials.
## 3. The declared state — name, shape, and how a pre-upgrade hub treats it
**Name:** `needs_credential`, on the report's existing `offsite` object (not a new top-level field).
Constants: `backup.OffsiteStateNeedsCredential` / `offsiteheal.StateNeedsCredential`, pinned to each
other by `TestDeclaredStateStringMatchesTheReconciler`.
**Shape, as received by the hub live** (report id 16743):
```json
{"enabled": false, "escrow_state": "", "state": "needs_credential",
"snapshot_count": 0, "repo_size_bytes": 0, "quota_gb": 0}
```
**A pre-upgrade hub treats it as INERT, established from the readers' code rather than assumed:**
| Reader | Behaviour on the declaration | Why |
|---|---|---|
| `OffsiteChecker.isStale` | no staleness alarm | returns early on `!off.Enabled` |
| `OffsiteChecker.fillBand` | `bandOK` | returns OK on a zero quota/size |
| `encoding/json` | ignores `state` | unknown field, not an error |
| **`reportHasOffsite`** | **would have MISREAD it** | see below — the one reader that needed changing |
**A configured box's report JSON is byte-identical to v0.198.0's**`state` is `omitempty` and is
never set on a configured box (`TestOffsiteDeclare_ConfiguredBoxJSONIsUnchanged`).
## 4. The debounce: TWO distinct reports, derived not chosen
The controller reports every ~15 minutes. Two distinct declarations mean the state survived a full
report cycle, and a restart, a slow first report or a transient config read all resolve well inside
one. **One** would act on a blip; **three** would leave a genuinely stranded customer waiting ~45
minutes for the one thing they cannot obtain any other way. The reconciler's own sweep is 5 minutes —
deliberately faster than the report cadence so it adds no latency of its own — and the debounce counts
**fresh evidence, not ticks**, so a fast sweep cannot shorten it.
## 5. §8.4 — is there a deliberately-unhealed state here? **Yes, and it is excluded upstream.**
`pbsdrheal` refuses to heal `verify_failed` because re-staging would not help it and it must stay
loud. The off-site analogue is the **regressed** shape: a box that HAD a working tier and lost its
target while still holding its repository password. Re-arming a credential would not help it either.
**It cannot reach this reconciler at all** — the controller's declaration predicate requires the
repository password to be **absent**, so a box that still holds one never declares. The unhealable
case is excluded *by construction*, upstream, rather than filtered out in a switch. Stated explicitly
because "there is nothing like that here" is usually wrong, and this was checked rather than assumed.
## 6. Files modified
| File | Change |
|---|---|
| `hub/internal/store/store.go` | **new** `RestageOneTimeSecret`, **new** `LatestReportOffsiteDeclaration`; `reportHasOffsite` tightened to require `enabled:true` (+ its comment corrected) |
| `hub/internal/offsiteheal/reconciler.go` | **new package** — the reconciler |
| `hub/internal/offsiteheal/{reconciler,wiring}_test.go` | **new** — Scenarios AF + the wiring AST test |
| `hub/internal/store/offsite_restage_test.go` | **new** — the value-survives proof + the `reportHasOffsite` equivalence table |
| `hub/internal/monitor/offsite_delivery.go` | `shapeDeclared` outranks both inferred shapes; `maybeHeal` stands down with a record |
| `hub/internal/monitor/offsite_declared_test.go` | **new** — the demo-hp shape + the cross-package string pin |
| `hub/cmd/hub/main.go` | wires + runs the reconciler (`OFFSITEHEAL_ONLY_CUSTOMER` for a supervised rollout) |
| `.githooks/pre-push` | the workspace-root assertion (rider) |
| `manifests/hub.yaml` | image `0.95.0``0.96.0` |
**Commits on `main`:** `f62a115` (hub half) · `fe1e816` (rider) · `b2462b8` (CHANGELOG) ·
`4114c5f` (manifest). **Deployed:** `felhom-hub:0.96.0`, ArgoCD **Synced/Healthy**, `deploy/hub`
rolled out, startup log carries `offsite credential self-heal reconciler started (interval 5m,
debounce 2 reports)`.
## 7. Tests and red-proofs
Green gate: `go build ./... && go vet ./... && go test ./...` in `hub/`**rc=0**.
`python3 scripts/repo_gates.py --fast`**all five gates OK**.
| Scenario | Test | Result | Red-proof — what was mutated | Outcome |
|---|---|---|---|---|
| A+D | `TestScenarioAD_DeclaringBoxIsRestagedNotMinted` | PASS | inverted `heal()` so Reissue runs before Restage | **FAILED***"a sustained declaration was not served: restages=0"* |
| B | `TestScenarioB_SilentBoxIsNeverActedOn` | PASS | — | — |
| C | `TestScenarioC_HealthyBoxIsAPureNoOp` | PASS | — | — |
| E | `TestScenarioE_NothingToRestageEscalatesOnce` | PASS | — | — |
| F | `TestScenarioF_BlipIsAbsorbedByTheDebounce` | PASS | `debounceReportsDefault` 2 → 1 | **FAILED***"a one-report blip triggered a credential action"* (and Scenario A+D also failed, *"acted on a SINGLE declaration"*) |
| H | `TestMainWiresTheOffsiteHealReconciler` | PASS | (AST; comments dropped) | — |
| — | `TestRestageOneTimeSecret_ReArmsTheSameValue` | PASS | — | — |
| — | `TestReportHasOffsite_EnabledOnly` | PASS | — | — |
| — | `TestShapeOf_DeclarationOutranksBothInferredShapes` | PASS | — | — |
| — | `TestDisabledDescriptorIsNeverHealed`, `TestBlockedCustomerIsNeverHealed`, `TestRestageErrorDoesNotEscalate` | PASS | — | — |
**A test that first passed for the wrong reason, caught and fixed.** `TestBlockedCustomerIsNeverHealed`
initially seeded `Status: "blocked"` through `SaveCustomerConfig`, whose INSERT does not carry the
column — so the customer was never actually blocked and the assertion would have been vacuous. It now
goes through `SetCustomerConfigStatus` **and asserts `IsCustomerBlocked` before proceeding**.
## 8. Part 4 — HALTED AT ITS STOP. Nothing was deleted.
§8.7: *"If the paths do not match R-193's record exactly, STOP. A near-match on a protected endpoint
is not a match."* **They do not match.**
Measured read-only over SFTP, using each box's own credential, from inside its guest:
| Customer | Path | Size | What it is |
|---|---|---|---|
| demo-felhom (`u629488-sub1`) | `/home/felhom-repo` | **1.2 G** | **LIVE** — the configured `repo_path`. Unopenable by the box (R-193), but NOT a set-aside store |
| demo-felhom | `/home/felhom-repo.orphaned-20260717` | **1.4 G** | set aside |
| demo-felhom | `/home/felhom-repo.orphaned-20260718` | **3.0 M** | set aside |
| demo-hp (`u629488-sub3`) | `/home/felhom-repo` | **582 K** | **LIVE** |
| demo-hp | `/home/felhom-repo.orphaned-20260804` | **43 M** | set aside |
The ruling says *"~1.2 GB across the two demo boxes, in set-aside stores"*. Reality: **three** set-aside
stores totalling **~1.45 GB** — and **the figure that matches ~1.2 GB is demo-felhom's LIVE
`felhom-repo`**. Had the size been used to identify the target, the live repository would have been
deleted. **Filed as R-212, WAITING-ON-OPERATOR.**
## 9. Live validation
| # | What | Observable |
|---|---|---|
| 1 | **A healthy box: no action, no events** | demo-hp reported healthy throughout (`enabled:true`, no `state` key — report id 16742). Zero `offsite_selfheal_*` rows in the hub DB. **Honest limit:** the reconciler is silent by design on a healthy sweep, so there is no per-tick positive observable; what I have is the startup line proving `Run` was entered, the DB showing no events, and Scenario C. |
| 2 | **The declared state, produced live without wiping a box** | demo-felhom 9201 arranged **reversibly** into the stranded shape (settings + `offbox/` backed up first; the `offbox` key removed and `repo_password` moved aside). Report **id=16743** reached the hub carrying `{enabled:false, state:"needs_credential", quota_gb:0, repo_size_bytes:0}`. Restored the same minute; report **id=16744** is healthy again. **The single declaration was absorbed by the debounce — no self-heal event fired** — which is Scenario F demonstrated on live infrastructure rather than in a fake. |
| 2b | **The ACK field is no longer discarded** | Both demo boxes' `settings.json` now carry `hub_escrow_identity_present = true` — the recorder working on a HEALTHY box, which is the case that used to return early. |
| 3 | **A push from outside the workspace is refused** | A scratch clone at `/tmp/.../outside-clone`: `pre-push: PUSH REFUSED - this clone is OUTSIDE the felhom workspace`, naming `/mnt/5_hdd/felhom.eu`, **before the gates run**. **Red-proof:** with the assertion removed the same push **succeeded** (`rc=0`, new branch on a throwaway bare remote). In-workspace pushes ran normally all session. |
| 4 | **Part 4** | **Not performed — halted at the STOP.** Listings above. |
**Not fired live: the hub actually re-staging a credential.** Doing so would have re-applied
demo-felhom's off-site target mid-session and changed the very state Part 4's listing describes. It is
proven by Scenarios A/D/E with the store primitive proven separately against a real SQLite database.
**Teardown:** the scratch clone and throwaway remote are removed; demo-felhom's `settings.json` and
`offbox/` restored from the backup taken first (verified: `enabled=True`, `escrow_state=escrowed`,
`repo_password present=True`); the backup copy remains at `/root/r204-backup` on the guest for
traceability. Nothing else was provisioned.
## 10. Registers
- **R-204 — ALL FOUR ITEMS CLOSED.** Both 2026-08-05 rulings recorded on the row: the declared-state
trigger with its four-meanings-of-absence reasoning, and that the recovery preview's
dashboard-password exposure is **metadata, not content, and accepted**.
- **R-193 — credential half CLOSED.** Still open under this ID: the customer-facing recovery **screen**,
and the ciphertext deletion (now R-212).
- **R-192 — CLOSED, guard half by REPLACEMENT.** The counting inference is outranked by the declaration.
- **R-202 — untouched, still open.**
- **R-212 — NEW** (R-211 was the highest; grepped): the halted deletion, with the full measured listing.
## 11. CI
felhom-agent **0404f60**, app-catalog **ee2c810**, felhom-controller **992803c**, felhom.eu
**4114c5f** — run IDs and conclusions confirmed in the controller's `REPORT.md` §11 and re-checked at
session end. **`--no-verify` was NOT used**; every push ran the pre-push gate, including the new
workspace-root assertion.
## 12. Observations — noticed, NOT acted on
- **A second, accidental barrier exists outside the workspace and should not be relied on:**
`felhom-agent`'s `reuse-refs` gate FAILS in a clone outside the workspace because the shared
`reuse_refs_check.py` lives in the `felhom.eu` sibling. That is why the first Scenario-G red-proof
had to be redone with `app-catalog-felhom.eu`, whose fast gate is self-contained. It is incidental,
repo-specific and not a substitute for the assertion.
- **`offsite_credential_restaged` (the R-71c event) has still never fired for any customer.** The new
reconciler emits its own `offsite_selfheal_*` events, so the old one may now be permanently dead —
worth a deliberate look rather than leaving two event families for one concern.
- **demo-felhom's live `felhom-repo` is 1.2 GB of ciphertext its own box cannot open** (0 snapshots,
`last_status: error`). It is not a set-aside store, so it is out of R-212's scope as filed — but it is
the largest single block of unrecoverable data in the fleet and nothing currently plans its disposal.