Control Plane API

The kernel serves the control plane at /_seed and its JSON API at /_seed/api. Everything outside /_seed is reverse-proxied to the running organism.

Every endpoint except the logo needs X-Seed-Token (or ?token= for the event stream): the API token embedded in the control-plane page of a signed-in browser, or the CLI's token from .seed/control-token. Sign-in itself happens at GET /_seed/login?code= (a one-time link; it sets the session cookie and redirects to /_seed/).

All responses are JSON. Errors are {"error": "message"} with a non-2xx status. Timestamps are RFC 3339 strings.

Types

type Identity = {
  name: string;                    // display name the Seed currently goes by ("Seed" until it becomes something)
  tagline: string;
  accent: string;                  // CSS color, e.g. "#3b82f6" ("" = default)
  logo_url: string;                // "/_seed/api/identity/logo?v=<hash>" (SVG; changes when the logo changes)
};

type Status = {
  name: string;                    // technical slug from seed.yaml (stable)
  identity: Identity;              // who the Seed currently is — evolves with generations
  purpose: string;                 // "" while the Seed has no purpose yet
  generation: Generation | null;   // current generation
  organism: { state: "stopped" | "building" | "starting" | "running" | "failed"; error?: string };
  model: { provider: string; name: string; configured: boolean };
  active_evolution: Evolution | null;
  pending_approvals: number;
};

type Message = {
  id: string;
  conversation_id: string;
  role: "user" | "seed" | "system";
  content: string;                  // markdown
  evolution_id?: string;            // set when the message announces/tracks an evolution
  kind: "chat" | "report";          // report = written by the kernel about an evolution
  created_at: string;
};

type EvolutionStatus =
  | "requested" | "planning" | "planned" | "mutating" | "building" | "testing"
  | "running" | "observing" | "reflecting" | "ready" | "applying" | "complete"
  | "failed" | "needs_input" | "cancelled" | "rolled_back";

type Plan = {
  title: string;
  scope: string;                    // short slug, e.g. "tasks"
  summary: string;
  steps: { title: string; detail?: string }[];
  capabilities: string[];           // capabilities the Seed will have afterwards
  risks: string[];
  // Staging (absent for single-step evolutions): the owner's whole goal, the
  // full ordered roadmap, and the 1-based stage this evolution builds.
  goal?: string;
  stages?: { title: string; summary?: string }[];
  stage?: number;
};

type Question = { question: string; why?: string; options?: string[] };  // options: suggested answers, recommended first
type Clarification = { questions: Question[]; answer: string };

type Check = {
  name: string;                     // build | migrate | test | launch | health | http:<path>
  ok: boolean;
  detail: string;                   // short human-readable result
  attempt: number;
  tests_passed?: number;
  tests_failed?: number;
};

type Reflection = {
  summary: string;
  goal_satisfied: boolean;
  gaps?: string[];                  // when not fully satisfied: what falls short, and why
  decisions: string[];
  learnings: string[];
  skills: string[];                 // skills created or improved
  debt: string[];
};

type Evolution = {
  id: string;                       // evo_...
  intent: string;
  title: string;
  status: EvolutionStatus;
  plan: Plan | null;
  base_generation: number;
  new_generation?: number;
  branch: string;
  commit?: string;
  attempts: number;
  checks: Check[];
  reflection: Reflection | null;
  summary?: string;                 // what the builder says it did
  error?: string;
  kind: "evolve" | "rollback";
  questions?: Question[];           // set while status is "needs_input" because the Seed asked its owner
  clarifications?: Clarification[]; // earlier question/answer rounds
  created_at: string;
  updated_at: string;
  completed_at?: string;
};

type EvolutionEvent = {
  id: number;
  evolution_id: string;
  kind: "phase" | "plan" | "tool_call" | "tool_result" | "check" | "note" | "error" | "approval" | "agent_text" | "question";
  summary: string;                  // one line, suitable for a progress list
  data: any;                        // details (tool input/output, etc.)
  created_at: string;
};

type Generation = {
  number: number;
  title: string;
  intent: string;
  evolution_id?: string;
  commit: string;
  parent_commit?: string;
  current: boolean;
  tests_passed?: number;
  build_ok?: boolean;
  health_ok?: boolean;
  created_at: string;
};

type Approval = {
  id: string;
  evolution_id?: string;
  action: string;                   // e.g. "write_file kernel/agent/loop.go"
  level: "safe" | "review" | "dangerous";
  detail: string;
  status: "pending" | "approved" | "denied";
  created_at: string;
};

type Skill = { name: string; description: string; path: string; files: string[]; content?: string };

Endpoints

Method Path Body Response
GET /status Status
GET /identity/logo the current logo (image/svg+xml)
GET /messages?limit=200 Message[] (oldest first)
POST /messages {content} Message (the stored user message; replies arrive via SSE)
GET /evolutions Evolution[] (newest first)
POST /evolutions {intent} Evolution
GET /evolutions/:id {evolution: Evolution, events: EvolutionEvent[]}
GET /evolutions/:id/diff {stat: string, diff: string}
POST /evolutions/:id/cancel Evolution
POST /evolutions/:id/answer {answer} Message (stored as the owner's message). 409 if the evolution is not waiting. While an evolution waits on questions, a normal POST /messages is also routed to it as the answer.
GET /approvals Approval[] (pending)
POST /approvals/:id {approved: boolean} Approval
GET /generations Generation[] (newest first)
POST /generations/:n/rollback {with_data?} Evolution (a kind: "rollback" evolution); with_data also restores the data of then
GET /skills Skill[]
GET /skills/:name Skill with content
GET /knowledge {self: string, docs: {path,title}[], decisions: {path,title}[]}
GET /knowledge/file?path= {path, content}
GET /logs?source=organism|kernel&tail=500 {lines: string[]}
GET /settings redacted configuration object
GET /extensions {id, title, path}[] — control-plane pages contributed by the organism
POST /evolutions/:id/decide {action: "apply"|"changes"|"discard", feedback?} {ok}: the owner's decision after trying a ready evolution
GET/POST /evolution-settings {preview} {preview}: whether evolutions wait to be tried before going live
GET /incidents Incident[]: live ones first (see health)
POST /incidents/:id/fix Evolution: the fix evolution
POST /incidents/:id/diagnose {ok}: investigate again
POST /incidents/:id/ignore {ok}
GET /spending {month, budget_usd, paused}: this month's spending by kind, day and item (see spending)
POST /spending/budget {budget_usd} same as GET /spending; 0 removes the budget
GET /notifications {public_key, subscriptions, kinds, about} (see notifications)
POST /notifications/subscriptions {endpoint, p256dh, auth} PushSubscription
POST /notifications/subscriptions/:id/remove same as GET /notifications
POST /notifications/kinds {kinds} same as GET /notifications
POST /notifications/test {delivered}
GET /backups {backups, daily, unavailable, keep} (see backups)
POST /backups Backup: a copy of the live data now
POST /backups/settings {daily} same as GET /backups
POST /backups/:id/restore {state}: restores in the background
GET /backups/:id/download the pg_dump archive
GET/POST /health/settings {auto_fix} {auto_fix}: whether the Seed fixes problems on its own
GET /routines Routine[] with schedule_text and last_run (see routines)
POST /routines {name, kind: "agent"|"job", schedule, timezone, prompt?, method?, path?} Routine
POST /routines/:id/update {enabled?, name?, schedule?, timezone?, prompt?, method?, path?} Routine. Only the owner's own routines can be edited; the organism's jobs can be paused.
POST /routines/:id/run RoutineRun (started now; 409 if already running)
POST /routines/:id/delete {ok} (owner's routines only)
GET /routines/:id/runs RoutineRun[] (newest first, up to 30)
GET /outbound {hosts, secrets, denied, available_secrets}: what the live organism may reach and read, and recently refused connections
POST /outbound/grant {hosts?, secrets?, reason?} same as GET, after granting
POST /outbound/revoke {kind: "host"|"secret", value} same as GET, after revoking
POST /login-links {path, expires_at}: a one-time sign-in link (one use, 15 minutes)
GET /owner/sessions {id, label, created_at, last_seen_at, current}[]: signed-in browsers
POST /owner/sessions/:id/revoke {ok}: signs that browser out
POST /owner/logout {ok}: signs out the calling browser and clears its cookie

Live events

GET /_seed/api/events is a Server-Sent Events stream. Each event has an event: name and JSON data:.

event data
message Message
evolution Evolution (full object, sent on every change)
evolution_event EvolutionEvent
approval Approval
status Status
chat {thinking: boolean}

Model ("mind") configuration

A Seed stores no credentials. The owner passes secrets as key/value pairs when starting it (seed run -e OPENROUTER_API_KEY, --env-file); a deployed Seed gets the same variables from its platform. The kernel holds them in memory only. If no key was passed, the owner may paste one in the control plane for this session only (memory; gone on restart). Keys are never returned by the API. The model choice (not a secret) is kept in the Seed's memory, and SEED_MODEL can also be passed at start. When a key is present and no model was chosen, the provider's default model is used.

type ProviderInfo = {
  id: string;                      // v0.1: "openrouter" only
  label: string;
  has_key: boolean;                // a key is available right now
  key_source: "env" | "session" | "";   // passed at start, pasted this session, or none
  key_env: string;                 // the variable to pass, e.g. "OPENROUTER_API_KEY"
  needs_key: boolean;
  needs_base_url: boolean;
  base_url: string;
  default_model: string;
  keys_url?: string;               // where to get a key
};

type ModelConfig = {
  provider: string;                // selected provider id
  name: string;                    // selected model id (the default when none was chosen)
  configured: boolean;             // the Seed can think (a key is present)
  providers: ProviderInfo[];
};

type ModelOption = { id: string; name: string; context_length?: number; description?: string };
Method Path Body Response
GET /model ModelConfig
GET /model/options?provider=<id> {models: ModelOption[], error?: string}
POST /model {provider, name, api_key?} ModelConfig. Validates with a tiny test call; 400 {error} changes nothing. api_key is held for this session only.
POST /model/forget-key {provider} ModelConfig. Forgets a session key (keys passed at start cannot be forgotten).

Status.model ({provider, name, configured}) reflects changes; a status SSE event is sent after a change.

Edit this page on GitHub