AI Agents

Use Spunto workers as isolated sandboxes for AI coding agents — full API control, zero setup overhead.

Spunto workers are ideal environments for AI coding agents. Each worker is an isolated Docker container with a full development stack — code execution, git, terminal access — exposed via a clean REST API.

  • Isolation — each agent run gets a fresh, contained environment; no host system access
  • Pre-configured — repos are cloned, credentials are set, tools are installed at spawn
  • Ephemeral — delete the worker when done; no cleanup needed
  • API-driven — spawn, interact, read output, and destroy entirely via HTTP

1. POST /api/orgs/{orgId}/projects/{projectId}/workers spawn worker
2. Poll GET /api/orgs/{orgId}/projects/{projectId}/workers/{workerId} wait for "ready"
3. Use the worker (terminal, git, file system via code-server)
4. DELETE /api/orgs/{orgId}/projects/{projectId}/workers/{workerId} destroy

Create an API key scoped to the org the agent should operate in — from Account Settings, pick workers:read + workers:write at minimum (add workers:exec if the agent needs an interactive terminal, and projects:read/projects:write if it also manages projects). For an agent deployed long-term outside any one person's account, create an org-owned (service account) key instead of a personal one — it keeps working if the creator leaves the org.

curl -H "Authorization: Bearer spk_..." \
  https://spunto.net/api/orgs/{orgId}/projects

curl -X POST \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  https://spunto.net/api/orgs/{orgId}/projects/{projectId}/workers

Response:

{
  "id": "wkr_abc123",
  "state": "spawning",
  "setupStatus": { "phase": "initializing", ... }
}

Poll the worker endpoint every 2 seconds until state === "ready":

curl -H "Authorization: Bearer <token>" \
  https://spunto.net/api/orgs/{orgId}/projects/{projectId}/workers/wkr_abc123

The setupStatus.phase field tracks each step: cloningfeatureslifecycleready.

The worker terminal is a full PTY — the same interactive shell a human gets in the dashboard — exposed over WebSocket. It accepts the same Authorization: Bearer <api key> header as every REST route, so no browser session is needed.

wss://spunto.net/api/workers/{workerId}/terminal?mode=terminal&cols=220&rows=50
Query paramDefaultDescription
modeterminal (interactive PTY) or logs (stdout/stderr stream, read-only)
cols / rows80 / 24PTY dimensions — set to a wide value (e.g. 220 × 50) to avoid line-wrapping in agent output
sessionmaintmux session name — reuse the same name to reconnect to a running session

Auth — either the browser session cookie (existing behavior) or an Authorization: Bearer spk_... header with the workers:exec scope (or *). A missing/invalid token returns 401; a valid token with the wrong scope returns 403.

Protocol:

  1. Wait for { "type": "ready" } — the PTY is attached
  2. Send { "type": "input", "data": "<base64>" } — raw keystrokes, including \r to submit a line
  3. Receive { "type": "output", "data": "<base64>" } — raw PTY output (ANSI included)
  4. Send { "type": "resize", "cols": N, "rows": M } to resize the PTY

The WebSocket closes when the shell exits. Reconnecting with the same ?session=<name> reattaches the tmux session — the shell and its history survive a disconnect. This is the right way to run a long-lived agent command (e.g. claude code --yes) without needing to keep the connection open for the full duration.

Node.js example:

import WebSocket from "ws"
import { Buffer } from "node:buffer"
 
const ws = new WebSocket(
  `wss://spunto.net/api/workers/${workerId}/terminal?mode=terminal&cols=220&rows=50`,
  { headers: { Authorization: `Bearer ${apiKey}` } }
)
 
// Wait for ready, then send a command
ws.once("message", (raw) => {
  const msg = JSON.parse(raw.toString())
  if (msg.type !== "ready") return
 
  ws.send(JSON.stringify({
    type: "input",
    data: Buffer.from("claude 'implement feature X' --yes\r").toString("base64"),
  }))
})
 
// Stream output to stdout
ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString())
  if (msg.type === "output") {
    process.stdout.write(Buffer.from(msg.data, "base64"))
  }
})

Tip

For a long-running agent command, send the command in one shot and close the WS immediately — the tmux session keeps the process alive. Reconnect later with the same ?session= name to check on it or read the scrollback.

curl -H "Authorization: Bearer <token>" \
  "https://spunto.net/api/orgs/{orgId}/projects/{projectId}/workers/{workerId}/logs?tail=100"

Returns plain text stdout/stderr from the container.

Use User Secrets to inject credentials (API keys, tokens) as environment variables into every worker, without storing them in the project config:

curl -X POST \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "ANTHROPIC_API_KEY", "value": "sk-ant-..." }' \
  https://spunto.net/api/users/me/secrets

The secret is encrypted at rest and injected at spawn time. Values are never returned by the API.

A minimal setup for running Claude Code in an isolated Spunto worker:

  1. Create a project with your codebase repo and ANTHROPIC_API_KEY in User Secrets
  2. Set postCreateCommand: "npm install -g @anthropic-ai/claude-code"
  3. Spawn a worker via API
  4. Wait for state === "ready"
  5. Connect via WebSocket terminal and run claude "implement feature X"
  6. Read output, commit changes, delete worker

Tip

Workers have GITHUB_TOKEN pre-injected. Claude Code (and gh CLI) will pick it up automatically for PR creation, issue reading, etc.

After an agent run, check what changed:

curl -H "Authorization: Bearer <token>" \
  https://spunto.net/api/orgs/{orgId}/projects/{projectId}/workers/{workerId}/git-status

Returns per-repo branch and modified file count — useful for CI pipelines that spawn a worker, run an agent, and then open a PR.