Skip to content

Serve mode

swarmkit serve starts a persistent HTTP server for production workloads.

Quick start

swarmkit serve ./workspace --host 0.0.0.0 --port 8000

Endpoints

The complete list — every route, generated from the server's OpenAPI document — is the HTTP API reference. Below are the ones a caller integrates with and what they take.

Jobs

Method Path Description
POST /run/{topology} Submit a topology run (async); returns a job id. Body: input, optional attachments, correlation_id, labels, supersedes
GET /jobs/{id} Poll job status (in-memory, then the durable store)
GET /jobs/{id}/stream The job's events as server-sent events
GET /jobs/{id}/diff The unified diff a harness run produced, per agent
POST /jobs/{id}/resume Continue a run parked on a human gate (deferred)
POST /jobs/{id}/stop Ask a running job to stop at its next agent boundary
GET /jobs/history Every recorded run, newest first (survives restart)

Attachments

A caller that already holds a file — a snapshot, an uploaded photo — passes it beside the input instead of making an agent go and fetch it:

POST /run/describe-scene
{
  "input": "What is at the gate?",
  "attachments": [
    { "path": "snapshots/gate-1732.jpg" }
  ]
}
Field
path workspace-relative exactly one of path / data
data base64 for a caller holding bytes rather than a file
name optional display/filename; derived from path when absent
handling preprocess (default) or native intent for non-image types; inert today

There is no type field. The media type is read from the file's content — sending one is a 422, because a caller's claim about bytes that are about to be forwarded to a model is not evidence. url and stream sources are refused for the same reason and a related one: the runtime does not fetch caller-supplied addresses, and an attachment is re-read on every turn of a tool loop, so it has to be re-readable.

Attachments reach the entry agent's first message and no downstream node. An agent that wants a file it was not handed asks for one through a skill.

A bad path is a 422 on this request, not a job that fails a moment later — so a job id means the file was readable. Only images are carried today (image/png, image/jpeg, image/gif, image/webp); anything else is refused by name. Per-attachment ceiling is 20 MB (SWARMKIT_ATTACHMENT_MAX_BYTES).

Every attachment is written to the audit log as a run.attachments event carrying name, media type, size, SHA-256 and source path — never the bytes. The digest is what makes the reference checkable later; storing content would put arbitrary material into a log meant to stay readable.

The CLI equivalent is swarmkit run <ws> <topology> --attach <path> (repeatable).

Conversations

Method Path Description
POST /conversations Create a new conversation
GET /conversations List conversations
GET /conversations/{id} Load full conversation history
POST /conversations/{id}/messages Send message (SSE streaming)

CRUD (topologies, skills, archetypes)

Method Path Description
GET /api/topologies List topologies
GET /api/topologies/{id} Get topology details
GET /api/topologies/{id}/yaml Get raw YAML
PUT /api/topologies/{id} Replace the YAML (validated before it is written)
POST /api/topologies Create new topology
DELETE /api/topologies/{id} Delete topology

Same pattern for /api/skills and /api/archetypes.

Usage tracking

Method Path Description
GET /usage Global token usage summary
GET /usage/{job_id} Per-job usage breakdown

MCP endpoint

Method Path Description
POST /mcp/ Streamable HTTP MCP endpoint — every topology as a run_<name> tool, behind the same auth (Level 11)

Each topology becomes an MCP tool. External agents can call your swarm topologies via standard MCP protocol.

A2A endpoint

Off by default; server.a2a.enabled: true turns it on (no restart — a reload is enough).

Method Path Description
GET /.well-known/agent-card.json The Agent Card: one A2A skill per topology; public (no auth)
GET /a2a/{topology}/card The same card narrowed to one topology
POST /a2a JSON-RPC 2.0: message/send, message/stream, tasks/get, tasks/list, tasks/cancel, tasks/subscribe; the message's metadata.skill names the topology
POST /a2a/{topology} The same, bound to one topology (the card's per-skill url)

A2A is a transport onto jobs, not a second execution path: message/send is POST /run/{topology} (contextIdcorrelation_id, file parts → attachments), tasks/get is GET /jobs/{id}, tasks/cancel is POST /jobs/{id}/stop, and the streaming methods re-emit GET /jobs/{id}/stream as A2A status/artifact events. The task id is the job id; the run carries source: a2a and appears in the portal's jobs page like any other. Task states map from job status: pendingsubmitted, runningworking, deferredinput-required, completed, failed, stoppedcanceled.

A run parked on a human gate reports input-required with the gate's URL in the status message, but the A2A caller cannot supply that input: approval scopes are un-grantable to agents, so a follow-up message/send on the task is refused with UnsupportedOperationError and the gate URL. A person resolves it through the review queue; a subscribed client sees the task go working again. Push notifications, gRPC and card signing are not implemented (-32003 / -32004). Streaming methods need Accept: text/event-stream. Full mapping: design/details/a2a-interop.md.

Webhooks

Method Path Description
POST /hooks/{topology_name} Fire a webhook trigger: an HMAC-signed request starts the named topology

Webhook signatures are validated with HMAC-SHA256 when secret is configured on the trigger.

Canary

Method Path Description
GET /canary List canary routes and metrics
POST /canary/{topology}/promote Promote canary version
POST /canary/{topology}/rollback Rollback canary version

Authentication

Configure in workspace.yaml:

server:
  auth:
    provider: jwt
    jwks_url: https://your-idp/.well-known/jwks.json
    audience: swarmkit
    issuer: https://your-idp/

Available providers:

Provider Description
none No authentication (default)
api_key Bearer keys declared in workspace.yamlserver.auth.config.keys[], each a key_ref: env:<VAR> (a reference, never the literal), a client_id and a tier (read / run / admin) or explicit scopes. See the serve auth guide
jwt JWT with JWKS auto-discovery

Production profile (fail-closed preflight)

swarmkit serve --profile production refuses to start unless the deployment is safe to expose, listing every gap at once instead of running permissively. It is a deployment-time assertion — it does not change request handling (governance is already deny-by-default), it checks that you configured the safe thing. --profile standard (the default) is the unchanged laptop behaviour.

The preflight requires all of:

Requirement Why
Auth is not none/anonymous production does not serve anonymous, on any bind
--insecure is not set the escape hatch is incompatible with the profile
SWARMKIT_OAUTH_KEY is set else the token-encryption key regenerates on restart and silently invalidates stored tokens
Every mcp_servers entry is sandboxed: true a tool process must not run unisolated on the host (design §8.8)
No wildcard --cors-origin * exact origins only
$ swarmkit serve ./ws --profile production
--profile production refuses to start: the deployment is not fail-closed.
  1. auth provider is 'none' — configure server.auth (api_key or jwt); production does not serve anonymous, on any bind
  2. SWARMKIT_OAUTH_KEY is not set — the OAuth token-encryption key would regenerate on restart and silently invalidate every stored token; set a persistent key
  3. MCP server 'files' is not sandboxed — set `sandboxed: true` so the tool process runs isolated (design §8.8)
Fix these, or run without --profile production for the permissive default.

The existing default-secure refusal (an unauthenticated non-loopback bind) still runs regardless of profile; production adds to it. create_app(profile="production") carries the preflight, so an embedder inherits it too.

Server configuration

server:
  host: "0.0.0.0"
  port: 8000
  jobs:
    max_concurrent: 5
    timeout_seconds: 300
  mcp:
    enabled: true
  a2a:
    enabled: false          # publish the Agent Card + serve /a2a
    identity:               # optional; what the card says about this instance
      name: Review desk
      description: Code review and triage swarms, human-gated.
      url: https://swarm.example.com   # set behind a proxy; default is the request's host
      organization: Example Org

/capabilities reports features.a2a so a fleet can see which instances publish a card.

Triggers

Cron and webhook triggers are defined in triggers/*.yaml:

apiVersion: swarmkit/v1
kind: Trigger
metadata:
  id: nightly-review
  name: Nightly Code Review
type: cron
schedule: "0 2 * * *"
topology: code-review
input: "Review all PRs opened today"
apiVersion: swarmkit/v1
kind: Trigger
metadata:
  id: pr-webhook
  name: PR Webhook
type: webhook
topology: code-review
secret: ${WEBHOOK_SECRET}

Docker

docker run -v ./workspace:/workspace \
  -e OPENROUTER_API_KEY=$OPENROUTER_API_KEY \
  -p 8000:8000 \
  ghcr.io/delivstat/swarmkit:latest