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.
- Fingerprint — the lookup handle: the first 16 hex characters of
sha256(pubkey) (the 32-byte raw Ed25519 public key).
- Roles —
r1(operator) andr2(admin). A human-only bucket (r3) covers
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:
| Header | Value |
|---|---|
X-Burrowee-Agent-Key | key fingerprint (lookup handle) |
X-Burrowee-Timestamp | unix seconds |
X-Burrowee-Nonce | random hex, per request |
X-Burrowee-Signature | base64 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 · Path | Auth | Purpose |
|---|---|---|
POST /api/agent/v1/bind/start | none | Begin a binding → device_code, user_code, verification_url, interval, expires_at. |
POST /api/agent/v1/bind/poll | none (device_code) | Poll a binding request → status (+ account on approval). |
GET /api/agent/v1/bind/authorize | none (browser) | The GitHub-consent landing the verification_url points at. |
GET /api/agent/v1/whoami | signed | Bound identity: fingerprint, role, tenant_id, label, last_seen_at. |
GET /api/agent/v1/keys | signed (R2) | List the account's agent keys. |
POST /api/agent/v1/keys/{fp}/revoke | signed (R2) | Revoke a key (instant lockout). |
POST /api/agent/v1/keys/approve | browser session (human) | Approve a pending key binding (R3 — human only, not agent-callable). |
L1 — product operations
| Method · Path | Auth | Purpose | ||
|---|---|---|---|---|
GET /api/agent/v1/account | signed (R1) | Account envelope: user, tenant, plan, status. | ||
PATCH /api/agent/v1/account | signed (R2) | Profile patch (forward-compatible; re-returns the envelope). | ||
GET /api/agent/v1/gateways | signed (R1) | List the tenant's gateways. | ||
POST /api/agent/v1/gateways/enroll | signed (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-domains | signed (R1) | List a gateway's custom domains. | ||
GET /api/agent/v1/gateways/{fp}/sessions | signed (R1) | List a gateway's sessions. | ||
GET /api/agent/v1/relays | signed (R1) | List edge relays. | ||
POST /api/agent/v1/relays | signed (R1) | Mint an edge relay (body { hostname_base, region, name, lan_mode }). | ||
POST /api/agent/v1/relays/{id}/approve | signed (R1) | Approve a pending edge relay. | ||
GET /api/agent/v1/acm/wildcards | signed (R1) | List verified wildcard apexes. | ||
POST /api/agent/v1/acm/wildcards | signed (R1, plan-gated) | Verify a wildcard apex (body { apex }); 403 plan_limit if not in plan. | ||
GET /api/agent/v1/sessions | signed (R1) | List sessions (`?filter=mine | shared | all`). |
POST /api/agent/v1/sessions/invite | signed (R1) | Mint a session share invite (body { gateway_fp, sid }) → invite_url. | ||
GET /api/agent/v1/teams | signed (R2) | List teams. | ||
POST /api/agent/v1/teams | signed (R2) | Create a team (body { name }). | ||
GET /api/agent/v1/teams/{id}/members | signed (R2) | List team members. | ||
POST /api/agent/v1/teams/{id}/members | signed (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 value — wrote 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:
- done — report
summary; mention anywrotepaths by path only (never open
them — they may be secrets).
- need_decision — ask the human
decision.prompt(offeringoptions/default),
then re-invoke the same verb with --decision <id>=<answer>. Decisions gate one at a time and accumulate on the command line.
- need_human — relay
message+url; do not proceed. - error — surface
message;codeis machine-readable (e.g.plan_limit,
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
| Verb | Purpose | |
|---|---|---|
bootstrap | Create an identity and bind it to a NEW account (GitHub). Flags: `--role r1\ | r2 (default r2), --label, --url`. |
bind | Bind this identity to an EXISTING account (web approval). Same flags. | |
whoami | Show the bound identity (signed). | |
keys / keys revoke <fp> | List / revoke bound keys (R2). | |
status | Show local identity + config state (no network). | |
version | Print the version. |
Operations (each emits a next-action; --decision k=v repeatable)
| Verb | Decisions |
|---|---|
gateway setup | expose_target, service_mode |
gateway list | — |
gateway rename <fp> | new_name |
cli pair | pairing_blob (from the gateway loopback console) |
edge setup | hostname_base, service_mode (+ optional lan_mode, region, name) |
edge approve | relay_id |
domain add | apex |
domain list | — |
session list | filter (mine/shared/all) |
session share | gateway_fp, sid |
session create / session revoke <sid> | — (gateway box only; else error:not_on_gateway) |
account show | — |
account set | profile fields (each a --decision) |
account billing / account upgrade / account delete | — (always need_human) |
team list | — |
team create | team_name |
team member add <team_id> | member_login (+ optional member_role) |
Note:cli connectandcli sshare notburrowee-agentverbs — once paired,
connections are run by theburrowee-clibinary 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.