Skip to content

feat(cli): route custody through the hub /api/v1 (thin CLI Step 10) - #290

Merged
madarco merged 1 commit into
feat/cli_api_consolidationfrom
agentbox/step10-custody
Jul 30, 2026
Merged

feat(cli): route custody through the hub /api/v1 (thin CLI Step 10)#290
madarco merged 1 commit into
feat/cli_api_consolidationfrom
agentbox/step10-custody

Conversation

@madarco

@madarco madarco commented Jul 30, 2026

Copy link
Copy Markdown
Owner

What

Step 10 of docs/hub-api-single-path-plan.md: move custody off the internal /admin/custody wire 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, the hub worker seed 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, .env files and per-box SSH private keys — the highest-value target in the API — and the /api/v1 gate is the hub API key, which on a control box travels to the tray/web/thin clients. So:

  • list / PUT / DELETE authorize with the hub API key and return metadata only (path, size, sha256, mode, mtime; changed on PUT) — never a stored value.
  • byte-read 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 via X-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 is 401. 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.ts asserts 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/custody serves identically local and remote; the relay's /admin/custody wire 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/v1 sink; the create path keeps its /admin sink (it holds the admin token — internal registration flow, Step 11 territory).

Verified end-to-end (ground truth, not exit codes)

  • Local hub (token profile): credentials pushcustody list (curl-confirmed no data field) → credentials pull + custody pull --dest round-tripped byte-for-byte; store landed at ~/.agentbox/hub/custody/.
  • Password-profile hub (spawned standalone): byte-read with the API key alone → 401; with a wrong admin token → 401 (fail closed); with the admin token → 200 value; list/PUT metadata only; no-auth → 401.
  • Remote-shaped CLI (--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, and eslint all green.

Notes

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/custody instead of /admin/custody: CLI hub credentials|secrets|project|custody commands, background credential sync, worker seed fetch, and SSH-key adoption now resolve targets via resolveCustodyApiTarget and CustodyClient (with delete()). CustodyClient prefers the API key on /api/v1 and falls back to admin-only /admin when no key is present so SSH pulls do not silently skip.

The hub adds GET/PUT/DELETE /api/v1/custody/{path} plus custodyByteReadAuthorized: list/put/delete stay metadata-only on the hub API key; byte-read GET on a control box requires X-Agentbox-Admin-Token (fail-closed). Custody store is wired on every hub; pushProjectSeedToCustody takes an injected SeedCustodySink (CLI uses v1; create keeps adminCustodySink).

Reviewed by Cursor Bugbot for commit a738c31. Configure here.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentbox-web Ready Ready Preview Jul 30, 2026 3:31am

Request Review

Comment thread apps/cli/src/commands/control-plane.ts Outdated
@madarco
madarco force-pushed the agentbox/step10-custody branch from b63a448 to 2e35356 Compare July 30, 2026 03:16
@madarco

madarco commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Addressed the Bugbot finding (admin-only custody clients silently failing) in 2e35356.

The API key is genuinely required to reach /api/v1/custody at all — the proxy gate needs it on a control box, so returning null without it is correct. The bug was that it was silent. resolveCustodyApiTarget({remoteOnly}) now distinguishes the two cases: "no control box" → silent null (the expected no-op); "control box configured but no API key" → an actionable error naming AGENTBOX_HUB_API_KEY for interactive callers, while the best-effort quiet callers degrade downstream — createCloudBoxViaHubAndAdopt still adopts the record and adoptHubBox flags sshKeysMissing (with a 'try hub pull' hint), so an SSH provider's missing key is reported, never silently dropped. Comment at the create-via-hub call site updated to say so.

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
@madarco

madarco commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@madarco
madarco merged commit 9ee09f6 into feat/cli_api_consolidation Jul 30, 2026
5 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant