API Reference
This document is a hand-maintained API reference for the Tyrum Gateway HTTP and WebSocket APIs.
This is a manually written first version; future automation may generate this document (or an OpenAPI/JSON Schema equivalent) from packages/gateway/src/routes/* and @tyrum/schemas.
Table of Contents
Conventions
- Base URL:
http(s)://<gateway-host>:<port> - All JSON requests/responses use
Content-Type: application/jsonunless noted. - Most error responses are JSON shaped like:
{ "error": "<code>", "message": "<human-readable message>" }
- When enabled, the gateway returns a stable
x-request-idresponse header.
Authentication & Authorization
HTTP auth
When gateway auth is enabled (default for most deployments), requests are authenticated via:
Authorization: Bearer <token>(preferred)- Cookie
tyrum_admin_token=<token>(primarily for browser/uiusage)
Public allowlist (no token required):
GET /healthzGET /uiandGET /ui/*POST /auth/sessionandPOST /auth/logoutGET /providers/:provider/oauth/callback(OAuth callback; state/PKCE protected)
Token types
- Admin token: Break-glass; bypasses scope enforcement.
- Device token: Scoped; per-request scope enforcement applies (HTTP + WS).
HTTP scopes (device tokens)
For device tokens, HTTP routes are scope-checked based on method + path template:
- Admin surfaces (examples:
/policy/*,/secrets/*,/snapshot/*,/routing/*,/providers/*) requireoperator.admin. /approvals/*requiresoperator.approvals./pairings/*requiresoperator.pairing.- Most operator surfaces default to:
GET→operator.readPOST|PUT|PATCH|DELETE→operator.write
If a route is not in the authorization matrix, device tokens are forbidden (deny-by-default).
WebSocket scopes (device tokens)
For device tokens, each WS request type is scope-checked via packages/gateway/src/modules/authz/ws-scope-matrix.ts.
HTTP API
Public endpoints
GET /healthz
- Auth: Public
- Request: None
- Response:
200JSON{ status: "ok", is_exposed: boolean }
GET /ui
- Auth: Public
- Request: None
- Response:
200HTML (operator SPA shell)404textoperator_ui_assets_unavailable(if UI assets are missing)
GET /ui/*
- Auth: Public
- Request: Path tail (static asset or SPA route)
- Response:
200HTML (SPA routes) or bytes (static assets)404textnot_found
POST /auth/session
- Auth: Public (bootstrap endpoint)
- Availability: Only when gateway auth is enabled (TokenStore is wired)
- Request: JSON
{ token: string } - Response:
204(setstyrum_admin_tokenhttpOnly cookie)400invalid JSON / missing token401invalid token
POST /auth/logout
- Auth: Public
- Availability: Only when gateway auth is enabled (TokenStore is wired)
- Request: None
- Response:
204(clearstyrum_admin_tokencookie)
GET /providers/:provider/oauth/callback
- Auth: Public
- Purpose: OAuth authorization-code callback (PKCE + state)
- Request: Query params include
state,code(orerror,error_description) - Response:
200HTML success/failure page400for invalid/expired state, missing params, etc.
Runtime & diagnostics
GET /status
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200JSON runtime details (version,instance_id,role,db_kind,ws,policy, etc.)401missing/invalid token403insufficient scope (device tokens)
GET /metrics
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200Prometheus text format (content-type set by registry)401,403
GET /connections
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200JSON WebSocket connection stats (fromConnectionManager.getStats())401,403
GET /presence
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200JSON{ status: "ok", generated_at, entries: [...] }401,403
Contracts (JSON Schema)
GET /contracts/jsonschema/catalog.json
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200JSON schema catalog500{ error: "contracts_unavailable", ... }when schemas are not available401,403
GET /contracts/jsonschema/:file
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request:
:filemust be a safe*.jsonfilename (no paths);catalog.jsonis not served here - Response:
200JSON schema file contents404{ error: "not_found", ... }(missing/invalid filename)500{ error: "contracts_unavailable", ... }401,403
Usage
GET /usage
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Optional query params (mutually exclusive):
run_id,key,agent_id - Response:
200JSON usage totals (local DB) + optional provider polling status400for invalid scope param combinations401,403
Policy
POST /policy/check
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
PolicyCheckRequest(@tyrum/schemas) - Response:
200JSONPolicyDecision(@tyrum/schemas)400invalid request401,403
GET /policy/bundle
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: None
- Response:
200JSON{ status: "ok", generated_at, effective: { sha256, bundle, sources } }401,403
GET /policy/overrides
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: Query params (validated by
PolicyOverrideListRequest):agent_id,tool_id,status,limit,cursor - Response:
200JSONPolicyOverrideListResponse400invalid request401,403
POST /policy/overrides
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
PolicyOverrideCreateRequest - Response:
201JSONPolicyOverrideCreateResponse400invalid request401,403
POST /policy/overrides/revoke
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
PolicyOverrideRevokeRequest - Response:
200JSONPolicyOverrideRevokeResponse404override not found / not active400,401,403
Approvals
GET /approvals
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.approvals - Request: Optional query param
status(pending|approved|denied|expired|cancelled) - Response:
200JSON{ approvals: [...] }400invalid status401,403
GET /approvals/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.approvals - Request:
:idnumeric - Response:
200JSON{ approval: ... }400invalid id404not found401,403
POST /approvals/:id/respond
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.approvals - Request: JSON (supports both):
{ decision: "approved" | "denied", reason?: string, mode?: "once"|"always", overrides?: [...] }- legacy
{ approved: boolean, reason?: string, ... }
- Response:
200JSON{ approval: ..., created_overrides?: [...] }(idempotent if already resolved)400invalid request404not found401,403
GET /approvals/:id/preview
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.approvals - Request:
:idnumeric - Response:
200JSON{ id, plan_id, step_index, prompt, context, status, expires_at }400invalid id404not found401,403
Pairing (node enrollment)
GET /pairings
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.pairing - Request: Optional query param
status(pending|approved|denied|revoked) - Response:
200JSON{ status: "ok", pairings: [...] }401,403
POST /pairings/:id/approve
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.pairing - Request: JSON
{ trust_level: "local"|"remote", capability_allowlist: CapabilityDescriptor[], reason?: string } - Response:
200JSON{ status: "ok", pairing: ... }400invalid request404pairing not found / not pending401,403
POST /pairings/:id/deny
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.pairing - Request: JSON
{ reason?: string } - Response:
200JSON{ status: "ok", pairing: ... }400,404,401,403
POST /pairings/:id/revoke
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.pairing - Request: JSON
{ reason?: string } - Response:
200JSON{ status: "ok", pairing: ... }400,404,401,403
Auth profiles & session pins
GET /auth/profiles
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: Optional query params:
agent_id,provider,status=active|disabled - Response:
200JSONAuthProfileListResponse401,403
POST /auth/profiles
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
AuthProfileCreateRequest - Response:
201JSONAuthProfileCreateResponse400,401,403
PATCH /auth/profiles/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
AuthProfileUpdateRequest - Response:
200JSON{ status: "ok", profile: AuthProfile }404profile not found400,401,403
POST /auth/profiles/:id/disable
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
AuthProfileDisableRequest - Response:
200JSON{ status: "ok", profile: AuthProfile }404profile not found400,401,403
POST /auth/profiles/:id/enable
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
AuthProfileEnableRequest - Response:
200JSON{ status: "ok", profile: AuthProfile }404profile not found400,401,403
GET /auth/pins
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: Optional query params:
agent_id,session_id,provider - Response:
200JSONSessionProviderPinListResponse401,403
POST /auth/pins
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
SessionProviderPinSetRequest - Response:
201JSON{ status: "ok", pin: SessionProviderPin }(set)200JSON{ status: "ok", cleared: boolean }(clear whenprofile_id: null)400,401,403
Device tokens
POST /auth/device-tokens/issue
- Auth: Admin token required
- Availability: Only when gateway auth is enabled (TokenStore is wired)
- Request: JSON
DeviceTokenIssueRequest - Response:
201JSONDeviceTokenIssueResponse403if admin token is missing/invalid400invalid request
POST /auth/device-tokens/revoke
- Auth: Admin token required
- Availability: Only when gateway auth is enabled (TokenStore is wired)
- Request: JSON
DeviceTokenRevokeRequest - Response:
200JSONDeviceTokenRevokeResponse403if admin token is missing/invalid404token not found / already revoked
Provider OAuth (authorization code + PKCE)
POST /providers/:provider/oauth/authorize
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when auth profiles are enabled and OAuth providers are configured
- Request: JSON (partial; see
packages/gateway/src/routes/provider-oauth.ts):agent_id?: stringpublic_base_url?: string(http/https)
- Response:
200JSON{ status: "ok", provider, state, expires_at, authorize_url }404oauth provider not configured400invalid request / missing env / etc.401,403
Routing config
GET /routing/config
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when gateway auth is enabled (TokenStore is wired)
- Request: None
- Response:
200JSON{ revision, config, created_at?, created_by?, reason?, reverted_from_revision? }500{ error: "corrupt_state", ... }401,403
PUT /routing/config
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when gateway auth is enabled (TokenStore is wired)
- Request: JSON
RoutingConfigUpdateRequest - Response:
201JSON{ revision, config, created_at, created_by, reason?, reverted_from_revision? }400invalid request401,403
POST /routing/config/revert
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when gateway auth is enabled (TokenStore is wired)
- Request: JSON
RoutingConfigRevertRequest - Response:
201JSON{ revision, config, created_at, created_by, reason?, reverted_from_revision }404revision not found400,401,403
Secrets
POST /secrets
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when a
SecretProvideris configured - Request: JSON
SecretStoreRequest - Response:
201JSON{ handle: SecretHandle }(never returns secret value)400invalid request401,403
GET /secrets
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when a
SecretProvideris configured - Request: Optional query param/header to select agent:
agent_idorx-tyrum-agent-id - Response:
200JSON{ handles: SecretHandle[] }400invalid agent401,403
DELETE /secrets/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when a
SecretProvideris configured - Request:
:idsecret handle id - Response:
200JSON{ revoked: true }404not found401,403
POST /secrets/:id/rotate
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when a non-env
SecretProvideris configured - Request: JSON
SecretRotateRequest - Response:
201JSON{ revoked: boolean, handle: SecretHandle }404not found400invalid request / env secrets not rotatable500rotation propagation failures (best-effort rollback)401,403
Snapshot export/import
GET /snapshot/export
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: Optional query param
tables(comma-separated); defaults to gateway snapshot table set - Response:
200JSONSnapshotBundle(formattyrum.snapshot.v2)400invalid table name500unexpected export failure401,403
POST /snapshot/import
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
SnapshotImportRequest - Response:
200JSON{ status: "ok", imported_at, format, tables, inserted_total, inserted_by_table }403{ error: "disabled", ... }unlessTYRUM_SNAPSHOT_IMPORT_ENABLED=1400invalid request / unknown tables500import refused (non-empty tables) or internal failures401,403
Memory exports (artifact bytes)
GET /memory/exports/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request:
:idis anArtifactId(@tyrum/schemas) - Response:
200bytes (download) withContent-Disposition: attachment; filename="tyrum-memory-export-<id>.json"404not found (or not a memory export artifact)400invalid artifact id401,403
Models.dev catalog
GET /models/status
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200JSON{ status: "ok", models_dev: ... }401,403
POST /models/refresh
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin(special-cased) - Request: None
- Response:
200JSON{ status: "ok", models_dev: ... }401,403
GET /models/providers
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200JSON provider summary list401,403
GET /models/providers/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path param
:id - Response:
200JSON provider details404provider not found401,403
GET /models/providers/:id/models
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path param
:id - Response:
200JSON provider + model list404provider not found401,403
Plugins
GET /plugins
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when plugins are enabled
- Request: None
- Response:
200JSON{ status: "ok", plugins: [...] }401,403
GET /plugins/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Availability: Only when plugins are enabled
- Request: Path param
:id - Response:
200JSON{ status: "ok", plugin: ... }404plugin not found401,403
Additional plugin-defined routers may be mounted under:
/plugins/<plugin_id>/rpc/*(methods + paths defined by the plugin)
Plan runner
POST /plan
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request: JSON
PlanRequest(@tyrum/schemas) - Response:
200JSONPlanResponse(@tyrum/schemas)400invalid request401,403
Workflow engine API (feature-gated)
POST /workflow/run
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Availability: Only when
TYRUM_ENGINE_API_ENABLED=1 - Request: JSON
{ key, lane?, plan_id?, request_id?, steps: ActionPrimitive[], budgets? } - Response:
200JSON{ status: "ok", job_id, run_id, plan_id, request_id, key, lane, steps_count }400invalid request401,403
POST /workflow/resume
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Availability: Only when
TYRUM_ENGINE_API_ENABLED=1 - Request: JSON
{ token: string } - Response:
200JSON{ status: "ok", run_id }404resume token not found400,401,403
POST /workflow/cancel
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Availability: Only when
TYRUM_ENGINE_API_ENABLED=1 - Request: JSON
{ run_id: string, reason?: string } - Response:
200JSON{ status: "ok", run_id, cancelled: boolean }404run not found400,401,403
Playbooks
GET /playbooks
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200JSON{ playbooks: [...] }401,403
GET /playbooks/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path param
:id - Response:
200JSON playbook record404not found401,403
POST /playbooks/:id/run
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request: None
- Response:
200JSON playbook run result (non-durable runner)404not found401,403
POST /playbooks/runtime
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request: JSON
PlaybookRuntimeRequest - Response:
200JSON runtime envelope (run/resume)400unsupported (engine not configured) or invalid request401,403
POST /playbooks/:id/execute
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Availability: Only when
TYRUM_ENGINE_API_ENABLED=1 - Request: JSON (optional overrides):
{ key?, lane?, plan_id?, request_id?, budgets? } - Response:
200JSON{ status: "ok", job_id, run_id, playbook_id, plan_id, request_id, key, lane, steps_count }400unsupported / invalid request404playbook not found401,403
Watchers
POST /watchers
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request: JSON
{ plan_id: string, trigger_type: string, trigger_config?: unknown } - Response:
201JSON{ id, plan_id, trigger_type }400invalid request401,403
GET /watchers
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: None
- Response:
200JSON{ watchers: [...] }401,403
PATCH /watchers/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request: JSON
{ active?: boolean }(onlyactive:falseis meaningful) - Response:
200JSON{ id, updated: true }400invalid id401,403
DELETE /watchers/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request: None
- Response:
200JSON{ id, deleted: true }400invalid id401,403
POST /watchers/:id/trigger/webhook
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request:
- Headers:
x-tyrum-webhook-signature: sha256=<hex>x-tyrum-webhook-timestamp: <unix seconds|ms>x-tyrum-webhook-nonce: <base64url|uuid>
- Body: raw text (signed as
<timestamp>.<nonce>.<body>)
- Headers:
- Response:
200JSON{ ok: true }(or trigger-specific result)401invalid/missing signature envelope / replay window404watcher not found / not webhook503misconfigured (missing secret provider / invalid watcher config)401,403
Canvas artifacts
POST /canvas/publish
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request: JSON
{ plan_id?, title, content_type, html_content, metadata? } - Response:
201JSON{ id, created_at }400invalid request401,403
GET /canvas/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path param
:id - Response:
200HTML/text bytes with restrictive CSP404not found401,403
GET /canvas/:id/meta
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path param
:id - Response:
200JSON metadata404not found401,403
Artifacts (execution scope-bound)
GET /artifacts/:id/metadata
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path param
:id - Response:
400{ error: "invalid_request", message: "artifact fetch APIs must be scope-bound; use GET /runs/:runId/artifacts/:id/metadata" }401,403
GET /artifacts/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path param
:id - Response:
400{ error: "invalid_request", message: "artifact fetch APIs must be scope-bound; use GET /runs/:runId/artifacts/:id" }401,403
GET /runs/:runId/artifacts/:id/metadata
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path params
:runId,:id(ArtifactId) - Response:
200JSON{ artifact, scope }403forbidden (missing durable scope linkage / policy denies / requires approval)404not found400,401,403
GET /runs/:runId/artifacts/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Request: Path params
:runId,:id(ArtifactId) - Response:
302redirect to signed URL (when artifact store supports it)200bytes (when artifact bytes are served directly)403forbidden (missing durable scope linkage / policy denies / requires approval)404not found400,401,403
Agent runtime (feature-gated)
GET /agent/status
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Availability: Only when
TYRUM_AGENT_ENABLED=1 - Request: Optional query param
agent_id(default:default) - Response:
200JSON agent runtime status400invalid agent id401,403
POST /agent/turn
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Availability: Only when
TYRUM_AGENT_ENABLED=1 - Request: JSON
AgentTurnRequest - Response:
200JSON agent turn result400invalid request502{ error: "agent_runtime_error", ... }401,403
Context reports (feature-gated)
GET /context
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Availability: Only when
TYRUM_AGENT_ENABLED=1 - Request: Optional query param
agent_id(default:default) - Response:
200JSON{ status: "ok", report }400invalid agent id401,403
GET /context/list
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Availability: Only when
TYRUM_AGENT_ENABLED=1 - Request: Optional query params:
session_id,run_id,limit - Response:
200JSON{ status: "ok", reports: [...] }401,403
GET /context/detail/:id
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.read - Availability: Only when
TYRUM_AGENT_ENABLED=1 - Request: Path param
:id - Response:
200JSON{ status: "ok", report }404not found401,403
Ingress (Telegram)
POST /ingress/telegram
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.write - Request:
- Raw body text (Telegram update JSON)
- When Telegram integration is enabled, requires header
x-telegram-bot-api-secret-token - Optional query param
agent_idto force routing
- Response:
200JSON normalized update (legacy behavior when agent runtime disabled)200JSON{ ok: true, ... }when processed/queued401invalid telegram webhook secret (when enabled)503misconfigured (missingTELEGRAM_WEBHOOK_SECRET) or temporary queue failure400invalid request / normalization failure401,403
Audit
GET /audit/export/:planId
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: Path param
:planId - Response:
200JSON receipt bundle404no events found401,403
POST /audit/verify
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
{ events: ChainableEvent[] } - Response:
200JSON verification result400invalid request401,403
POST /audit/forget
- Auth: Required (unless gateway auth is disabled)
- Device scope:
operator.admin - Request: JSON
AuditForgetRequest - Response:
200JSON{ decision, deleted_count, proof_event_id }400,401,403
WebSocket API
URL and authentication
- Upgrade endpoint:
GET /ws(HTTP upgrade) - WebSocket URL:
ws(s)://<gateway-host>:<port>/ws
When gateway auth is enabled, the /ws upgrade requires a valid token, provided via one of:
Authorization: Bearer <token>header- Cookie
tyrum_admin_token=<token>(same-origin upgrades only) Sec-WebSocket-Protocoltoken transport:- Offer subprotocols including
tyrum-v1andtyrum-auth.<base64url(token)>
- Offer subprotocols including
Handshake (connect.init / connect.proof)
After upgrade, the client must complete the v2 handshake:
- Send
connect.init(includes role, device identity proof material, and capability descriptors) - Receive
connect.initresponse (includes aconnection_idand a server challenge) - Send
connect.proof(signature over a stable transcript includingconnection_id+ challenge) - Receive
connect.proofresponse (includesclient_id,device_id, androle)
Message envelopes
All non-handshake messages are JSON envelopes:
- Requests:
{ request_id: string, type: string, payload: unknown } - Responses:
{ request_id: string, type: string, ok: boolean, result?: unknown, error?: { code, message, details? } } - Events:
{ event_id: string, type: string, occurred_at: string, payload: unknown, scope?: ... }
Client-sent events are rejected.
Message types
connect.init
- Direction: client → gateway (request), gateway → client (response)
- Schema:
WsConnectInitRequest(@tyrum/schemas) - Result:
{ connection_id: string, challenge: string } - Notes:
protocol_revmust match the gateway protocol rev; device proof is validated.
connect.proof
- Direction: client → gateway (request), gateway → client (response)
- Schema:
WsConnectProofRequest(@tyrum/schemas) - Result:
{ client_id: string, device_id: string, role: WsPeerRole }
connect
- Direction: client → gateway (legacy request)
- Notes: Deprecated; the gateway closes with
"legacy connect is deprecated; use connect.init/connect.proof".
ping
- Direction:
- client → gateway (request)
- gateway → client (request) and client → gateway (response) (heartbeat)
- Scope (device tokens): allowed (no scopes required)
- Schema:
WsPingRequest - Result: none (
ok: true)
approval.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.approvals - Schema:
WsApprovalListRequest(payload parsed asApprovalListRequest) - Result:
ApprovalListResponse
approval.resolve
- Direction: client → gateway (request)
- Scope (device tokens):
operator.approvals - Schema:
WsApprovalResolveRequest(payload parsed asApprovalResolveRequest) - Result:
ApprovalResolveResponse
approval.request
- Direction:
- gateway → client (request)
- client → gateway (response)
- Notes: Used for interactive approval flows; client responses are validated with
WsApprovalDecision.
pairing.approve
- Direction: client → gateway (request)
- Scope (device tokens):
operator.pairing - Schema:
WsPairingApproveRequest - Result:
WsPairingResolveResult
pairing.deny
- Direction: client → gateway (request)
- Scope (device tokens):
operator.pairing - Schema:
WsPairingDenyRequest - Result:
WsPairingResolveResult
pairing.revoke
- Direction: client → gateway (request)
- Scope (device tokens):
operator.pairing - Schema:
WsPairingRevokeRequest - Result:
WsPairingResolveResult
capability.ready
- Direction: node → gateway (request)
- Scope (device tokens): request is scope-authorized only for connected nodes (device scopes not required)
- Schema:
WsCapabilityReadyRequest - Result: none (
ok: true)
attempt.evidence
- Direction: node → gateway (request)
- Schema:
WsAttemptEvidenceRequest - Result: none (
ok: true) - Notes: Evidence is broadcast as an event on success.
task.execute
- Direction:
- gateway → node (request)
- node → gateway (response)
- Notes: Nodes respond with
WsTaskExecuteResult(success) or an error with evidence details.
session.send
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsSessionSendRequest - Result:
WsSessionSendResult
command.execute
- Direction: client → gateway (request)
- Scope (device tokens):
operator.admin - Schema:
WsCommandExecuteRequest - Result:
WsCommandExecuteResult
subagent.spawn
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsSubagentSpawnRequest - Result:
WsSubagentSpawnResult
subagent.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsSubagentListRequest - Result:
WsSubagentListResult
subagent.get
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsSubagentGetRequest - Result:
WsSubagentGetResult
subagent.send
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsSubagentSendRequest - Result:
WsSubagentSendResult
subagent.close
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsSubagentCloseRequest - Result:
WsSubagentCloseResult
workflow.run
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkflowRunRequest - Result:
WsWorkflowRunResult
workflow.resume
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkflowResumeRequest - Result:
WsWorkflowResumeResult
workflow.cancel
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkflowCancelRequest - Result:
WsWorkflowCancelResult
memory.search
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsMemorySearchRequest - Result:
WsMemorySearchResult
memory.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsMemoryListRequest - Result:
WsMemoryListResult
memory.get
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsMemoryGetRequest - Result:
WsMemoryGetResult
memory.create
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsMemoryCreateRequest - Result:
WsMemoryCreateResult
memory.update
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsMemoryUpdateRequest - Result:
WsMemoryUpdateResult
memory.delete
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsMemoryDeleteRequest - Result:
WsMemoryDeleteResult
memory.forget
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsMemoryForgetRequest - Result:
WsMemoryForgetResult
memory.export
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsMemoryExportRequest - Result:
WsMemoryExportResult
presence.beacon
- Direction: client|node → gateway (request)
- Scope (device tokens): allowed (no scopes required)
- Schema:
WsPresenceBeaconRequest - Result:
WsPresenceBeaconResult
work.create
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkCreateRequest - Result:
WsWorkCreateResult
work.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkListRequest - Result:
WsWorkListResult
work.get
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkGetRequest - Result:
WsWorkGetResult
work.update
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkUpdateRequest - Result:
WsWorkUpdateResult
work.transition
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkTransitionRequest - Result:
WsWorkTransitionResult
work.link.create
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkLinkCreateRequest - Result:
WsWorkLinkCreateResult
work.link.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkLinkListRequest - Result:
WsWorkLinkListResult
work.artifact.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkArtifactListRequest - Result:
WsWorkArtifactListResult
work.artifact.get
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkArtifactGetRequest - Result:
WsWorkArtifactGetResult
work.artifact.create
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkArtifactCreateRequest - Result:
WsWorkArtifactCreateResult
work.decision.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkDecisionListRequest - Result:
WsWorkDecisionListResult
work.decision.get
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkDecisionGetRequest - Result:
WsWorkDecisionGetResult
work.decision.create
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkDecisionCreateRequest - Result:
WsWorkDecisionCreateResult
work.signal.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkSignalListRequest - Result:
WsWorkSignalListResult
work.signal.get
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkSignalGetRequest - Result:
WsWorkSignalGetResult
work.signal.create
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkSignalCreateRequest - Result:
WsWorkSignalCreateResult
work.signal.update
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkSignalUpdateRequest - Result:
WsWorkSignalUpdateResult
work.state_kv.get
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkStateKvGetRequest - Result:
WsWorkStateKvGetResult
work.state_kv.list
- Direction: client → gateway (request)
- Scope (device tokens):
operator.read - Schema:
WsWorkStateKvListRequest - Result:
WsWorkStateKvListResult
work.state_kv.set
- Direction: client → gateway (request)
- Scope (device tokens):
operator.write - Schema:
WsWorkStateKvSetRequest - Result:
WsWorkStateKvSetResult