Reference
Server
createWorkerServer options, queue options, and the full route table.
@workerdeck/server is the gateway:
HTTP + WebSocket over node:http + ws, a session registry, and optional job-queue routes.
Node ≥ 22, real filesystem, no serverless - see
Deployment.
createWorkerServer(options)
Returns a WorkerServer: { server, registry, queue?, preflight(), listen(port, host?), drain(), close(), ... } -
server is the underlying node:http server, queue is set when queue options were provided.
preflight() reads the stored state (the session record store and the agent store) and throws
an UnsupportedStateError for anything this gateway cannot read, writing nothing. listen() runs
it first; a host that writes state of its own at startup calls it before those writes. It covers
the built-in stores: a custom SessionRecordStore validates its own raw data. State written by a
WorkerDeck release before 4.0.0 is refused this way and must be migrated once before 4.0.0 starts
on it.
WorkerServerOptions
| Option | Default | Effect |
|---|---|---|
authenticate |
- | (req: IncomingMessage) => unknown | Promise<unknown>. Return a truthy principal to accept, null/undefined to reject with 401. Required unless allowUnauthenticated: true - the worker must never be exposed bare. Covers every route including WS upgrades. A principal object may carry allowedProfiles: string[] to scope which profiles the caller can use, canManageProfiles: true to allow profile create/edit/delete (which also needs profileStore), and scope: Record<string, string> for session visibility (below). This is the place to be expensive: it is already async and already runs once per request, so a lookup - which spaces is this user in? - belongs here, landing its answer on the principal. |
authorizeSession |
every pinned key must match | (principal, session: SessionInfo) => boolean - may this caller see this session at all? Enforced at the /sessions/:id/* gate, the session list, the WS attach (before the wake - waking rebuilds a runner and reconnects MCP, which is not work to do for someone about to get a 404), POST /executions/:id/result, and the job routes via JobInfo.scope. A miss is 404, never 403: a 403 confirms the session exists. The default rule needs no policy - every key the principal’s scope pins must match the session’s, and an unset principal scope is unrestricted, so an operator’s dashboard is untouched. Deliberately synchronous: it runs per route and per row of every list. A principal carrying a scope is an embedded end user, not the operator, and is refused the operator-privileged surfaces outright (/fs/*, /sdk-sessions, /queue, /queue/ws) - those answer about the gateway, so there is nothing to filter. With a policy declared, operator principals must say operator: true. True means full control, not read access: an attach can send user_message, permission_decision, interrupt and close. A read-only-for-my-team policy is not expressible here - do not approximate it with the panel’s readOnly, which removes affordances in a client and is not an authorization boundary. |
allowUnauthenticated |
false |
Explicit opt-in to run without auth (local dev only). Without it and without authenticate, createWorkerServer throws. |
allowedCwdRoots |
off | Session cwd (and job session.cwd) must resolve inside one of these roots, else 403. Also constrains /sdk-sessions: a dir must be inside the roots, and a request without one lists the roots themselves rather than the whole host. Strongly recommended. |
profiles |
auto-detect | What sessions run as ({ name, configDir?, engine?, provider?, description?, defaults? }[]). A claude profile’s configDir becomes the session’s CLAUDE_CONFIG_DIR; an engine: 'provider' profile runs the model-agnostic engine instead and needs createEngineRunner. More than one declared → creates must name a profile; exactly one, or one named default, → implicit. Unset: profiles are auto-detected from $CLAUDE_CONFIG_DIR/~/.claude (as default) and $CODEX_HOME/~/.codex (as codex) for each directory that exists - seeded into profileStore on first launch when one is wired, so they stay editable, and declared read-only when none is; [] disables. See Profiles. |
profileStore |
- | Persistence for dashboard-managed profiles; setting it mounts POST /profiles and PATCH/DELETE /profiles/:name. createMemoryProfileStore() and createFileProfileStore(path) are bundled; the type is a three-method seam for anything else. Profiles declared in profiles are never stored and never editable over HTTP. |
accounts |
off | Connecting a Claude account to a managed profile (claude setup-token brokered by the gateway). canConnect(principal, profile) lets a principal without canManageProfiles connect a profile, e.g. its own; claudeExecutable overrides the bundled CLI. Routes are on whenever profile management is. See Auth. |
allowedConfigDirRoots |
off | Credential-directory roots a managed profile may point at - a Claude profile’s configDir, a Codex profile’s codexHome (mirrors allowedCwdRoots). Unset means the management routes create provider profiles only - naming a credential directory is choosing which credential store a session runs on. A Codex profile with no codexHome names none, and is exempt. |
hostFiles |
reads follow allowedCwdRoots |
Tunes the host filesystem routes (/v1/fs/*): `{ roots?: string[], write?: boolean, maxFileBytes?: number, maxEntries?: number, ignore?: string[], ripgrep?: string |
createEngineRunner |
- | (ctx: { config, profile, bridge }) => Runner | Promise<Runner> - build the runner for an engine: 'provider' profile. Required if any is declared (the server refuses to start otherwise). Kept as a host hook so the server imports no model SDK and never resolves provider credentials itself; may be async for assembly that has to await (a per-session MCP connect). |
buildRunnerConfig |
identity | (req: CreateSessionRequest) => SessionRunnerConfig - map/patch the incoming request into the runner config (inject env, tool policy, per-skill constraints). Applied to client sessions and queue jobs alike. |
attachments |
10 MiB / 64 MiB | Sizes the message-attachment hold: { maxFileBytes?: number, maxSessionBytes?: number }. Always on and needs no grant - an upload lands in the session’s own in-memory store and reaches the model as message content, which is what typing already does. What it can cost is memory, which is what these bound. |
basePath |
'/v1' |
URL prefix for all routes. |
fallback |
404 | (req, res) => void | Promise<void> - handle requests outside basePath instead of 404ing them. This is how the turnkey CLI serves the dashboard and how an embedding app mounts its own routes on the gateway’s port, and it exists for one reason: a browser cannot put a header on a WebSocket handshake, so the only credential a tab can present on a session attach is a cookie - and a cookie only rides requests to the origin that set it. One origin is not a convenience, it is what makes an authenticated browser client possible without a stamping proxy. It is a plain Node handler, which is exactly what an Express app is. |
cors |
off | { origins: string[] } - let a dashboard on another origin call this gateway. Sharing policy, not a credential: preflights are answered before auth (browsers strip credentials from them, so they would otherwise 401), but every real request still goes through authenticate, and an allowlisted page without the key gets nothing. Exact origins only - no wildcards, no suffix matching - and Access-Control-Allow-Credentials is never sent, which is what stops an ambient cookie becoming cross-origin authority. WS upgrades are exempt from CORS entirely; their credential is whatever authenticate accepts on the handshake. |
maxBodyBytes |
1 MiB | Max JSON body size. |
disableBypassPermissions |
false |
Server-wide bypass policy: session/job creates requesting permissionMode: 'bypassPermissions' are rejected (403); the allowDangerouslySkipPermissions pre-authorization is stripped from requests, and the WS set_permission_mode command refuses the mode. Mirrors Claude Code’s permissions.disableBypassPermissionsMode. 'sessions' applies all of that except to an operator’s POST /jobs, which may run in bypass; while such a job runs, typed messages and peer messages into its session are refused. |
approvalTimeoutMs |
null (never) |
How long a permission prompt or an AskUserQuestion may wait unanswered before the engine denies it with resolvedBy: 'timeout'. null/0/omitted means it waits as long as the session lives. Reaches every engine as the default a session may override with approvalTimeoutMs on CreateSessionRequest (where null opts back out). CLI: --approval-timeout 5m. |
engineSleepAfterMs |
0 (never) |
Stop a live session’s engine process once it has sat idle with no client attached for this long. The session stays listed and resumes its conversation on the next message; SessionInfo.engineAsleep marks it meanwhile. Frees the engine child and its MCP servers, which is nearly all of a gateway’s memory. CLI: --engine-sleep-after 30m. |
agentSleepAfterMs |
900000 (15 minutes) |
The idle timeout for an agent’s session when the agent sets no sleepAfterMs of its own; it applies whatever engineSleepAfterMs says, so agents sleep even on a gateway where plain sessions never do. 0 means agents never sleep on their own; an agent’s own sleepAfterMs (including 0) wins either way. CLI: --agent-sleep-after 30m (never for 0). |
owner |
none | Whom sessions answer to when their profile names no owner of its own (ProfileInfo.owner). An agent stores its owner at create. A gateway whose profiles, records or relay enrollment name more than one owner refuses a session or agent that resolves to none. CLI: --owner tobias. |
agentSharing |
{ default: 'private', allowShared: true } |
Whether a new agent is shared with other owners’ shared agents (a card and messages, never a peek). A create request’s sharing, then the profile’s defaults.sharing, then default here; allowShared: false publishes no agent as shared. CLI: --agent-sharing private|shared|never. |
agentContextReset |
{} (available, off by default) |
The context_reset tool, which lets a session start clean once its turn ends and continue from a prompt it wrote: every session, named or not, starts a new conversation under the same id (the old one kept in its history, like New conversation). false forbids it everywhere. Otherwise a session gets it from agentContextReset on its create request, then its profile’s defaults.agentContextReset, then default here. minIntervalMs (default 10 minutes) and maxPerHour (default 3) rate-limit each session; a refused reset answers the agent with the reason. SessionInfo.agentContextReset marks a session that holds the tool, and the reset event carries agentReason. CLI: --agent-context-reset on|off|never. |
effortDefaults |
none (engine defaults) | Reasoning effort per model, { opus: 'high', 'gpt-6-sol': 'xhigh' }, keyed by catalog value or resolved id. A session starts at the level for its model and moves to the new model’s level on a model switch; a model with no entry keeps its engine’s own default, and an explicit reasoningEffort on the create request wins. A profile’s defaults.efforts overrides it per key. CLI: --effort-default opus=high (repeatable). |
requireApiKey |
false |
Fail closed on subscription credentials: a session initializing with apiKeySource: 'oauth' is terminated with a session_error. Recommended for services and unattended use; off, the server logs a one-time notice instead. See Auth. |
checkCredentials |
false (CLI: true) |
Launch-time credential sanity check: after listen() binds, each Claude profile’s session environment is probed with the SDK-bundled CLI’s claude auth status (concurrent, fire-and-forget) and a logged-out profile gets one console warning. Warn, never fail; a probe that can’t run stays silent; only the logged-in/logged-out verdict is read. boolean | { probe?, timeoutMs? }. See Profiles. |
probeModels |
false (CLI: true) |
Ask each Claude profile’s account for its model list after listen() binds and after a profile save: the SDK’s supportedModels() on an engine that is never sent a prompt (no tokens, no MCP servers). A profile then offers the models only its account holds (Mythos 5.1 for the Cyber Verification Program) on top of the shipped catalog. Sessions teach the same list without it. boolean | { probe?, timeoutMs? }. |
requireAvailableProfile |
false |
Refuse to create a session or submit a job on a profile the credential probe reports unavailable - 503 carrying the probe’s own reason - rather than letting the run start and die mid-turn on a raw provider error. Off is right for an operator’s own gateway: the verdict goes stale in both directions, and turning a probe bug into an outage is worse than one confusing failure. It is wrong in front of an end user, who cannot read a provider stack trace and did not choose the deployment’s credentials - which is why every embedder otherwise grows its own available flag in front of the create button. Requires checkCredentials, and only a definite false blocks: a profile never probed stays allowed, because “couldn’t check” is not “not available”. |
listSdkSessions |
claude adapter’s lister | Injectable lister for GET /sdk-sessions (tests) - honored for the Claude engine only; other engines always answer through their adapter. |
sessions |
in-memory store | { store? }: the SessionRecordStore that keeps one record per session id (its conversations, settings, and what parking and dormancy write for it). Two ship: MemorySessionRecordStore (default - a park survives a disconnect, not a restart) and createFileSessionRecordStore({ dir }) (one JSON file per session, read on listen()). Give it a directory of its own: a record written before 4.0.0, a file of another format number, or any other JSON file in it refuses the start, while a file that is not valid JSON or an incomplete current-format record is reported to onError and skipped. A custom store implements the four methods (save, get, list, delete) and validates its own raw data. This replaces the parking.store option of releases before 4.0.0. |
parking |
Deferred execution: { parkDelayMs?, expiredGraceMs?, persistLive?, onError? }. A session that parks on an execution nothing here is running is snapshotted, its runner torn down, and its state kept in its record in the sessions store. parkDelayMs (2000) is the grace after the last client detaches; expiredGraceMs (60000) is the boot grace for a deadline that lapsed while the process was down. See Deployment and Job queue. |
|
notifications |
off | Out-of-band notification for interactive sessions: { webhook?: { url, headers?, events? }, onNotification?, attempts?, retryDelayMs?, onError? }. `onError(error, { op: ‘hook’ |
peers |
on | Session-to-session messaging: { enabled?, maxMessageChars?, perMinute?, maxHops? } (defaults on, 16k, 10, 12). Every session on every engine gets peers_list, peers_peek, peers_send and peers_reset (a lead resets its own member’s context; needs agentContextReset not false); a message is delivered as a user_message with origin: { kind: 'peer' } and never interrupts a running turn. Visibility is the sender session’s scope under the default rule (authorizeSession is not consulted: it takes a principal, and a session is not one), so an embedder with a richer policy scopes every session or sets enabled: false. |
relay |
off | Reach peers on other gateways through a workerdeck relay: { url, gateway, key?, keyFile?, caFile?, expose?: { scope?, allow? }, teams?: { acceptFrom? }, log? }. The gateway dials out, publishes the sessions inside expose.scope, and accepts only the expose.allow operations (default send and peek; add team for teams across gateways) from other gateways. teams.acceptFrom lists gateways whose same-owner agents may join a lead here without an invitation. Needs peers on. See the cross-gateway peers guide. |
queue |
off | Enable the job queue routes - see below. |
onDiagnostic |
silent | (error: unknown, where: string) => void - errors the gateway swallows because nobody is waiting on them: a stale-usage refresh, a /queue/ws stats frame, the shell flush at close, a credential probe that threw, and (unless notifications.onError is set) a throwing onNotification or a webhook that failed every attempt. Never changes control flow. |
QueueServerOptions
All JobQueue options (minus createRunner/buildRunnerConfig, which the server wires
itself - job sessions are ordinary registry sessions and go through the same config hook and
auth-provenance watcher as client sessions):
| Option | Default | Effect |
|---|---|---|
maxConcurrency |
1 | Concurrent job sessions. |
sessionTokenLimit |
off | Token cap per job session (input+output+cache); exceeding it kills the run. |
dailyTokenLimit |
off | Global job-token budget per UTC day; queued jobs held once exhausted. |
maxJobDurationMs |
off | Wall-clock cap per run - the watchdog against stuck CLIs. Time parked on a deferred execution doesn’t count: the run isn’t stuck, it’s waiting. |
maxParkedDurationMs |
off | Cap on time parked, across all parks of one run. The execution’s own deadline usually fires first and lets the agent adapt; this one fails the job. |
killGraceMs |
5000 | Grace between interrupting a killed run and force-closing it. |
retention |
keep forever | { maxAgeMs, sweepIntervalMs? } - expire terminal jobs (the in-memory adapter otherwise grows unboundedly). |
adapter |
in-memory | QueueAdapter backend. The bundled adapter is single-process, non-persistent. |
webhookAttempts |
3 | Delivery attempts per webhook event, exponential backoff. |
webhookRetryDelayMs |
500 | Base delay between webhook delivery retries. |
onEvent |
- | Local observer for every JobEvent, in addition to per-job webhooks. |
See Job queue for semantics.
Routes
Default basePath: '/v1'; every route goes through authenticate.
Every mutation names the conversation it is for (any call while a new conversation is starting is a 409 too):
- Ordinary session mutations (every
PATCH,DELETEandPOSTunder/v1/sessions/:id/outside/conversations: permission answers, uploads, sleep, shells, tasks, MCP actions) carry thex-workerdeck-generationheader (GENERATION_HEADER) set to the session’s currentconversation. Missing or stale: 409. - The conversation POSTs (
/v1/sessions/:id/conversations,…/discard,…/retry,…/pending) requireexpectedGenerationin the body instead (missing: 400, stale: 409), and so doesDELETE /v1/agents/:id. DELETE /v1/sessions/:id/conversations/:nnames its conversation by the pathnand needs neither.
@workerdeck/client sends the header itself; callers pass expectedGeneration to the cut, retry
and pending methods explicitly. See Protocol.
| Route | What it does |
|---|---|
GET /v1/sessions |
List sessions (ListSessionsResponse: { sessions: SessionInfo[] }; client.listSessions() unwraps it); ?closed=1 lists closed ones. |
POST /v1/sessions |
Create a session (CreateSessionRequest). cwd is required for engines that run against a host directory and 403s outside allowedCwdRoots - an engine whose capability record clears hostCwd (the provider engine) may omit it, and for that engine allowedCwdRoots is not the boundary. profile is required when several are declared, 403 outside the caller’s allowedProfiles, and 503 when requireAvailableProfile is on and its credentials probe as unavailable. Fields the chosen engine’s capability record forswears are a 400 rather than a silent no-op. A non-operator caller (scoped, or under authorizeSession without operator: true) gets a 403 for a claude or codex profile its allowedProfiles does not name explicitly, for bypassPermissions/dontAsk/allowDangerouslySkipPermissions, settingSources, a stdio MCP server, and a resume of an SDK session it cannot see. A non-empty body must be application/json (415 otherwise). scope tags are stamped here and are immutable within a conversation (an operator’s new conversation may set new ones). 201 with the SessionInfo. |
GET /v1/sessions/:id |
Session info. A parked session answers from its snapshot, with status: 'parked'. |
PATCH /v1/sessions/:id |
Rename the session (UpdateSessionRequest): title sets meta.title, which SessionInfo.title prefers over the derived one, and null clears it so the derived title (the first prompt) comes back. Answers with the updated SessionInfo. Nothing reaches the engine - a rename does not speak to the model. A stored (parked or dormant) session is renamed in place in its record, without waking it. |
PATCH /v1/sessions/:id with agent |
Operator only: name the session (SessionAgentRequest: name, brief, lead, owner, sharing, …), which makes it an agent under the same id, or update a named one’s identity. agent: null un-names it; 409 while it leads or belongs to a team. |
PATCH /v1/sessions/:id with config |
Set what the session’s next conversation starts from (SessionSettingsPatch: cwd, profile, model, reasoningEffort, permissionMode, agentContextReset; null clears one). Vetted like a create, applied at the next new conversation, never to the running one; answers settings beside session. Works on every session, named or not (a named one updates its agent’s settings too). 409 while a new conversation is starting and on a job run. |
DELETE /v1/sessions/:id |
Close the session - including a parked one, whose stored state is dropped so a late execution result can no longer wake it. Its current conversation joins its history, which stays readable (GET /v1/sessions?closed=1 lists closed sessions). On a named session (an agent) it retires the agent: operator only, members released. |
GET /v1/sessions/:id/conversations / …/conversations/:n |
The session’s conversations (ConversationsResponse) / one past conversation’s events (ConversationHistoryResponse). Authorized on each entry’s frozen owner and scope, not the current session’s, and never decorated with the live session’s name, team or owner. |
POST /v1/sessions/:id/conversations |
New conversation (NewConversationRequest: expectedGeneration, optional first prompt, optional target). The old conversation joins the history, the engine’s store keeps it, and conversation n+1 starts fresh under the same id; answers NewConversationResponse. 409 while busy or starting, on a job run, and with unconfirmed_input when the engine cannot confirm pending input was consumed. A target.scope is operator only. |
POST /v1/sessions/:id/conversations/discard |
The same cut after a person confirmed losing pending input (DiscardAndCutRequest, binding the refusal’s inputRevision); 409 input_changed if input arrived since. |
POST /v1/sessions/:id/conversations/retry / …/pending |
{ expectedGeneration }: start a committed conversation that did not start. { expectedGeneration, send }: Send again (true) or Dismiss a first message whose delivery is uncertain. |
DELETE /v1/sessions/:id/conversations/:n |
Operator only: erase the gateway’s copy of a past conversation (EraseConversationResponse). The entry keeps its number and reads as transcript: 'unavailable'; the engine’s own store is never touched, so claude --resume and the CLI’s stats keep working. 409 for the current conversation, 403 for a caller who can see it but is not an operator. |
GET /v1/sessions/:id/files / …/files/<path> |
List the session’s scratch-filesystem deliverables (ListSessionFilesResponse) / download one (attachment disposition, nosniff). 404 for engines without a VFS (Claude sessions). Served from the snapshot while parked. |
POST /v1/sessions/:id/attachments?name=… |
Upload one file to send with a message (UploadAttachmentResponse). The body is the raw bytes and content-type is its media type - no multipart, so a browser and a phone both upload in one plain request. Images (jpeg/png/gif/webp), PDFs and text types only - further narrowed to the kinds the session’s engine capability record accepts (e.g. a codex session takes images and text, never PDFs); anything else is a 415, over the per-file cap a 413. The returned id goes in the user_message command’s attachmentIds. |
GET /v1/sessions/:id/attachments/:attachmentId |
The uploaded bytes back, for rendering a thumbnail (attachment disposition, nosniff). Held in memory for the session’s lifetime, like /files - a 404 after a restart is expected and does not affect the message, which the model already saw. |
GET /v1/sessions/:id/peers |
The sessions this one may address (PeerSessionsResponse), the same rows its peers_list tool answers: its own scope, itself excluded, and sessions on other gateways (id gateway:session, with gateway and allow) when a relay is configured and the session is unscoped. Feeds the composer’s # picker. 404 when peer messaging is off. |
GET /v1/sessions/:id/mcp |
The session’s MCP servers, live from the engine (McpServersResponse): status, scope, transport/command/args or URL, and each connected server’s tools - with each tool’s JSON Schema where the engine reports one (codex does; the Agent SDK does not). Each server’s env and HTTP headers are stripped - this route is not a way to read the operator’s tokens. All three engines answer it: a provider session reports what the host actually assembled through connectMcpTools, which is an empty list when it wired none, never a 501. 501 only when an engine reports no MCP surface at all (EngineCapabilities.mcpStatus: false); 409 while the session is parked. |
GET /v1/sessions/:id/produced / …/produced/:fileId |
Files the session’s engine wrote on the host - codex’s generated images. List them, or download one (attachment disposition, nosniff). Unlike /v1/fs/* this needs no hostFiles.roots and applies no byte cap, because the allowlist is the exact set of paths this session’s own runner announced in file_produced events, not a directory grant; a generated PNG is routinely megabytes. Held for the session’s lifetime; a 404 means the file has left the disk. |
POST /v1/sessions/:id/mcp/:name |
{ action: 'reconnect' | 'enable' | 'disable' } - the CLI’s own three /mcp actions, scoped to this session (no config file is written). Answers with the refreshed list. 501 when the engine lists its servers but cannot act on one (EngineCapabilities.mcpServerActions: false, which is codex) - clients hide the controls off that flag rather than discovering the refusal. |
POST /v1/executions/:executionId/result |
Deliver a deferred execution’s result (SubmitExecutionResultRequest). Wakes the parked session, applies it to the agent loop, answers { applied, sessionId }. Idempotent by executionId - a duplicate, or one racing the watchdog, answers applied: false. 404 = unknown id, or a session outside the caller’s allowedProfiles. |
WS /v1/sessions/:id/ws?afterSeq=n |
Attach: attached frame (protocol version + snapshot), replay of events past n, then live events; accepts ClientFrames (a command plus its generation). A cut sends conversation_started and closes the socket with 4011. &conversation=n attaches read-only to past conversation n instead. |
POST /v1/sessions/:id/permissions/:requestId |
Resolve a pending approval over REST (ResolvePermissionRequest). 404 = unknown, already resolved, or expired. |
POST /v1/sessions/:id/sleep |
Stop the session’s engine process now, keeping the session ({ session }, with engineAsleep: true). The next message wakes it. 409 with the reason when the session is not idle, has a pending approval or a running subagent or background task, or has no conversation yet; 501 for an engine without a process (EngineCapabilities.engineSleep: false); 200 no-op for a session that is not live. The WS sleep command does the same. |
POST /v1/sessions/:id/tasks/:toolUseId/stop |
Stop a running background task (a sub-agent record with stoppable: true). 404 = nothing stoppable under that id, 409 = session parked, 501 = the engine cannot stop tasks. |
POST /v1/sessions/:id/tasks/:toolUseId/background |
Move a running foreground Bash call or sub-agent to the background (Claude’s Ctrl+B): the call returns at once, the turn continues, the task reports when it settles. Without :toolUseId (/tasks/background) every foreground task moves. 404 = nothing matched, 501 = the engine cannot (only claude can; capabilities.backgroundTasks). |
GET /v1/agents / POST /v1/agents |
List the named sessions (AgentsResponse) / create one (CreateAgentRequest: name, config, prompt, lead, owner, sharing, crossOwner; any other field is refused with 400). ?retired=1 lists retired agents with their conversations. Operator only. |
GET /v1/agents/:id / PATCH /v1/agents/:id / DELETE /v1/agents/:id |
One agent / update it (UpdateAgentRequest; config keys merge, null clears one) / retire it (RetireAgentRequest: expectedGeneration, members: 'release' | 'retire'). A retired agent’s conversations stay readable. |
GET /v1/profiles |
List profiles (ListProfilesResponse), filtered to the caller’s allowedProfiles. Store-backed ones carry managed: true; canManage says whether this caller may create more. |
POST /v1/profiles |
Create a managed profile (CreateProfileRequest). 404 without a profileStore, 403 without canManageProfiles on the principal, 409 if the name is taken, 400 if the profile is one startup would have refused. |
PATCH /v1/profiles/:name |
Merge into a managed profile (UpdateProfileRequest). The name is the route - profiles cannot be renamed, since sessions and jobs are pinned to it. 403 for a startup-declared profile. |
DELETE /v1/profiles/:name |
Delete a managed profile (204). 403 for a startup-declared one. |
GET /v1/profiles/:name |
One profile plus a fresh view-only config snapshot (GetProfileResponse: settings.json highlights, memory/skills/agents/commands - env var names only, never values). Same allowedProfiles scoping. |
GET /v1/sdk-sessions?profile=…&dir=…&limit=…&offset=… |
List an engine’s on-disk resumable sessions. profile names whose store: a claude profile lists the Agent SDK’s session files, a codex profile lists CODEX_HOME’s threads (over a short-lived thread/list child - no live session involved). Absent, the profile is implicit when the server declares exactly one; with several, the Claude engine’s store is listed (the pre-engine-aware behavior). With allowedCwdRoots set, a dir must be inside the roots; omit it and the server lists everything and filters to the roots, so a client with no directory to name still gets an answer inside the policy. |
GET /v1/fs/roots |
The host directories this server exposes, plus canWrite and canSearch (ListHostRootsResponse). 404 unless hostFiles.roots is configured - as does every route below. |
GET /v1/fs/list?path=… |
One host directory, not recursive (ListHostDirResponse). Entries are classified with lstat semantics, so a symlink lists as a symlink and is never followed while rendering. |
GET /v1/fs/find?path=…&q=…&limit=… |
Recursive fuzzy file search under one directory (FindHostFilesResponse) - what an @file picker needs. Subsequence matching, filename hits ranked above path hits, shallowest first; empty q returns the shallowest files. Skips hostFiles.ignore directories and anything behind a symlink, and truncates rather than erroring. |
POST /v1/fs/search |
Content search under one directory with ripgrep (HostSearchRequest: query, isRegex, matchCase, wholeWord, includes/excludes globs, useIgnoreFiles, maxResults up to 20,000). The answer streams as NDJSON (application/x-ndjson): one HostSearchFile per file with its matching lines and match ranges (0-based lines, UTF-16 columns), then done (files, matches, truncated) or error (a bad regex, say). Closing the request stops the search. Respects .gitignore inside git repos unless useIgnoreFiles is false, searches hidden files, never follows symlinks. 501 when no ripgrep was found. The client call is searchHostFiles, an async iterable; searchGlobs in @workerdeck/protocol turns VS Code’s “files to include” text into the globs. |
GET /v1/fs/read?path=… |
One host file (ReadHostFileResponse): content plus the sha256 a write will need. Non-UTF-8 content comes back base64; over maxFileBytes is a 413. |
PUT /v1/fs/write |
Replace or create one file (WriteHostFileRequest). Requires hostFiles.write, else 403. Always conditional: expectedHash must match what is on disk (409 otherwise), and omitting it means create (409 if the path exists). A missing parent directory is a 404 - directories are never created implicitly. |
POST /v1/fs/mkdir |
Create one directory (MakeHostDirRequest, { path }). Requires hostFiles.write. 409 if the path exists; a missing parent is a 404 (no -p). |
POST /v1/fs/delete |
Delete one entry (DeleteHostEntryRequest, { path, recursive? }). Requires hostFiles.write. A non-empty directory without recursive: true is a 409. A root is never an entry, so it cannot be deleted. |
POST /v1/fs/rename |
Rename or move one entry (RenameHostEntryRequest, { from, to }), both inside the roots. Requires hostFiles.write. An existing destination is a 409: never overwrites. These three resolve the parent and only lstat the final component, so a symlink is deleted or renamed itself, never its target. All answer HostEntryResponse ({ path }); the client calls are makeHostDir, deleteHostEntry, renameHostEntry. |
GET /v1/jobs / POST /v1/jobs |
List jobs / schedule one (CreateJobRequest; session.cwd + session.prompt required; session.profile follows the same rules as session creation). Queue-configured servers only, else 404. |
GET /v1/jobs/:id / DELETE /v1/jobs/:id |
Job info / cancel. |
GET /v1/queue |
Queue stats (QueueStats). |
WS /v1/queue/ws |
One-way live stream: queue_attached, then job_event + refreshed queue_stats frames. No replay - re-list jobs on (re)connect. |
Anthropic credentials
The server implements no Anthropic auth: the SDK/CLI resolves credentials from the operator’s
environment (ANTHROPIC_API_KEY, Bedrock/Vertex, or a personal claude login). Each session’s
provenance surfaces as apiKeySource on SessionInfo and the system_init event - the full
posture, including requireApiKey and the contributor red lines, is in
Auth & the providers’ terms.