# osmoda — Agent Skill Document

> **You are reading the machine-readable reference for osmoda and the spawn.os.moda hosted-provisioning API.**
> Optimized for AI agents that need to spawn, control, monitor, or integrate with osmoda servers.
> For the rendered HTML, see https://spawn.os.moda/skill.

Version: **v1.3.36** (gateway v0.2.4) · Updated: 2026-05-21

| Resource | URL |
|---|---|
| This document (Markdown) | https://spawn.os.moda/SKILL.md |
| Rendered HTML | https://spawn.os.moda/skill |
| Interactive API explorer (Swagger) | https://spawn.os.moda/docs |
| OpenAPI 3.0.3 spec (JSON) | https://spawn.os.moda/api/v1/docs |
| AI-readable site descriptor (llmstxt.org) | https://spawn.os.moda/llms.txt |
| Agent Card (A2A / ERC-8004) | https://spawn.os.moda/.well-known/agent-card.json |
| Source code | https://github.com/bolivian-peru/os-moda |
| Telegram (talk to humans) | https://t.me/osmodasystems |

---

## TL;DR for AI agents

osmoda is a **NixOS distribution where an AI agent has root access to the whole machine**. You spawn a dedicated VM through `spawn.os.moda`, pay with USDC via x402, and within 5–10 minutes you have a Linux box with:

- An AI agent running 24/7 — on the `openclaw` runtime by default/executive (the original osModa design; loads the 91-tool osmoda-bridge natively), with `claude-code` as a one-click selectable peer
- **91 MCP tools** registered by `osmoda-bridge` — structured access to processes, services, files, network, deploys, crypto wallets, P2P mesh
- A **SHA-256 hash-chained audit ledger** for every mutation
- **NixOS atomic rollback** for any system change
- **Three reach channels**: WebSocket chat, Telegram, WhatsApp
- A **dashboard config API** that lets you swap runtime/credential/model live (no SSH, no rebuild)

If you are an AI agent reading this to drive osmoda programmatically, jump to [§ How to spawn a server](#how-to-spawn-a-server) and [§ All public endpoints](#all-public-endpoints).

If you are an AI agent running *on* a spawned osmoda box, jump to [§ Agent behavior rules](#agent-behavior-rules) and [§ Daemon sockets reference](#daemon-sockets-reference).

---

## What's new since v1.3.0

Each line below is a single backwards-compatible change. If you are integrating with the API and your code worked on v1.3.0, it still works on v1.3.25 — these are additions and bug fixes, not breaking changes.

| Version | Date | Change |
|---|---|---|
| **v1.3.18** | 2026-05-11 | Gateway binds to `127.0.0.1` by default (was `0.0.0.0`). Public ingress is via the spawn-server SSH proxy. Single `bindCredentialToGateway()` path for dashboard + reseller key delivery. |
| **v1.3.20** | 2026-05-13 | Stop button kills the entire process group: `detached: true` spawn + `process.kill(-pid, "SIGTERM")` + 2 s SIGKILL escalation. Previously SIGTERM only hit the runtime leader and child shells kept running. |
| **v1.3.23** | 2026-05-13 | `POST /chat-abort` always reaches the gateway regardless of local tracking state. |
| **v1.3.24** | 2026-05-14 | Long-running tasks supported. `CHAT_WATCHDOG_MS=15 min`, `FIRST_SIGNAL_TIMEOUT_MS=10 min`, `OSMODA_CHAT_HARD_CAP_MS=8 h` (all env-overridable). Same caps apply to both runtimes. |
| **v1.3.25** | 2026-05-14 | **Dual-signal wedge detection.** Wedge requires *both* `last_heartbeat` AND `agent_last_frame_at` to be stale. Eliminates the false-positive class where the heartbeat sender was broken but the agent was happily streaming chat. Recovery logs include `alive_via: "heartbeat" \| "agent_frame"`. |
| **gateway v0.2.1** | 2026-05-14 | **Sessions persist to disk** at `/var/lib/osmoda/state/sessions.json` (mode 0600, atomic tmp+rename, debounced 250 ms). Claude session id survives gateway restarts. Sessions are **runtime-tagged**: swapping `claude-code` ↔ `openclaw` via the Engine tab wipes the foreign session id so the new runtime starts cleanly. |
| **install.sh** | 2026-05-14 | OpenClaw binary installed on every spawn (was: only when `--runtime=openclaw`). The Engine-tab runtime swap now always lands on a present binary. |
| **v1.3.32** | 2026-05-20 | Heartbeat body limit raised to 1 MB (was global 16 KB) — full heartbeats were 413'ing → `last_heartbeat` never updated → false "stalled" on healthy agents. Header now treats a fresh chat frame as proof the agent works (dual-signal). 3-column server-detail grid + redesigned Apps tiles. Model switching consolidated to the Engine tab. |
| **gateway v0.2.2** | 2026-05-20 | **CodeGraph code-intelligence MCP server** (colbymchenry/codegraph, MIT, pure-WASM, installed by default, env-gated `OSMODA_CODEGRAPH_ENABLED=1`). Adds 9 `codegraph_*` tools + a pre-indexed code knowledge graph, auto-synced across `/opt/osmoda`, `/workspace/*`, `/srv/*` every 30 min. ~90% fewer grep/Read tool calls on code tasks. Security-audited before integration. |
| **v1.3.35** | 2026-05-21 | **Chat replay + streaming overhaul.** Single-source replay (server NDJSON is the only renderer — no more localStorage dual-render that duplicated messages); restored replies are markdown-formatted (incl. tables) identically to live ones; a clear history/live boundary divider; smooth requestAnimationFrame text reveal so buffered chunks stream instead of popping; even spacing (stray `<br>` around lists/headers/tables stripped). |
| **gateway v0.2.3** | 2026-05-21 | **OpenClaw 2026.5+ driver ported** (selectable in the Engine tab — `available:true` again; was blocked since the `run`→`agent` rename). **claude-code reply-corruption fix**: text de-dup is now per assistant `message.id`, so multi-message turns (text → tool → text) no longer glue/garble (`"…happened:cess running…"`). **Tool-action targets**: every `tool_use` carries a command/path/url hint so the chat action log shows *what* each tool did, not just its name. |
| **runtime** | 2026-05-22 | **OpenClaw is now the default/executive runtime** (its original osModa role); claude-code remains a selectable peer. OpenClaw updated to 2026.5.20. The `osmoda-bridge` (91 system tools) is installed as a native OpenClaw plugin (compiled `dist/index.js` + `contracts.tools` manifest, `openclaw plugins install --link`). The openclaw driver now accepts **API key OR OAuth** (OAuth written as a "token" AuthProfile). Verified live: a real gateway turn loaded 77+ osModa tools under openclaw. |
| **gateway v0.2.4** | 2026-05-21 | **Always-rememberable chat (OpenClaw-style).** (1) **Gateway-owned canonical transcript** — the gateway writes a JSONL transcript per session to `/var/lib/osmoda/state/transcripts/<agent>/<key>.jsonl`; `GET /sessions` + `GET /sessions/:agent/:key/transcript` expose it; the dashboard reads this as the single source of truth (kills the record-drift class). (2) **Durable `MEMORY.md` auto-load** — `MEMORY.md` + today/yesterday `daily/*.md` are injected into the system prompt every turn, so the agent remembers facts/preferences/decisions across brand-new conversations. (3) **Re-seed** — if the runtime session is missing (fresh box, wiped file, or a claude-code↔openclaw swap) the gateway prepends a compact recap from the transcript, so memory survives beyond the runtime's own storage. |

If you query `GET /api/v1/status/{orderId}`, new informational fields are present: `chat_responsive`, `agent_last_frame_at`, `auto_restart_attempts`, `auto_restart_status`, `last_auto_restart_attempt_at`. Treat them as advisory — `status: "running"` is still the canonical health signal.

---

## What is osmoda

osmoda is a NixOS distribution purpose-built for AI agent infrastructure. When you spawn a server, you get a dedicated VM with:

- **An AI agent already running.** Claude Opus on the `osmoda` agent profile (web + API); Claude Sonnet on the `mobile` agent profile (Telegram + WhatsApp). Both have full tool access.
- **10 daemons** (9 Rust + 1 TypeScript). All inter-process communication is over Unix domain sockets at `/run/osmoda/*.sock`. No exposed TCP except the encrypted mesh peer port `:18800`.
- **91 MCP tools** registered by `osmoda-bridge`, available to whichever runtime the gateway is running.
- **A modular gateway** (`osmoda-gateway`, TypeScript) that routes per-agent to a pluggable runtime driver. Drivers today: `claude-code`, `openclaw`. Adding a driver = one file under `src/drivers/`.
- **Hot-reloadable config.** `SIGHUP` re-reads `agents.json` and `credentials.json.enc` without dropping in-flight WebSocket chats.

### The 10 daemons

| Daemon | Socket / port | Responsibility |
|---|---|---|
| `agentd` | `/run/osmoda/agentd.sock` | System bridge — health, query, events, memory, backups, approval gate, agent card |
| `osmoda-keyd` | `/run/osmoda/keyd.sock` | Crypto wallets — ETH + SOL, AES-256-GCM at rest, policy-gated signing, `PrivateNetwork=true` |
| `osmoda-watch` | `/run/osmoda/watch.sock` | SafeSwitch deploys + autopilot watchers with auto-rollback |
| `osmoda-routines` | `/run/osmoda/routines.sock` | Background cron/interval/event automation |
| `osmoda-mesh` | `/run/osmoda/mesh.sock` + TCP `:18800` | P2P encrypted agent-to-agent (Noise_XX + X25519 + ChaChaPoly + ML-KEM-768) |
| `osmoda-mcpd` | `/run/osmoda/mcpd.sock` | MCP server lifecycle (start/stop/restart/configure) |
| `osmoda-teachd` | `/run/osmoda/teachd.sock` | System learning: OBSERVE (30 s) → LEARN (5 min) → SKILLGEN (6 h) |
| `osmoda-voice` | `/run/osmoda/voice.sock` | Local STT (whisper.cpp) + TTS (piper) — no cloud |
| `osmoda-egress` | localhost-only HTTP CONNECT proxy | Domain-allowlisted egress for sandboxed tools |
| `osmoda-gateway` | `127.0.0.1:18789` (HTTP + WS) | Modular agent gateway — `claude-code` + `openclaw` drivers |
| `agentctl` | CLI binary (not a daemon) | `verify-ledger`, `events` |

---

## How to spawn a server

You pay with USDC via the x402 protocol on **Base Sepolia testnet** or **Solana Devnet**. As of 2026-05-19, no public x402 facilitator supports mainnet — the spawn server runs in testnet mode until that changes. No signup, no accounts, no API keys for the spawn flow itself.

### 1. List plans

```http
GET https://spawn.os.moda/api/v1/plans
```

Returns JSON with all plans, pricing, regions, and x402 payment info. Free, no auth.

### 2. Spawn (triggers x402 payment)

```http
POST https://spawn.os.moda/api/v1/spawn/{planId}
Content-Type: application/json
Idempotency-Key: <16-128 chars, [A-Za-z0-9_-]>      # strongly recommended

{
  "region":        "eu-central",
  "ssh_key":       "ssh-ed25519 AAAA...",
  "runtime":       "claude-code",
  "default_model": "claude-opus-4-6",
  "credentials": [
    {
      "label":    "My Claude Pro",
      "provider": "anthropic",
      "type":     "oauth",
      "secret":   "sk-ant-oat01-..."
    }
  ]
}
```

All body fields are optional. Sensible defaults: `region=eu-central`, `runtime=claude-code`, `default_model=claude-opus-4-6` (the `osmoda` agent's default; switch to `claude-opus-4-7` from the Engine tab if you want the newer model).

**First call** returns `402 Payment Required` with an x402 envelope (price, asset, network, `payTo`). Your x402 client signs and retries with a `PAYMENT` header. On success you get `200`:

```json
{
  "order_id":    "550e8400-e29b-41d4-a716-446655440000",
  "api_token":   "osk_a1b2c3d4e5f6...",
  "plan":        "Solo",
  "price_usd":   14.99,
  "server_ip":   "1.2.3.4",
  "status":      "provisioning",
  "status_url":  "https://spawn.os.moda/api/v1/status/550e8400-...",
  "chat_url":    "wss://spawn.os.moda/api/v1/chat/550e8400-...",
  "ssh":         "ssh root@1.2.3.4",
  "message":     "Server provisioning. osmoda installs in 5-10 minutes."
}
```

**Save `api_token` — it is shown once.** You need it for status, chat, events, and token-lifecycle calls.

### 3. Idempotency

Pass `Idempotency-Key` on every spawn. It is safe across retries:

- **Same key + same body** → returns the original response with `Idempotent-Replayed: true` for 24 hours. **No re-charge at x402.**
- **Same key + different body** → `409 idempotency_key_reused`.
- **No header** → first success charges; retries re-charge.

Generate per business-level spawn, e.g. `myapp-2026-05-19-<uuid>`.

### 4. Wait for ready (5–10 min)

```http
GET https://spawn.os.moda/api/v1/status/{orderId}
Authorization: Bearer osk_{token}
```

Poll every 15–30 s. Transitions: `pending` → `provisioning` → `running` (or `failed` / `expired`).

Without a token you get basic status (`{order_id, status, plan, created_at}`). With Bearer `osk_` you get `server_ip`, `region`, `chat_url`, `ssh`, `provision_steps[]`, `install_error`, and the v1.3.1 wedge fields (`chat_responsive`, `agent_wedged`, `auto_restart_*`, `agent_last_frame_at`).

Better than polling: subscribe to the SSE event stream (see [§ v1.3.0 Unified server event plane](#v130--unified-server-event-plane)).

### 5. Use the server

Once `status === "running"`:

- **SSH:** `ssh root@{server_ip}`
- **WebSocket chat:** `wss://spawn.os.moda/api/v1/chat/{orderId}?token=osk_{token}`
- **Server events (SSE):** `GET /api/v1/servers/{orderId}/events`
- **Dashboard:** https://spawn.os.moda — Engine tab swaps runtimes / credentials / models without SSH or rebuild.

---

## Plans

| Plan ID | Name | Price | CPU | RAM | Disk | Use case |
|---|---|---|---|---|---|---|
| `test` | Solo | $14.99 | 2 | 4 GB | 40 GB | 1 agent, light tasks |
| `starter` | Pro | $34.99 | 4 | 8 GB | 80 GB | 2–4 agents, real work |
| `developer` | Team | $62.99 | 8 | 16 GB | 160 GB | 5–10 agents, heavy loads |
| `production` | Scale | $125.99 | 16 | 32 GB | 320 GB | 10–20+ agents, full fleet |

All plans include: NixOS, 10 daemons, 91 MCP tools, modular gateway with `claude-code` + `openclaw` drivers, heartbeat monitoring, auto-healing watchers, the v1.3.0 unified event stream.

## Regions

| ID | Location |
|---|---|
| `eu-central` | Frankfurt, Germany |
| `eu-north` | Helsinki, Finland |
| `us-east` | Virginia, USA |
| `us-west` | Oregon, USA |

Default: `eu-central`.

---

## All public endpoints

### Free (no payment, no auth)

```
GET  /api/v1/plans                     List plans + pricing + x402 info
GET  /api/v1/status/{orderId}          Basic status (full with Bearer)
GET  /api/v1/docs                      OpenAPI 3.0 spec (JSON)
GET  /.well-known/agent-card.json      A2A / ERC-8004 discovery
GET  /llms.txt                         AI-readable site descriptor
GET  /SKILL.md                         This document
GET  /skill                            Rendered HTML
```

### x402 payment required

```
POST /api/v1/spawn/test                Spawn Solo   ($14.99)
POST /api/v1/spawn/starter             Spawn Pro    ($34.99)
POST /api/v1/spawn/developer           Spawn Team   ($62.99)
POST /api/v1/spawn/production          Spawn Scale  ($125.99)
```

### Bearer `osk_` (reseller / programmatic)

```
GET    /api/v1/status/{orderId}                          Full status
GET    /api/v1/tokens/{token_id}                         Token metadata
DELETE /api/v1/tokens/{token_id}                         Revoke token (204)
GET    /api/v1/spec-kit/projects                         Spec-driven projects across fleet
GET    /api/v1/servers/{orderId}/events                  SSE event stream (v1.3.0)
GET    /api/v1/servers/{orderId}/requests                Recent request receipts
GET    /api/v1/servers/{orderId}/requests/{request_id}   Single receipt
GET    /api/v1/servers/{orderId}/spawn-log               NDJSON event log (v1.3.1)
POST   /api/v1/servers/{orderId}/agents/{agent}/restart  Restart wedged agent (202)
POST   /api/v1/servers/{orderId}/api-key                 Set/rotate AI key (202)
WS     /api/v1/chat/{orderId}?token=osk_{token}          WebSocket chat
GET    /api/v1/servers/{orderId}/chat-history?since=N    Server-side chat replay
```

### Dashboard (`sk_live_` Bearer or session cookie)

```
GET    /api/dashboard/servers                              List your servers
POST   /api/dashboard/servers/{id}/api-key                 Set/rotate AI key (202)
DELETE /api/dashboard/servers/{id}/api-key                 Remove AI key
POST   /api/dashboard/servers/{id}/agents/{agent}/restart  Restart wedged agent
POST   /api/dashboard/servers/{id}/chat-async              Async chat (202)
POST   /api/dashboard/servers/{id}/chat-abort              Hard-abort the active turn (v1.3.20+)
GET    /api/dashboard/servers/{id}/chat-stream/{conv_id}   SSE chat stream
GET    /api/dashboard/servers/{id}/chat-history/{conv_id}  Cold load
GET    /api/dashboard/servers/{id}/events                  Unified SSE events
GET    /api/dashboard/servers/{id}/requests                Request history
GET    /api/dashboard/servers/{id}/requests/{request_id}   Single receipt
GET    /api/dashboard/servers/{id}/config/drivers          List runtimes available
GET    /api/dashboard/servers/{id}/config/agents           Agent profiles
PATCH  /api/dashboard/servers/{id}/config/agents/{aid}     Update agent (SIGHUP)
GET    /api/dashboard/servers/{id}/config/credentials      Credentials (encrypted)
POST   /api/dashboard/servers/{id}/config/credentials      Add credential
POST   /api/dashboard/servers/{id}/config/credentials/{c}/test   Test credential
POST   /api/dashboard/servers/{id}/config/credentials/{c}/default Set as default
DELETE /api/dashboard/servers/{id}/config/credentials/{c}  Remove credential
```

Reseller (`osk_`) and dashboard (`sk_live_`) surfaces emit **identical event shapes** — only the auth differs. SDKs can target one schema.

---

## v1.3.0 — Unified server event plane

Every async action emits typed lifecycle events on a per-server SSE stream. One subscription, all actions.

```http
GET /api/v1/servers/{orderId}/events?cursor=N&filter=request,state,agent,heartbeat,install
Authorization: Bearer osk_{token}
Accept: text/event-stream
```

Cursor-resumable. 15 s keepalive comment frames. 30 min hard timeout (reconnect with the last seen `id` as `cursor`).

### Event types

| Type | Payload |
|---|---|
| `request.accepted` | `{request_id, action, accepted_at, expected_completion_within_seconds}` |
| `request.progress` | `{request_id, action, progress: {stage, tool?, ...}}` |
| `request.completed` | `{request_id, action, result}` |
| `request.failed` | `{request_id, action, failure: {code, message, fallback_recommendation?}}` |
| `request.superseded` | `{request_id, action, superseded_by}` |
| `state.changed` | `{field, value}` — e.g. `has_api_key` flips true |
| `agent.wedged` | `{heartbeat_stale_min, frame_stale_min, last_heartbeat, agent_last_frame_at}` (v1.3.25 dual-signal) |
| `agent.healed` | `{via: "auto_recovery" \| "manual", alive_via: "heartbeat" \| "agent_frame"}` |
| `heartbeat.received` | `{server_status, agents_count, lag_ms}` |
| `install.progress` | `{step, status, detail}` |

### Failure codes

```
agent_silent          The agent stopped emitting frames mid-turn.
agent_disconnected    No WebSocket session on the customer gateway.
no_credential         No AI credential bound. POST /config/credentials.
gateway_wedged        The gateway daemon is alive but unresponsive.
gateway_unreachable   SSH proxy hop failed (commonly transient PAM/network).
ssh_pam_expired       Hetzner-style root password expired — auto-recovery fires.
ssh_restart_failed    Customer-box ssh restart hop failed.
timeout               Action exceeded its hard cap.
exception             Uncaught exception in the spawn-app.
```

### fallback_recommendation values

```
add_api_key                   Bind a credential.
restart_agent                 POST /agents/{agent}/restart.
wait_for_wedge_auto_restart   The auto-restart loop is already running.
delete_and_respawn            Order is unrecoverable; spawn a fresh one.
retry                         Transient. Re-send with a fresh idempotency key.
```

### Request receipts

Don't want SSE? Poll the receipt:

```http
GET /api/v1/servers/{orderId}/requests/{request_id}
Authorization: Bearer osk_{token}
```

Returns `{request_id, order_id, action, status, accepted_at, completed_at?, progress, result?, failure?}` plus `_links` to events and status.

---

## v1.3.1 — Wedge detection & self-serve recovery

Driven by a real integrator bug report: a wedged-agent state where heartbeats kept landing (daemon alive) but the chat agent was silent. The v1.2.7 detector was heartbeat-only; auto-restart was one-shot. v1.3.1 added retry-with-backoff. **v1.3.25 made detection dual-signal.**

### Detection (v1.3.25)

A server is flagged `agent_wedged: true` only when **both** signals are stale ≥ 5 min:

- `last_heartbeat` — agentd's 60 s heartbeat cron, the daemon-alive signal
- `agent_last_frame_at` — last frame the customer gateway WebSocket emitted, the agent-responsive signal

Recovery clears `agent_wedged` the moment **either** signal goes fresh. Logged with `alive_via: "heartbeat" | "agent_frame"`.

Why this matters: previously a broken heartbeat sender (cron drift, transient SSH PAM lock, boot-time issue) would flip `agent_wedged=true` and trigger a restart even though the agent was actively answering chat. Real wedge requires both planes silent.

### Auto-restart (v1.3.1)

When wedged, the detector retries `agent_restart` across `0 / 5 / 10 / 30` min backoff. After 4 attempts, emits `agent.escalation_required` (operator action: respawn or fix manually).

Surfaces on every status response:

```
auto_restart_attempts             0-4
auto_restart_status               restarting | ready | failed | timeout | exhausted
last_auto_restart_attempt_at      ISO timestamp
agent_last_frame_at               ISO timestamp (v1.3.25)
agent_wedged_frame_stale_min      int, only present when wedged (v1.3.25)
```

Counters reset on recovery.

### chat_responsive (v1.3.1)

New boolean on server-list responses, independent of heartbeat:

- `true` — frame in last 5 min
- `false` — WS connected but silent for >5 min
- `null` — no recent session (fall back to `agent_responsive`)

**Integrator gate:** show "Talk to agent" only when `chat_responsive !== false`.

### Reseller spawn-log

```http
GET /api/v1/servers/{orderId}/spawn-log?since_ms=…&level=warn,error&limit=100
Authorization: Bearer osk_{token}
```

NDJSON event log. Provision steps, heartbeats, every wedge transition, every auto-restart attempt with status, terminal `agent_recovered` / `agent_escalation_required` events. Self-serve diagnosis without tickets.

### Typed 503 codes (chat-async)

```
gateway_wedged         Room exists, agent_wedged=true
gateway_unreachable    Room exists, WS closed
agent_disconnected     No room
```

Each carries `fallback_recommendation`. Stale-conversation auto-clear after 6 min so a dead prior chat doesn't permanently 409-block future requests.

---

## v1.2.5 — Async chat (dashboard SSE)

```
POST /api/dashboard/servers/{id}/chat-async              → 202 {conversation_id, request_id}
GET  /api/dashboard/servers/{id}/chat-stream/{conv_id}   → SSE (cursor-resumable)
GET  /api/dashboard/servers/{id}/chat-history/{conv_id}  → JSON cold load
POST /api/dashboard/servers/{id}/chat-abort              → kill the active turn (v1.3.20+)
```

The SSE stream emits `text`, `tool_use`, `tool_result`, `done`, `error` frames, plus the v1.3.0 `request.*` lifecycle events for the chat turn. Reconnect with `?cursor=N` to resume cleanly.

---

## v1.2.6 — Managed agent restart

```
POST /api/v1/servers/{orderId}/agents/{agent}/restart       → 202 {request_id, restart_id}
GET  /api/v1/servers/{orderId}/agents/{agent}/restart/{rid} → {status: restarting|ready|timeout|failed}
```

Use when chat returns `agent_silent`. Returns 202 immediately; watch the events stream for `request.completed` on heartbeat resume (typically 5–15 s).

---

## x402 payment protocol

Plans return an x402 envelope. Your client signs (Base Sepolia EVM or Solana Devnet SPL) and retries with a `PAYMENT` header. See https://x402.org and the OpenAPI spec for current `payTo`, asset, and network details. Mainnet support pending public facilitator deployment.

---

## Authentication

Three token classes:

| Token prefix | Purpose | Issued by | Use |
|---|---|---|---|
| `osk_` | Reseller / programmatic | `POST /spawn` response | Bearer on `/api/v1/*` |
| `sk_live_` | Dashboard API | Dashboard → Settings → API keys | Bearer on `/api/dashboard/*` |
| Session cookie | Browser dashboard | Login flow | Same surface as `sk_live_` |
| `<gateway-token>` | Customer-box gateway | `cat /var/lib/osmoda/config/gateway-token` on the box | Bearer on `/config/*` (customer-box-local) |

### Read your own token metadata

```http
GET /api/v1/tokens/{token_id}
Authorization: Bearer osk_{token}
```

### Revoke your own token

```http
DELETE /api/v1/tokens/{token_id}
Authorization: Bearer osk_{token}
```

Returns `204` on success. Subsequent calls return `401 token_revoked`.

---

## Error envelope (v1.1+)

Every error response uses a uniform shape:

```json
{
  "code":        "agent_silent",
  "message":     "Agent stopped emitting frames mid-turn.",
  "detail":      "request_id=req_..., last_frame_at=2026-05-19T...",
  "request_id":  "req_chat_message_a1b2...",
  "error":       "agent_silent"
}
```

`error` is a legacy alias for `code`. Always check `code`. `X-Request-Id` is on every response. Rate-limited (429) responses include `Retry-After` in seconds.

---

## Rate limits

Per `osk_` token:

| Endpoint | Limit |
|---|---|
| `POST /spawn/*` | 10/h |
| `GET /status/*` | 120/min |
| `WS /chat/*` | 3 concurrent sessions/token |

Unauthenticated requests are also rate-limited per IP. Exceed → 429 with `Retry-After`.

---

## WebSocket chat — `/api/v1/chat/{orderId}`

```
wss://spawn.os.moda/api/v1/chat/{orderId}?token=osk_{token}
```

- **Send:** `{"type":"chat","text":"your instruction"}` or `{"type":"abort"}` to stop the active turn.
- **Receive:** `{"type":"text","text":"..."}` (delta), `{"type":"tool_use","name":"..."}`, `{"type":"tool_result"}`, `{"type":"thinking","text":"..."}`, `{"type":"done"}`, `{"type":"error","text":"...","code":"..."}`.

Heartbeat: ping/pong every 30 s. Idle close at 10 min (code 4003). Backpressure pause/resume. Max 3 concurrent sessions per token (4th gets 1008 `rate_limited`).

**Long tasks:** the gateway accepts up to 8 hours of active streaming per turn (v1.3.24, override via `OSMODA_CHAT_HARD_CAP_MS`). Send `{"type":"abort"}` at any time — the gateway kills the entire process group (v1.3.20).

---

## What osmoda can do on a spawned server (91 tools)

The agent has 91 MCP tools registered by `osmoda-bridge` via `api.registerTool()`. Tool counts per group:

| Group | Count | Examples |
|---|---|---|
| System | 13 | `system_health`, `system_query`, `system_discover`, `event_log`, `shell_exec`, `file_read`, `file_write`, `directory_list`, `service_status`, `journal_logs`, `network_info` |
| Memory | 2 | `memory_store`, `memory_recall` (FTS5 keyword + markdown ground truth) |
| Deploy (SafeSwitch) | 5 | `safe_switch_begin`, `safe_switch_list`, `safe_switch_status`, `safe_switch_commit`, `safe_switch_rollback` |
| Apps | 6 | `app_deploy`, `app_list`, `app_logs`, `app_stop`, `app_restart`, `app_remove` |
| Watchers + Routines | 5 | `watcher_add`, `watcher_list`, `routine_add`, `routine_list`, `routine_trigger` |
| Wallets | 7 | `wallet_create`, `wallet_list`, `wallet_sign`, `wallet_send`, `wallet_build_tx`, `wallet_delete`, `wallet_receipt` |
| Mesh | 11 | `mesh_identity`, `mesh_invite_create/accept`, `mesh_peers`, `mesh_peer_send/disconnect`, `mesh_health`, `mesh_room_create/join/send/history` |
| MCP servers | 4 | `mcp_servers`, `mcp_server_start/stop/restart` |
| Teach (learning) | 14 | `teach_status`, `teach_observations`, `teach_patterns`, `teach_knowledge*`, `teach_context`, `teach_optimize_*`, `teach_skill_*` |
| Voice | 5 | `voice_status`, `voice_speak`, `voice_transcribe`, `voice_record`, `voice_listen` |
| Identity + Receipts + Backup | 6 | `agent_card`, `receipt_list`, `incident_create`, `incident_step`, `backup_create`, `backup_list` |
| Safety | 4 | `safety_rollback`, `safety_status`, `safety_panic`, `safety_restart` |
| Approval gate | 4 | `approval_request`, `approval_pending`, `approval_approve`, `approval_check` |
| Sandbox + capabilities | 2 | `sandbox_exec`, `capability_mint` |
| Fleet (multi-server) | 4 | `fleet_propose`, `fleet_status`, `fleet_vote`, `fleet_rollback` |

**Spec-kit** ships `spec_kit_init` + `spec_kit_run` as MCP-protocol tools (registered via `osmoda-mcpd`, not `osmoda-bridge`) — separate count.

**CodeGraph (code intelligence)** ships 9 more tools via a separate MCP server (colbymchenry/codegraph, MIT, pure-WASM tree-sitter + SQLite, 100% local, installed by default, env-gated `OSMODA_CODEGRAPH_ENABLED=1`):

| Tool | Use |
|---|---|
| `codegraph_context` | PRIMARY — build task context (entry points + related symbols) in one call. Use specific symbol/file names, not sentences. |
| `codegraph_search` | Find a symbol by name (faster than grep). |
| `codegraph_callers` / `codegraph_callees` | Who calls this / what this calls. |
| `codegraph_impact` | Change blast-radius — run before editing a shared symbol to know what breaks. |
| `codegraph_node` | A symbol's source + signature. |
| `codegraph_explore` | Deep survey of an unfamiliar module (heavier). |
| `codegraph_files` / `codegraph_status` | Indexed file structure / index health. |

A pre-indexed code knowledge graph (symbols, call graph, imports) over 19+ languages — ~90% fewer grep/Read tool calls on code tasks. Auto-indexed across the OS's own source (`/opt/osmoda`), spec-kit projects (`/workspace/*`), and deployed apps (`/srv/*`), synced every 30 min by `osmoda-codegraph-index.timer`. `spec_kit_init` indexes new projects; `spec_kit_run implement` re-syncs. The agent knows your code's structure before it reads a single file.

**Channel routing** (Telegram, WhatsApp, web) is not a tool. The gateway routes outbound text via `agents.json · bindings[]` automatically.

Ask the agent anything. It will chain these tools.

---

## Per-server config API

Lets you manage agents, credentials, and runtime engines on a spawned server without SSH.

### Dashboard surface (proxied over SSH from the spawn server)

```
GET    /api/dashboard/servers/{id}/config/drivers
GET    /api/dashboard/servers/{id}/config/agents
PUT    /api/dashboard/servers/{id}/config/agents
PATCH  /api/dashboard/servers/{id}/config/agents/{agentId}
DELETE /api/dashboard/servers/{id}/config/agents/{agentId}
GET    /api/dashboard/servers/{id}/config/credentials
POST   /api/dashboard/servers/{id}/config/credentials
POST   /api/dashboard/servers/{id}/config/credentials/{credId}/test
POST   /api/dashboard/servers/{id}/config/credentials/{credId}/default
DELETE /api/dashboard/servers/{id}/config/credentials/{credId}
```

### Customer-box gateway surface (direct, 127.0.0.1)

If you're on the box, talk to the gateway directly using its token:

```bash
GATEWAY_TOKEN=$(cat /var/lib/osmoda/config/gateway-token)

# List drivers (claude-code, openclaw)
curl -s -H "Authorization: Bearer $GATEWAY_TOKEN" \
  http://127.0.0.1:18789/config/drivers

# Add a credential (encrypted at rest in credentials.json.enc, AES-256-GCM)
curl -s -X POST -H "Authorization: Bearer $GATEWAY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"my-key","provider":"anthropic","type":"api_key","secret":"sk-ant-api03-..."}' \
  http://127.0.0.1:18789/config/credentials

# Swap runtime on an agent (fires SIGHUP, in-flight chats keep their snapshot)
curl -s -X PATCH -H "Authorization: Bearer $GATEWAY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"runtime":"openclaw"}' \
  http://127.0.0.1:18789/config/agents/osmoda
```

Valid runtimes: `claude-code`, `openclaw`.
Valid providers: `anthropic`, `openai`, `openrouter`.
Valid credential types: `oauth` (Anthropic only), `api_key`.
Models: `claude-opus-4-6` (default), `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`, `gpt-4o`, `gpt-5`, `o3-mini`.

**Saving a PATCH fires SIGHUP.** In-flight chat sessions keep their original driver + credential snapshot; new sessions see the new config. Zero WebSocket drops.

---

## Runtime swap — `claude-code` ↔ `openclaw` (gateway v0.2.3)

> **OpenClaw is fully runnable as of gateway v0.2.3 (2026-05-21).** The driver
> is ported to the OpenClaw 2026.5+ CLI (`openclaw agent --local --json`), so
> the Engine-tab swap to `openclaw` succeeds instead of returning
> `422 driver_unavailable`. Provide an `api_key` credential (OpenClaw does not
> accept OAuth).

Each agent independently picks `claude-code` or `openclaw`. Both runtimes:

- Consume the same 91-tool MCP bridge
- Stream the same `text` / `tool_use` / `tool_result` event shapes
- Support the same Stop button + 8 h hard cap (v1.3.20 + v1.3.24)
- Survive gateway restarts (sessions persisted to disk)

| | `claude-code` | `openclaw` |
|---|---|---|
| Anthropic Claude Pro OAuth | ✅ | ❌ (api_key only) |
| API keys (Anthropic, OpenAI, ...) | ✅ | ✅ |
| Provider variety | Anthropic only | Anthropic + OpenAI |
| Installed | Always | Always (since 2026-05-14) |
| Driver file | `packages/osmoda-gateway/src/drivers/claude-code.ts` | `packages/osmoda-gateway/src/drivers/openclaw.ts` |

**Session id is runtime-tagged.** Flipping `claude-code → openclaw` (or vice versa) via Engine tab → SIGHUP wipes the foreign session id and starts a clean conversation in the new runtime. No "session not found" errors. The chat history NDJSON on the dashboard is preserved.

**Credentials are checked on swap.** If you flip an OAuth-credentialed agent to `openclaw` (which doesn't accept OAuth), the swap is accepted but the next chat returns `no_credential` until you POST an `api_key` credential.

---

## What happens after spawn

1. Cloud provider creates a dedicated VM in your region.
2. Cloud-init fetches `install.sh` from GitHub raw, converts to NixOS via `nixos-infect`, builds the 10 daemons (`cargo build --release --workspace`, ~5 min first build), installs `osmoda-gateway` + `claude` CLI + `openclaw` + the 91-tool MCP bridge (~3 more min).
3. If you passed `credentials` / `runtime` / `default_model` at spawn time, they are written into `/var/lib/osmoda/config/agents.json` and `credentials.json.enc` at install time. Agent ready on first boot.
4. Server heartbeats to `spawn.os.moda` every 60 s. Each heartbeat emits a `heartbeat.received` event.
5. Status `provisioning → running` once daemons are up. When the customer box confirms credential delivery, `state.changed {field: 'has_api_key', value: true}` fires.
6. Agent reachable via WebSocket chat, Telegram (if configured), WhatsApp (if configured), or direct SSH.

The server runs autonomously. The agent monitors its own health, restarts crashed services via systemd, deploys apps as `osmoda-app-<name>` systemd units, edits NixOS config through SafeSwitch (with auto-rollback if any health gate fails). Wedge detector (v1.3.25 dual-signal) auto-kicks a restart on stale heartbeat AND stale frames.

---

## Memory + state directories

| Path | What |
|---|---|
| `/var/lib/osmoda/memory/` | Markdown ground truth + FTS5 index (memory_store/recall). |
| `/var/lib/osmoda/ledger/events.db` | Hash-chained audit ledger (SQLite). |
| `/var/lib/osmoda/config/agents.json` | Agent profiles (id, runtime, credential_id, model, channels, workspace dir). |
| `/var/lib/osmoda/config/credentials.json.enc` | AES-256-GCM credential store. |
| `/var/lib/osmoda/config/gateway-token` | Bearer for `/config/*` calls. |
| `/var/lib/osmoda/state/sessions.json` | Disk-persisted gateway sessions (v0.2.1, mode 0600). |
| `/var/lib/osmoda/keyd/keys/` | Encrypted wallet keys (mode 0700). |
| `/var/lib/osmoda/mesh/` | Mesh identity (mode 0700). |
| `/var/lib/osmoda/teachd/` | Observations, patterns, knowledge docs. |
| `/var/lib/osmoda/workspace-osmoda/` | Main agent workspace (claude-code profile_dir). |
| `/var/lib/osmoda/workspace-mobile/` | Mobile agent workspace. |
| `/workspace/<slug>/` | Spec-kit projects (each with `.specify/`, `memory/`, `.claude/`, `specs/`). |
| `/run/osmoda/*.sock` | Unix sockets for the 9 Rust daemons. |

---

## Agent behavior rules

If you are an AI agent running on a spawned osmoda box, follow these in order:

1. **Diagnose before fixing.** Always use `system_health`, `journal_logs`, `service_status`, and `memory_recall` before mutating state. You may have solved this exact problem last week — `memory_recall` finds it.
2. **Explain before changing.** State what you intend to do, then why, then call the tool. Never mutate silently.
3. **Use SafeSwitch for deploys.** Never run `nixos-rebuild switch` directly. Use `safe_switch_begin` with health checks + TTL. Auto-rollback protects the user.
4. **Log everything.** Every mutation creates a hash-chained event in the audit ledger automatically. Use `event_log` to query history.
5. **Ask for approval on destructive ops.** Deleting files, dropping services, modifying firewall rules — call `approval_request` and wait for `approval_approve` before acting. Today enforcement is convention-based; hard-blocking enforcement is on the v1.4 roadmap.
6. **Remember.** Store diagnoses, user preferences, and system patterns via `memory_store`. The memory daemon is FTS5-backed (vector search via ZVEC + nomic-embed is planned, not yet shipped).
7. **Default to private.** Bind new services to `127.0.0.1`. For public reach, set up Cloudflare Tunnel or Tailscale via the NixOS options `services.osmoda.remoteAccess.cloudflare.enable` / `.tailscale.enable`. Never bind to `0.0.0.0` without explicit user direction.

---

## Daemon sockets reference

Direct curl examples for when you need to bypass the MCP tool layer.

```bash
# Health (agentd)
curl -s --unix-socket /run/osmoda/agentd.sock http://l/health | jq

# Query processes sorted by CPU
curl -s --unix-socket /run/osmoda/agentd.sock \
  -X POST http://l/system/query \
  -H "Content-Type: application/json" \
  -d '{"query":"processes","args":{"sort":"cpu","limit":10}}'

# Store a memory
curl -s --unix-socket /run/osmoda/agentd.sock \
  -X POST http://l/memory/store \
  -H "Content-Type: application/json" \
  -d '{"summary":"User prefers Rust","detail":"Asked for Rust 3 times","category":"user_pattern","tags":["preference"]}'

# Recent audit events
curl -s --unix-socket /run/osmoda/agentd.sock 'http://l/events/log?limit=20' | jq

# Begin a SafeSwitch deploy (5 min probation)
curl -s --unix-socket /run/osmoda/watch.sock \
  -X POST http://l/switch/begin \
  -H "Content-Type: application/json" \
  -d '{"plan":"nginx-upgrade","ttl_secs":300,"health_checks":[{"type":"http_get","url":"http://localhost/health","expect":200}]}'

# Get teach context for a current issue
curl -s --unix-socket /run/osmoda/teachd.sock \
  -X POST http://l/teach \
  -H "Content-Type: application/json" \
  -d '{"context":"nginx returning 502 errors"}'

# Mesh: create an invite for another osmoda instance
curl -s --unix-socket /run/osmoda/mesh.sock \
  -X POST http://l/invite/create \
  -H "Content-Type: application/json" \
  -d '{"ttl_secs":3600}'

# Wallet: list local wallets
curl -s --unix-socket /run/osmoda/keyd.sock http://l/wallet/list | jq
```

---

## Audit ledger — SHA-256 hash chain

Every mutation creates an append-only SQLite event:

```sql
CREATE TABLE events (
  id        INTEGER PRIMARY KEY AUTOINCREMENT,
  ts        TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
  type      TEXT    NOT NULL,
  actor     TEXT    NOT NULL,
  payload   TEXT    NOT NULL,
  prev_hash TEXT    NOT NULL,
  hash      TEXT    NOT NULL
);
-- hash = SHA-256(id|ts|type|actor|payload|prev_hash)  -- pipe-delimited
```

Tamper any row and the chain breaks. Verify with:

```bash
agentctl verify-ledger
# Walks the chain, reports first broken link or "Chain integrity verified".
```

Useful for: post-incident forensics, compliance reporting, proving exactly what the agent did at 3am. The ledger is a foundation for audit programs, not a turnkey SOC 2 / HIPAA package.

---

## Mesh networking — agent-to-agent

Multiple osmoda servers form a direct encrypted mesh. Noise_XX + X25519 + ChaChaPoly + BLAKE2s + ML-KEM-768 hybrid post-quantum. No central server. Invite-based pairing, short-TTL codes.

Ask either agent: *"Create a mesh invite, 1 hour TTL."* → copy the code → ask the other: *"Accept mesh invite `<code>`."* Now they're connected. Direct messages, group rooms, gossip-replicated history.

Use cases: multi-server fleets coordinating deploys, edge devices reporting to a central node, distributed agent swarms operating without internet.

---

## Self-install (bring your own server)

If you have a server and want osmoda without spawn.os.moda:

```bash
# Ubuntu 22.04+ / Debian 12+ / existing NixOS, x86_64 or aarch64. Root access.
curl -fsSL https://raw.githubusercontent.com/bolivian-peru/os-moda/main/scripts/install.sh | sudo bash

# Pre-configure the agent at install time (no setup wizard, no SSH session):
curl -fsSL https://raw.githubusercontent.com/bolivian-peru/os-moda/main/scripts/install.sh | sudo bash -s -- \
  --runtime claude-code \
  --default-model claude-opus-4-6 \
  --credential "My Claude Pro|anthropic|oauth|$(printf 'sk-ant-oat01-...' | base64)"
```

Or with NixOS flakes:

```nix
# flake.nix
inputs.osmoda.url = "github:bolivian-peru/os-moda";

# configuration.nix
imports = [ inputs.osmoda.nixosModules.default ];
services.osmoda = {
  enable = true;
  # Optional channels:
  # channels.telegram.enable = true;
  # channels.telegram.tokenFile = "/var/lib/osmoda/secrets/telegram-bot-token";
  # channels.whatsapp.enable = true;
  # Optional remote access (no public IP needed):
  # remoteAccess.cloudflare.enable = true;
  # remoteAccess.tailscale.enable = true;
  # remoteAccess.tailscale.authKeyFile = "/var/lib/osmoda/secrets/tailscale-key";
};
```

```bash
sudo nixos-rebuild switch
```

After install, all 10 systemd units come up:

```bash
systemctl start osmoda-agentd osmoda-keyd osmoda-watch osmoda-routines \
                osmoda-mesh osmoda-mcpd osmoda-teachd \
                osmoda-egress osmoda-voice osmoda-gateway

for svc in osmoda-{agentd,keyd,watch,routines,mesh,mcpd,teachd,egress,voice,gateway}; do
  echo "$svc: $(systemctl is-active $svc)"
done
```

---

## Setting up messaging channels

Three channels share one conversation: web chat (`:18789`), Telegram, WhatsApp.

### Telegram

1. Create a bot via `@BotFather` on Telegram. Save the token.
2. Write the token to the box (mode 0600, root-owned):
   ```bash
   echo "YOUR_BOT_TOKEN" > /var/lib/osmoda/secrets/telegram-bot-token
   chmod 600 /var/lib/osmoda/secrets/telegram-bot-token
   ```
3. Enable via NixOS option **or** the gateway config API.

   **NixOS option** (proper way on a flake-managed box):
   ```nix
   services.osmoda.channels.telegram.enable = true;
   services.osmoda.channels.telegram.tokenFile = "/var/lib/osmoda/secrets/telegram-bot-token";
   services.osmoda.channels.telegram.allowedUsers = [ "your_username" ];
   ```
   ```bash
   sudo nixos-rebuild switch
   ```

   **Gateway API** (on a spawn-installed box):
   ```bash
   GATEWAY_TOKEN=$(cat /var/lib/osmoda/config/gateway-token)
   curl -s -X PATCH http://127.0.0.1:18789/config/agents/mobile \
     -H "Authorization: Bearer $GATEWAY_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"channels":["telegram","whatsapp"]}'
   ```

### WhatsApp

Same shape — `services.osmoda.channels.whatsapp.enable = true;` (NixOS) or the corresponding `/config/agents` PATCH. After enabling, scan the QR code:

```bash
journalctl -u osmoda-gateway --since '30 sec ago' --no-pager
# Look for the QR ASCII block. Scan via WhatsApp → Settings → Linked Devices.
```

---

## Remote access (no public IP)

### Cloudflare Tunnel (quick, no account)

```nix
services.osmoda.remoteAccess.cloudflare.enable = true;
```

```bash
sudo nixos-rebuild switch
journalctl -u osmoda-cloudflared --since '1 min ago' --no-pager
# Look for the trycloudflare.com URL — random, regenerates on restart.
```

### Cloudflare Tunnel (persistent, with account)

```bash
# Locally:
cloudflared tunnel create osmoda
scp cf-creds.json root@YOUR_SERVER:/var/lib/osmoda/secrets/cf-creds.json
```

```nix
services.osmoda.remoteAccess.cloudflare.enable = true;
services.osmoda.remoteAccess.cloudflare.credentialFile = "/var/lib/osmoda/secrets/cf-creds.json";
services.osmoda.remoteAccess.cloudflare.tunnelId = "YOUR-TUNNEL-ID";
```

Add a CNAME in your Cloudflare dashboard. `nixos-rebuild switch`. Done.

### Tailscale

```bash
echo "tskey-auth-..." > /var/lib/osmoda/secrets/tailscale-key
```

```nix
services.osmoda.remoteAccess.tailscale.enable = true;
services.osmoda.remoteAccess.tailscale.authKeyFile = "/var/lib/osmoda/secrets/tailscale-key";
```

`nixos-rebuild switch` — server auto-joins your tailnet.

---

## TypeScript SDK

`@osmoda/client` mirrors this API.

```ts
import { OsmodaClient } from "@osmoda/client";

const client = new OsmodaClient({ bearer: "osk_..." });

const { plans } = await client.listPlans();

const server = await client.spawn("starter", {
  region:        "eu-central",
  runtime:       "claude-code",
  default_model: "claude-opus-4-6",
  credentials: [
    {
      label:    "My Claude Pro",
      provider: "anthropic",
      type:     "oauth",
      secret:   "sk-ant-oat01-...",
    },
  ],
}, { idempotencyKey: `myapp-${crypto.randomUUID()}` });

const ready = await client.waitForRunning(server.order_id);

// Live events
for await (const event of client.streamEvents(server.order_id, { filter: ["request", "state", "agent"] })) {
  console.log(event.type, event);
}

// Chat (WebSocket)
const chat = client.openChat(server.order_id);
chat.on("text", (delta) => process.stdout.write(delta));
chat.send({ type: "chat", text: "Install nginx and reverse-proxy port 3000 with health checks." });
```

Repo: https://github.com/bolivian-peru/os-moda/tree/main/packages/osmoda-client

---

## Trust model

```
Tier 0:  osmoda agent + agentd       Root. Full system. This IS the OS interface.
Tier 1:  Approved apps                Designed: sandboxed, declared capabilities. Today: tool surface
                                      (sandbox_exec, capability_mint) is shipped; bubblewrap
                                      enforcement on the live execution path is on the v1.4 roadmap.
Tier 2:  Untrusted tools              Same caveat. Egress proxy + capability allowlist are live;
                                      bubblewrap not yet wired into sandbox_exec's default path.
```

Every system mutation is logged to the SHA-256 hash-chained ledger (`agentctl verify-ledger`). Destructive operations *should* route through `approval_request` / `approval_approve` — enforcement is convention-based today (agent system prompt instructs it), hard-blocking enforcement is on the v1.4 roadmap. NixOS atomic rollback covers every system change. NixOS rollback does **not** undo data sent to external APIs, signed transactions, or exposed secrets.

Full security model: https://github.com/bolivian-peru/os-moda/blob/main/docs/SECURITY.md

---

## Quick reference (copy-paste)

Spawn Solo:

```http
POST https://spawn.os.moda/api/v1/spawn/test
Content-Type: application/json
Idempotency-Key: my-key-001

{}
```

Spawn Pro in us-east with your Claude Pro OAuth:

```http
POST https://spawn.os.moda/api/v1/spawn/starter
Content-Type: application/json
Idempotency-Key: my-key-002

{
  "region": "us-east",
  "runtime": "claude-code",
  "default_model": "claude-opus-4-6",
  "credentials": [{
    "label": "My Claude Pro",
    "provider": "anthropic",
    "type": "oauth",
    "secret": "sk-ant-oat01-..."
  }]
}
```

Check ready:

```http
GET https://spawn.os.moda/api/v1/status/{orderId}
Authorization: Bearer osk_{token}
```

Live events:

```http
GET /api/v1/servers/{orderId}/events?cursor=0&filter=request,state,agent
Authorization: Bearer osk_{token}
Accept: text/event-stream
```

Chat (WebSocket):

```
wss://spawn.os.moda/api/v1/chat/{orderId}?token=osk_{token}

Send:    {"type":"chat","text":"your instruction"}
         {"type":"abort"}     (kills the current turn's process group)

Receive: {"type":"text",       "text":"..."}      (deltas)
         {"type":"tool_use",   "name":"shell_exec"}
         {"type":"tool_result"}
         {"type":"thinking",   "text":"..."}
         {"type":"done"}
         {"type":"error",      "text":"...", "code":"..."}
```

Swap runtime on a spawned box (Engine tab equivalent):

```bash
curl -s -X PATCH http://127.0.0.1:18789/config/agents/osmoda \
  -H "Authorization: Bearer $(cat /var/lib/osmoda/config/gateway-token)" \
  -H "Content-Type: application/json" \
  -d '{"runtime":"openclaw"}'
# SIGHUP fires automatically. Sessions are runtime-tagged → foreign session id
# wiped, fresh conversation in the new runtime, NDJSON history preserved.
```

Revoke your token:

```http
DELETE https://spawn.os.moda/api/v1/tokens/{token_id}
Authorization: Bearer osk_{token}
```

---

## Versioning

- **API:** semver. v1.x is stable, additive-only. v1.3.x adds — v1.2.x clients still work.
- **Daemons + gateway:** the Rust workspace and `osmoda-gateway` package ship together. Today: `v1.3.35` / gateway `v0.2.3`.
- **Spec:** `GET /api/v1/docs` always reflects the deployed version. Pin to a specific `X-Osmoda-Version` header value if you need build-level pinning.

If you depend on a behavior, prefer the `X-Request-Id` receipt + the events-stream `request_id` / `request.completed` shape over polling. Both are stable across the v1.3.x line.
