hub v0.118.0: the household's e-mails follow the household's language (R-558 Part A)
gates / gates (push) Successful in 23s
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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user