e0b56c976f
gates / gates (push) Successful in 15s
Three rules carried forward. A name must separate on the STEM, not the noun — naming this secret after the act it is used in would have recreated the trap, because the other factor on the same page is the „Párosító kód". A guard is worth what its positive control is worth: this one's selftest convicted its own step-3 case and found a defect in the guard itself. And a suppression must rest on the machine's own declaration, then be checked for the SECOND door — recording the disabled state rather than deleting it is what let the deadline check skip it too. Yesterday's report is preserved to audits/ because it carries the only record of the self-heal verdict (Part C was dropped, so that reasoning is in no register row) — the rule written last night, applied to itself the first time it mattered.
354 lines
20 KiB
Markdown
354 lines
20 KiB
Markdown
# 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 `<selftest>` 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.
|