arizuko

arizukoreference › OpenAPI

openapi

Every HTTP-serving daemon exposes its API at GET /openapi.json. The document is generated from the resource registry — components/schemas comes from resreg.Resource.RowType reflection, and 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.

What you get

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.

Endpoints by daemon

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)
proxyd http://proxyd:8080/openapi.json proxyd_routes
onbod http://onbod:8080/openapi.json onboarding, onboarding_gates, invites
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 nine resources above — sixteen paths — and only those, because it advertises only what it actually mounts. An endpoint in the document is an endpoint you can call; there is no separate list to forget to update. Four of the eight 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 export/apply 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 is dashd-FS-managed; proxyd_routes lives in routd's database but is served by proxyd, which advertises it in its own document.

How it works

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:

Spec: specs/5/17-openapi-mcp.md, which owns OpenAPI emission. The row-schema half of resreg.ResourceRowType, Table, PKFields, Scope — belongs to specs/5/8-yaml-manifests.md.

The MCP twin: x-mcp-when

Each of these operations has a second face. The agent reaches the very same handler as an MCP tool named <resource>.<action> over its per-folder unix socket, 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.

Note: 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 use different identity sources — a bearer token for REST, the folder-bound socket for MCP — against the same evaluator.

Browsing the spec

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.

SDK generation

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.

Caveats