arizuko

arizuko › reference › OpenAPI

openapi

Every HTTP-serving daemon exposes its API at GET /openapi.json. The resource registry generates the document. components/schemas comes from resreg.Resource.RowType reflection. The paths come from the resource's Endpoints, the same slice the daemon mounts its handlers from. Doc and handler read one declaration, so they move together.

Every HTTP daemon publishes an OpenAPI 3.1 document

OpenAPI 3.1 JSON. Public — no auth gate. Cached for the process lifetime; reflection runs once per daemon. Paste a URL into any conformant tool (Swagger UI, Redoc, Stoplight Elements, an SDK generator) and the daemon's surface is browsable, callable, and codegen-ready.

Each daemon owns its resources

DaemonURLOwned resources
routd https://<host>/openapi.json via proxyd, or http://routd:8080/openapi.json intra-container routes, web_routes, acl, secrets (write-only), route_tokens, installed_packages (read-only), scheduled_tasks (at /v1/tasks), groups (read-only), chats, acl_membership, pending_actions, dead_letters, github_sources, gdrive_files, audit, network_rules (MCP only)
proxyd http://proxyd:8080/openapi.json proxyd_routes
onbod http://onbod:8080/openapi.json onboarding, onboarding_gates, invites, audit
webd http://webd:8080/openapi.json none — forwards proxyd_routes CRUD to proxyd; doc is informational
dashd http://dashd:8080/openapi.json none — HTMX operator UI; cold-tier CRUD lives in routd
timed http://timed:8080/openapi.json none — routd owns scheduled_tasks; timed drives the fire loop as a client of it
authd http://authd:8080/openapi.json audit, signing_keys (read-only), sessions (read + revoke) — its own audit trail, signing-key metadata, and refresh-token families; the token endpoints themselves are hand-rolled and carry no row schema
runed http://runed:8080/openapi.json audit (read-only) — its own audit trail; runs and holds stay hot-tier
litellm http://litellm:4000/openapi.json none of arizuko’s — upstream litellm’s own FastAPI document for the model gateway package; not engine-generated, not gated by the resource registry

Daemons that own no resources still emit a valid OpenAPI document so external tooling can introspect them uniformly. The schemas + paths blocks are empty but info, servers, and the standard components/responses (400, 401, 403, 404, 409, 500) are present.

routd's own /openapi.json lists the resources above and only those, because it advertises only what it mounts. An endpoint in the document is an endpoint you can call; there is no separate list to forget to update. Several are narrower than full CRUD, and deliberately so:

One path is not /v1/<name>: scheduled_tasks is served at /v1/tasks. timed already calls GET /v1/tasks and its fire loop shares the prefix (/v1/tasks/due, /v1/tasks/runlog, /v1/tasks/{id}/reschedule), all from a separate container that restarts on its own schedule — renaming half of a live control surface is the drift, not the fix. So this one is a deliberate exception, written down here so nobody files it as a bug. The name is still the identity everywhere else: the MCP tool prefix, the operationId, and the resource= column in the audit log all read scheduled_tasks. A new resource does not get the exception — it is served at /v1/<name>.

Three more resources live in the same registry and drive the arizuko dump/apply/plan YAML surface (see CLI reference) without appearing in routd's REST document. network_rules is agent-MCP-only by design, so it has no REST face to advertise; acl_membership has one face, the pairing undo DELETE /v1/acl_membership (unpair for the agent); proxyd_routes lives in routd's database but is served by proxyd, which advertises it in its own document.

One handler serves two faces

Here is what sits behind every operation in the document. One declaration, two ways in, one handler, one audit row. Read the gates: the REST face checks the bearer, the agent face checks the folder’s grants against the actual target.

┌───────────────────────────────────────────────────────────────────────────┐
│ resreg.Resource, declared once in resreg/resources/<name>.go:             │
│ Name, Table, RowType, PKFields, Endpoints, MCPDoc, MCPArgs                │
│ the daemon takes it by resreg.Mount and sets Authz, Handler, Store, Gate  │
└──────────────┬─────────────────────────────────────────────┬──────────────┘
               ▼                                             ▼
┌──────────────────────────────────┐ ┌──────────────────────────────────────┐
│ REST face: RegisterREST          │ │ MCP face: MCPTools                   │
│ /v1/<name>, one op per Endpoint; │ │ deriveMCPTools: one tool per         │
│ /openapi.json from the mounts    │ │ Endpoint that has an MCPDoc,         │
│ caller: the bearer's sub, scope  │ │ <name>.<action> or MCPNames          │
│ and folder claim                 │ │ caller: the turn's folder and sender │
│ Gate: a bearer-scope check, or   │ │ Gate: AuthorizeTurn on the target;   │
│ Authorize on <name>:<action>     │ │ target-folder resources run it in    │
│ target-folder resources add      │ │ containFn behind agentAllowGate      │
│ containFn, one per resource      │ │                                      │
└──────────────┬───────────────────┘ └──────────────────┬───────────────────┘
               └────────────────────┬───────────────────┘
                                    ▼
┌───────────────────────────────────────────────────────────────────────────┐
│ invoke: Authz ► Gate ► Handler ► one GuardedBatch with the handler's      │
│ writes and one audit_log row; a successful read writes no row             │
└───────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────┐
│ beside it: a forwarder (Store nil: no gate, no row here) for secrets on   │
│ routd and proxyd_routes on webd; the hot tier, MCP-only in ipc/ipc.go:    │
│ reply, send, like, inspect_*                                              │
└───────────────────────────────────────────────────────────────────────────┘

Reflection on the row type builds the document

Each cold-tier resource is one Go struct with db:, json:, and yaml: tags — the registry lives in routd. resreg.OpenAPI(daemon, baseURL, resources) walks the registered resources and emits:

The engine and its rules: resreg/README.md.

x-mcp-when marks the operations for agents

Each of these operations has a second face. The agent reaches the same handler as an MCP tool over its per-folder unix socket. The tool name is <name>.<action>, or the flat name that the MCPNames map of the resource declares, such as add_route, with arguments reflected from the same RowType. One handler, one transaction, one audit row — two ways in.

What ties them together in the document is x-mcp-when: a vendor extension carrying the one-line description of when an agent should reach for this action. The same line is written twice — once as the operation's description, so a tool that has never heard of arizuko reads a normal OpenAPI 3.1 doc, and once as x-mcp-when, so an agent or a tool browser can pick out the agent-facing operations mechanically, without a second hand-written tool list to keep in sync.

curl -s https://<host>/openapi.json \
  | jq '.paths | to_entries[] | .value | to_entries[]
        | select(.value["x-mcp-when"]) | .value.operationId'

The rule is strict on purpose: an action with no agent-facing description gets neither an MCP tool nor an x-mcp-when annotation. That is how an operation is made REST-only — by saying nothing, rather than by maintaining an exclusion list. The reverse also holds: the hot-tier agent actions (reply, send, inspect_*) are MCP-only and appear in no OpenAPI document, because there is no operator endpoint to mirror — nobody administers a chat reply.

The document shows shape, not permission. The document describes the shape of a surface, never a permission. Whether a given caller may perform an operation is decided per call by grants, and the two faces check different things — the REST face the bearer’s scope and folder claim, the MCP face the turn folder’s grants against the actual target.

curl and jq read the document

The JSON is a regular text file — curl + jq works fine for most lookups:

curl -s https://<host>/openapi.json | jq '.paths | keys'
curl -s https://<host>/openapi.json | jq '.components.schemas.Routes'

For an interactive renderer, point Swagger UI or Redoc at the URL. Neither is bundled with arizuko; pick the tool your operator workflow already uses.

Standard generators build typed clients

Any OpenAPI 3.1 codegen tool (openapi-generator, swagger-codegen, oapi-codegen) can consume the spec to produce a typed client. Run the generator against the live daemon's URL or save the JSON and run offline. Re-run on every release — the spec moves with the struct, so clients stay current by re-generating.

The document has three limits