hub v0.118.0: the household's e-mails follow the household's language (R-558 Part A)
gates / gates (push) Successful in 23s

The hub has written every customer e-mail in Hungarian whatever the box was set
to. The box has published its language since controller v0.247.0; nothing read
it. Now it does.

Nothing an operator reads changes. The Hungarian mails are byte-identical, and
that is a diff rather than a reading: 56 goldens per language captured from
v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged after
every sentence was routed through the new bundle.

- internal/i18n: flat bundle, 79 keys, hu authoritative + hu fallback, ceiling 0.
- customerMessages/severityLabels are DERIVED from the bundle, so a sentence is
  written in one place and all 40+ tests that read those maps still work.
- Language order: last reported -> created-with -> hu. reports.language defaults
  to EMPTY, never hu: "never told us" is not "chose Hungarian".
- message_customer on POST /api/v1/event, additive and optional forever, for the
  sentences the box composes and the hub cannot translate.
- The bind page is per-language, and its `expired` state stays Hungarian: it is
  the state an unknown token lands in, so rendering a real English customer's
  token in English would make the LANGUAGE answer what the TEXT refuses to.

Two defects found inside the release:
- R-581: the newest report was picked by received_at, which has SECOND
  granularity, so same-second reports tied and the winner was arbitrary. Ordered
  by the autoincrement id now. GetCustomers() still has the shape - row open.
- R-582: the English copy-guard stems, ported word for word from Hungarian,
  convicted 141 honest sentences. The English claim is a phrase with a modal.

R-555 closed: the language allowlist entry is out of wire_contract_gate.py.
hub_copy_gate.py follows the sentences into the bundle - without that it would
have scanned four files that no longer hold any customer text and reported
success. Three new decoys incl. an innocent control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-18 16:20:11 +02:00
parent 20aafc3dec
commit 9167cf53af
150 changed files with 4063 additions and 410 deletions
+80 -71
View File
@@ -1,90 +1,99 @@
## Claims in the prompt that turned out wrong — settled so far (measured, not argued)
# REPORT — localisation slice 3 Part A: the hub's e-mails follow the household's language
1. **"A custom PBS role can carry just `Datastore.Modify`" — FALSE, and the prompt itself flagged it
as unverified.** Proxmox Backup Server has **no role-create command** and no custom roles: the CLI
describes `<role>` as "Enum representing roles via their [PRIVILEGES] combination", and
`proxmox-backup-manager` offers no `role` subcommand at all. I then measured the narrowest
BUILT-IN role by applying it and reading the effective permissions back: `DatastorePowerUser`
grants **Datastore.Backup + Datastore.Prune only** — it does not help. `DatastoreAdmin` grants
Audit, Backup, Modify, Prune, Read, Verify, and is therefore the narrowest role that works. It is
applied for the hub's `felhom@pbs` on `/datastore/felhom-offsite` only; the per-customer
`DatastoreBackup` entries are untouched.
**hub v0.118.0** · felhom.eu base `20aafc3dec20` · 2026-09-18 · R-558 (Part A), R-555 closed
2. **"The banner unit can learn the claimed state" — NOT REACHABLE, as the prompt suspected.** Two
measurements: the one-shot bind delivery emits `FELHOM_CUSTOMER_ID`, `FELHOM_RETRIEVAL_PASSPHRASE`,
`FELHOM_MODE` and `FELHOM_EXTRA_ARGS` — **no domain** — so the console cannot name the dashboard
URL without inventing it; and the unit hands over to the host install and exits, so the later CLAIM
happens when nothing is watching. What IS reachable, and is what shipped: the pairing code stops
being the last thing on the screen the moment the bind lands. The rest of R-535 is recorded as a
residue rather than implied away.
## Claims in the task that turned out wrong, named first
3. **The baseline "ISO 1.27.1 published → target 1.28.0" needed care, and the care found a trap.**
`installer-v1.28.0` **already existed as a git tag** — from 2026-08-13 — because the install SCRIPT
and the ISO IMAGE are two separately numbered artifacts (`SCRIPT_VERSION` vs `ISO_VERSION`, and
`build-felhom-iso.sh` says so in a comment). The published ISO really was 1.27.1 (confirmed live:
the bucket serves 1.27.1 and 404s 1.28.0), so the target is right — but a session that read the tag
list as the ISO history would have concluded 1.28.0 was already published.
1. **"`customerMessages` 40 entries (L70)"** — **39 entries, at L69.**
2. **"`customers.language`, `appliances.reported_language`"** — **neither table exists.** There is no
`customers` table and no `appliances` table. The real ones are **`customer_configs`** (the customer
record) and **`reports`** (the box's heartbeat, one row per report). The columns added are
`customer_configs.language` and `reports.language`.
3. **"no `ALTER TABLE customers ADD` found by grep — the migration pattern is something else; if it
is not obvious, stop and report."** The pattern is completely obvious and there are ~20 instances:
`s.db.Exec("ALTER TABLE <t> ADD COLUMN <c> <type> NOT NULL DEFAULT <v>")`, error deliberately
ignored so it is idempotent. The grep failed only because it named a table that does not exist.
4. **"`renderBackupRunFailures` gains a `lang` parameter"** — it is **operator-only and already
English**. Its single caller is `FormatOperatorEmail`. Untouched.
5. **"`message_customer`'s length is capped like `message`"** — **`message` has no length cap.** Both
are bounded only by the 1 MB `LimitReader` on the request body. I wrote a cap, then removed it: a
byte-count truncation would also cut a UTF-8 sequence in half.
6. **"`python3 scripts/hub_gates.py` (or the runner the hub uses — name it)"** — there is no
`hub_gates.py`. The hub's gates run from **`scripts/repo_gates.py`**, this repo's single runner.
7. **Line numbers** were mostly off by one or two (`customerMessages` 69 not 70, `severityLabels` 158
not 159, `FormatCustomerEmail` 166 not 167).
4. **Still open at the time of writing:** "the off-site wizard's full restore brings back deleted files
for nextcloud" — proven for immich in July and read from the design for nextcloud; Part E walks it.
5. **„The off-site wizard's full restore brings back deleted files for nextcloud" — TRUE, now measured.**
It was proven for immich in July and read from the design for nextcloud. Tonight it was walked:
five photos, deleted, returned byte-identical (sha256 5/5, negative control).
6. **My own wrong reading, recorded because I nearly filed it as a product fault.** I reported the data
drive as „formatted but not mounted, 42 minutes on" from `/api/disks/candidates`. The storage page
said the opposite and was right — the drive was mounted, registered and default. The endpoint reports
raw disks from the agent, not what the controller has registered (now **R-542**).
Claims that were **right**: no mail goldens existed; the hub read no `language` anywhere outside a
comment; `05-hub-architecture.md` had no customer-mail section; the box has published the field since
v0.247.0. I briefly believed `Report.Language` was never assigned and was wrong — `cmd/` is
gitignored, so `rg` skips `main.go`, where all four assignments live.
## What shipped
**controller v0.244.0** — the backup label is PER TIER and no longer claims files a Tier-1 unit cannot
hold (R-537); a unit restore REFUSES before touching anything when it cannot return the app's drive-side
files, and names the route that can (R-538); `app_deployed` moved from the deploy's acceptance to its
completion, with `app_deploy_started` / `app_deploy_failed` as the honest pair (R-536). Plus the pending
„0 B" tile fix.
The hub reads the language the box reports and writes the household's e-mail in it. **Nothing an
operator reads changed.** Hungarian is byte-identical, measured.
**hub v0.116.0** — off-site backup is ON by default for a new customer (shared, 100 GB prefilled), and
the two new deploy event types are registered in both `allowedEventTypes` and `customerMessages`.
- **`internal/i18n`** — flat bundle, 79 keys, hu authoritative + hu fallback, missing ceiling 0.
- **56 mail goldens per language**, captured from v0.117.0 **before** any string moved. All 56
Hungarian ones pass unchanged after the rewrite. A `nowFn` seam makes them byte-stable.
- **`customerMessages`/`severityLabels` are derived from the bundle** — one place per sentence, and
all 40+ existing tests and comments that read them still work.
- **Order: last reported → created-with → `hu`.** `reports.language` defaults to **empty**, never
`hu`: "never told us" is not "chose Hungarian".
- **`message_customer`** accepted on `POST /api/v1/event` (additive, optional forever).
- **Per-language bind page**, built the controller's way (substitute markers, then parse).
- Customer form `language` select; `customer.language` in `controller.yaml` (diff: one line).
**ep0** — one narrow grant: `DatastoreAdmin` for the hub's `felhom@pbs` on `/datastore/felhom-offsite`
only. The narrowest role was MEASURED: `DatastorePowerUser` carries Backup+Prune only, and PBS has no
custom roles. Per-customer `DatastoreBackup` entries untouched.
## Live evidence, read off the running hub
**ISO 1.28.0** — built, gate-checked, installed and walked. **NOT published** — that is the operator's
call and the one STOP of this task.
The wire already worked and I measured it before writing anything (read-only copy of `hub.db` + WAL):
**golden 0.244.0** — baked, published, vouched as a three-field change (agent and min_agent unchanged at
0.131.0), fleet floor raised 0.242.0 → 0.244.0 and already delivering (demo-felhom moved itself).
```
demo-felhom 2026-09-18 13:42:56 language='hu' controller 0.255.0
demo-hp 2026-09-18 13:45:24 language='en' controller 0.255.0
drill-r50 / peti-felhom / tester-1 language=<ABSENT> (0.213.0 / 0.115.0 / 0.245.0)
```
## Red-proofs
That is the positive and the negative control in one read: boxes ≥ 0.247.0 report the field, older
ones send nothing at all — which is exactly the case the empty default exists for.
Each fix seen failing with its own sentence, then passing: the app-shaped label restored → the Tier-1
assertion fails; the guard disabled → „a restore that cannot return the files must refuse" fails; the
accept-time call put back → „the deploy handler announces an INSTALLED app at accept time" fails; the
success hook removed → „the deploy ended and nothing was told about it" fails; the hub default dropped →
„the new-customer form does not default the off-site copy ON" fails.
## Two defects found inside this release
## The walk, end to end (evidence: `audits/evidence-backup-promise-2026-09-16/`)
- **R-581 (P2, partly still open).** `CustomerLanguage` picked the newest report by `received_at`,
which has **second** granularity — same-second reports tie and the winner is arbitrary. A household
that had just switched would get the old language back at random. Fixed by ordering on the
autoincrement `id`; caught by a test that failed on the first version. **Still open:
`GetCustomers()` has the same shape** and feeds the whole operator dashboard.
- **R-582 (closed, kept for the lesson).** The English copy-guard stems, ported word-for-word from the
Hungarian, convicted **141 honest sentences**. In Hungarian the stem *is* the claim
(`visszaállíthat` = *can restore*); English splits the modal from the verb, so the claim is a
phrase. Then the decoy suite caught the fix being too narrow — "can **still** be restored" walked
through a pattern written for "can be restored".
Fresh VM from the BUILT image → Felhom's own first screen, no admin URL → registered itself → **bound
with zero operator presses** (the mail the hub sent itself after the morning's host delete) → claimed →
landed on agent 0.131.0 + controller 0.244.0 → data drive registered → Nextcloud deployed → five photos
in → tier-1 leg → **the PBS cascade stopped at R-511's refusal and needed ONE operator press**, which
then succeeded because of this morning's grant → escrow ceremony (re-auth required; code shown once,
captured out-of-band) → tier-3 „Sikeres" → photos deleted → **the old route REFUSED and touched nothing**
→ off-site restore (verification copy, then reconstitution: „5 fájl és 3 adatkötet és az adatbázis") →
**the photos open, byte-identical** → teardown in three layers → the automatic connect e-mail again, one
second after the host delete.
## Gate work
## Rows
`wire_contract_gate.py`: the R-555 `language` allowlist entry **deleted** — the field is genuinely
decoded now and the gate checks it (202 tags, was 201). `hub_copy_gate.py`: **the sentences moved into
the bundle**, so `CUSTOMER_SURFACES` had to move with them — without that the four declared Go files
would all still exist, the gate would still report success, and it would be scanning nothing. It
learned English, and found one real occurrence (operator-tier, registered with its reason). Three new
decoys including an **innocent control**; the `hub-copy` exemption is removed from
`decoy_coverage_gate.py`.
Opened: R-539, R-540, R-541, R-542, **R-543** (P1 — off-site on by default is not off-site working on day
one), R-544. Closed: R-511, R-534, R-536, R-537, R-538. Register 236 → 244 open.
## The mails
## Checks
| Group | Count | Golden | English | Live-tested |
|---|---|---|---|---|
| Event types (`mail.event.*`) | 39 | ✅ hu + en | ✅ | pending (§13.1) |
| Severity labels | 4 | ✅ | ✅ | — |
| Customer wrapper (subject, body, 2 lines, sign-off) | 5 | ✅ | ✅ | — |
| Claim arc (claim / reset / reenroll / claimed) | 4×2 | ✅ | ✅ | pending (§13.2) |
| Self-bind mail | 2 | ✅ | ✅ | pending |
| Bind page | 21 | — (render tests) | ✅ | pending |
| Operator mails | 3 | ✅ (unchanged) | n/a — never localised | — |
`repo_gates.py --fast` green at every push; controller `go build/vet/test` green; controller gates 15/15;
`unproven.py --summary` unchanged at 35 of 55 not walked. Secret-leak check on the evidence: six real
secret values as needles, planted control matched 6/6, committed evidence 0.
## Green
`go build` / `go vet` / `go test ./...` clean. All 14 felhom.eu gates OK. 15/15 decoys behave.
**Not yet done:** deploy + live proof (§13), and Part B (controller v0.256.0, the box's own sentence).