Skip to main content
REST surface for the unified capability inventory: one merged stream of every skill, tool, and connector across all five runtimes, plus a single manageability-gated action endpoint. GET /api/capabilities is the one read both the Ghost Graph and the Capabilities dashboard consume; POST /api/capabilities/:action installs a curated skill or connector, toggles a manageable capability, or resolves a pending tool-call approval. The read fans a CapabilityMultiplexer over five per-runtime CapabilitySource adapters (native, hermes, claude-code, codex, openclaw), persists each OK source’s records, and serves last-good rows for any degraded source, so a blipped Gateway never blanks the inventory. The merged result carries both the records and a per-source read-status list.
A capability is acted on by a pure function of its manageability tier. managed and external-write rows are writable; observe-only rows (runtime built-ins, external-vendor CLIs) are read-only and reject enable/disable with 422. A runtime-of-record row may also be non-writable when its source emits writable: false. See Concepts → Capabilities for the tier model.
The POST body is parsed by express.json({ limit: '2mb' }).

Routes


GET /api/capabilities

Returns the merged CapabilityRecord[] plus a SourceReadStatus[] for every source. Records are deduplicated by id with fresh records winning over stale (last-good) rows, then filtered by the supplied query params. Filtering happens after the merge; an unknown filter value yields an empty records[], not an error.
  • Path params: none.
  • Query params:
Each param is applied only when present and non-empty; omit all four for the full inventory.
  • Request body: none.

Responses

200 OK: the merged view:
sources[] reports each adapter’s read outcome. A source with ok: false, degraded: true did not read fresh; its records in records[] are the last-good rows from the durable table (served unchanged so the inventory stays populated through a disconnect). 500 Internal Server Error: any failure loading the inventory:

Example


POST /api/capabilities/:action

A single action endpoint dispatched on the :action path segment. install adds a curated skill or connector to an agent; enable/disable toggle an existing manageable capability by its id; approve resolves a pending tool-call approval. Writes route to the owning source’s write() through the multiplexer (which is also where the durable audit happens); approve reuses the existing approval handshake (resolveApproval) and never routes through a source.
  • Path params: action (one of install | enable | disable | approve; any other value returns 400).
  • Request body: shape depends on :action (below).

Action: install

Installs a skill or connector onto an agent. The body is the install spec, accepted either nested under spec or as the top-level body. The target agent must exist (an unknown agentId would produce an invisible orphan annotation, so it is rejected up front). The owning runtime is resolved authoritatively from the agent row; the client’s runtime field is a placeholder and is overwritten before the write. Request body:
400 Bad Request: the body is not a valid install spec (missing/non-string via, agentId, runtime, kind, or name):
404 Not Found: the target agentId is not a known agent row:
200 OK: the capability was installed; the fresh record is returned (or null for an acknowledgement that yields no new row):
422 Unprocessable Entity: the owning source refused the write for its tier (an UnsupportedCapabilityWriteError, e.g. an observe-only source):

Action: enable / disable

Toggles an existing capability identified by its composite id. The handler resolves the row, then enforces the same tier the UI shows: an observe-only capability, or any capability whose source marked it writable: false, is rejected with 422 before any source write is attempted. Request body:
400 Bad Request: missing id:
(For disable, the message reads disable requires { id }.) 404 Not Found: no capability row with that id:
422 Unprocessable Entity: the capability cannot be modified (observe-only, or writable === false):
503 Service Unavailable: the write reached the owning source, but that source needs a live connection it does not have (the OpenClaw operator connection is down):
200 OK: the toggle was applied; the updated record is returned (or null):

Action: approve

Resolves a pending tool-call approval row (the same tool_call_approvals handshake the Approvals panel uses, approval ids carry no source prefix, so this is not a source-routed write). The resolve is idempotent: a row already resolved is a no-op and returns the existing row. Request body:
400 Bad Request: missing id or a decision outside the allowed set:
404 Not Found: no approval row with that id:
200 OK: the approval was resolved; the row is returned:

Catch-all

400 Bad Request: :action is not one of the four known actions:
404 Not Found: the multiplexer routed to an unknown source / unparseable id (an UnknownCapabilityError thrown from a write):
422 Unprocessable Entity: a source threw UnsupportedCapabilityWriteError (a write aimed at a tier that forbids it):
500 Internal Server Error: any other failure:

Examples


Error envelope

Every error response on these routes is the standard { error: string } envelope. The two manageability-gated 422 responses additionally carry manageability (and, for the enable/disable pre-check, writable) so a client can render the right read-only affordance without a second lookup. A 503 means the tier allowed the write but the owning source could not reach the runtime, so a retry after reconnecting is the remedy. The error string on a 500 is passed through a display-layer redactor before it is sent.

See also

Last modified on August 11, 2026