feat(cli): route custody through the hub /api/v1 (thin CLI Step 10) - #290
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
b63a448 to
2e35356
Compare
|
Addressed the Bugbot finding (admin-only custody clients silently failing) in 2e35356. The API key is genuinely required to reach |
hub credentials/secrets/custody/project (push/pull/list/rm), the project seed push, and per-box SSH-key adoption now speak /api/v1/custody instead of /admin/custody — the CLI's last routine /admin client use is gone, and the tray/web can drive custody. Two-tier, metadata-only contract on the new route: list/PUT/DELETE authorize with the hub API key and return metadata only (never a value); the byte-read GET /api/v1/custody/<path> additionally requires the admin token on a control box (X-Agentbox-Admin-Token) and FAILS CLOSED, so a thin client holding only the widely-distributed API key can never read a stored credential / .env / per-box SSH private key. A plain local hub (token profile) authorizes byte-reads with its machine-local hub token. The decision is a pure, unit-tested helper so the invariant survives a route refactor. Custody is now wired on every hub (not just a control box), so /api/v1/custody serves identically local and remote; the relay's /admin/custody wire stays admin-token-gated. The project seed push became transport-agnostic (SeedCustodySink): the CLI uses a /api/v1 sink, the create path keeps its /admin sink. Verified e2e: local-hub push->list->pull byte-for-byte round-trip; password-profile byte-read refused with the API key alone, served with the admin token, refused with a wrong token; remote-shaped CLI round-trip (push/list/pull/rm) + a thin-client pull refusal. Claude-Session: https://claude.ai/code/session_0173RGmXq6CGQgxuhcvgLRkh
2e35356 to
a738c31
Compare
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit a738c31. Configure here.
What
Step 10 of
docs/hub-api-single-path-plan.md: move custody off the internal/admin/custodywire onto the public/api/v1/custody, so the CLI, tray and web all drive custody through one client surface. This removes the CLI's last routine/admin/*client use.Converted:
hub credentials push|pull,hub secrets push,hub project push(seed),hub custody list|pull|rm, the login-time + change-detected agent-credential sync, thehub workerseed fetch, and per-box SSH-key adoption (hub adopt/hub pull, auto-adopt, recover, cloud-agent-via-hub).The two-tier, metadata-only contract (the load-bearing part)
Custody holds agent credentials,
.envfiles and per-box SSH private keys — the highest-value target in the API — and the/api/v1gate is the hub API key, which on a control box travels to the tray/web/thin clients. So:list/PUT/DELETEauthorize with the hub API key and return metadata only (path, size, sha256, mode, mtime;changedon PUT) — never a stored value.GET /api/v1/custody/<path>is the one route that returns bytes, so it needs a second, non-distributed credential: on the password profile (a real/exposed control box) the admin token viaX-Agentbox-Admin-Token. It fails closed — missing header, wrong token, or an unset admin-token env all refuse; there is no path that degrades to API-key-only. A caller with only the API key is401. On a plain local hub (token profile) the byte-read is authorized by the machine-local hub token itself (single trusted machine).The decision is the pure, unit-tested
custodyByteReadAuthorized(apps/hub/lib/custody-auth.ts) so the invariant survives a route refactor —apps/hub/test/custody-auth.test.tsasserts an API-key-only caller cannot read bytes, and the client test asserts the same end.Custody is now wired on every hub (not just a control box), so
/api/v1/custodyserves identically local and remote; the relay's/admin/custodywire stays admin-token-gated (a plain local hub serves custody only over its token-gated/api/v1). The project seed push became transport-agnostic (SeedCustodySink): the CLI injects a/api/v1sink; the create path keeps its/adminsink (it holds the admin token — internal registration flow, Step 11 territory).Verified end-to-end (ground truth, not exit codes)
credentials push→custody list(curl-confirmed nodatafield) →credentials pull+custody pull --destround-tripped byte-for-byte; store landed at~/.agentbox/hub/custody/.list/PUTmetadata only; no-auth → 401.--url+AGENTBOX_HUB_API_KEY+AGENTBOX_RELAY_ADMIN_TOKEN): push → list → pull (byte match) → rm; and a thin client (API key, no admin token) is refused on pull.pnpm typecheck(cli/hub/sandbox-cloud), unit tests, andeslintall green.Notes
feat/cli_api_consolidation. Step 9 (PR feat(cli): route checkpoint/prune/agent/logs through the hub /api/v1 (thin CLI Step 9) #289) is in parallel; expected overlap iscontrol-plane.ts.apps/web/content/docs/api.mdx+ the OpenAPI spec document the new verbs and the two-tier contract; the plan doc is ticked with a Notes-for-later-steps block.https://claude.ai/code/session_0173RGmXq6CGQgxuhcvgLRkh
Note
High Risk
Changes authentication and transport for agent credentials, env secrets, and per-box SSH private keys, with a new fail-closed byte-read gate that must not regress to API-key-only reads.
Overview
Step 10 routes routine custody work through
/api/v1/custodyinstead of/admin/custody: CLIhub credentials|secrets|project|custodycommands, background credential sync, worker seed fetch, and SSH-key adoption now resolve targets viaresolveCustodyApiTargetandCustodyClient(withdelete()).CustodyClientprefers the API key on/api/v1and falls back to admin-only/adminwhen no key is present so SSH pulls do not silently skip.The hub adds
GET/PUT/DELETE /api/v1/custody/{path}pluscustodyByteReadAuthorized: list/put/delete stay metadata-only on the hub API key; byte-read GET on a control box requiresX-Agentbox-Admin-Token(fail-closed). Custody store is wired on every hub;pushProjectSeedToCustodytakes an injectedSeedCustodySink(CLI uses v1; create keepsadminCustodySink).Reviewed by Cursor Bugbot for commit a738c31. Configure here.