Skip to content

Latest commit

 

History

History
513 lines (400 loc) · 27.1 KB

File metadata and controls

513 lines (400 loc) · 27.1 KB

Management API Reference

springtaled exposes a REST API over HTTP for connector management, rule CRUD, formation orchestration, event streaming, canvas updates, configuration, and webhook ingestion.

1. Overview

  • Default bind: 127.0.0.1:8080 (configurable via [api] bind)
  • Transport: HTTP (use a reverse proxy for remote TLS termination)
  • Content type: application/json
  • Live streams: one multiplexed Server-Sent Events endpoint, GET /stream, opened with a one-time ticket from POST /stream/ticket (see §1)
  Client                       springtaled
       │                            │
       │  HTTP request              │
       │  Authorization: Bearer X   │
       ├───────────────────────────>│
       │                            │
       │                     ┌──────┴────────────────────┐
       │                     │  Middleware Stack         │
       │                     │                           │
       │                     │  1. ValidatedPath (≤256B) │
       │                     │  2. require_auth          │
       │                     │  3. RateLimit (100 req/s) │
       │                     │  4. Buffer                │
       │                     │  5. BodyLimit (1 MiB)     │
       │                     │  6. Timeout (30 s)        │
       │                     │  7. CSP / X-Frame-DENY    │
       │                     └──────┬────────────────────┘
       │                            │
       │  JSON response             │
       │<───────────────────────────┤

Fig. 1. Request flow through the middleware stack.


2. Authentication

Every route except /health, /ready, /openapi.json, POST /auth/login, POST /vault/unlock, and /ui requires a bearer token. Webhook routes (/webhook/{connector}/{trigger}) also require the token — the connector then performs its own signature verification on the body (HMAC-SHA256 for GitHub, RSA for Kick, etc.) inside its verify_webhook() implementation.

Bearer tokens are issued, never derived. POST /auth/login takes the vault passphrase, compares HMAC-SHA256(passphrase, "springtale-api-token") against the value the daemon computed at boot — in constant time (subtle::ConstantTimeEq) — and only on a match mints a token: 32 bytes (256 bits) straight from the OS CSPRNG, hex-encoded, with no structure at all. That passphrase-derived hash is the login verifier only; it is never accepted as a bearer.

Two kinds of bearer exist, and require_auth accepts either:

Kind Minted by Lifetime Revoked by
Session POST /auth/login Idle + absolute timeouts from bot:settings (session_idle_secs, session_absolute_secs). Held in process memory only, so a daemon restart or a vault lock drops every one. POST /auth/logout
Long-lived named POST /auth/tokens Until revoked. Persisted in the api_tokens table. DELETE /auth/tokens/{id}

Neither kind is ever stored in the clear: both live as sha256(token) — sessions in the in-memory map, long-lived tokens in api_tokens. The token string is returned exactly once, in the minting response, and nothing keeps it. A presented bearer is hex-decoded, hashed, and looked up (sessions first, then api_tokens) with a constant-time compare, so a hit and a miss cost the same work. A token that was never issued, one that expired, and one that was revoked are all the same answer: 401.

POST /auth/login is unauthenticated by definition, so it carries its own tight rate limit (5 req/s) on top of the global 100 req/s.

Authenticated routes also go through a CSRF-protection middleware (require_csrf_protection) that rejects cross-origin requests with unsafe methods. SSE readers are exempt because the browser EventSource cannot originate a state-changing request.

# Log in once, then use the minted token
TOKEN=$(curl -sX POST http://127.0.0.1:8080/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"passphrase":"..."}' | jq -r .token)

curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/connectors

SSE authentication. EventSource cannot set custom headers, and the bearer token is never put in a URL. The client calls POST /stream/ticket (bearer-authenticated) for a one-time ticket valid for 30 seconds, then opens the single multiplexed GET /stream with it. A ticket is consumed on first use and expires unused.


3. Endpoint Catalogue

                           springtaled HTTP API
                                    │
      ┌─────────┬──────────┬────────┼────────┬──────────┬──────────┐
      ▼         ▼          ▼        ▼        ▼          ▼          ▼
   Health   Connectors  Rules  Formations  Canvas   Events      Config
                                                     (SSE)
      ┌─────────┬──────────┬────────┼────────┬──────────┬──────────┐
      ▼         ▼          ▼        ▼        ▼          ▼          ▼
   Agents   Authors   Bot admin Sessions   Memory    Safety       Data
      ┌─────────┬──────────┬──────────┬──────────┬─────────────┐
      ▼         ▼          ▼          ▼          ▼             ▼
   Send    Diagnostics  Fixes    Onboarding   Recipes      Webhooks
      ┌──────────────┬──────────────────┬──────────────┬──────────┐
      ▼              ▼                  ▼              ▼          ▼
  Approvals     Chat (SSE)       Dashboard (SPA)     Auth       Vault
                                       │
                                       ▼
                                      MCP

Fig. 3. Route groups at a glance. Public routes: /health, /ready, /openapi.json, POST /auth/login, POST /vault/unlock, /ui, /ui/*. Everything else requires the bearer token.

3.1 Health

Method Path Auth Description
GET /health Liveness. Returns {"status":"ok"}.
GET /ready Readiness. 200 when booted, 503 while booting.

3.2 Connectors

Method Path Description
GET /connectors List installed connectors with enabled state
GET /connectors/schemas JSON Schema for every connector's config
GET /connectors/available Built-in + community connectors discoverable for install
POST /connectors/setup Interactive setup wizard — validates config
POST /connectors/install Install from manifest. Verifies Ed25519 signature before registering
DELETE /connectors/{name} Remove a connector. Leaves rules intact.
DELETE /connectors/{name}/cascade Remove connector and any rules that reference it
GET /connectors/{name}/config Stored config (secrets redacted)
POST /connectors/{name}/upsert-config Update connector config atomically
GET /connectors/{name}/outputs Recent action outputs (cap 100, oldest dropped)
POST /connectors/{name}/enable Enable a disabled connector
POST /connectors/{name}/disable Disable without removing
POST /connectors/{name}/reload G4 — hot-reload a connector's running instance against the latest stored config without restarting the daemon
POST /connectors/{name}/test Dry-run an action with synthetic input

3.3 Rules

Method Path Description
GET /rules List all rules with status + trigger type
POST /rules Create a rule from JSON
POST /rules/parse Natural-language → Rule via the configured AI adapter
GET /rules/schema JSON Schema for the Rule type
PUT /rules/{id} Replace a rule
DELETE /rules/{id} Delete a rule
POST /rules/{id}/toggle Enable ↔ disable
POST /rules/{id}/run Dry-run against a synthetic trigger event
POST /rules/{id}/reassign Reassign a rule to a different connector or agent
POST /rules/connector Create a rule for a connector event (convenience wrapper)
GET /rules/connector/{name} List rules triggered by a specific connector

3.4 Formations & cooperation streams

Formations are cooperating groups of agents with a shared intent. See docs/guide/cooperation.md. Every formation command pushes onto the bot's FormationCommand channel; the bot is the only code path that materialises live Formation structs from DB rows.

Method Path Description
GET /formations List all formations with member count, intent, momentum tier
POST /formations Create a formation
GET /formations/{id} Live formation detail (momentum, rally tokens, attention load, guard status, member health + liveness) via LiveFormationReader
GET /formations/{id}/commands 3×3 colony-canvas command grid for this formation, with status-aware enable flags. Used by the dashboard formation detail card.
GET /formations/{id}/members/eligible Eligible-for-removal member list for the formation member overlay.
GET /formations/intents List available intent templates (Reconnoiter / Execute / Stabilize / Surge / Dissolve)
POST /formations/deploy-team Deploy a multi-agent team in one call
POST /formations/{id}/deploy Move formation to deployed state
POST /formations/{id}/pause Pause (members stop acting on cadence ticks)
POST /formations/{id}/resume Resume from paused
POST /formations/{id}/dissolve Dissolve, persist mental model first, stop all members
POST /formations/{id}/rally Manual rally — consume a rally token around the weakest member
PUT /formations/{id}/intent Change intent
POST /formations/{id}/members Add a member (by connector name)
DELETE /formations/{id}/members Remove a member (by connector name)
POST /formations/{id}/cycle-intent Cycle to the next intent template
POST /formations/{id}/cycle-autonomy Cycle autonomy level of all members (observe → suggest → approve → autonomous → observe)
POST /formations/{id}/toggle-guard Toggle the formation guard rails
GET /cooperation/events SSE stream of formation lifecycle, momentum transitions, rally events, and interference events. Carried on the multiplexed GET /stream (ticket from POST /stream/ticket); optional ?formation_id=... filter. The dashboard's live cooperation overlay consumes this.

3.5 Agents

Autonomy levels: observe → suggest → act-with-approval → act-autonomously.

Method Path Description
GET /agents/states All agents with current autonomy and formation membership
GET /agents/{name}/autonomy Current autonomy level for one agent
PUT /agents/{name}/autonomy Set autonomy level
POST /agents/{name}/autonomy/step Step one level toward more autonomous

3.6 Canvas

The colony canvas is a live pixel-art visualisation of connectors, rules, agents, and formations.

Method Path Description
GET /canvas Full canvas state (nodes + edges)
GET /canvas/connections Just the edge metadata
GET /canvas/stream SSE stream of canvas updates. Carried on the multiplexed GET /stream; ticket from POST /stream/ticket

Note: there is no POST /canvas/update HTTP endpoint. Canvas layout changes (drag, reposition, re-wire) happen in the Tauri desktop via the dashboard's IPC layer, which writes directly to the dashboard's local layout state. The daemon-side CanvasState is read-only over HTTP.

3.7 Events

Method Path Query params Description
GET /events limit, offset, connector Paginated event log
GET /events/stream ticket SSE stream of new events, carried on the multiplexed GET /stream

3.8 Configuration

Method Path Description
GET /config List all config keys
GET /config/{key} Get a single config value
PUT /config/{key} Set a single config value
POST /config/ai Select AI adapter (noop, ollama, openai, anthropic) — hot-swaps at runtime
POST /config/ai/configure Adapter-specific settings (endpoint, model, API key)
POST /config/connector/{name} Store encrypted connector config
GET /config/heartbeat Heartbeat interval (seconds)
PUT /config/heartbeat Update heartbeat interval

3.9 Trusted Authors

Author Ed25519 public keys used to verify signed manifests.

Method Path Description
GET /authors List registered author keys
POST /authors/{name} Register a new author key
DELETE /authors/{name} Remove an author

3.10 Bot

Method Path Description
GET /bot/status Bot connection status for each chat connector
GET /bot/formations Active bot formations
GET /bot/memory Memory session statistics

3.11 Sessions & Memory

Method Path Description
GET /sessions Active agent sessions
POST /memory/audit Inspect memory session counts
POST /memory/compact Delete oldest entries beyond per-session limit

3.12 Safety

Method Path Description
GET /safety Current sentinel / behavioural monitor config
PUT /safety Update full safety config (window title, auto-lock, content protection, disguise profile, quick-hide shortcut)
POST /safety/disguise/active G5d — flip just the disguise-active flag. Focused endpoint that avoids the lost-update race two tabs would hit on the full-config PUT path. Body: { "active": bool }
POST /safety/disguise/profile G5f — switch the disguise icon profile (one of calculator, files, notes, springtale). Body: { "profile": "<id>" }. Tray icon is swapped at runtime
POST /safety/panic_tap_count Set the number of taps required on the panic gesture before the wipe fires. Body: { "count": u32 }

3.13 Data

Method Path Description
POST /data/export Export all user data. Optional encryption with vault passphrase.

Note: import is CLI-only. springtale-cli data import --input <path> calls the runtime's import_data() function directly against the local SQLite backend. There is no HTTP endpoint because import is an offline operation that requires the daemon to not be writing concurrently.

3.14 Send

Method Path Description
POST /send Execute an Action directly against a connector. Capability-checked through the sentinel, same as rule-dispatched actions. No back door.

3.15 Diagnostics, Fixes, Onboarding, Recipes

These routes back the Doctor and Onboarding flows in the desktop shell.

Method Path Description
GET /diagnostics Run the current set of runtime health checks; returns a list of issues with severity + description
GET /fixes List available auto-repair suggestions bound to diagnostic ids
GET /fixes/{id} Fetch a single fix with its proposed action
POST /fixes/{id}/apply Apply a fix
GET /onboarding/platforms List the platform forms the first-run wizard knows. Collected at runtime from every registered connector factory that returns an onboarding_form() — currently telegram, discord, slack, signal
POST /onboarding/{platform} Persist a completed answer set for that platform as a connector config
GET /recipes List curated automation recipes (browseable cookbook surface)
GET /recipes/categories Recipe categories
GET /recipes/{id} One recipe by id
GET /recipes/{id}/pieces Modular pieces composing the recipe
GET /recipes/{id}/export Export the recipe as TOML
POST /recipes/{id}/favorite Toggle favorite
POST /recipes/{id}/recent Mark recently viewed
POST /recipes/{id}/apply Apply the recipe — installs rules / connectors as defined
POST /recipes/{id}/render Render the recipe against current state (preview-friendly)
POST /recipes/{id}/preflight Check preconditions (capability grants, connector availability) before apply
POST /recipes/{id}/preview Dry-run preview of what apply would do
POST /recipes/{id}/fork Fork a recipe into a user-saved variant
POST /recipes/user Save a custom user recipe
DELETE /recipes/user/{id} Delete a user-saved recipe
POST /recipes/import Import a recipe from TOML

3.16 Webhooks

Method Path Description
POST /webhook/{connector}/{trigger} Inbound webhook, forwarded to the connector's trigger handler after token auth

The endpoint requires the bearer token like every other authenticated route. External senders need the token in the Authorization header. In addition, each connector performs its own webhook signature verification on the body via Connector::verify_webhook() — HMAC-SHA256 for GitHub, RSA for Kick, and so on.

3.17 Dashboard

Method Path Auth Description
GET /ui Embedded SPA index
GET /ui/{*path} SPA static assets

3.18 Approvals

Blocking-approval queue for capabilities that can never be auto-granted (currently ShellExec — the OpenClaw CVE-2026-25253 1-click-RCE class). When a connector invokes such a capability, the requestor blocks until a decision lands here or the gate's deny-fallback timeout fires (default 60s). Every decision is written to the sentinel audit trail.

Method Path Description
GET /approvals List outstanding approval requests. Returns {"pending": [...]} — each entry carries the connector name, requested capability, human-readable summary, and the request id
POST /approvals/{id} Resolve a pending request

POST /approvals/{id} body:

{ "decision": "approve" | "deny", "approver": "optional label", "reason": "optional deny reason" }

approver defaults to "maintainer" (the bearer token already proves authority; the field is only audit-row attribution). Status codes: 200 decision recorded, 400 malformed id/body, 404 no pending request with that id (timeout already fired), 409 already resolved, 503 no approval gate wired.

3.19 Chat

In-app chat surface (W5). The desktop app, web dashboard, and PWA talk to the bot through one channel without any external chat platform. Messages are injected as if they arrived from a synthetic connector named in-app; replies stream back over SSE. Approvals raised by in-app tasks surface through the §3.18 endpoints.

Method Path Description
POST /chat Inject a chat message. Body {"text": "...", "session": "optional"} (session defaults to in-app). Fire-and-forget — returns 202 Accepted with {"status":"queued","session":"..."} once queued; 400 on empty text, 503 if the bot runtime is unavailable
GET /chat/stream SSE stream of bot replies. Each event's data is {"session": "...", "text": "..."}

3.20 Authentication, sessions & tokens

See §2 for the token model. POST /auth/login is public; everything else in this family requires an existing bearer.

Method Path Auth Description
POST /auth/login Verify the vault passphrase and mint a session token. Body {"passphrase": "..."}; returns {"token": "<hex>", "expires_in": <idle secs>}. 401 on mismatch. Rate-limited to 5 req/s on top of the global limit
POST /auth/logout Drop the presented session. Returns {"logged_out": true|false}. A long-lived token is not a session — revoke those with DELETE /auth/tokens/{id}
POST /auth/tokens Mint a long-lived named token. Body {"name": "springtale-cli@laptop"} (1–128 chars). Returns {"id", "name", "token"} — the token string appears here and nowhere else, ever
GET /auth/tokens List long-lived tokens: id, name, created_at, last_used. Metadata only — the hash never crosses the wire
DELETE /auth/tokens/{id} Revoke a long-lived token. Revocation is immediate: the next request carrying it fails its lookup
POST /stream/ticket One-time 30 s ticket for the SSE routes, bound to the presented bearer. Logging out or revoking that bearer invalidates every outstanding ticket

3.21 Vault lock & unlock

While the vault is locked the daemon swaps in an outer router: /health, /ready, and the two vault routes always answer, and everything else is refused until the vault is opened.

Method Path Auth Description
POST /vault/unlock Open the vault. Body {"passphrase": "..."}. Deliberately unauthenticated: while locked there is no bearer that could be presented — sessions are dropped with the process state on lock, and a long-lived token can only be looked up against that same dropped state. Vault::open is the check: Argon2id over the wrong passphrase fails at AEAD decryption, with no comparison to shortcut. Rate-limited per minute so the KDF cannot be driven by a flood of guesses. 409 if already unlocked, 401 on failure
POST /vault/lock Lock the vault: signals the SSE streams, unwires the connector chat loops, pauses the scheduler, clears the session and stream-ticket maps, joins every background task, and zeroizes the vault key. Idempotent — locking an already-locked daemon is a 200, so a panic-button UI never has to reason about current state. Served by the outer router, which has no AppState, so the bearer check is run by hand inside the handler rather than by require_auth

3.22 Model Context Protocol

springtaled is the MCP server (Streamable HTTP transport). Tool calls dispatch through the same sentinel, approval gate, and executions recorder as a rule-dispatched action — there is no separate path.

Method Path Auth Description
ANY /mcp MCP Streamable HTTP endpoint

Two layers run in front of it, outside-in: an Origin check (require_local_origin) rejects any non-loopback origin per the MCP transports spec's DNS-rebinding requirement, then require_auth checks the bearer, and only then is any MCP framing parsed. A missing Origin header is accepted (non-browser clients do not send one); anything non-loopback is 403. The Mcp-Session-Id header is never authentication.


4. Middleware Stack

  ┌────────────────────────────────────────────────────────────────┐
  │                      Middleware Stack                          │
  │                                                                │
  │   1. TraceLayer                    HTTP trace span             │
  │   2. SetResponseHeaderLayer × 5    security headers (§4.1)     │
  │   3. RequestBodyLimitLayer         1 MiB per request           │
  │   4. HandleErrorLayer              maps rate-limit err → 429   │
  │   5. BufferLayer (256)             fronts the rate limiter     │
  │   6. RateLimitLayer                100 req/s (configurable)    │
  │   7. TimeoutLayer                  30 s per request → 503      │
  │                                                                │
  │   require_auth middleware          Bearer header or stream ticket │
  │   ValidatedPath extractor          path segments ≤ 256 bytes   │
  │                                                                │
  └────────────────────────────────────────────────────────────────┘

Fig. 2. Applied to all routes.

4.1 Security Response Headers

Five headers are set on every response by SetResponseHeaderLayer:

Header Value
X-Frame-Options DENY
Content-Security-Policy default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://127.0.0.1:*; img-src 'self' data:; frame-ancestors 'none'
X-Content-Type-Options nosniff
Referrer-Policy no-referrer
Permissions-Policy camera=(), microphone=(), geolocation=(), accelerometer=(), gyroscope=()

HSTS is deliberately omitted. RFC 6797 §8.1 requires that Strict-Transport-Security not be sent over plain HTTP, and springtaled binds 127.0.0.1 without TLS by default. An operator terminating TLS in front of the daemon (reverse proxy, mesh sidecar) should set HSTS at that layer.


5. Error Format

All errors return JSON:

{
  "error": "connector not found: connector-foo"
}

TABLE I. STATUS CODES

Code Meaning
200 Success
400 Bad request (invalid JSON, missing fields, validation failure)
401 Unauthorized (missing or invalid bearer token)
403 Capability denied (manifest lacks required permission)
404 Not found
409 Conflict (name collision, toxic pair on install)
413 Payload too large (body > 1 MiB)
422 Unprocessable (JSON Schema validation failed)
429 Rate limited
500 Internal server error
503 Service unavailable (booting or degraded)

6. Live Streams

All four SSE endpoints emit event: + data: lines. Payloads are JSON.

6.1 /events/stream

Emits every event logged to the events table, in order.

event: event
data: {"id":"...","connector":"connector-telegram","trigger_type":"message_received","timestamp":"2026-04-10T12:34:56Z"}

6.2 /canvas/stream

Emits CanvasUpdate deltas for dashboard rendering.

event: canvas
data: {"kind":"node_moved","id":"...","x":120,"y":80}

Broadcast semantics: slow consumers receive RecvError::Lagged(n) and must reconnect. The dashboard auto-refetches GET /canvas on reconnect.

6.3 /cooperation/events

Emits formation lifecycle, momentum transition, rally, and interference events. Accepts an optional ?formation_id=... filter. See §3.4.

6.4 /chat/stream

Emits the bot's in-app chat replies (§3.19). Each event's data is a {"session": "...", "text": "..."} object. Same lagged-subscriber semantics as the other broadcast streams.


References