Burrowee Agent API

This is the agent-facing API for Burrowee: the binding ceremony, the request-signing protocol, the /api/agent/v1 endpoint reference, the next-action protocol, and the burrowee-agent CLI reference.

burrowee-agent is open-source and embeds no secrets — every user generates their own keypair locally. The binding + signing protocol below is therefore a public, reimplementable protocol: anyone can build an independent client. Security rests on the private key (which never leaves the machine), not on client obscurity.

The control plane lives in the Burrowee console service; the production base URL is https://console.burrowee.com. All paths below are relative to it.


1. Identity

The agent holds an Ed25519 keypair, like an SSH key. Only the public key is registered with the account; the agent proves itself by signing each request. The private key is encrypted at rest locally and never transmitted.

sha256(pubkey) (the 32-byte raw Ed25519 public key).

binding approval, tier assignment, billing, and account deletion — an agent can never perform these.

Local layout (~/.burrowee/agent/): the encrypted private key, the raw public key, and config.json ({ control_plane_url, fingerprint, role, tenant_id }).


2. Binding ceremony

Binding is the one human touch and the only operation an agent cannot complete alone. It is the OAuth 2.0 Device Authorization Grant pattern, with GitHub as the upstream identity provider and the agent's pubkey registered as part of the grant.

The binding endpoints are unsigned — the key is not trusted yet.

New account (bootstrap)

1. The agent generates a keypair and calls POST /api/agent/v1/bind/start with { pubkey, requested_role: "r2", mode: "new", label }. 2. The response carries `{ device_code, user_code, verification_url, interval, expires_at }. The agent prints verification_url` for the human. 3. The human opens it and consents via GitHub. The GitHub consent screen is the binding approval (only for a genuinely new account — see below). The callback creates the tenant, binds the pubkey as the first R2 agent, and marks the request approved. 4. The agent polls POST /api/agent/v1/bind/poll with { device_code } until status: "approved", then persists its config.

Auto-binding to R2 happens only when no Burrowee account exists yet for that GitHub identity. If the identity already has an account, the callback logs the human in and routes them to explicit approval (below) — adding a key to an existing account always takes a deliberate human approval.

Existing account (bind)

Same bind/start with mode: "existing". The agent prints verification_url and user_code; the human opens the URL, signs in, finds the pending key by that code, picks its tier (additional agents default to R1), and approves. The agent polls bind/poll to approved.

binding_request.status ∈ { pending, approved, denied, expired }; requests expire after a short TTL (default 10 min). The CLI backs off at the server-provided interval.


3. Request-signing protocol

Every authenticated control-plane call carries four headers:

HeaderValue
X-Burrowee-Agent-Keykey fingerprint (lookup handle)
X-Burrowee-Timestampunix seconds
X-Burrowee-Noncerandom hex, per request
X-Burrowee-Signaturebase64 Ed25519 signature of the canonical string

The canonical string is newline-joined (no trailing newline), in this exact order:

<HTTP-METHOD>
<request-path-with-query>
<hex sha256 of the raw request body>
<timestamp>
<nonce>

For example, a GET /api/agent/v1/whoami with an empty body signs:

GET
/api/agent/v1/whoami
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1700000000
ab12cd34…

(e3b0c4…b855 is sha256("").)

Server verification, in order: look up the key by fingerprint (401 if unknown); reject if revoked (401); recompute the canonical string and verify the Ed25519 signature against the stored pubkey (401 on mismatch); reject if |now − timestamp| > 60s (401); reject a replayed nonce within the window (401); load tenant_id + role and enforce the role for the operation (403 if out of tier); update last_seen_at; proceed.


4. Endpoint reference (/api/agent/v1)

Binding endpoints are unsigned; everything else requires a valid signature (§3) and the role shown.

L0 — identity

Method · PathAuthPurpose
POST /api/agent/v1/bind/startnoneBegin a binding → device_code, user_code, verification_url, interval, expires_at.
POST /api/agent/v1/bind/pollnone (device_code)Poll a binding request → status (+ account on approval).
GET /api/agent/v1/bind/authorizenone (browser)The GitHub-consent landing the verification_url points at.
GET /api/agent/v1/whoamisignedBound identity: fingerprint, role, tenant_id, label, last_seen_at.
GET /api/agent/v1/keyssigned (R2)List the account's agent keys.
POST /api/agent/v1/keys/{fp}/revokesigned (R2)Revoke a key (instant lockout).
POST /api/agent/v1/keys/approvebrowser session (human)Approve a pending key binding (R3 — human only, not agent-callable).

L1 — product operations

Method · PathAuthPurpose
GET /api/agent/v1/accountsigned (R1)Account envelope: user, tenant, plan, status.
PATCH /api/agent/v1/accountsigned (R2)Profile patch (forward-compatible; re-returns the envelope).
GET /api/agent/v1/gatewayssigned (R1)List the tenant's gateways.
POST /api/agent/v1/gateways/enrollsigned (R1)Mint a gateway enroll blob (body { hostname }).
PATCH /api/agent/v1/gateways/{fp}signed (R1)Rename a gateway (body { label }).
GET /api/agent/v1/gateways/{fp}/custom-domainssigned (R1)List a gateway's custom domains.
GET /api/agent/v1/gateways/{fp}/sessionssigned (R1)List a gateway's sessions.
GET /api/agent/v1/relayssigned (R1)List edge relays.
POST /api/agent/v1/relayssigned (R1)Mint an edge relay (body { hostname_base, region, name, lan_mode }).
POST /api/agent/v1/relays/{id}/approvesigned (R1)Approve a pending edge relay.
GET /api/agent/v1/acm/wildcardssigned (R1)List verified wildcard apexes.
POST /api/agent/v1/acm/wildcardssigned (R1, plan-gated)Verify a wildcard apex (body { apex }); 403 plan_limit if not in plan.
GET /api/agent/v1/sessionssigned (R1)List sessions (`?filter=minesharedall`).
POST /api/agent/v1/sessions/invitesigned (R1)Mint a session share invite (body { gateway_fp, sid }) → invite_url.
GET /api/agent/v1/teamssigned (R2)List teams.
POST /api/agent/v1/teamssigned (R2)Create a team (body { name }).
GET /api/agent/v1/teams/{id}/memberssigned (R2)List team members.
POST /api/agent/v1/teams/{id}/memberssigned (R2)Add a member (body { github_login, role }).

Plan-gating (domain/edge) is enforced server-side from the tenant's plan tier, so an agent cannot exceed the account's entitlements.

Local plane. Some operations target one gateway's own state and never touch the cloud: the cli pairing blob is minted by the gateway's loopback console (127.0.0.1:16518), and session create / session revoke run against that local console. These are authorized by local presence (being on the box), not by the account key — there is no /api/agent/v1 endpoint for them.


5. Next-action protocol

Every burrowee-agent workflow verb (gateway, cli, edge, domain, session, account, team) exits in exactly one state, emitted as a single line of JSON on stdout. The object is machine-readable and never carries a secret valuewrote lists file PATHS only.

// success
{"status":"done","summary":"gateway up: target 127.0.0.1:22","wrote":["~/.burrowee/gateway/psk"]}

// a human decision is needed
{"status":"need_decision","decision":{"id":"expose_target","prompt":"Which local service should this gateway expose?","options":["127.0.0.1:22"],"default":"127.0.0.1:22"}}

// a human-only operation (R3)
{"status":"need_human","reason":"upgrade","message":"Upgrading the plan needs you.","url":"https://console.burrowee.com/billing"}

// a recoverable error
{"status":"error","code":"plan_limit","message":"custom domains not in plan"}

A client consumes it as:

them — they may be secrets).

then re-invoke the same verb with --decision <id>=<answer>. Decisions gate one at a time and accumulate on the command line.

not_on_gateway).

The identity verbs (bootstrap, bind, whoami, status, keys) print plain human-readable lines instead of this JSON.


6. burrowee-agent CLI reference

The binary is burrowee-agent. Identity verbs print plain text; workflow verbs emit the next-action JSON (§5) and accept repeatable --decision key=value flags. The control-plane URL comes from --url or BURROWEE_CONTROL_PLANE_URL; the at-rest key passphrase from BURROWEE_AGENT_PASSPHRASE.

Identity

VerbPurpose
bootstrapCreate an identity and bind it to a NEW account (GitHub). Flags: `--role r1\r2 (default r2), --label, --url`.
bindBind this identity to an EXISTING account (web approval). Same flags.
whoamiShow the bound identity (signed).
keys / keys revoke <fp>List / revoke bound keys (R2).
statusShow local identity + config state (no network).
versionPrint the version.

Operations (each emits a next-action; --decision k=v repeatable)

VerbDecisions
gateway setupexpose_target, service_mode
gateway list
gateway rename <fp>new_name
cli pairpairing_blob (from the gateway loopback console)
edge setuphostname_base, service_mode (+ optional lan_mode, region, name)
edge approverelay_id
domain addapex
domain list
session listfilter (mine/shared/all)
session sharegateway_fp, sid
session create / session revoke <sid>— (gateway box only; else error:not_on_gateway)
account show
account setprofile fields (each a --decision)
account billing / account upgrade / account delete— (always need_human)
team list
team createteam_name
team member add <team_id>member_login (+ optional member_role)
Note: cli connect and cli ssh are not burrowee-agent verbs — once paired,
connections are run by the burrowee-cli binary directly (burrowee-cli connect …
/ burrowee-cli ssh …).

7. Trust boundary

The cornerstone: **the LLM never sees or handles keys, signatures, tokens, or raw API calls.** The agent only shells out to burrowee-agent commands; all crypto and HTTP happen inside the CLI. A leaked or prompt-injected context contains nothing reusable, and the worst a hijacked context can do is invoke commands the bound key already authorizes — bounded by the key's role and killable by revoking the key. Clients (and skills) must always invoke the CLI; never construct a signature, curl the API by hand, or echo a token or a written secret file.