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 |
|---|---|
| The entire action API. Requires |
| Status, version, HM/IPFS URLs, web-tools and code-exec availability with reasons |
| Build info |
| Signed WebSocket subscriptions |
| Inspector overview: agents, sessions, triggers, watermarks, connection count |
| Debug detail for one session |
| 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
Architecture — where the protocol sits
Sessions — the actions that drive a conversation
Security — what the signature does and does not protect
Persistence — where actions land
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime