hub v0.68.0 — auth_failed self-heal, consumed_at honesty gauge, wrapper drift (R-39 + R-50b(a))

Completes the hub half of R-39's fleet fix on top of the generation core (c484aa2).

pbsdrheal gains an auth_failed TRIGGER — a new trigger in the existing machine, not a
new machine. A box whose credential PBS rejects escalates to a fresh mint, never a
re-stage (which would re-feed the secret PBS just rejected), through the EXISTING damper:
a 401 flap must not become a secret-minting chain. With the generation stamp this closes
the loop end to end — agent proves the 401, hub re-keys, generation advances, descriptor
hash moves, agent re-consumes.

consumed_at honesty gauge: a staged secret still unconsumed past a 15-minute grace while
the box reports `applied` is surfaced with its own event. That is the exact 2026-07-18
fingerprint and a disagreement no single tier can see alone. Deliberately a SURFACE, not
a heal — auto-re-issuing on it would mint a second secret on top of an unconsumed one,
which is the mint/consume race R-39(a) already recorded. One event per distinct report,
and an honestly-stuck box does not double-report (its unconsumed secret is the symptom
being healed, not a contradiction).

R-50b(a): ArtifactManifest.WrapperSHA256 + operator field + host-page drift surface. The
PBS wrapper is root-owned 0755 and the pinned sudoers vector, yet installed unversioned
from raw/branch/main and absent from every manifest. Agents >=0.91.0 report the installed
hash; a mismatch is surfaced. An unknown on EITHER side reads as quiet, never as drift —
lighting every host amber on rollout day is how a warning becomes background noise. The
delivery channel itself stays R-50b(b)/(c).

Compatibility unchanged: safe for 0.90.0 agents (unknown JSON key dropped); the re-arm
and auth-honesty guarantees need agent >=0.91.0, so MinAgent moves only after the fleet
has self-updated.

Tests: auth_failed escalate/debounce/recovery-forgets-streak; honesty gauge incl. grace
window, the restage edge (consumed_at deliberately NULLed), consumed-never-alarms, and
honest-stuck-no-double-report; wrapper drift incl. both unknown directions. Red-proof run
at the assertion level: removing the auth_failed arm fails the escalation tests with
reissues=0.
This commit is contained in:
2026-07-21 10:01:35 +02:00
parent c484aa204e
commit 107f74ea3c
10 changed files with 561 additions and 38 deletions
+64
View File
@@ -1,5 +1,69 @@
# Felhom Hub — Changelog
## v0.68.0 — a credential re-issue finally re-arms the box (R-39 fleet fix) + wrapper drift is visible (R-50b(a)) (2026-07-21)
**Coupling, stated honestly: this release is SAFE for agents at 0.90.0** — the new descriptor field
is an unknown JSON key to them; they drop it and behave exactly as today (inert, not breaking).
**The re-arm and auth-honesty guarantees require agent >= 0.91.0.** Raise MinAgent to 0.91.0 only
after the fleet's agents have self-updated.
### The defect (R-39, fleet half)
An ep0 credential re-issue re-keys the **secret of an existing token**. `token_id`, `fingerprint`,
`datastore` and `namespace` all come back byte-identical — only the side-table `host_pbs_secrets`
row rotates. The agent's re-apply trigger is a change in the **descriptor content hash**
(`felhom-agent internal/pbsdr/manager.go` `descriptorHash`). Same hash → the converged agent
short-circuits → the fresh secret is never consumed → the box keeps presenting a revoked credential
**401 forever, while both tiers report `applied`**. Proven on the N100 2026-07-18: the agent's
`consumed-failed.json` hash was byte-identical to the `marker.json` written two minutes before the
re-issue.
### The fix
- **`host_pbs_secrets.generation`** — a monotonic per-host counter advanced by every fresh **mint**
and by nothing else, stamped into the descriptor as `secret_generation`. That is now the only
field a re-key moves, and it is what re-arms the agent.
- **A re-stage deliberately does not advance it**: it re-arms the *same* secret, the descriptor
content genuinely has not changed, and a bump would cause a pointless agent refetch loop.
- `omitempty` is load-bearing — emitting a zero into every pre-existing descriptor would itself be
a fleet-wide spurious re-apply.
- **Deviation from the spec, deliberate:** the brief said to reuse "the new row's id … no schema
change". There is no row id — the table is keyed by `host_id` and UPSERTed last-write-wins, so a
new row never exists, and `created_at` collides for two mints in one second. An additive counter
column (existing idempotent `ALTER TABLE` idiom) is the only monotonic source available.
- **`pbsdrheal` gains an `auth_failed` trigger** — a NEW trigger in the existing machine, not a new
machine. A box whose credential PBS rejects is escalated to a fresh mint (never a re-stage: that
re-feeds the secret PBS just rejected), through the **existing damping** — a 401 flap must not
become a secret-minting chain. This closes the loop end to end: agent proves the 401 → hub re-keys
→ generation advances → descriptor hash moves → agent re-consumes.
- **`consumed_at` honesty gauge** — a staged secret left **unconsumed** past a 15-minute grace while
the box reports `applied` is surfaced loudly with its own event. This is the exact July-18
fingerprint and a disagreement **no single tier can detect alone**. Deliberately a *surface*, not
a heal: auto-re-issuing here would mint a second secret on top of an unconsumed one — the
mint/consume race R-39(a) already recorded.
- **Corrected a comment that stated a falsehood**: `ReissuePBSDR` claimed it refreshed the descriptor
"with the NEW token_id/fingerprint". That is false for a re-key, and believing it is why nobody
expected the descriptor to come back identical.
### R-50b(a) — wrapper drift is answerable
`ArtifactManifest.WrapperSHA256` + an operator field. Unlike the agent binary and the golden, the
PBS-DR wrapper is installed from `raw/branch/main` — unversioned, unpinned, absent from every
manifest — yet it is root-owned 0755 and the pinned sudoers vector. Agents (>= 0.91.0) report the
installed file's hash and the host page surfaces a mismatch. **An unknown on either side reads as
quiet, never as drift** — lighting every host amber on rollout day is how a warning becomes noise.
This does not fix the delivery channel; that stays R-50b(b)/(c).
### Tests
Store-level generation monotonicity, per-host isolation and restage-leaves-it-alone; descriptor
byte-change, `omitempty` and sibling-key round-trip; a **flow-level** test driving `ReissuePBSDR`
against a fake that models a real re-key; `auth_failed` escalate/debounce/recovery; the honesty
gauge incl. its grace window, the restage edge, the consumed case and the honest-stuck case; wrapper
drift incl. both unknown directions. **Two red-proofs run at the assertion level** (not the
compiler): removing the generation stamp makes the flow test fail with both byte-identical blocks
printed; removing the `auth_failed` arm makes the escalation tests fail with `reissues=0`.
## v0.67.0 — the hub stops keeping things to itself: auto-minted self-bind link, post-RESET staleness, unprovisioned-offsite warning (2026-07-18)
Four small items, each one a case where the hub already knew something and said nothing. Green: