# REPORT — the third near-homograph, a machine told to be quiet, and a picture you can query (2026-08-13, late) **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. Part 1 — the enumeration, the name, and the reasoning **Enumerated before editing. Five customer-facing sites, all in the hub:** | `file:line` | what it is | |---|---| | `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. ### The name: **„Tulajdonosi jelmondat"** 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.** **Both of your suggestions are rejected, and the reasons are the argument:** - **„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:** | | stem | noun | |---|---|---| | „**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** | *If a plainer noun is ever wanted, „Tulajdonosi jelszó" is a one-word change — the **stem** is what carries the separation.* ### As shipped, bytes confirmed | 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. --- ## 2. Does any customer-facing document name the old phrase? **No.** 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. **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."* --- ## 3. Part 1b — the hub guard, and the control that found a bug in itself `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** 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 | Plus `TestStaleness_DisabledIsAlsoSkippedByTheDeadlineCheck` for the second door. ### Red-proofs — every mutation asserted applied by grep, and reverted after | # | Mutation | Asserted applied | Outcome | |---|---|---|---| | **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 | **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. --- ## 5. Part 3 — the real counts, and the number that was wrong **Yes, a number I have been repeating is wrong.** ``` 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 ``` **"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**. --- ## 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.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** | `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 verification of the running artifact**, ASCII-only patterns against `/usr/local/bin/felhom-hub` in the running pod: ``` Tulajdonosi jelmondat 1 ← the field label tulajdonosi jelmondatodat 2 ← the page lead + the mail jelszavadat 0 ← the retired possessive is GONE ``` 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**. --- ## 8. `demo-hp` was not touched **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. Confirmed read-only, via the hub's own hosts page (`GET /hosts`, HTTP 200): ``` 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 - **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.