hub v0.14.0: passphrase-authed host enrollment (Day-0 option C)

New POST /api/v1/host-enroll (handleHostEnroll): X-Retrieval-Password authed,
body {customer_id} -> {host_id, api_key}. Mint-once-reuse (201 first, 200
reuse) so re-running the host-bootstrap never orphans a running agent's key;
auth checked before any mint. Backed by new Store.GetHostByCustomer
(ORDER BY updated_at DESC LIMIT 1, idx_hosts_customer).

GET /config/{id} and global-key POST /admin/hosts left untouched. Exact-match
route (path == "/host-enroll") to avoid the /hosts/ prefix collision.

Tests: host_enroll_test.go (mint/reuse/401-no-mint/404/400) + GetHostByCustomer
store test; companion red-proof verified always-mint fails the reuse assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
This commit is contained in:
2026-06-26 15:35:24 +02:00
parent 230980f7a8
commit 8098237ce1
7 changed files with 331 additions and 1 deletions
+40
View File
@@ -45,6 +45,46 @@ func TestUpsertHost_AndLookup(t *testing.T) {
}
}
func TestGetHostByCustomer(t *testing.T) {
s := newTestStore(t)
// none → nil, nil
got, err := s.GetHostByCustomer("c1")
if err != nil || got != nil {
t.Fatalf("no host: got %+v / %v (want nil,nil)", got, err)
}
// one → that host
if err := s.UpsertHost(&Host{HostID: "c1-aaa111", CustomerID: "c1", APIKey: "k1"}); err != nil {
t.Fatal(err)
}
got, err = s.GetHostByCustomer("c1")
if err != nil || got == nil || got.HostID != "c1-aaa111" || got.APIKey != "k1" {
t.Fatalf("one host: got %+v / %v", got, err)
}
// two for the same customer → most-recently-updated wins (never a duplicate on reuse).
// updated_at is second-resolution, so set it explicitly to make the ordering deterministic.
if err := s.UpsertHost(&Host{HostID: "c1-bbb222", CustomerID: "c1", APIKey: "k2"}); err != nil {
t.Fatal(err)
}
if _, err := s.db.Exec(`UPDATE hosts SET updated_at='2026-01-01 00:00:00' WHERE host_id='c1-aaa111'`); err != nil {
t.Fatal(err)
}
if _, err := s.db.Exec(`UPDATE hosts SET updated_at='2026-06-26 00:00:00' WHERE host_id='c1-bbb222'`); err != nil {
t.Fatal(err)
}
got, err = s.GetHostByCustomer("c1")
if err != nil || got == nil || got.HostID != "c1-bbb222" {
t.Fatalf("two hosts: want most-recent c1-bbb222, got %+v / %v", got, err)
}
// a different customer is unaffected
if other, err := s.GetHostByCustomer("c2"); err != nil || other != nil {
t.Fatalf("other customer: got %+v / %v (want nil,nil)", other, err)
}
}
func TestSaveHostReport_BumpsRealityPreservesIntent(t *testing.T) {
s := newTestStore(t)
if err := s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"}); err != nil {