The Signed ProtocolEvery action is a signed DAG-CBOR envelope; 52 actions, one endpoint, and a signed WebSocket.

The agents service has no session cookies, no bearer tokens, and no login. Every request is a signed DAG-CBOR envelope, and the signature is the authorization.

type SignedActionEnvelope = { type: 'AgentsAction' signer: Uint8Array sig: Uint8Array account: Uint8Array action: AgentAction // = UnsignedAgentAction & {ts: number} }

Signing is Ed25519 over the canonical DAG-CBOR encoding of the envelope with sig zeroed (@shm/shared/blobs). On the desktop, the signing key is held by the daemon for the selected account.

verifyEnvelope (agents/src/auth.ts:18-33) checks in a fixed order: shape, then timestamp, then signature, then authorization. Authorization means a row in account_authorizations granting the signer the OWNER or AGENT role on that account.

The clock matters. MAX_ACTION_CLOCK_SKEW_MS = 30_000 (auth.ts:5) — an action whose ts is more than 30 seconds from server time is rejected. There is no nonce cache, so that 30-second window is also a replay window; it is a known gap, recorded in Security.

Idempotency

action_idempotency is keyed (account_id, action, client_request_id) and stores both the request and response bytes. Same key with identical request bytes replays the stored response; same key with different bytes is a 409. Actions that carry one: CreateAgent, CreateSession, CreateAgentTrigger, CreateSigningIdentity (via clientRequestId) and MessageSession (via clientMessageId — which doubles as the id the durable user event echoes back so a client can match its optimistic row).

Routes

Route

Purpose

POST /api/message, POST /agents/api/message

The entire action API. Requires Content-Type: application/cbor (else 415)

GET /api/health, /agents/api/health

Status, version, HM/IPFS URLs, web-tools and code-exec availability with reasons

GET /api/version, /agents/api/version

Build info

GET /agents/ws

Signed WebSocket subscriptions

GET /agents/api/status

Inspector overview: agents, sessions, triggers, watermarks, connection count

GET /agents/api/session?id=…

Debug detail for one session

GET /agents

The built-in inspector UI

Errors come back as {_: 'Error', message} at the APIError's status; anything unhandled is a generic 500. The body-size limit is deliberately unbounded (maxRequestBodySize: Number.MAX_SAFE_INTEGER) because agent memory accepts files of any size.

The 52 actions

UnsignedAgentAction (agents/protocol/src/index.ts:128-180) is the complete API surface:

Agents — ListAgents, CreateAgent, GetAgent, UpdateAgent, DeleteAgent
Providers & secrets — ListModelProviders, SetModelProvider, DeleteModelProvider, ListProviderModels, SetSecret, StartProviderOAuth, SubmitProviderOAuthCode, GetProviderOAuthStatus, CancelProviderOAuth
Signing identities — ListSigningIdentities, CreateSigningIdentity, ImportSigningIdentity, UpdateSigningIdentity, DeleteSigningIdentity
Triggers — ListAgentTriggers, GetAgentTrigger, CreateAgentTrigger, UpdateAgentTrigger, DeleteAgentTrigger
Memory & tools — ListAgentMemory, ReadAgentMemoryFile, WriteAgentMemoryFile, DeleteAgentMemoryFile, DownloadAgentMemoryFile, UploadAgentMemoryFileToIpfs, ListAgentTools
Sessions — CreateSession, ListSessions, GetSession, UpdateSession, DeleteSession, MessageSession, StopSession, RetrySession, InvokeSessionTool
Attachments & uploads — UploadSessionAttachment, ReadSessionAttachment, BeginFileUpload, AppendFileUploadChunk, CommitFileUpload, AbortFileUpload
Runs — GetRun, ListRuns, CancelRun, SignalRun, GetRunJournal
Live — Subscribe

(ImportSigningIdentity exists in code but is missing from agents/docs/signed-api.md — one of several places where that document has drifted behind the protocol.)

The WebSocket

Subscriptions are the same signed envelope, sent as a binary frame with action._ === 'Subscribe':

type Subscribe = { _: 'Subscribe' key: `account/${string}` | `agents/${string}` | `sessions/${string}` | `runs/${string}` afterSeq?: number }

verifySubscription (agents/src/api-service.ts:5205-5256) authorizes each key shape separately — an agents/ key 404s if the agent is not on the account, a sessions/ key replays history after afterSeq, and a runs/ key (which takes the root run id) replays the run tree with each run's journal. Switching accounts on an open socket is a 403.

Server events: connected, subscribed, append, appendPartial, change, error. Streaming text arrives as appendPartial patches carrying {textDelta?, done?, usage?, activity?}.

Known limitations, all real: no unsubscribe message, no signature on server-sent events, no heartbeat or backpressure protocol. A v2 is on the roadmap.

Related

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime