v0.40.0: bootstrap pull+merge onboarding (controller pulls config from hub)

Fix the onboarding 401: instead of seeding controller.yaml from the agent's
HOST hub key (which the hub's customer-scoped /api/v1/report rejects), the
controller now PULLS its full controller.yaml from the hub on first boot using
the bootstrap's retrieval passphrase (yielding the customer-scoped key) and
MERGES in the per-guest local_api block.

- internal/bootstrap: contract v1->v2 (customer.id + hub.url +
  hub.retrieval_password + local_api; drop host key/identity). MaybeIngest gains
  an injected PullFunc (keeps bootstrap free of the heavy report package),
  pulls with bounded transient-only retry, merges local_api at YAML-map level
  (preserves all hub-emitted fields), idempotent + fail-safe + never-crash.
- main.go: wire report.PullConfig as the pull adapter (maps ErrHubUnreachable
  -> ErrPullTransient; auth/not-found permanent).
- Lockstep with felhom-agent v0.19.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 13:22:37 +02:00
parent b76d8b298c
commit 6a594f9ec2
4 changed files with 347 additions and 132 deletions
+30
View File
@@ -1,5 +1,35 @@
## Changelog
### v0.40.0 — bootstrap pull+merge onboarding (controller pulls its config from the hub) (2026-06-11)
Lockstep with `felhom-agent` v0.19.0. Fixes the onboarding 401: a freshly provisioned guest used to
seed a "configured" controller.yaml from the agent's **host** hub key, which the hub's `/api/v1/report`
(customer-scoped auth) rejects → the controller could never report ONLINE. Now the controller **pulls**
its full controller.yaml from the hub on first boot (the hub mints the **customer-scoped** key) and
**merges in** the per-guest `local_api` block.
#### Changed — bootstrap contract `v1 → v2` (`internal/bootstrap`)
- `SchemaV1 → SchemaV2 = "felhom.bootstrap/v2"`. `BootstrapCustomer` drops `name`/`domain`/`email` (keeps
`id`); `BootstrapHub` drops `api_key`/`host_id`, adds **`retrieval_password`** (SECRET). `local_api`
unchanged. A non-v2 schema → setup mode.
- **`MaybeIngest(configPath, cfg, logger, pull PullFunc)`** — new injected `pull` arg (decision (b): keeps
`bootstrap` from importing the heavy `internal/report` package; wired in `main.go` to `report.PullConfig`).
Flow: idempotent (configured → return, **no pull**) → parse + validate v2 → **pull** the hub config with
bounded retry (1 + 3 backoff attempts on transient `ErrPullTransient` only; auth/not-found fail fast) →
**merge** the per-guest `local_api` at the YAML-map level (preserves every hub-emitted field — assets,
CF, backup) → write 0600 atomic → reload. Fail-safe throughout: a hub outage at first boot leaves the
guest in setup mode (the manual wizard remains the fallback), never crashes.
- New sentinel **`ErrPullTransient`**; `main.go`'s pull adapter maps `report.ErrHubUnreachable` onto it
(transient/retryable) and passes auth/not-found through as permanent. Removed `configFromBootstrap`
(the host-key-seeding path) and the struct-marshal writer.
#### Tests (`internal/bootstrap`)
- Pull+merge (asserts the merged controller.yaml carries the **customer** key + identity + a preserved
unmodeled `assets.source_url` **and** the bootstrap's `local_api`, with **no host key**); idempotency
(pull **never invoked** when configured); transient-retry (N attempts then setup); permanent-no-retry;
non-v2 schema reject; missing-required reject; malformed/absent. Cross-repo render→ingest round-trip
verified against the agent's v2 renderer. `go build ./... && go test ./...` green.
### v0.39.1 — 8C orphan-template cleanup (source hygiene) (2026-06-11)
Dead-template removal — no behaviour change. Slice 8C de-privileged the controller and retired the