v0.80.0: PBS DR tier slice 2 — the apply-bridge (pbs_dr consumer, felhom-pbs-apply set-only wrapper, verify-pin-before-consume, adoption-first, loud consumed-failed, escrow seed)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -193,6 +193,50 @@ func (c *Client) RegisterWG(ctx context.Context, pubkey string) (*WGRegisterResp
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// ErrNoPBSSecret is the typed "404: no unconsumed PBS token secret staged for this host" outcome
|
||||
// (PBS DR slice 2). Absent-or-already-consumed are indistinguishable by design (consume-once).
|
||||
var ErrNoPBSSecret = fmt.Errorf("hub: no unconsumed PBS token secret staged for this host")
|
||||
|
||||
// ConsumePBSToken fetches this host's one-time PBS token secret — EXACTLY ONCE (PBS DR slice 2;
|
||||
// POST /api/v1/hosts/{host_id}/pbs/consume-token, per-host key, self-scoped; NOTE the PLURAL
|
||||
// /hosts/ — the slice-1 route). A 200 burns the secret hub-side: the caller MUST apply it or
|
||||
// surface a loud consumed-but-failed state (never silent-retry). The secret is returned to the
|
||||
// caller only — never logged, never in an error.
|
||||
func (c *Client) ConsumePBSToken(ctx context.Context) (string, error) {
|
||||
if c.hostID == "" {
|
||||
return "", fmt.Errorf("hub: ConsumePBSToken requires a configured host_id")
|
||||
}
|
||||
url := c.baseURL + "/api/v1/hosts/" + c.hostID + "/pbs/consume-token"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return "", &TransportError{Err: err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return "", ErrNoPBSSecret
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", &HTTPError{StatusCode: resp.StatusCode, BodyTail: tail(raw, 256)}
|
||||
}
|
||||
var out struct {
|
||||
TokenSecret string `json:"token_secret"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return "", fmt.Errorf("hub: decoding consume-token response (body withheld — secret channel)")
|
||||
}
|
||||
if out.TokenSecret == "" {
|
||||
return "", fmt.Errorf("hub: consume-token returned an empty secret")
|
||||
}
|
||||
return out.TokenSecret, nil
|
||||
}
|
||||
|
||||
// JobWire is one queued signed-op job as served by GET /hosts/{id}/jobs (slice 10A). The blob is
|
||||
// OPAQUE to the hub — for slice 10B it is a base64 `SignedJobEnvelope` (op-blob + armored SSHSIG)
|
||||
// the agent verifies before executing.
|
||||
|
||||
@@ -55,6 +55,12 @@ type WireguardReporter interface {
|
||||
WireguardStatus(ctx context.Context) *WireguardStatus
|
||||
}
|
||||
|
||||
// PBSDRReporter is the slice-2 seam the pbsdr bridge loop plugs into (same consumer-side
|
||||
// pattern — hub does not import pbsdr). nil (feature not wired) → no pbs_dr stanza.
|
||||
type PBSDRReporter interface {
|
||||
PBSDRStatus(ctx context.Context) *PBSDRStatus
|
||||
}
|
||||
|
||||
// Collector builds a HostReport from read-only sources. All deps are behind narrow
|
||||
// interfaces for unit testing.
|
||||
type Collector struct {
|
||||
@@ -68,6 +74,7 @@ type Collector struct {
|
||||
capProbe func(ctx context.Context) []capability.Status // v0.44.0: privileged-capability self-check (nil → empty)
|
||||
leafFP string // v0.48.0: served local-API leaf fp (static per process; "" when local API disabled)
|
||||
wg WireguardReporter // S3: offsite-tunnel status (nil → stanza omitted)
|
||||
pbsdr PBSDRReporter // slice 2: PBS DR tier bridge state (nil → stanza omitted)
|
||||
selfUpdate SelfUpdateReporter // D1: agent self-update pending status (nil → false)
|
||||
mgmtPlane MgmtPlaneReporter // G1: management-plane health (nil → stanza omitted)
|
||||
oob OOBReporter // H1: operator-access health (nil → stanza omitted)
|
||||
@@ -127,6 +134,13 @@ func (c *Collector) SetWireguardReporter(w WireguardReporter) *Collector {
|
||||
return c
|
||||
}
|
||||
|
||||
// SetPBSDRReporter wires the PBS-DR-tier bridge state source (slice 2; nil-safe → stanza
|
||||
// omitted). Returns the collector for chaining.
|
||||
func (c *Collector) SetPBSDRReporter(p PBSDRReporter) *Collector {
|
||||
c.pbsdr = p
|
||||
return c
|
||||
}
|
||||
|
||||
// SelfUpdateReporter is the D1 seam the selfupdate commit-manager plugs into (same consumer-side
|
||||
// pattern — hub does not import selfupdate). nil (feature not wired) → pending=false on the report.
|
||||
type SelfUpdateReporter interface {
|
||||
@@ -204,6 +218,10 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
if c.wg != nil {
|
||||
report.Wireguard = c.wg.WireguardStatus(ctx)
|
||||
}
|
||||
// Slice 2: PBS DR tier bridge state (nil reporter = feature not wired → stanza omitted).
|
||||
if c.pbsdr != nil {
|
||||
report.PBSDR = c.pbsdr.PBSDRStatus(ctx)
|
||||
}
|
||||
// D1: agent self-update pending status (nil reporter → pending=false, the steady state).
|
||||
if c.selfUpdate != nil {
|
||||
report.SelfUpdatePending, report.SelfUpdatePendingVersion = c.selfUpdate.SelfUpdatePending()
|
||||
|
||||
@@ -77,6 +77,14 @@ type HostReport struct {
|
||||
// hub-schema change and are absent when the reporter is not wired.
|
||||
MgmtPlane *MgmtPlaneStatus `json:"mgmt_plane,omitempty"`
|
||||
|
||||
// PBSDR is the PBS-DR-tier bridge status stanza (slice 2). Present only when the pbsdr
|
||||
// consumer is wired. `consumed_failed` is the LOUD persistent state: the one-time token
|
||||
// secret was consumed but the apply failed afterwards — the secret is burned, the bridge
|
||||
// will NOT silently retry, the operator must Re-issue on the hub. Stored opaquely hub-side
|
||||
// (the Wireguard precedent) — additive, no hub-schema change; hub rendering joins in slice 3.
|
||||
// Carries NO secret.
|
||||
PBSDR *PBSDRStatus `json:"pbs_dr,omitempty"`
|
||||
|
||||
// OOB is the operator-access health stanza (TASK H1). It answers the operator's question — "can I
|
||||
// get into this box right now, and if not, why" — from the hub: felhom-sshd up + on which port,
|
||||
// locally reachable, the tunnel handshake age (the OOB path rides wg-felhom), whether the operator
|
||||
@@ -86,6 +94,21 @@ type HostReport struct {
|
||||
OOB *OOBStatus `json:"oob,omitempty"`
|
||||
}
|
||||
|
||||
// PBSDRStatus is the per-heartbeat PBS-DR-tier bridge state (slice 2). States:
|
||||
// "adopted" (existing entry verified + reconciled, no consume), "applied" (fresh entry created,
|
||||
// K born), "waiting_secret" (verified but no unconsumed secret staged — retrying),
|
||||
// "verify_failed" (fingerprint/reachability pre-consume check failing — retrying, NOTHING
|
||||
// consumed), "consumed_failed" (LOUD: secret burned, apply failed, no auto-retry — operator
|
||||
// re-issue required), "disabled" (descriptor enabled:false). Carries no secret.
|
||||
type PBSDRStatus struct {
|
||||
State string `json:"state"`
|
||||
StorageID string `json:"storage_id,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
ConsumedFailed bool `json:"consumed_failed,omitempty"`
|
||||
AppliedAt string `json:"applied_at,omitempty"` // RFC3339; set on adopted/applied
|
||||
}
|
||||
|
||||
// OOBStatus is the per-heartbeat operator-access health (TASK H1). Carries no secret.
|
||||
type OOBStatus struct {
|
||||
FelhomSshdActive bool `json:"felhom_sshd_active"` // the felhom-sshd unit is active
|
||||
@@ -372,6 +395,21 @@ type WireDesiredState struct {
|
||||
PBSNamespace string `json:"pbs_namespace,omitempty"`
|
||||
RestoreDirective *WireRestoreDirective `json:"restore_directive,omitempty"` // slice 10D (forward-compat)
|
||||
Wireguard *WireWireguard `json:"wireguard,omitempty"` // S3 (doc 06 §3.2; golden-pinned)
|
||||
PBSDR *WirePBSDR `json:"pbs_dr,omitempty"` // PBS DR tier (slice 2 consumer)
|
||||
}
|
||||
|
||||
// WirePBSDR is the hub's PBS-DR-tier descriptor (PBS DR slice 1, hub/internal/web/pbsdr.go
|
||||
// pbsDRDescriptor — field-exact, cross-repo). NON-SECRET by contract: the token secret NEVER
|
||||
// rides the desired-state; the agent fetches it consume-once via ConsumePBSToken. Absent/nil on
|
||||
// pre-v0.44.0 hubs → the pbsdr consumer no-ops (old-hub compat).
|
||||
type WirePBSDR struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
StorageID string `json:"storage_id,omitempty"`
|
||||
PBSTunnelIP string `json:"pbs_tunnel_ip,omitempty"`
|
||||
Datastore string `json:"datastore,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
TokenID string `json:"token_id,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
// WireWireguard is the hub-owned offsite-tunnel assignment (S3) — field-exact with the S2 golden
|
||||
|
||||
Reference in New Issue
Block a user