opbox

Opbox has one capability surface: a verb. There is no second API, no GraphQL layer, no hidden admin path. Everything you can do, a human or an agent does by calling a verb through the kernel. This page is the integrator’s view of that surface: how to authenticate, how to call a verb over HTTP, and how the same verbs appear to a caged agent over MCP.

If you have not read it yet, The kernel explains why the surface is shaped this way. This page is the how.

One surface, two doors

The kernel is a single Rust binary that serves an axum HTTP router. The router is deliberately tiny:

RoutePurposeAuth
POST /v/<verb>Call a verb. The raw JSON request body is the verb input.Bearer
POST /mcpThe Model Context Protocol door - the caged agent’s surface.Bearer
POST /auth/loginEmail + password to a session bearer token.none (bootstrap)
POST /auth/reset/request, POST /auth/reset/confirmPassword reset.none (bootstrap)
GET /healthLiveness probe for the proxy/orchestrator.none (the only open door)

Two of these are the “real” doors:

  • POST /v/<verb> is for humans, clients, and the CLI - anything that already knows which verb it wants to call.
  • POST /mcp is for the agent. The agent does not hard-code verb names; it discovers them as MCP tools and calls them. See MCP for agents below.

Both doors run the same dispatch and the same checks over the same registry and the same database pool (INV-14). There is no second backend hiding behind the agent door - an agent and a human invoking matter.get traverse identical code, identical authz, identical audit. The only difference is which source the door stamps on the request (http vs mcp), and that stamp is used to enforce that an agent identity may only speak through MCP and a human identity may only speak through HTTP.

GET /health is the single unauthenticated endpoint. It returns a static {"status":"ok"} with no version or build string (an open edge must not disclose what it is running). Every other route requires a valid bearer token; the /auth/* bootstrap doors are not “open” in the privileged sense - they take credentials and hand back a token, they do not dispatch verbs.

Auth

Authentication is a bearer token in the standard HTTP header:

Authorization: Bearer <token>

Every call to /v/<verb> and /mcp carries this header. The kernel resolves the token to an identity before any dispatch happens; a missing or malformed header, or an invalid/expired/revoked token, fails closed with 401 (no identity) - it never falls back to a default actor.

Getting a token

The first token comes from the genesis ceremony when a box is provisioned (see Genesis). After that, an interactive human gets a session token by posting credentials to the unauthenticated login door:

curl -sS https://box.example.com/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email": "[email protected]", "password": "..."}'

On success the response is a session bearer plus the actor it belongs to:

{
  "sessionToken": "tok_...",
  "actorId": "act_...",
  "workspaceId": "ws_...",
  "displayName": "Your Name"
}

You then send sessionToken as the Authorization: Bearer value on every subsequent verb call. If the actor is enrolled in two-factor auth, login returns {"mfaRequired": true} instead and the MFA step must complete first. A wrong email, wrong password, or inactive actor all return the same 401 INVALID_CREDENTIALS - the door is deliberately enumeration-safe (you cannot tell “no such user” from “wrong password”).

What a token can do

A token is not all-or-nothing. The identity it resolves to carries three independent limits, and the dispatch gate checks all three before a verb runs:

  • Authz tier - the caller’s standing (MEMBER -> ADMIN -> OWNER, or EXTERNAL for portal/provider surfaces) must meet the verb’s required tier.
  • Autonomy ceiling - the per-request autonomy level is min(actor autonomy, token ceiling), in the range 0-3. A token can only ever lower the actor’s autonomy, never raise it. Level 0 is read-only; sensitive and owner operations need higher levels.
  • Capability scope - if the token carries a verb allow-list, the verb being called must be on it. This is the narrowest, per-token fence.

This is why least-privilege in Opbox is tier + autonomy + scope, never a feature flag. A bounded agent token at autonomy 1 can read and create matters but cannot perform a sensitive or owner operation, whatever its scope says. The full model is in Security & permissions.

No dev fallback in release

A development build can resolve a “dev identity” for a bearerless request, which is convenient locally. A release binary is compiled without that path (the dev-identity cargo feature is dropped), so a production server is token-only: there is no way to reach a verb without a valid bearer. This is a compile-time property, not a config switch - a production build literally cannot construct the privileged fallback identity.

Calling a verb

A verb call is one HTTP request:

  • Method/route: POST /v/<verb>, e.g. POST /v/matter.get.
  • Headers: Authorization: Bearer <token>; Content-Type: application/json is conventional but not required (the kernel parses the body as JSON itself, so a plain curl -d still works).
  • Body: the raw JSON input for the verb. The body is the input object - there is no envelope, no {"params": ...} wrapper. An empty body is treated as {}.

The response is JSON. On success it is the verb’s output object (a 200). On failure it is a small error object:

{ "error": "human-readable message", "code": "MACHINE_CODE" }

with an HTTP status that matches the code (400 bad input, 401 unauthenticated, 403 forbidden, 409 conflict, 500 internal). Branch on code, not on the message text.

Example: read a matter

curl -sS https://box.example.com/v1/v/matter.get \
  -H 'Authorization: Bearer tok_...' \
  -H 'Content-Type: application/json' \
  -d '{"matterId": "mat_01H..."}'

A 200 returns the matter (its steps, dependencies, and current cursor). matter.get is a matter.read verb at MEMBER tier, so any member who can see that matter may call it; an actor who cannot see it gets a 403 rather than a leak.

Example: create a matter

curl -sS https://box.example.com/v1/v/matter.create \
  -H 'Authorization: Bearer tok_...' \
  -H 'Content-Type: application/json' \
  -d '{"templateVersionId": "mtpl_v_01H...", "title": "Acme - incorporation"}'

This instantiates a new matter from a template version. It is a matter.write verb at MEMBER tier.

Idempotency

matter.create is not idempotent on its own - call it twice and you get two matters. For any verb, you can make a retry safe by sending an Idempotency-Key header. A repeated POST /v/<verb> carrying the same key returns the first call’s stored response instead of re-executing. The dedup is keyed (workspace, key) and applies to the HTTP door only; with no header the path is byte-identical, so it is purely additive. This is what lets a durable orchestrator (see Hatchet orchestration) retry a step without double-running it. Sending the same key with a different verb, or while the first call is still in flight, returns 409 CONFLICT.

The verb catalogue

This page shows the shape of a call, not the list of verbs. For that:

  • Verb reference - the exhaustive, generated list of every verb in every noun domain (matter, form, doc, board, bill, party, org, token, and the rest), with each verb’s tier, risk, autonomy, and idempotency.
  • Capabilities - the same surface organised by capability: what the substrate can do, grouped by what an integrator is trying to achieve.

The verb name in POST /v/<verb> is exactly the name shown in those references (e.g. matter.get, form.submit, token.mint).

MCP for agents

The /mcp door speaks JSON-RPC 2.0 over HTTP (the MCP “Streamable HTTP” transport). To an agent, the kernel is an MCP server and the verbs are its tools:

  • tools/list returns the verbs the bearer is allowed to see - the kernel filters by the token’s standing, there is no client-side allow-list to trust.
  • tools/call invokes one verb. Behind the scenes this lands in the same dispatch as POST /v/<verb>: same registry, same pool, same authz/autonomy/scope checks, same audit event.

A minimal tools/call body looks like:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "matter.get",
    "arguments": { "matterId": "mat_01H..." }
  }
}

with the same Authorization: Bearer <token> header as any other call. A JSON-RPC notification (no id) gets a 202 with no body; a request gets the JSON-RPC response.

The MCP door is the agent door, and only the agent door. The kernel asserts that an identity arriving over /mcp is an AGENT; a human or system token presented there is rejected (403). Conversely an agent identity may only speak through /mcp - it cannot reach the HTTP verb door. This is what makes the cage real: the agent is kernel-MCP-only and egress-locked, with no native shell, file, web, or code-execution tools, so its entire reach into firm and matter data is exactly the verb surface its bearer can see. The full agent story - the bounded chat-agent, why the public chat never runs under the owner key, and the caged engine - is in Agents & MCP.

Errors and audit

Two properties hold for every call, on both doors:

  • Fail-closed. A call that cannot be authorised, parsed, or run does not partially apply and does not leak. It returns {error, code} with the matching status, and nothing was written. The gate runs before the handler, so an unauthorised call never touches data. Common codes: BAD_INPUT (malformed JSON / missing field), INVALID_CREDENTIALS (login), CONFLICT (idempotency or in-flight collision), and the authz denials surfaced as 401/403.

  • Audited. Every verb call appends exactly one hash-chained audit event, in the same database transaction as the work it records. The event is attributable (which actor, which token, which door) and tamper-evident (the hash chain). There is no “quiet” path: if a verb ran, there is an event; if there is no event, the verb did not run. You can read the trail back through the audit.* verbs in the Verb reference.

Together these are the integration contract: one front door, one bearer, one dispatch, and an immutable record of everything that went through it.