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:
2026-07-10 21:50:36 +02:00
parent a6e8bcb475
commit e5e8f3920a
15 changed files with 1309 additions and 2 deletions
+44
View File
@@ -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.