v0.89.0: pbsdr self-grant (R-22) + escrow config live-reload + agent-plane poke listener (Direction-2a)
- pbsdr: on a 403 pre-check (non-default storage id, no ACL yet) self-grant via the root wrapper then re-read, instead of aborting before the grant — closes F4/R-22. Red-proof TestSelfGrant_PreCheck403DoesNotAbortBeforeGrant. - escrow preflight: late-bound CurrentPBSStorageID re-reads agent.json so a pbsdr-seeded pbs_storage_id flips the row green in-process (no restart). Red-proof TestEscrowPreflight_PBSStorageIDLiveReload. - internal/poke: contentless UDP poke listener bound exclusively to the box WG /32 (port 51822), leading-edge debounced, fires the hub-loop out-of-band trigger for an immediate desired-state cycle. First slice of R-13. Red-proofs TestBindConfinement + TestDebounceCoalescesBurst.
This commit is contained in:
@@ -34,8 +34,17 @@ import (
|
||||
type EscrowCeremonyConfig struct {
|
||||
// SudoPath is the sudo binary ("" → "sudo").
|
||||
SudoPath string
|
||||
// PBSStorageID is cfg.Escrow.PBSStorageID ("" = not configured — preflight red).
|
||||
// PBSStorageID is cfg.Escrow.PBSStorageID at daemon-start ("" = not configured — preflight
|
||||
// red). It is the FALLBACK snapshot; the live value is CurrentPBSStorageID when wired.
|
||||
PBSStorageID string
|
||||
// CurrentPBSStorageID, when set, is called at preflight time to read the LIVE
|
||||
// escrow.pbs_storage_id. The pbsdr bridge SEEDS this key into agent.json on DR convergence
|
||||
// (finishConverged → seedEscrowStorageID); a static snapshot taken at daemon start would then
|
||||
// stay red until a service restart (v0.89.0 live-reload). The wired closure re-reads config
|
||||
// from disk — exactly what the ceremony subprocess itself loads — so the preflight reflects the
|
||||
// real state the bare `--selftest=escrow-create` one-liner will see. nil → the PBSStorageID
|
||||
// snapshot is used (old wiring / tests).
|
||||
CurrentPBSStorageID func() string
|
||||
// HubConfigured: hub url + host id + api key all present (the --upload target).
|
||||
HubConfigured bool
|
||||
// DRConfigured answers "is the DR tier applied on this box?" (pbsdr.Manager.DRConfigured,
|
||||
@@ -362,8 +371,14 @@ func (s *Server) handleEscrowPreflight(w http.ResponseWriter, r *http.Request, v
|
||||
}
|
||||
items := make([]item, 0, 6)
|
||||
|
||||
items = append(items, item{ID: "pbs_storage_id", OK: cfg.PBSStorageID != "",
|
||||
Detail: map[bool]string{true: cfg.PBSStorageID, false: "escrow.pbs_storage_id not configured"}[cfg.PBSStorageID != ""]})
|
||||
// Live-reload (v0.89.0): prefer the current on-disk value over the daemon-start snapshot, so a
|
||||
// pbsdr convergence that just seeded escrow.pbs_storage_id flips this row green with no restart.
|
||||
storageID := cfg.PBSStorageID
|
||||
if cfg.CurrentPBSStorageID != nil {
|
||||
storageID = cfg.CurrentPBSStorageID()
|
||||
}
|
||||
items = append(items, item{ID: "pbs_storage_id", OK: storageID != "",
|
||||
Detail: map[bool]string{true: storageID, false: "escrow.pbs_storage_id not configured"}[storageID != ""]})
|
||||
|
||||
drOK := cfg.DRConfigured != nil && cfg.DRConfigured()
|
||||
items = append(items, item{ID: "dr_tier", OK: drOK,
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -61,6 +63,82 @@ func startAndWait(t *testing.T, srv *Server, h http.Handler) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEscrowPreflight_PBSStorageIDLiveReload pins the v0.89.0 live-reload: the pbsdr bridge seeds
|
||||
// escrow.pbs_storage_id into agent.json on DR convergence; the preflight must see that seed IN THE
|
||||
// SAME PROCESS (no restart). The daemon-start snapshot (cfg.PBSStorageID) is empty; only the
|
||||
// late-bound CurrentPBSStorageID resolver (re-reads disk) reflects the seed. RED-PROOF: the pre-fix
|
||||
// preflight read cfg.PBSStorageID directly and ignored the resolver, so step 3 stays red → FAIL.
|
||||
func TestEscrowPreflight_PBSStorageIDLiveReload(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "agent.json")
|
||||
// Pre-convergence: agent.json has no escrow.pbs_storage_id.
|
||||
if err := os.WriteFile(cfgPath, []byte(`{"escrow":{"posture":"zero_knowledge"}}`), 0o600); err != nil {
|
||||
t.Fatalf("seed config: %v", err)
|
||||
}
|
||||
readCurrent := func() string {
|
||||
raw, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var doc struct {
|
||||
Escrow struct {
|
||||
PBSStorageID string `json:"pbs_storage_id"`
|
||||
} `json:"escrow"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &doc)
|
||||
return doc.Escrow.PBSStorageID
|
||||
}
|
||||
|
||||
srv := newEscrowTestServer(t, okRunner(nil))
|
||||
srv.escrowCeremony.PBSStorageID = "" // daemon-start snapshot: empty (the pre-fix source)
|
||||
srv.escrowCeremony.CurrentPBSStorageID = readCurrent // live resolver → current disk state
|
||||
h := srv.Handler()
|
||||
|
||||
rowOK := func(t *testing.T) (bool, string) {
|
||||
t.Helper()
|
||||
w := do(t, h, "GET", "/escrow/preflight", "A", "")
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("preflight status %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp struct {
|
||||
Data struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
OK bool `json:"ok"`
|
||||
Detail string `json:"detail"`
|
||||
} `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode preflight: %v (%s)", err, w.Body.String())
|
||||
}
|
||||
for _, it := range resp.Data.Items {
|
||||
if it.ID == "pbs_storage_id" {
|
||||
return it.OK, it.Detail
|
||||
}
|
||||
}
|
||||
t.Fatal("pbs_storage_id row missing from preflight")
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// 1. Before convergence: the row is RED.
|
||||
if ok, _ := rowOK(t); ok {
|
||||
t.Fatal("pbs_storage_id row is green before any seed")
|
||||
}
|
||||
// 2. Convergence seeds the id IN-PROCESS (simulating pbsdr finishConverged → seedEscrowStorageID).
|
||||
if err := os.WriteFile(cfgPath, []byte(`{"escrow":{"posture":"zero_knowledge","pbs_storage_id":"felhom-offsite"}}`), 0o600); err != nil {
|
||||
t.Fatalf("seed after convergence: %v", err)
|
||||
}
|
||||
// 3. Same process, NO restart: the row flips GREEN and reports the seeded id.
|
||||
ok, detail := rowOK(t)
|
||||
if !ok {
|
||||
t.Fatal("pbs_storage_id row still red after the in-process seed — the preflight is not live-reloading (restart-only)")
|
||||
}
|
||||
if detail != "felhom-offsite" {
|
||||
t.Fatalf("pbs_storage_id detail = %q, want the seeded id felhom-offsite", detail)
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario A/D happy path: run → done → claim ONCE (R delivered, no-store) → 410 on re-claim,
|
||||
// holder zeroed. R never appears in the start or status payloads.
|
||||
func TestEscrowCeremony_OneShotClaim(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user