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.
|
||||
|
||||
Reference in New Issue
Block a user