diff --git a/CONTEXT.md b/CONTEXT.md index d16b5cb..a57f6e9 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -15,6 +15,96 @@ > would make one of the two audiences stop reading. `STATUS.md` is also a **view of `OPEN-ITEMS.md`** > and holds nothing of its own; this file does hold its own content, namely the standing rulings below. +## A name must differ from its neighbours on a stem, not on a noun (2026-08-13, R-323) + +The third near-homograph is renamed: the five-word phrase that proves an account owns the box being +bound is „**Tulajdonosi jelmondat**". It was „Visszaállító jelszó" — one word from the „Visszaállító +kód" retired days earlier, and false besides: **the phrase restores nothing.** + +**The rule this instance yields, and it generalises past this secret.** Both obvious replacements were +rejected, and the reasons are the transferable part: + +- **Do not name a secret after the ACT it is used in, when another secret on the same screen is named + after the same act.** The other factor on that page is the „**Párosító kód**". „Összekötési jelszó" + would have left the two things a customer types in one sitting separated only by *kód*-versus- + *jelszó* — **structurally the very trap being removed**. A name that matches the page's verb feels + right and is the wrong axis. +- **Do not name a secret after a THING that already has a password.** „Fiókjelszó" collides with the + dashboard login, i.e. trades one homograph for a worse one. +- **Separate on the STEM first, the noun second.** Beállító / Helyreállítási / Párosító / **Tulajdonosi** + — four distinct stems; *kód* / *jelmondat* is the second axis, not the first. A pair distinguished + only by its noun is what failed twice. + +**Naming is not function, and the pin says so** (`TestSelfBindPassphrase_StillAcceptedAfterTheRename` +drives the real handler with human-messy spacing). **Nothing printed was stranded** — no customer +document names the phrase; it is delivered out-of-band, so the description in the hint sentence is +what identifies it, which is why that sentence was kept and extended rather than trimmed. + +## A guard is worth what its positive control is worth (2026-08-13, R-324) + +The hub's customer copy came under a guard for the first time — every customer e-mail and both binding +pages, the sentences a customer reads *before* they have seen any box screen. Nothing had ever looked +at them, in either repo. + +**The part worth carrying: the selftest found a defect in the guard itself on its first run.** The +plant→convict→remove→pass control convicted its own step-3 case (a phrase inside a comment) because +the synthetic source was named `` and comment-stripping keys off the `.go` extension. **A +guard that had shipped without a control would have been quietly over-broad**, and the first person to +write an honest explanatory comment would have been convicted by it. + +**And on sharing a word list across two repos:** the vocabulary lives once, in `scripts/`, the same +home both repos already consume gates from. The consumer that could not be edited that evening is +**drift-checked rather than duplicated** — the gate reads the other gate's literal and fails on +divergence. That is a scaffold with a register row (R-325) to remove it, not a design; **two copies of +a word list is the R-299 defect waiting to happen**, and a drift check is the cheapest way to refuse +the copy without making a cross-repo edit at the wrong moment. + +## Suppress an alarm on the machine's own declaration, never on an inference — and check for the second door (2026-08-13, R-321) + +A box whose reporting is deliberately switched off announces it (`health.status = "disabled"`) and +then goes quiet by design. The hub stored that, rendered it, and **alarmed on it anyway**, because the +staleness verdict was computed from report AGE alone. + +**Three things worth carrying:** + +1. **The discriminator must be the box's own last word.** That is what made this a suppression rather + than a guess — and the standing rule is right that suppressing on a guess is worse than the false + alarm it removes. +2. **RECORD the state; do not delete it.** The `blocked` precedent deletes, and `GetState` then + returns `""` — which is not `"down"`, so **`CheckBackupDeadlines` would have gone on alarming from + a second function.** One fix, two doors; R-195's shape returning through the one nobody looked at. + *Ask of any suppression: which OTHER checker reads this state, and what does it see now?* +3. **A clock must restart at the event that ends the suppression**, not at the last observation before + it began. Timing from the pre-disable report would fire an instant stale/down for a quiet period we + requested. Re-entering the new-customer branch gives a first observation instead of a recovery + event. + +**The limit is stated in the code, not hidden:** a box re-enabled that then fails to report keeps +being suppressed, because the hub's view changes only when a report arrives. **That is why the state +is made visible** — an operator who re-enabled a box and still sees `disabled` is being told it has +not come back. + +**And a test lesson:** red-proof 2 revealed that scenario A passed on its state assertion alone even +with the suppression deleted, because it seeded the customer *already* disabled and the new-customer +branch swallows the first observation. **A test that cannot see the alarm it exists to prevent is not +a test** — it now observes the machine healthy first. + +## A number everyone repeats is a claim, and claims get checked (2026-08-13, R-326) + +"The nine grey claims" was repeated across sessions and **exists nowhere**. A session asked to report +on them could not determine which nine and declined to guess; that refusal was correct and is the +finding. + +**Nine is real and answers a different question: it is the count of claims carrying +`verdict: downgraded`** — the ones the 2026-08-09 pass *lowered*. The count of unproven claims is +**32 of 55** (`walked` 23, `partial` 14, `built` 14, `missing` 4), and of those 32 only **6** cite an +evidence document. + +`scripts/unproven.py` makes it a command, wired into the end-of-session checklist. **The generalisable +part: a picture built for a person to read is not a picture anyone can check.** The dataset carried the +statuses all along; what was missing was one command that answers the question the operator actually +asked. Its first run found a stale claim (R-327) — which is the argument for having built it. + ## A fact the boxes send and the hub cannot read is a future false green — and one now has a reader (2026-08-13, R-319) **The first of R-264's twenty-one unconsumed facts is read.** The agent emitted `guest_net` on every diff --git a/REPORT.md b/REPORT.md index ac97310..aab3c2e 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,393 +1,353 @@ -# REPORT — the small debts paid, and one unread fact given a reader (2026-08-13, evening) +# REPORT — the third near-homograph, a machine told to be quiet, and a picture you can query (2026-08-13, late) -**Shipped: hub v0.104.0, live and verified.** Parts A and B complete; **Part C dropped**, named in §12. -No controller change, no agent change, no wire change — **so there was no bake and no approval**, as the -task expected. +**Shipped: hub v0.105.0, deployed and verified.** All three parts complete; **nothing dropped**. +**`demo-hp` was not touched** — see §8. No controller change, no agent change, no wire change, so no +bake and no approval. --- -## 1. A4 FIRST — what the fleet actually is, and the entry corrected +## 1. Part 1 — the enumeration, the name, and the reasoning -**The task's premise for A4 was wrong, and the evidence is not ambiguous.** It asked me to establish -the facts and not accept the framing; doing that reverses the conclusion. Two different records have -been called "the tester", and only one of them carries the risk. +**Enumerated before editing. Five customer-facing sites, all in the hub:** -**Read from the hub's own store** (`hub.db` + `-wal` + `-shm`, copied together — the WAL rule): - -| | `peti-felhom` | `david` → `tester-1` | -|---|---|---| -| customer row | created 2026-02-27 | `david` created 2026-08-01, **deleted today 07:55:49**; `tester-1` created **07:56:47** | -| host row | `peti-felhom-86d37d`, deleted 2026-07-15 08:56:22 (`host_deletions` id 1) | **never, either name** | -| controller reports | **482**, 2026-02-27 → 2026-07-15 08:39:00 | **0** | -| host reports | present until deletion | **0** | -| escrow rows | none | none | -| events | staleness alarms after it went silent | **4 total** — three hub-side `expected_dbdump_missed` false alarms (R-195's subject) and its own deletion | - -**So:** - -- **"The tester" in this project's own vocabulary is Peti** — `documentation/pilot/PETI-tester-agreement.md` - says *"Operator: Viktor. Tester: Peti"* — and the machine is **a real 80-core Proxmox server - belonging to a real person**, running Felhom as a BYO guest. It reported for four and a half months. - `target-selection.md` puts it at **Tier 2 — protected**, *"because there is a real person behind it"*. - **The 3.6 GB with no key, no off-site copy and no local backup is REAL, and the entry keeps its rank.** -- **`tester-1` is a record with no machine** — no host, no escrow, no report, ever; and `david` before - it was the same. **A record with no machine behind it can lose nothing.** - -**Reconciling this with what the operator said** (*"there is no actual tester yet — only a pre-created -customer, now renamed"*): both statements are true of different things. The **pilot programme** never -began — the agreement was drafted 2026-07-09, the onboarding runbook stopped at P1 — and the -pre-created record is `tester-1`. **Meanwhile the hardware and the data have existed the whole time.** -Nothing about the risk changed; only the word that names it. - -**The corrected entry, as shipped** (`STATUS.md`, "Broken, or knowingly incomplete"): - -> - **Peti's machine has no recovery route at all** — see the `PETI` row. **This is a real machine -> belonging to a real person**, not one of ours and not a record: it reported to the hub for four and a -> half months and has been silent since 15 July, when its host record was deleted. There is no key, no -> off-site copy and no local backup. **If that drive fails, everything on it is lost.** First act of the -> visit: copy the ~3.6 GB off before anything is reinstalled — it is currently the only copy in -> existence. Whether it stays parked is your call and is deliberately left open. - -The `PETI` register row carries the full disambiguation with every count measured, and ends -*"Wherever a document says 'the tester's machine', read `peti-felhom`."* - -**What the fleet actually is, in one sentence, now on `STATUS.md`:** the hub holds **five customer -records and three machines** — `demo-felhom` and `demo-hp` (ours, disposable), `drill-r50` (a nested -drill VM, reverted and powered off); `peti-felhom` is a real machine we have not heard from since 15 -July and has no host record; `tester-1` is a record with no machine. - ---- - -## 2. The hub strings as shipped, with bytes confirmed - -Enumerated at `file:line` before editing. All written from explicit bytes and verified as hex — no -non-ASCII crossed a shell chain; the mojibake check (`Ã`/`Å`/`â€`) is clean in every touched file. - -| Where | Was | Is | Hex of the shipped name | -|---|---|---|---| -| `notify/templates.go:216` subject | „Jelszó-visszaállítási kód" | **„Beállító kód a jelszavad visszaállításához"** | `4265c3a16c6cc3ad74c3b3206bc3b364…` | -| `notify/templates.go:221` body | „Visszaállító kód: %s" | **„Beállító kód: %s"** | `4265c3a16c6cc3ad74c3b3206bc3b3643a202573` | -| `notify/templates.go` **new** `reenroll` | *(did not exist)* | subject **„Új beállító kód — újratelepült a szervered"**, body names **„A szerver beállítása"** | `224120737a6572766572206265c3a16c6cc3ad74c3a1736122` | -| `notify/templates.go:72` lockout | „beállító/visszaállító kód próbálkozás" | **„beállító kód próbálkozás"** | `…6265c3a16c6cc3ad74c3b3206bc3b364…` | -| `web/templates/customer_unified.html:476` | „Visszaállító kód küldése" | **„Beállító kód küldése"** | `4265c3a16c6cc3ad74c3b3206bc3b364206bc3bc6c64c3a97365` | - -**The page-naming fix, established at `file:line` rather than assumed.** `ReissueForReenroll` sent the -**reset** mail (`claim/engine.go:181`), which directs the customer to an „Elfelejtett jelszó" page. A -**rebuilt** box has no password, so the controller computes `reset := s.authEnabled()` → false -(`felhom-controller/controller/internal/web/claim.go:279`), renders **„A szerver beállítása"**, and -serves **no login page at all** — the named route is not on their screen. Only the hub can tell the two -situations apart, because the hub chose which call site fired, so the fix is a **new `EmailKind`**, not -a reworded shared template. Same secret, same name, different sentence. - -The re-enrol mail deliberately says **nothing** about apps or backups — -`TestFormatClaimEmail_ReenrollPromisesNothingAboutTheData` pins it. A clean-slate reinstall is -precisely where such a reassurance could be false, and this project has spent four register rows -removing promises it could not see were still true. - -**Live-verified** (endpoint-level, the exact URL the operator UI serves; ClusterIP + Basic auth): -`GET /customers/demo-felhom` → **„Beállító kód küldése" present, „Visszaállító kód" absent**. - -### Had the claim guard ever scanned the hub? **No — never.** - -`retrieval_promise_gate.py` lives in `felhom-controller/controller/scripts/`; its declared surfaces are -that repo's `internal/web/templates` plus **one** Go handler file, added on 2026-08-12 by R-311 on the -express ground that the highest-stakes customer copy *"had never been scanned"*. **The same sentence is -true one repo over** — the hub composes every customer e-mail, i.e. the copy a customer reads *before* -they see any box screen. - -**I scanned it by hand with the gate's own four stems** (`visszaállíthat`, `visszaszerezhet`, -`visszahozhat`, `visszanyit`) across every non-test `.go`/`.html` under `hub/internal/`: **zero -occurrences.** So nothing was hiding, and my new strings contain no stem and need no registration. -**It is a scope gap, not a live defect — filed as R-322 rather than fixed**, because pointing a -controller gate at a sibling repo makes a controller gate fail on a felhom.eu edit, and G-1 already -taught this project what a cross-repo gate costs. *(A recommendation not followed gets its line: I -recommend a hub-side sibling in `repo_gates.py` sharing ONE stem list — two copies of a word list is -how the plural got past the singular in R-299.)* - ---- - -## 3. The runbook correction, and where else the claim lived - -`runbooks/day0-install.md` §C.1 said *"there is no release tag… pushing `scripts/felhom-host-install.sh` -publishes it."* **It has published nothing since R-110 shipped on 2026-08-03.** §C.1 now opens by naming -its own former error and states the three acts that actually publish: - -1. bump `SCRIPT_VERSION` and push to `main` — **publishes nothing**; -2. cut and push the tag `installer-v`; -3. move **BOTH** `--ref=installer-v…` pins in `manifests/webpage.yaml` — the git-sync **sidecar** and the - **init container** (lines 327 and 372) — commit, sync. **The one-pin trap is named**: the running pod - keeps serving until it restarts, and a fresh pod seeded by a stale init container then serves the OLD - script with no error anywhere. - -**How to verify from outside**, since a push, a green sync and a correct-looking manifest are each -consistent with nothing having been published: - -``` -curl -fsS https://felhom.eu/scripts/felhom-host-install.sh | grep -m1 '^SCRIPT_VERSION' -``` - -**Measured while writing it: served `1.28.0`, `main` `1.28.0`, both pins `installer-v1.28.0`** — the -three agreeing is the observation; any one alone is not. - -**Copied elsewhere? Yes, once, and it was hunted.** -`audits/SPIKE-universal-iso-3-2026-07-31.md:184` says the same thing **and cites `day0-install.md` as -its source** — which is how it spread. It was **true on the day it was written**, so the dated finding -is kept verbatim and carries a SUPERSEDED note; falsifying a dated record to tidy it is its own defect. -Two other hits are correct in context: `hostinstall_gates.py:198` states the consequence of the -manifest *losing* its tag, and the 2026-08-12 drill record already names the sentence as false. - ---- - -## 4. The evidence rule, as written and where it lives - -> **Evidence is copied off the machine at the end of the phase that produced it — before any revert, -> snapshot restore or teardown. Not at the end of the session.** -> -> **The mechanism, because a rule without one is a wish:** the last act of a phase that ran on a machine -> is `scp`/`pct pull` of its logs into the evidence directory on DooPlex — the same act that ends the -> phase, not a separate step to remember later. -> -> **When a session notices the evidence is already gone: say so plainly in the report and REPRODUCE it -> independently.** That is the documented expectation, not an improvisation invented under pressure. - -**Four homes**, chosen so a session meets it before the revert rather than after: - -1. `runbooks/workspace-CLAUDE.md` — **standing rule 5**, so it loads in every session, with the - two-incident record in the R-96 rationale comment beneath it. -2. `runbooks/target-selection.md` — its own section, in the doc you read *before* picking a machine to - break. Tier 0 machines are disposable, which is exactly why nothing you need may be left on one. -3. `RUNBOOK-rehearsal-v3.md` — in the standing-rules block, flagged as applying to every phase boundary. -4. `PROMPT-TEMPLATE.md` — a new **report item 8**, so a session must state that it did this. - -**Provenance, both occurrences verified, not recalled:** 2026-08-12 the retained-key drill lost its -Phase A logs to the revert to `virgin` between Phase A and Phase B -(`audits/DRILL-retained-key-2026-08-12.md` §11.5); 2026-08-13 R-316 lost its Part 1 logs, **same box, -same revert, same point** (§9 — *"the same mistake as Tuesday, in the same place"*). Both times the -existing "scp the log OUT first" was applied to the **final** teardown and not the **intermediate** one. -Filed as **R-320**. - -**A side effect worth naming:** that second record lived in `REPORT.md`, which this report overwrites — -**writing tonight's report would have destroyed the record of a destroyed record.** It was preserved to -`audits/REPORT-r316-installer-v1.28.0-2026-08-13.md` first, and both citations now point there. - ---- - -## 5. The three rulings, recorded as decided - -**`STATUS.md`'s "Waiting on you" section is now empty**, and says so: *"Nothing. All three questions -that stood here were answered on 12–13 August and have moved to Decided below."* A new **Decided** -section carries them, and each register row's status field changed from `READY` to `DECIDED`. - -| Ruling | Trigger that reopens it | +| `file:line` | what it is | |---|---| -| **R-312 — recovering an old backup stays a phone call.** Not built, deliberately; the operator path genuinely works (the 2026-08-12 drill restored planted files byte-identical by hand) | **A real need appearing — one request from a customer who is not us.** Until then R-304's honest position stands: retention is operator-only, and nothing may promise the customer can do it | -| **R-313 — the unopenable set-aside store on `demo-felhom` is KEPT, as a test fixture.** The specific advantage the operator accepted: it is the only state in existence where a set-aside store is present and **cannot be opened** — a case that cannot be manufactured without destroying another key on purpose | **Delete it when the work it is a fixture FOR ships, or is abandoned** — i.e. when R-312 is built, or when R-312's ruling is made permanent. On either event, delete deliberately and record why | -| **R-303 — a machine in two kinds of trouble says both things: left as it is.** Real-world likelihood unknown; the tidier fix risks hiding a genuine second failure | **An observation of the combined state occurring OUTSIDE a constructed test.** One sighting on a real machine reopens it | +| `hub/internal/web/selfbind.go:255` | the binding page's lead sentence | +| `hub/internal/web/selfbind.go:256` | the failure banner (said „a jelszót" — generic, but naming the secret) | +| `hub/internal/web/selfbind.go:261` | the field label — „Visszaállító jelszó" | +| `hub/internal/web/selfbind.go:263` | the hint under the field | +| `hub/internal/notify/templates.go:331` | the self-bind e-mail, item 2 | ---- +**No halt. The name appears nowhere in `felhom-controller` or `felhom-agent`** — the only hits there +are comments and a test asserting the *already-retired* „Visszaállító **kód**" is absent, which is a +different secret. Operator surfaces call it *"Retrieval Password"* in English and the installer uses +`FELHOM_RETRIEVAL_PASSPHRASE` as a shell identifier; neither is customer copy and neither was touched. -## 6. B0 — do the facts arrive, and is this hub-only? +### The name: **„Tulajdonosi jelmondat"** -**Yes, and yes.** Both halves established before any code was written. +Checked against the table rather than against a habit. The phrase **proves the account owns the box +being bound — it restores nothing, so the old name was simply false.** -- **On the wire and in the database.** `demo-felhom-8363b5`'s newest `host_reports` row carries - `guest_net.checked_at` plus per-guest `vmid/state/mode/ip/has_route/dhclient_alive/checked_at/message`. - The agent's wire type (`felhom-agent/internal/hub/report.go:139-160`) additionally declares `healed`, - `heal_succeeded`, `last_heal_at`, `heals_last_hour`, `damped` — absent from the live rows only because - they are `omitempty` on a box that has never needed a repair. -- **Read by nothing.** The string `guest_net` occurred **nowhere** in `felhom.eu/hub/` — no struct, no - template, no checker. Stored as raw text inside `report_json`. +**Both of your suggestions are rejected, and the reasons are the argument:** -**So no wire change and no agent change were needed: hub-only.** The halt condition did not fire. +- **„Fiókjelszó" is worse than the trap it fixes.** There *is* an account password — the dashboard + login. This name would collide with a **different real secret**, trading one homograph for a worse + one. +- **„Összekötési jelszó" recreates the trap structurally.** The other factor on this very page and in + the same mail is the **„Párosító kód"** (`selfbind.go:258`, `templates.go:330`). Naming this one + after the same act would leave the two factors a customer types **in one sitting** separated only by + *kód*-versus-*jelszó* — which is precisely the „Visszaállító kód"/„Visszaállító jelszó" shape being + removed. It matches the page's verb, and that is exactly the problem. ---- +**„Tulajdonosi jelmondat" is distinct on BOTH axes:** -## 7. Reclassification counts — measured, not estimated - -The register row said *twenty-one*; **the gate's allowlist held twenty.** Twenty is what the -dispositions account for, exactly: - -| State | Count | Which | +| | stem | noun | |---|---|---| -| **Read** (removed from the allowlist) | **8** | `guest_net` + its seven children | -| **Deliberately not consumed** (new third kind, with the ruling and its date) | **5** | `mgmt_plane.healed_recently`, `pbs_dr.applied_at`, `config_hash`, `stacks`, `storage.migrated_to` | -| **Redundant** (decided on its own merits) | **1** | `reporting_disabled` | -| **Still owed a reader** (R-264, open) | **6** | `selfupdate_pending`, `selfupdate_pending_version`, `restore_tests.mount_parity`, `restore_tests.mount_inventory`, `backup.last_db_dump`, `backup.last_integrity_check` | +| „**Beállító** kód" | Beállító | kód | +| „**Helyreállítási** kód" | Helyreállítási | kód | +| „**Párosító** kód" (the other factor) | Párosító | kód | +| „**Tulajdonosi** jelmondat" | **Tulajdonosi** | **jelmondat** | -**The eight are REMOVED rather than re-labelled, and that is the point:** an allowlisted tag is -*skipped* by the gate, so leaving them would have meant the new reader's own fields were never checked -for reachability at all. +*If a plainer noun is ever wanted, „Tulajdonosi jelszó" is a one-word change — the **stem** is what +carries the separation.* -**A thing the task told me to re-read from the register, which was not there.** The operator's -dispositions were made on 2026-08-12; **R-264 still read `READY — owner Viktor` and all twenty -allowlist entries still said "arguably owed"**. A session told to re-read them from the register would -have found none. They are written down now, which is the point of writing them down. +### As shipped, bytes confirmed -**`reporting_disabled`, decided on its own merits — and the decision found a real defect the flag would -not have fixed.** The product does support the state: the controller sends one minimal report carrying -`reporting_disabled: true` **and** `health.status: "disabled"` (`cmd/controller/main.go:1246-1260`), -then goes quiet by design. The hub **already decodes** `health_status` and **already renders** that -customer as `disabled` (`web/rollup.go:25`) — so the flag is a second spelling of a fact already read: -**redundant**. But `StalenessChecker.Check` (`monitor/staleness.go:88+`) is **age-only**; its sole skip -is `IsCustomerBlocked`. **A deliberately-silent box still goes `node_stale` at 30 minutes and -`node_down` at 60** — exactly the false alarm B0 predicted. **Filed as R-321 and deliberately not fixed -here:** decoding the flag would have felt like progress and left the alarm firing. The fix belongs in -the checker, which already holds the status it needs. +| string | hex | +|---|---| +| `Tulajdonosi jelmondat` | `54756c616a646f6e6f7369206a656c6d6f6e646174` | +| `tulajdonosi jelmondatodat` | `74756c616a646f6e6f7369206a656c6d6f6e6461746f646174` | +| `tulajdonosi jelmondatot` | `74756c616a646f6e6f7369206a656c6d6f6e6461746f74` | +| `Ez igazolja, hogy a fiók a tiéd` | `457a206967617a6f6c6a612c20686f67792061206669c3b36b2061207469c3a964` | + +Mojibake check clean in both files. **Naming only:** the form field is still `name="passphrase"`, and +`TestSelfBindPassphrase_StillAcceptedAfterTheRename` drives the real handler with the same messy human +spacing (`" Alpha Beta gamma-delta epsilon "`) and asserts the appliance still binds. The whole +pre-existing self-bind suite (A, B, C1, C4, D, E, F, mint × 2) stayed green. --- -## 8. The reader as built, and the alarm judgement +## 2. Does any customer-facing document name the old phrase? **No.** -**`hub/internal/web/hosts.go`** — `parseGuestNet` / `guestNet` / `guestNetView`; rendered as a **Guest -network** card on the host-detail page, which is where a person looks at a machine. +Checked: `documentation/pilot/PETI-tester-agreement.md` does not mention the phrase **at all**; +`RUNBOOK-onboarding-draft-v4.md` and `RUNBOOK-byo-deployment.md` name it by its **English operator** +name ("retrieval passphrase"). The ISO/installer references are shell identifiers. -**The signal is the repair count, not the state.** A guest the watchdog keeps repairing is healthy at -every instant anyone looks and is nevertheless failing; rendering `state` alone would give it a green -tick — the exact shape of the failed-disk-drawn-as-a-healthy-empty-disk defect. `heals_last_hour` sits -**beside** the state and drives the summary badge. +**So nothing printed is stranded.** The one honest caveat: the phrase reaches a customer +**out-of-band** — `RUNBOOK-byo-deployment.md:11`, *"delivered to the box owner over a secure channel"* +— so the only stale copy of the old name is whatever was said in a message or on the telephone. A +person holding such a message would see „Tulajdonosi jelmondat" on the page, be told *"az öt szóból +álló kifejezés, amelyet a beállításkor kaptál"*, and be holding exactly that: **the description +identifies the thing even where the name has moved.** That was the reason for keeping the hint +sentence and extending it with *"Ez igazolja, hogy a fiók a tiéd."* -**`heal_succeeded` is decoded too, and that was a deliberate addition mid-build.** My first cut carried -the count and not the outcome — which is **R-260 exactly**: a hub decoder three fields short of the -agent, missing the one that decides the question. Six *failed* repairs is a guest that is down; six -successful ones is a nuisance. +--- -**The four scenarios, and what the card shows for each — all four verified by test, and three of them -observed on the live fleet:** +## 3. Part 1b — the hub guard, and the control that found a bug in itself -| | Rendered | Live sighting | +`scripts/hub_copy_gate.py`, registered as gate 9 in `repo_gates.py`. **Two checks, deliberately +different:** + +1. **Retired names — banned outright**, across **all 95** hub `.go`/`.html` files, no allowlist. A + name a different secret now owns is never correct anywhere. Comments are stripped, because the + register rows and the code comments that record these decisions must quote the retired names. +2. **Retrieval stems — registered, not banned**, in four declared customer surfaces + (`notify/templates.go`, `web/selfbind.go`, `api/handler.go`, `notify/dispatcher.go`). A missing + declared surface is a **FAILURE, never a skip**. The allowlist is **empty, and that is a + measurement**: the hub makes no retrieval promise today. + +### plant → convict → remove → pass + +``` +hub-copy gate SELFTEST + 1. clean tree : 0 conviction(s) OK + 2. planted „visszaállító jelszavadat”: CONVICTED (isszaállító jelsz) OK + 3. same phrase inside a comment : not convicted OK + 4. planting removed : 0 conviction(s) OK +hub-copy gate selftest OK — the guard has been watched catching, ignoring and releasing +``` + +**The control found a defect in its own instrument on the first run.** Step 3 convicted a comment, +because the synthetic source was named `` and comment-stripping keys off the `.go` +extension. **The bug was in the guard, and the control is what found it** — which is the entire +argument for insisting a guard be watched working. + +### One list, not two — and the gap is instrumented rather than hidden + +Both lists live in **`scripts/customer_copy_vocab.py`**, the same shared-gate home +(`reuse_refs_check.py`, `instructions_gate.py`) that both repos already consume without copying. + +**`retrieval_promise_gate.py` has NOT adopted it**, because the end state forbade touching +`felhom-controller` tonight. So rather than ship two copies that drift, **this gate reads the +controller gate's `STEMS` and fails if they disagree** — watched failing: + +``` +DRIFT: the controller gate's STEMS have diverged from customer_copy_vocab.py + controller : ['visszaállíthat', 'visszaszerezhet', 'visszahozhat', 'visszanyit'] + shared : ['visszaállíthat', 'visszaszerezhet', 'visszahozhat'] +HUB-COPY GATE FAILED: the shared vocabulary is no longer shared. +``` + +…and green again when restored. An **absent** sibling clone is **INCONCLUSIVE (exit 2), never a pass** +— the G-1 lesson. **This is a scaffold, not the destination: R-325** is the few-line felhom-controller +change that makes it import the shared list and delete both its literal and this drift check. + +--- + +## 4. Part 2 — the four scenarios, the clock, and the red-proofs + +**Re-established at `file:line`, not taken from the prompt.** The chain end to end: controller +`cmd/controller/main.go:1253` sets `Health.Status = "disabled"` in the final minimal report → hub +`store.go:953-955,968-975` parses `health.status` into `reports.health_status` → +`CustomerSummary.HealthStatus` (`store.go:40`) carries it into `GetCustomers()` → +`web/rollup.go:25` renders `disabled` → **`monitor/staleness.go` ignored it and measured age alone.** + +**The halt condition did not fire:** the discriminator is the box's **own last word**, present in the +data this checker already reads. That is not a guess. + +### It was TWO doors, not one + +Because the state is **recorded** (`StateDisabled`) rather than **deleted** (as the `blocked` +precedent does), `CheckBackupDeadlines` can skip it too. A deleted state returns `""` from `GetState`, +and `""` is not `"down"` — so that check would have gone on e-mailing `expected_backup_missed` every +morning about the same machine. **R-195's shape returning through a second door**, and it is closed +with the first. Recording the state also satisfies the visibility requirement: quiet-on-purpose and +quiet-by-accident no longer look identical. + +### The re-enablement judgement + +**The clock runs from the report the hub can actually see.** For a box that reports on re-enabling, +that report *is* the re-enablement, so the clock starts there — your recommendation, and the mechanism +already had this shape. Timing from the last report *before* the switch-off would fire an instant +stale/down for a quiet period we asked for: a false alarm produced by fixing false alarms. Leaving +`disabled` re-enters the **same branch as a new customer**, so no `node_recovered` fires for an outage +that never happened. `downtimeStart` is cleared **on entry**, so a later genuine outage cannot compute +its duration from a clock that started before the silence was requested. + +**LIMIT, stated rather than hidden:** a box re-enabled that then **fails to report at all** keeps being +suppressed — the hub sees only that final `disabled` report, and its view changes only when a report +arrives. It cannot distinguish that from *still switched off*. **This is exactly why the state is made +visible:** an operator who re-enabled a box and still sees `disabled` is being told it has not come back. + +### Scenarios + +| | outcome | verified | |---|---|---| -| **A** healthy, no repairs | `healthy` badge, address, route, dhclient, `0` repairs | **`demo-felhom-8363b5`** — 9201 healthy, `192.168.0.149` (dhcp), swept `08:45:33Z`; **`demo-hp-bb76ea`** — 9201 healthy, `192.168.0.118`, swept `08:52:35Z` | -| **B** repaired repeatedly | `needs attention`; the **count** rendered as a warn badge with the last repair time; a sentence naming the 1 h 15 m incident | not yet on hardware — neither demo box has needed a repair since the watchdog shipped | -| **C** does not report it | **`unknown`, never healthy** — three absences, three different sentences | **`drill-r50-0a4f9a`** — *"A capable agent sent no guest-network stanza"* → `unknown`. **The unknown branch fired on real data, not only in a test** | -| **D** malformed | `unknown`, and the page returns **200** | covered by test; the decode error is swallowed on purpose so one box's bad field cannot break the page for the fleet | +| **A** deliberately silent for days | no stale, no down, no e-mail; state visibly `disabled` | `TestStaleness_A` — observed healthy **first**, then switched off, then three passes | +| **B** simply stopped reporting | unchanged: stale then down, exactly as today | `TestStaleness_B` — state `down`, events emitted | +| **C** re-enabled, reports promptly | clean transition, **no** recovery event | `TestStaleness_C` — zero events, state `ok` | +| **D** re-enabled then genuinely quiet | stale and down fire normally, timed from re-enablement | `TestStaleness_D` — state `down`, events emitted | -Two extra branches beyond the four: an unrecognised guest `state` renders unknown (the switch is an -allow-list, so adding a state to the agent can never silently paint it green), and an **empty guest -list with a fresh sweep is NOT silence** — the agent's contract says that means "the watchdog ran and -found nothing", which must stay distinguishable from "the watchdog is not wired", the shape the -v0.91.0 inert seam hid behind. +Plus `TestStaleness_DisabledIsAlsoSkippedByTheDeadlineCheck` for the second door. -### The alarm judgement: **no email, deliberately** - -I agree with the task's recommendation and did not add one. The reasoning, since it is a judgement: - -- **The incident was a visibility failure, not a paging failure.** Nobody could *see* the condition for - 1 h 15 m. A card fixes what actually broke. -- **A new alarm on a fleet of two demo machines is untested noise**, on a dispatcher whose severity - contract is exact-match lowercase and whose customer-message allow-list must move with it — a - two-place change that fires against no real population. -- **There is no calibrated threshold to alarm on.** Neither demo box has produced a single repair since - the watchdog shipped, so any number I picked would be invented. **The visible count is what will - supply the threshold** — which is the argument for building the card first and the alarm second. - -**Revisit when a third machine exists, or when a repair count is seen climbing on real hardware.** - ---- - -## 9. Red-proofs — every mutation asserted applied before its run - -Five in total: three mandated for B2, two for A2. Each mutation was **confirmed present by grep before -the test ran**, and confirmed **absent by grep after restoring**. +### Red-proofs — every mutation asserted applied by grep, and reverted after | # | Mutation | Asserted applied | Outcome | |---|---|---|---| -| **1** *(the one that matters)* | The unknown branches in the card's badge chain replaced by the healthy badge | `grep -c 'title="MUTATED">healthy'` → **2** | **RED — both C sub-cases.** A silent machine seen rendering as healthy: *"a silent box must SAY it is unknown"* and *"an old agent's silence must be named as an old agent's silence"* | -| **2** | `RepairCount: 0` in the decoder — the climbing-repairs signal dropped | `hosts.go:385 RepairCount: 0, … // MUTATED` | **RED — B**: *"a guest repaired 6 times in an hour is reported as fine — the whole point of the card"* | -| **3** | `Degraded()` forced `true` — the healthy branch broken | `hosts.go:348 func … { return true /* MUTATED */ }` | **RED — A**: *"a machine that is fine is being alarmed on"* — the guard is reachable in **both** directions | -| **4** | `ReissueForReenroll` routed back to `EmailReset` | `grep` → line 195 reads `EmailReset` | **RED — two tests**: the new `TestReenrollSplit_ChangesTheMailNotTheSecret` **and** the pre-existing `TestReissueForReenroll` | -| **5** | „Visszaállító kód: %s" restored in the reset mail body | `grep -n` → `templates.go:241` | **RED — `TestFormatClaimEmail_OneNamePerSecret`** | +| **1** *(the one that matters)* | suppression made **unconditional** (`if true \|\| …`) | `staleness.go:132` grep | **RED — B**: *"a genuinely silent machine is `"disabled"`, want `"down"` — a real alarm was swallowed"*. **A genuinely dead machine was seen NOT alarming.** C and D also red | +| **2** | suppression **removed** (`if false && …`) | `staleness.go:132` grep | **RED — A**: *"a deliberately-disabled machine emitted `[node_down]` — three days quiet BY REQUEST"*. Today's false alarm, reproduced verbatim | +| **3** | state made **sticky** — remembered instead of re-read from the box, so the clock never leaves the pre-disable report | `staleness.go:132` grep | **RED — D**: *"a once-disabled machine was silenced for ever"*. C also red | -**After every restore the suite is green again**, and `grep -c MUTATED` returns **0** in both touched -files. - -**A positive control that the reader is WIRED and not merely written**, independent of the tests: the -wire-contract gate's checked-tag count rose **182 → 190** and its skipped count fell **88 → 80** as the -eight allowlist entries came out. The gate still passes — meaning those eight tags are now genuinely -reachable in the hub. +**A weakness in my own tests was found by red-proof 2 and fixed.** Scenario A originally seeded the +customer *already* disabled — and the checker's new-customer branch sets the first state without an +event, so **the test passed on its state assertion alone even with the suppression deleted.** It now +observes the machine healthy first, and the same mutation then fails on the **event**. A test that +cannot see the alarm it exists to prevent is not a test. --- -## 10. Part C — **DROPPED**, and one part of it answered anyway +## 5. Part 3 — the real counts, and the number that was wrong -**Dropped, as the task's own drop order specifies.** The honest reason is not only time: **the task -names "nine claims" that are grey, and I could not identify which nine.** The capability map holds -**22** rows marked `IMPLEMENTED`, five `PARTIAL` and four `MISSING`, and nothing in the tree enumerates -a set of nine. Picking nine myself and reporting on them would have produced a confident answer to a -question nobody asked — the A4 failure mode, one section later. +**Yes, a number I have been repeating is wrong.** -**The one I can answer, because the task identified it precisely.** The self-heal claim is the -capability-map row *"Box survives an unattended app or guest-network failure… it is noticed, and where -safe it is repaired"*, and its best document does **not** argue against it. The 1 h 15 m outage -(`audits/INCIDENT-guest-dhclient-killed-2026-07-20.md`) is the row's **origin**, not its evidence: the -evidence is a **deliberate replay of that incident the next day** — `kill -9` at 12:43:18, detected on -process liveness **57 s** later while the lease was still live, healed at 12:45:18 with the incident's -verbatim invocation, and **the tunnel never dropped** (`cloudflared Up 29 hours`). That is a walk, and -the outage was prevented rather than merely observed. **The claim does not need to move down.** +``` +where felhom stands — 55 claims, verified_on 2026-08-09 + walked 23 + partial 14 (6 cite evidence, 8 prose only) + built 14 (0 cite evidence, 14 prose only) + missing 4 (0 cite evidence, 4 prose only) + NOT WALKED: 32 of 55 +``` -**What I would flag instead:** the row's *guest-network* leg was proven on the box and, until today, -**the hub could not see the condition at all** — which is the gap this session closed and which no -capability row expressed. My new row states its own status honestly as **IMPLEMENTED, not -PROVEN-LIVE**, because **no machine has ever been observed with a climbing repair count on this card.** +**"Nine" is real, and it answers a different question: it is the count of claims carrying +`verdict: downgraded`** — the ones the 2026-08-09 verification pass **lowered**. That is "re-judged", +not "unproven". **Yesterday's session was right to refuse to guess**, and this is what it could not +have found without counting. + +The sharper cut is the evidence one: **all 23 walked claims cite an evidence document** (`check_stands.py` +convicts a `walked` claim without one), while of the **32** that are not walked, **only 6 cite evidence +and 26 are prose only**. --- -## 11. Versions, commits, deploy, CI +## 6. The command, and its output + +``` +$ python3 scripts/unproven.py --summary +where felhom stands — 55 claims, verified_on 2026-08-09 + walked 23 + partial 14 (6 cite evidence, 8 prose only) + built 14 (0 cite evidence, 14 prose only) + missing 4 (0 cite evidence, 4 prose only) + NOT WALKED: 32 of 55 +``` + +Without `--summary` it prints every not-walked claim with its id, band, verdict and whether it cites +evidence, grouped `partial → built → missing`. It reads the dataset **only** — opens no evidence, +judges nothing, contacts no machine. An unrecognised status prints with a `⚠ status not known to this +script` marker rather than being silently dropped. + +**Wired into the end-of-session checklist** in `felhom.eu/CLAUDE.md`, with the instruction to say in +the report if a number moved. + +**Its first run found a stale claim** — `claim.code-naming` is still `partial` and its title still +describes the defect R-295 and R-323 have now closed twice over. **I did not move it**, because the +dataset's own header forbids it: *"A status may not be RAISED here — the MAP changes first and this +file follows it."* Filed as **R-327**, with the honest difficulty noted: no customer has typed +„Tulajdonosi jelmondat" yet, so `walked` would be an over-claim. + +**The capability map is deliberately NOT restructured** — recorded in R-326 so it does not read as +forgotten. + +--- + +## 7. Versions, manifest, CI | | | |---|---| -| **Shipped** | **hub v0.104.0** (`gitea.dooplex.hu/admin/felhom-hub:0.104.0`, 25 MB) | +| **Shipped** | **hub v0.105.0** (`gitea.dooplex.hu/admin/felhom-hub:0.105.0`) | | **Unchanged** | controller **0.214.0**, agent **0.129.0**, installer **1.28.0**, golden 0.214.0, floor 0.214.0, MinAgent 0.129.0 | -| **Commits** | `4d6ec7c` the work · `7c97c94` the manifest bump | -| **Manifest** | `manifests/hub.yaml:128` → `felhom-hub:0.104.0`, GitOps only — **no `kubectl set image`** | -| **Deploy** | ArgoCD hard-refresh + sync → `Synced / Healthy`; pod `hub-5c4d8d4b49-864dc` **1/1 Running**; deployment image confirmed `0.104.0` | -| **CI, confirmed by run ID** | run **334** (`7c97c949f6`) **success** · run **333** (`4d6ec7c7bb`) **success** | -| **Gates** | `repo_gates.py --fast` — all **8** OK, and again in the pre-push hook on both pushes. **No `--no-verify`.** | -| **Green gate** | `hub/`: build rc=0, vet rc=0, **test rc=0 across 18 packages**. Run separately from every commit | +| **Commits** | `b03a105` the work · `bbd59f4` the manifest bump | +| **Manifest** | `manifests/hub.yaml:128` → `0.105.0`, GitOps only — **no `kubectl set image`** | +| **Deploy** | ArgoCD hard-refresh + sync → **Synced / Healthy**; pod `hub-5ff87b556b-bvtvt` **1/1 Running**; deployment image confirmed `0.105.0` | +| **CI, by run ID** | run **336** (`b03a105375`) **success** · run **337** (`bbd59f4a44`) **success** | +| **Gates** | `repo_gates.py --fast` — all **9** OK (the new `hub-copy` included), and again in the pre-push hook on both pushes. **No `--no-verify`** | +| **Green gate** | `hub/`: build rc=0, vet rc=0, **test rc=0 across 18 packages**, run separately from every commit | -**Live validation method — stated, as the fence requires:** endpoint-level. I invoked the exact URLs -the operator UI serves (`GET /hosts/`, `GET /customers/`) against the hub's ClusterIP with the -operator Basic-auth credential, so no server logic was skipped — only browser rendering. -`claude-in-chrome` is not available on DooPlex; a click-through remains the operator's. +**Live verification of the running artifact**, ASCII-only patterns against +`/usr/local/bin/felhom-hub` in the running pod: -**Files:** `hub/internal/web/hosts.go`, `hub/internal/web/templates/host_detail_body.html`, -`hub/internal/claim/engine.go`, `hub/internal/notify/templates.go`, `hub/internal/web/configs.go`, -`hub/internal/web/templates/customer_unified.html`, `scripts/wire_contract_gate.py`, plus -`hub/internal/web/hosts_guestnet_test.go` (**new**, 7 tests) and `hub/internal/claim/naming_test.go` -(**new**, 6 tests). Tests **+13**. +``` +Tulajdonosi jelmondat 1 ← the field label +tulajdonosi jelmondatodat 2 ← the page lead + the mail +jelszavadat 0 ← the retired possessive is GONE +``` -**Register:** R-319, R-320, R-321, R-322 minted (ceiling was R-318, grepped first). R-264, R-309, R-312, -R-313, R-303 and the `PETI` row updated. `STATUS.md` and the capability map updated. +The negative control is the point: the old copy is **absent**, not merely accompanied. + +**Method, stated: this proves the deployed artifact carries the new copy — it is not a rendering.** +Rendering the binding page requires minting a self-bind token, which mints a capability URL and sends +an e-mail against a real customer record; with `demo-hp` being re-deployed tonight I would not do that +to any customer row. The *rendering* is covered by `TestSelfBind_ThirdSecretNaming` and +`TestSelfBind_FailureBannerUsesTheSameName`, which drive the real handler through `ServeHTTP`. + +**Part 2 has no live observable tonight, and I will not manufacture one.** No machine is in the +disabled state, and producing one would mean switching a real box's reporting off — precisely the kind +of thing that must not happen to a fleet of two on the evening one of them is being re-deployed. The +fix is proven by four scenarios and three red-proofs and is **latent until a box is actually disabled**. --- -## 12. What was dropped, and observations +## 8. `demo-hp` was not touched -**Dropped — named plainly, nothing silently shortened:** +**No `ssh`, no `pct`, no controller call, no agent call, no hub write of any kind** was issued against +`demo-hp` or its records this session. Everything done here was in the `felhom.eu` repo, the hub image +and the hub deployment. -- **Part C in full**, except the self-heal verdict in §10. Reason in §10: the nine were not identifiable, - and inventing them would have been worse than the gap. +Confirmed read-only, via the hub's own hosts page (`GET /hosts`, HTTP 200): -**Nothing else was dropped.** Part A is complete (A1–A5), Part B is complete (B0, B1, and the one reader -B2 asked for — not four). +``` +demo-hp-bb76ea ONLINE +demo-felhom-8363b5 ONLINE +drill-r50-0a4f9a DOWN (reverted to `virgin`, powered off — expected) +``` + +`demo-hp` is **online and reporting normally**, exactly as it was found. **`felhom-agent` and +`felhom-controller` are byte-unchanged** — neither repo was written to. + +--- + +## 9. Register + +**Ceiling moved R-322 → R-327** (grepped before minting). + +| Row | State | +|---|---| +| **R-323** third near-homograph → „Tulajdonosi jelmondat" | **CLOSED — shipped** | +| **R-324** the hub's customer copy under a guard | **CLOSED — shipped, selftest green** | +| **R-325** controller gate should import the shared vocabulary | **READY (S)** — the drift check is the scaffold | +| **R-326** "what is unproven" made queryable | **CLOSED — shipped** | +| **R-327** the picture still describes a fixed defect | **READY (S)** — map moves first | +| **R-321** a disabled machine alarmed as dead | **CLOSED — both doors** | +| **R-322** the guard had never scanned the hub | **CLOSED by R-324** | + +--- + +## 10. What was dropped, and observations + +**Nothing was dropped.** Part 1, Part 1b, Part 2 and Part 3 are all complete. Part 3 was the +designated first drop and was not needed. ### Observations — noticed, not acted on -- **A "0 bytes" answer was nearly a wrong verdict, twice in one hour.** `demo-hp`'s host page came back - empty from a `kubectl run` curl pod, which reads exactly like "the card does not render". Retrying with - a clean pod name returned **HTTP 200** and a correct card. *An empty listing is not evidence of - emptiness* — the instrument was the fault, and the rule caught it. -- **R-264's count was wrong**: the row says twenty-one facts, the allowlist held twenty. The row is now - written against the measured number. -- **The wire gate does not detect a stale allowlist entry.** Its sibling `retrieval_promise_gate.py` - fails on one; this one silently skips. Had I re-labelled the eight `guest_net` entries instead of - deleting them, the gate would have reported a coverage it did not have and said nothing. Worth the - same treatment. -- **A third secret sits one homograph away from the two that collided.** The hub's self-bind flow calls - the **five-word** retrieval passphrase „**visszaállító jelszó**" (`web/selfbind.go:255,261`; - `notify/templates.go:285`) — *jelszó*, not *kód*, so it is outside R-295's ruling and I left it alone. - But „Visszaállító kód" was just retired for being a near-homograph of „Helyreállítási kód", and this - is a near-homograph of the name that was retired. **Three secrets, three names, and two of them still - begin with the same word.** A ruling for it is the operator's, not mine. -- **`agents.md`-style version drift in project memory**: `instructions_gate` emits 32 version-literal - warnings against `MEMORY.md`. Not a failure and not mine to fix tonight, but it is aimed at the next - model to edit that file — which is worth someone acting on before it is noise nobody reads. +- **I nearly published a false negative about my own deploy.** The first grep of the running binary + used accented patterns through `kubectl exec → sh -c` and returned **0 for every string, including + ones that are certainly present** — which reads exactly like "the rename did not deploy". The + standing rule (*never let an accented pattern gate a conclusion*) is what caught it; the ASCII-only + re-run gave the real answer. **The rule earns its place again, in a chain it was not written for** — + it was written for `ssh → pct exec`, and `kubectl exec` mangles identically. +- **The `blocked` branch does not clear `downtimeStart`.** The new `disabled` branch does. So a + customer that is blocked, later unblocked and later still goes down will compute its downtime from a + clock that started before the block. Small, pre-existing, and not changed under a row about + something else. +- **`gofmt -l internal/` lists 22 pre-existing unformatted files** in the hub, none of them touched by + this session. Worth one cleanup commit by someone, sometime — it makes `gofmt -l` useless as a + check, which is how a real formatting problem would hide. +- **The empty allowlist in the new gate is load-bearing and fragile in one direction.** It is empty + because the hub genuinely makes no retrieval promise. The moment someone adds one legitimately, they + must register it — and the gate's failure message says so, but nobody reads a failure message until + they hit it. +- **`unproven.py` shows 26 of 32 not-walked claims are prose only.** That is not a defect per claim — + a `missing` claim has nothing to cite — but **14 of 14 `built` claims cite no evidence at all**, and + "built" is the status that most invites being read as "done". Worth a look when the capability-map + session happens. diff --git a/documentation/audits/REPORT-small-debts-and-first-reader-2026-08-13.md b/documentation/audits/REPORT-small-debts-and-first-reader-2026-08-13.md new file mode 100644 index 0000000..ac97310 --- /dev/null +++ b/documentation/audits/REPORT-small-debts-and-first-reader-2026-08-13.md @@ -0,0 +1,393 @@ +# REPORT — the small debts paid, and one unread fact given a reader (2026-08-13, evening) + +**Shipped: hub v0.104.0, live and verified.** Parts A and B complete; **Part C dropped**, named in §12. +No controller change, no agent change, no wire change — **so there was no bake and no approval**, as the +task expected. + +--- + +## 1. A4 FIRST — what the fleet actually is, and the entry corrected + +**The task's premise for A4 was wrong, and the evidence is not ambiguous.** It asked me to establish +the facts and not accept the framing; doing that reverses the conclusion. Two different records have +been called "the tester", and only one of them carries the risk. + +**Read from the hub's own store** (`hub.db` + `-wal` + `-shm`, copied together — the WAL rule): + +| | `peti-felhom` | `david` → `tester-1` | +|---|---|---| +| customer row | created 2026-02-27 | `david` created 2026-08-01, **deleted today 07:55:49**; `tester-1` created **07:56:47** | +| host row | `peti-felhom-86d37d`, deleted 2026-07-15 08:56:22 (`host_deletions` id 1) | **never, either name** | +| controller reports | **482**, 2026-02-27 → 2026-07-15 08:39:00 | **0** | +| host reports | present until deletion | **0** | +| escrow rows | none | none | +| events | staleness alarms after it went silent | **4 total** — three hub-side `expected_dbdump_missed` false alarms (R-195's subject) and its own deletion | + +**So:** + +- **"The tester" in this project's own vocabulary is Peti** — `documentation/pilot/PETI-tester-agreement.md` + says *"Operator: Viktor. Tester: Peti"* — and the machine is **a real 80-core Proxmox server + belonging to a real person**, running Felhom as a BYO guest. It reported for four and a half months. + `target-selection.md` puts it at **Tier 2 — protected**, *"because there is a real person behind it"*. + **The 3.6 GB with no key, no off-site copy and no local backup is REAL, and the entry keeps its rank.** +- **`tester-1` is a record with no machine** — no host, no escrow, no report, ever; and `david` before + it was the same. **A record with no machine behind it can lose nothing.** + +**Reconciling this with what the operator said** (*"there is no actual tester yet — only a pre-created +customer, now renamed"*): both statements are true of different things. The **pilot programme** never +began — the agreement was drafted 2026-07-09, the onboarding runbook stopped at P1 — and the +pre-created record is `tester-1`. **Meanwhile the hardware and the data have existed the whole time.** +Nothing about the risk changed; only the word that names it. + +**The corrected entry, as shipped** (`STATUS.md`, "Broken, or knowingly incomplete"): + +> - **Peti's machine has no recovery route at all** — see the `PETI` row. **This is a real machine +> belonging to a real person**, not one of ours and not a record: it reported to the hub for four and a +> half months and has been silent since 15 July, when its host record was deleted. There is no key, no +> off-site copy and no local backup. **If that drive fails, everything on it is lost.** First act of the +> visit: copy the ~3.6 GB off before anything is reinstalled — it is currently the only copy in +> existence. Whether it stays parked is your call and is deliberately left open. + +The `PETI` register row carries the full disambiguation with every count measured, and ends +*"Wherever a document says 'the tester's machine', read `peti-felhom`."* + +**What the fleet actually is, in one sentence, now on `STATUS.md`:** the hub holds **five customer +records and three machines** — `demo-felhom` and `demo-hp` (ours, disposable), `drill-r50` (a nested +drill VM, reverted and powered off); `peti-felhom` is a real machine we have not heard from since 15 +July and has no host record; `tester-1` is a record with no machine. + +--- + +## 2. The hub strings as shipped, with bytes confirmed + +Enumerated at `file:line` before editing. All written from explicit bytes and verified as hex — no +non-ASCII crossed a shell chain; the mojibake check (`Ã`/`Å`/`â€`) is clean in every touched file. + +| Where | Was | Is | Hex of the shipped name | +|---|---|---|---| +| `notify/templates.go:216` subject | „Jelszó-visszaállítási kód" | **„Beállító kód a jelszavad visszaállításához"** | `4265c3a16c6cc3ad74c3b3206bc3b364…` | +| `notify/templates.go:221` body | „Visszaállító kód: %s" | **„Beállító kód: %s"** | `4265c3a16c6cc3ad74c3b3206bc3b3643a202573` | +| `notify/templates.go` **new** `reenroll` | *(did not exist)* | subject **„Új beállító kód — újratelepült a szervered"**, body names **„A szerver beállítása"** | `224120737a6572766572206265c3a16c6cc3ad74c3a1736122` | +| `notify/templates.go:72` lockout | „beállító/visszaállító kód próbálkozás" | **„beállító kód próbálkozás"** | `…6265c3a16c6cc3ad74c3b3206bc3b364…` | +| `web/templates/customer_unified.html:476` | „Visszaállító kód küldése" | **„Beállító kód küldése"** | `4265c3a16c6cc3ad74c3b3206bc3b364206bc3bc6c64c3a97365` | + +**The page-naming fix, established at `file:line` rather than assumed.** `ReissueForReenroll` sent the +**reset** mail (`claim/engine.go:181`), which directs the customer to an „Elfelejtett jelszó" page. A +**rebuilt** box has no password, so the controller computes `reset := s.authEnabled()` → false +(`felhom-controller/controller/internal/web/claim.go:279`), renders **„A szerver beállítása"**, and +serves **no login page at all** — the named route is not on their screen. Only the hub can tell the two +situations apart, because the hub chose which call site fired, so the fix is a **new `EmailKind`**, not +a reworded shared template. Same secret, same name, different sentence. + +The re-enrol mail deliberately says **nothing** about apps or backups — +`TestFormatClaimEmail_ReenrollPromisesNothingAboutTheData` pins it. A clean-slate reinstall is +precisely where such a reassurance could be false, and this project has spent four register rows +removing promises it could not see were still true. + +**Live-verified** (endpoint-level, the exact URL the operator UI serves; ClusterIP + Basic auth): +`GET /customers/demo-felhom` → **„Beállító kód küldése" present, „Visszaállító kód" absent**. + +### Had the claim guard ever scanned the hub? **No — never.** + +`retrieval_promise_gate.py` lives in `felhom-controller/controller/scripts/`; its declared surfaces are +that repo's `internal/web/templates` plus **one** Go handler file, added on 2026-08-12 by R-311 on the +express ground that the highest-stakes customer copy *"had never been scanned"*. **The same sentence is +true one repo over** — the hub composes every customer e-mail, i.e. the copy a customer reads *before* +they see any box screen. + +**I scanned it by hand with the gate's own four stems** (`visszaállíthat`, `visszaszerezhet`, +`visszahozhat`, `visszanyit`) across every non-test `.go`/`.html` under `hub/internal/`: **zero +occurrences.** So nothing was hiding, and my new strings contain no stem and need no registration. +**It is a scope gap, not a live defect — filed as R-322 rather than fixed**, because pointing a +controller gate at a sibling repo makes a controller gate fail on a felhom.eu edit, and G-1 already +taught this project what a cross-repo gate costs. *(A recommendation not followed gets its line: I +recommend a hub-side sibling in `repo_gates.py` sharing ONE stem list — two copies of a word list is +how the plural got past the singular in R-299.)* + +--- + +## 3. The runbook correction, and where else the claim lived + +`runbooks/day0-install.md` §C.1 said *"there is no release tag… pushing `scripts/felhom-host-install.sh` +publishes it."* **It has published nothing since R-110 shipped on 2026-08-03.** §C.1 now opens by naming +its own former error and states the three acts that actually publish: + +1. bump `SCRIPT_VERSION` and push to `main` — **publishes nothing**; +2. cut and push the tag `installer-v`; +3. move **BOTH** `--ref=installer-v…` pins in `manifests/webpage.yaml` — the git-sync **sidecar** and the + **init container** (lines 327 and 372) — commit, sync. **The one-pin trap is named**: the running pod + keeps serving until it restarts, and a fresh pod seeded by a stale init container then serves the OLD + script with no error anywhere. + +**How to verify from outside**, since a push, a green sync and a correct-looking manifest are each +consistent with nothing having been published: + +``` +curl -fsS https://felhom.eu/scripts/felhom-host-install.sh | grep -m1 '^SCRIPT_VERSION' +``` + +**Measured while writing it: served `1.28.0`, `main` `1.28.0`, both pins `installer-v1.28.0`** — the +three agreeing is the observation; any one alone is not. + +**Copied elsewhere? Yes, once, and it was hunted.** +`audits/SPIKE-universal-iso-3-2026-07-31.md:184` says the same thing **and cites `day0-install.md` as +its source** — which is how it spread. It was **true on the day it was written**, so the dated finding +is kept verbatim and carries a SUPERSEDED note; falsifying a dated record to tidy it is its own defect. +Two other hits are correct in context: `hostinstall_gates.py:198` states the consequence of the +manifest *losing* its tag, and the 2026-08-12 drill record already names the sentence as false. + +--- + +## 4. The evidence rule, as written and where it lives + +> **Evidence is copied off the machine at the end of the phase that produced it — before any revert, +> snapshot restore or teardown. Not at the end of the session.** +> +> **The mechanism, because a rule without one is a wish:** the last act of a phase that ran on a machine +> is `scp`/`pct pull` of its logs into the evidence directory on DooPlex — the same act that ends the +> phase, not a separate step to remember later. +> +> **When a session notices the evidence is already gone: say so plainly in the report and REPRODUCE it +> independently.** That is the documented expectation, not an improvisation invented under pressure. + +**Four homes**, chosen so a session meets it before the revert rather than after: + +1. `runbooks/workspace-CLAUDE.md` — **standing rule 5**, so it loads in every session, with the + two-incident record in the R-96 rationale comment beneath it. +2. `runbooks/target-selection.md` — its own section, in the doc you read *before* picking a machine to + break. Tier 0 machines are disposable, which is exactly why nothing you need may be left on one. +3. `RUNBOOK-rehearsal-v3.md` — in the standing-rules block, flagged as applying to every phase boundary. +4. `PROMPT-TEMPLATE.md` — a new **report item 8**, so a session must state that it did this. + +**Provenance, both occurrences verified, not recalled:** 2026-08-12 the retained-key drill lost its +Phase A logs to the revert to `virgin` between Phase A and Phase B +(`audits/DRILL-retained-key-2026-08-12.md` §11.5); 2026-08-13 R-316 lost its Part 1 logs, **same box, +same revert, same point** (§9 — *"the same mistake as Tuesday, in the same place"*). Both times the +existing "scp the log OUT first" was applied to the **final** teardown and not the **intermediate** one. +Filed as **R-320**. + +**A side effect worth naming:** that second record lived in `REPORT.md`, which this report overwrites — +**writing tonight's report would have destroyed the record of a destroyed record.** It was preserved to +`audits/REPORT-r316-installer-v1.28.0-2026-08-13.md` first, and both citations now point there. + +--- + +## 5. The three rulings, recorded as decided + +**`STATUS.md`'s "Waiting on you" section is now empty**, and says so: *"Nothing. All three questions +that stood here were answered on 12–13 August and have moved to Decided below."* A new **Decided** +section carries them, and each register row's status field changed from `READY` to `DECIDED`. + +| Ruling | Trigger that reopens it | +|---|---| +| **R-312 — recovering an old backup stays a phone call.** Not built, deliberately; the operator path genuinely works (the 2026-08-12 drill restored planted files byte-identical by hand) | **A real need appearing — one request from a customer who is not us.** Until then R-304's honest position stands: retention is operator-only, and nothing may promise the customer can do it | +| **R-313 — the unopenable set-aside store on `demo-felhom` is KEPT, as a test fixture.** The specific advantage the operator accepted: it is the only state in existence where a set-aside store is present and **cannot be opened** — a case that cannot be manufactured without destroying another key on purpose | **Delete it when the work it is a fixture FOR ships, or is abandoned** — i.e. when R-312 is built, or when R-312's ruling is made permanent. On either event, delete deliberately and record why | +| **R-303 — a machine in two kinds of trouble says both things: left as it is.** Real-world likelihood unknown; the tidier fix risks hiding a genuine second failure | **An observation of the combined state occurring OUTSIDE a constructed test.** One sighting on a real machine reopens it | + +--- + +## 6. B0 — do the facts arrive, and is this hub-only? + +**Yes, and yes.** Both halves established before any code was written. + +- **On the wire and in the database.** `demo-felhom-8363b5`'s newest `host_reports` row carries + `guest_net.checked_at` plus per-guest `vmid/state/mode/ip/has_route/dhclient_alive/checked_at/message`. + The agent's wire type (`felhom-agent/internal/hub/report.go:139-160`) additionally declares `healed`, + `heal_succeeded`, `last_heal_at`, `heals_last_hour`, `damped` — absent from the live rows only because + they are `omitempty` on a box that has never needed a repair. +- **Read by nothing.** The string `guest_net` occurred **nowhere** in `felhom.eu/hub/` — no struct, no + template, no checker. Stored as raw text inside `report_json`. + +**So no wire change and no agent change were needed: hub-only.** The halt condition did not fire. + +--- + +## 7. Reclassification counts — measured, not estimated + +The register row said *twenty-one*; **the gate's allowlist held twenty.** Twenty is what the +dispositions account for, exactly: + +| State | Count | Which | +|---|---|---| +| **Read** (removed from the allowlist) | **8** | `guest_net` + its seven children | +| **Deliberately not consumed** (new third kind, with the ruling and its date) | **5** | `mgmt_plane.healed_recently`, `pbs_dr.applied_at`, `config_hash`, `stacks`, `storage.migrated_to` | +| **Redundant** (decided on its own merits) | **1** | `reporting_disabled` | +| **Still owed a reader** (R-264, open) | **6** | `selfupdate_pending`, `selfupdate_pending_version`, `restore_tests.mount_parity`, `restore_tests.mount_inventory`, `backup.last_db_dump`, `backup.last_integrity_check` | + +**The eight are REMOVED rather than re-labelled, and that is the point:** an allowlisted tag is +*skipped* by the gate, so leaving them would have meant the new reader's own fields were never checked +for reachability at all. + +**A thing the task told me to re-read from the register, which was not there.** The operator's +dispositions were made on 2026-08-12; **R-264 still read `READY — owner Viktor` and all twenty +allowlist entries still said "arguably owed"**. A session told to re-read them from the register would +have found none. They are written down now, which is the point of writing them down. + +**`reporting_disabled`, decided on its own merits — and the decision found a real defect the flag would +not have fixed.** The product does support the state: the controller sends one minimal report carrying +`reporting_disabled: true` **and** `health.status: "disabled"` (`cmd/controller/main.go:1246-1260`), +then goes quiet by design. The hub **already decodes** `health_status` and **already renders** that +customer as `disabled` (`web/rollup.go:25`) — so the flag is a second spelling of a fact already read: +**redundant**. But `StalenessChecker.Check` (`monitor/staleness.go:88+`) is **age-only**; its sole skip +is `IsCustomerBlocked`. **A deliberately-silent box still goes `node_stale` at 30 minutes and +`node_down` at 60** — exactly the false alarm B0 predicted. **Filed as R-321 and deliberately not fixed +here:** decoding the flag would have felt like progress and left the alarm firing. The fix belongs in +the checker, which already holds the status it needs. + +--- + +## 8. The reader as built, and the alarm judgement + +**`hub/internal/web/hosts.go`** — `parseGuestNet` / `guestNet` / `guestNetView`; rendered as a **Guest +network** card on the host-detail page, which is where a person looks at a machine. + +**The signal is the repair count, not the state.** A guest the watchdog keeps repairing is healthy at +every instant anyone looks and is nevertheless failing; rendering `state` alone would give it a green +tick — the exact shape of the failed-disk-drawn-as-a-healthy-empty-disk defect. `heals_last_hour` sits +**beside** the state and drives the summary badge. + +**`heal_succeeded` is decoded too, and that was a deliberate addition mid-build.** My first cut carried +the count and not the outcome — which is **R-260 exactly**: a hub decoder three fields short of the +agent, missing the one that decides the question. Six *failed* repairs is a guest that is down; six +successful ones is a nuisance. + +**The four scenarios, and what the card shows for each — all four verified by test, and three of them +observed on the live fleet:** + +| | Rendered | Live sighting | +|---|---|---| +| **A** healthy, no repairs | `healthy` badge, address, route, dhclient, `0` repairs | **`demo-felhom-8363b5`** — 9201 healthy, `192.168.0.149` (dhcp), swept `08:45:33Z`; **`demo-hp-bb76ea`** — 9201 healthy, `192.168.0.118`, swept `08:52:35Z` | +| **B** repaired repeatedly | `needs attention`; the **count** rendered as a warn badge with the last repair time; a sentence naming the 1 h 15 m incident | not yet on hardware — neither demo box has needed a repair since the watchdog shipped | +| **C** does not report it | **`unknown`, never healthy** — three absences, three different sentences | **`drill-r50-0a4f9a`** — *"A capable agent sent no guest-network stanza"* → `unknown`. **The unknown branch fired on real data, not only in a test** | +| **D** malformed | `unknown`, and the page returns **200** | covered by test; the decode error is swallowed on purpose so one box's bad field cannot break the page for the fleet | + +Two extra branches beyond the four: an unrecognised guest `state` renders unknown (the switch is an +allow-list, so adding a state to the agent can never silently paint it green), and an **empty guest +list with a fresh sweep is NOT silence** — the agent's contract says that means "the watchdog ran and +found nothing", which must stay distinguishable from "the watchdog is not wired", the shape the +v0.91.0 inert seam hid behind. + +### The alarm judgement: **no email, deliberately** + +I agree with the task's recommendation and did not add one. The reasoning, since it is a judgement: + +- **The incident was a visibility failure, not a paging failure.** Nobody could *see* the condition for + 1 h 15 m. A card fixes what actually broke. +- **A new alarm on a fleet of two demo machines is untested noise**, on a dispatcher whose severity + contract is exact-match lowercase and whose customer-message allow-list must move with it — a + two-place change that fires against no real population. +- **There is no calibrated threshold to alarm on.** Neither demo box has produced a single repair since + the watchdog shipped, so any number I picked would be invented. **The visible count is what will + supply the threshold** — which is the argument for building the card first and the alarm second. + +**Revisit when a third machine exists, or when a repair count is seen climbing on real hardware.** + +--- + +## 9. Red-proofs — every mutation asserted applied before its run + +Five in total: three mandated for B2, two for A2. Each mutation was **confirmed present by grep before +the test ran**, and confirmed **absent by grep after restoring**. + +| # | Mutation | Asserted applied | Outcome | +|---|---|---|---| +| **1** *(the one that matters)* | The unknown branches in the card's badge chain replaced by the healthy badge | `grep -c 'title="MUTATED">healthy'` → **2** | **RED — both C sub-cases.** A silent machine seen rendering as healthy: *"a silent box must SAY it is unknown"* and *"an old agent's silence must be named as an old agent's silence"* | +| **2** | `RepairCount: 0` in the decoder — the climbing-repairs signal dropped | `hosts.go:385 RepairCount: 0, … // MUTATED` | **RED — B**: *"a guest repaired 6 times in an hour is reported as fine — the whole point of the card"* | +| **3** | `Degraded()` forced `true` — the healthy branch broken | `hosts.go:348 func … { return true /* MUTATED */ }` | **RED — A**: *"a machine that is fine is being alarmed on"* — the guard is reachable in **both** directions | +| **4** | `ReissueForReenroll` routed back to `EmailReset` | `grep` → line 195 reads `EmailReset` | **RED — two tests**: the new `TestReenrollSplit_ChangesTheMailNotTheSecret` **and** the pre-existing `TestReissueForReenroll` | +| **5** | „Visszaállító kód: %s" restored in the reset mail body | `grep -n` → `templates.go:241` | **RED — `TestFormatClaimEmail_OneNamePerSecret`** | + +**After every restore the suite is green again**, and `grep -c MUTATED` returns **0** in both touched +files. + +**A positive control that the reader is WIRED and not merely written**, independent of the tests: the +wire-contract gate's checked-tag count rose **182 → 190** and its skipped count fell **88 → 80** as the +eight allowlist entries came out. The gate still passes — meaning those eight tags are now genuinely +reachable in the hub. + +--- + +## 10. Part C — **DROPPED**, and one part of it answered anyway + +**Dropped, as the task's own drop order specifies.** The honest reason is not only time: **the task +names "nine claims" that are grey, and I could not identify which nine.** The capability map holds +**22** rows marked `IMPLEMENTED`, five `PARTIAL` and four `MISSING`, and nothing in the tree enumerates +a set of nine. Picking nine myself and reporting on them would have produced a confident answer to a +question nobody asked — the A4 failure mode, one section later. + +**The one I can answer, because the task identified it precisely.** The self-heal claim is the +capability-map row *"Box survives an unattended app or guest-network failure… it is noticed, and where +safe it is repaired"*, and its best document does **not** argue against it. The 1 h 15 m outage +(`audits/INCIDENT-guest-dhclient-killed-2026-07-20.md`) is the row's **origin**, not its evidence: the +evidence is a **deliberate replay of that incident the next day** — `kill -9` at 12:43:18, detected on +process liveness **57 s** later while the lease was still live, healed at 12:45:18 with the incident's +verbatim invocation, and **the tunnel never dropped** (`cloudflared Up 29 hours`). That is a walk, and +the outage was prevented rather than merely observed. **The claim does not need to move down.** + +**What I would flag instead:** the row's *guest-network* leg was proven on the box and, until today, +**the hub could not see the condition at all** — which is the gap this session closed and which no +capability row expressed. My new row states its own status honestly as **IMPLEMENTED, not +PROVEN-LIVE**, because **no machine has ever been observed with a climbing repair count on this card.** + +--- + +## 11. Versions, commits, deploy, CI + +| | | +|---|---| +| **Shipped** | **hub v0.104.0** (`gitea.dooplex.hu/admin/felhom-hub:0.104.0`, 25 MB) | +| **Unchanged** | controller **0.214.0**, agent **0.129.0**, installer **1.28.0**, golden 0.214.0, floor 0.214.0, MinAgent 0.129.0 | +| **Commits** | `4d6ec7c` the work · `7c97c94` the manifest bump | +| **Manifest** | `manifests/hub.yaml:128` → `felhom-hub:0.104.0`, GitOps only — **no `kubectl set image`** | +| **Deploy** | ArgoCD hard-refresh + sync → `Synced / Healthy`; pod `hub-5c4d8d4b49-864dc` **1/1 Running**; deployment image confirmed `0.104.0` | +| **CI, confirmed by run ID** | run **334** (`7c97c949f6`) **success** · run **333** (`4d6ec7c7bb`) **success** | +| **Gates** | `repo_gates.py --fast` — all **8** OK, and again in the pre-push hook on both pushes. **No `--no-verify`.** | +| **Green gate** | `hub/`: build rc=0, vet rc=0, **test rc=0 across 18 packages**. Run separately from every commit | + +**Live validation method — stated, as the fence requires:** endpoint-level. I invoked the exact URLs +the operator UI serves (`GET /hosts/`, `GET /customers/`) against the hub's ClusterIP with the +operator Basic-auth credential, so no server logic was skipped — only browser rendering. +`claude-in-chrome` is not available on DooPlex; a click-through remains the operator's. + +**Files:** `hub/internal/web/hosts.go`, `hub/internal/web/templates/host_detail_body.html`, +`hub/internal/claim/engine.go`, `hub/internal/notify/templates.go`, `hub/internal/web/configs.go`, +`hub/internal/web/templates/customer_unified.html`, `scripts/wire_contract_gate.py`, plus +`hub/internal/web/hosts_guestnet_test.go` (**new**, 7 tests) and `hub/internal/claim/naming_test.go` +(**new**, 6 tests). Tests **+13**. + +**Register:** R-319, R-320, R-321, R-322 minted (ceiling was R-318, grepped first). R-264, R-309, R-312, +R-313, R-303 and the `PETI` row updated. `STATUS.md` and the capability map updated. + +--- + +## 12. What was dropped, and observations + +**Dropped — named plainly, nothing silently shortened:** + +- **Part C in full**, except the self-heal verdict in §10. Reason in §10: the nine were not identifiable, + and inventing them would have been worse than the gap. + +**Nothing else was dropped.** Part A is complete (A1–A5), Part B is complete (B0, B1, and the one reader +B2 asked for — not four). + +### Observations — noticed, not acted on + +- **A "0 bytes" answer was nearly a wrong verdict, twice in one hour.** `demo-hp`'s host page came back + empty from a `kubectl run` curl pod, which reads exactly like "the card does not render". Retrying with + a clean pod name returned **HTTP 200** and a correct card. *An empty listing is not evidence of + emptiness* — the instrument was the fault, and the rule caught it. +- **R-264's count was wrong**: the row says twenty-one facts, the allowlist held twenty. The row is now + written against the measured number. +- **The wire gate does not detect a stale allowlist entry.** Its sibling `retrieval_promise_gate.py` + fails on one; this one silently skips. Had I re-labelled the eight `guest_net` entries instead of + deleting them, the gate would have reported a coverage it did not have and said nothing. Worth the + same treatment. +- **A third secret sits one homograph away from the two that collided.** The hub's self-bind flow calls + the **five-word** retrieval passphrase „**visszaállító jelszó**" (`web/selfbind.go:255,261`; + `notify/templates.go:285`) — *jelszó*, not *kód*, so it is outside R-295's ruling and I left it alone. + But „Visszaállító kód" was just retired for being a near-homograph of „Helyreállítási kód", and this + is a near-homograph of the name that was retired. **Three secrets, three names, and two of them still + begin with the same word.** A ruling for it is the operator's, not mine. +- **`agents.md`-style version drift in project memory**: `instructions_gate` emits 32 version-literal + warnings against `MEMORY.md`. Not a failure and not mine to fix tonight, but it is aimed at the next + model to edit that file — which is worth someone acting on before it is noise nobody reads.