Skip to content

System Anatomy

API Design

API Design ​

The Observatory exposes a REST API and WebSocket for real-time updates. All endpoints require authentication via a shared API key.

Authentication ​

All requests must include the API key in the header:

X-API-Key: your-shared-secret

Arm API Isolation ​

Critical: Arms must NOT have direct access to the Observatory API. If arms could call these endpoints with curl, they could:

  • Kill other arms (DELETE /api/arms/:id)
  • Override the brain (POST /api/brain/stop)
  • Approve their own proposals (POST /api/approvals/:id/approve)
  • Manipulate reputation scores

Isolation Architecture ​

┌─────────────────────────────────────────────────────────────┐
│                    API ACCESS MODEL                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Human (Browser/CLI)                                         │
│     │                                                        │
│     │ X-API-Key: human-secret                                │
│     ▼                                                        │
│  Observatory API (Full Access)                               │
│     │                                                        │
│     │                                                        │
│  Brain                                                       │
│     │                                                        │
│     │ MCP Protocol (not HTTP)                                │
│     ▼                                                        │
│  Arms (MCP Access Only)                                      │
│     │                                                        │
│     ✗ No HTTP access to Observatory                          │
│     ✗ No API key                                             │
│     ✗ Network blocked to localhost:observatory-port          │
│                                                              │
└─────────────────────────────────────────────────────────────┘

How Arms Communicate ​

Arms communicate ONLY through MCP, not HTTP:

typescript
// Arms use MCP tools, NOT curl
// WRONG - arm should never do this:
// curl -X DELETE http://localhost:8080/api/arms/other-arm

// RIGHT - arm uses MCP to request action:
await mcp.call("brain.request", {
  action: "pause_arm",
  target: "other-arm",
  reason: "Detected conflict in shared file",
});
// Brain validates, logs, and may reject the request

Network Isolation ​

Arms are blocked from accessing the Observatory:

typescript
const ARM_BLOCKED_HOSTS = [
  "localhost:8080",           // Observatory API
  "127.0.0.1:8080",
  "observatory",              // Docker network name
  "host.docker.internal:8080",
];

// In Docker, use network policies
// In local dev, use firewall rules or process sandboxing

Scoped API Keys (Future) ​

For production, implement scoped keys:

typescript
interface APIKeyScope {
  key: string;
  type: "human" | "service" | "readonly";
  permissions: Permission[];
  armId?: string;              // If service key, which arm
}

type Permission = 
  | "arms:read" | "arms:write" | "arms:kill"
  | "proposals:read" | "proposals:write" | "proposals:resolve"
  | "deploy:read" | "deploy:request" | "deploy:approve"
  | "brain:control"
  | "*";                       // Human admin only

const HUMAN_KEY: APIKeyScope = {
  key: "human-admin-key",
  type: "human",
  permissions: ["*"],
};

const READONLY_KEY: APIKeyScope = {
  key: "dashboard-readonly",
  type: "readonly",
  permissions: ["arms:read", "proposals:read", "deploy:read"],
};

Request Validation ​

All arm-affecting requests are validated:

typescript
async function validateArmAction(
  request: Request,
  action: string,
  targetArmId?: string
): Promise<ValidationResult> {
  const key = request.headers.get("X-API-Key");
  const scope = await getKeyScope(key);
  
  // Check if this is an arm trying to act on itself or others
  if (scope.type === "service" && scope.armId) {
    // Arms cannot kill other arms directly
    if (action === "arms:kill" && targetArmId !== scope.armId) {
      return { allowed: false, reason: "Arms cannot kill other arms" };
    }
    // Arms cannot approve proposals
    if (action === "proposals:resolve") {
      return { allowed: false, reason: "Arms cannot resolve proposals" };
    }
  }
  
  return { allowed: scope.permissions.includes(action) || scope.permissions.includes("*") };
}

REST Endpoints ​

System ​

http
GET /api/status

Returns overall system status.

json
{
  "brain": { "status": "running", "uptime": 3600 },
  "arms": { "total": 5, "active": 3, "paused": 1 },
  "proposals": { "open": 2, "pending_human": 1 },
  "garden": { "files": 1234, "conflicts": 0 }
}
http
GET /api/health

Health check endpoint (no auth required).


Brain ​

http
GET /api/brain

Get brain state.

http
POST /api/brain/start

Start the brain if stopped.

http
POST /api/brain/stop

Stop the brain gracefully.

Brain inbox/message endpoints ​

These are internal API endpoints used by the Brain worker and API bridge:

http
POST /api/brain/internal/messages/queue
GET /api/brain/internal/messages/pending?to=brain
GET /api/brain/internal/messages/deadletter
POST /api/brain/internal/messages/deadletter/:id/requeue
POST /api/brain/internal/messages/:id/status
POST /api/brain/internal/messages/cleanup

Notes:

  • Only allowlisted brain message types are accepted for to=brain.
  • Invalid/unsupported messages are dead-lettered (to_id = brain.deadletter) instead of silently dropped.
  • Brain workers acquire processing leases (success: true|false) before handling messages.

Arms ​

http
GET /api/arms

List all arms.

json
{
  "arms": [
    {
      "id": "ui-arm",
      "name": "UI Specialist",
      "domain": "ui",
      "status": "working",
      "reputation": 75,
      "contextUtilization": 0.65
    }
  ]
}
http
POST /api/arms

Spawn a new arm.

json
{
  "name": "Test Runner",
  "agent": "opencode",
  "domain": "testing",
  "workdir": "/path/to/project"
}
http
GET /api/arms/:id

Get arm details.

http
DELETE /api/arms/:id

Kill an arm.

http
PATCH /api/arms/:id

Update arm configuration.

http
GET /api/arms/:id/context

Get arm's current context (files, tokens).

http
GET /api/arms/:id/activity

Get arm's activity log.

Activity endpoint semantics ​

The API has two different "activity" surfaces:

  • /api/arms/:id/activity: arm-scoped activity history (single arm timeline).
  • /api/activity/*: cross-arm/system activity and transcript views from JetStream.

Use arm-scoped activity for per-arm debugging and /api/activity for global analysis dashboards.

http
POST /api/arms/:id/pause

Pause an arm.

http
POST /api/arms/:id/resume

Resume a paused arm.


Tasks ​

http
GET /api/tasks

List all tasks.

Query params:

  • status: "draft" | "pending" | "claimed" | "in_progress" | "completing" | "completed" | "failed" | "blocked" | "cancelled" (comma-separated for multiple)
  • priority: "critical" | "high" | "normal" | "low"
  • domain: filter by domain
  • assignedTo: filter by assigned arm
  • phase: filter by phase
  • limit: max results (default 100)
  • offset: pagination offset
http
POST /api/tasks

Create a new task.

http
GET /api/tasks/:id

Get task details.

Query params:

  • include: "discussions" to include task comments.
http
PATCH /api/tasks/:id

Update task fields.

http
DELETE /api/tasks/:id

Delete a task.

http
POST /api/tasks/reorder

Reorder a task to a specific position.

json
{
  "taskId": "task-123",
  "toSortOrder": 0
}
http
POST /api/tasks/:id/remove-from-plan

Remove a task from its source plan.md file and delete it from the database.

Task Discussions ​

Task Discussions provide threaded commenting functionality for tasks, allowing humans and arms to collaborate on task implementation. Each comment is associated with a specific task and can be a top-level comment or a reply to another comment.

http
GET /api/tasks/:id/discussions

List all comments for a task.

Query params:

  • limit: max results (default 50)
  • offset: pagination offset
  • threaded: "true" to return comments in a nested tree structure.

Response:

json
{
  "discussions": [
    {
      "id": "comment-123",
      "taskId": "task-456",
      "content": "This task is blocked by PR-789",
      "authorType": "human",
      "authorId": "user-1",
      "authorName": "Tim",
      "client": "web",
      "edited": false,
      "deleted": false,
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T10:30:00Z"
    }
  ],
  "totalCount": 1
}
http
POST /api/tasks/:id/discussions

Add a new comment to a task discussion.

Body:

json
{
  "content": "This task is blocked by PR-456",
  "parentId": "comment-789",
  "authorType": "human",
  "authorId": "user-1",
  "authorName": "Tim",
  "client": "web"
}
http
PATCH /api/tasks/:id/discussions/:commentId

Edit a comment (24-hour window, author only).

Body:

json
{
  "content": "Updated comment content",
  "authorId": "user-1"
}
http
DELETE /api/tasks/:id/discussions/:commentId

Soft delete a comment (author only).

Body:

json
{
  "authorId": "user-1"
}
http
POST /api/tasks/:id/discussions/mark-read

Mark comments as read for a user.

Body:

json
{
  "userId": "user-1",
  "lastReadCommentId": "comment-789"
}
http
GET /api/tasks/:id/discussions/unread

Get unread comment count for a user.

Query params:

  • userId: User ID to check for unread comments

Response:

json
{
  "unreadCount": 3
}

Comment Structure ​

typescript
interface TaskComment {
  id: string;                    // Unique comment identifier
  taskId: string;                // Associated task ID
  parentId?: string;            // Parent comment ID (for replies)
  content: string;               // Comment content
  authorType: "human" | "arm" | "brain";  // Who created the comment
  authorId: string;              // Author identifier
  authorName?: string;           // Display name for author
  client: "web" | "mail" | "mcp" | "cli"; // How the comment was created
  edited: boolean;                // Whether comment was edited
  deleted: boolean;              // Whether comment was soft-deleted
  createdAt: string;             // ISO timestamp
  updatedAt: string;             // ISO timestamp
}

Threaded Discussions ​

Comments can be organized in a threaded structure where replies are nested under parent comments. The API supports both flat and threaded views of discussions.

Comment Lifecycle ​

  1. Creation: Comments are created with content, author information, and optional parent ID for replies
  2. Editing: Authors can edit their comments within a 24-hour window
  3. Soft Deletion: Comments can be deleted by their author but are soft-deleted to preserve context
  4. Read Tracking: Users can mark comments as read for notification purposes

Comment Metadata ​

Comments track additional metadata for better collaboration:

  • Author Information: Type (human/arm/brain), ID, and display name
  • Client Context: How the comment was created (web UI, email, MCP tool, CLI)
  • Edit History: Whether the comment was edited and when
  • Deletion Status: Soft deletion preserves context while hiding content

Task Discussions ​

http
GET /api/tasks/:id/discussions

List all comments for a task.

Query params:

  • limit: max results (default 50)
  • offset: pagination offset
  • threaded: "true" to return comments in a nested tree structure.
http
POST /api/tasks/:id/discussions

Add a new comment.

json
{
  "content": "This task is blocked by PR-456",
  "parentId": "comment-789",
  "authorType": "human",
  "authorId": "user-1",
  "authorName": "Tim",
  "client": "web"
}
http
PATCH /api/tasks/:id/discussions/:commentId

Edit a comment (24-hour window, author only).

http
DELETE /api/tasks/:id/discussions/:commentId

Soft delete a comment (author only).

http
POST /api/tasks/:id/discussions/mark-read

Mark comments as read for a user.

json
{
  "userId": "user-1",
  "lastReadCommentId": "comment-789"
}
http
GET /api/tasks/:id/discussions/unread

Get unread comment count for a user.


Garden ​

http
GET /api/garden

Get full garden topology (3D coordinates for all files).

json
{
  "nodes": [
    {
      "path": "src/components/Button.tsx",
      "type": "file",
      "coords": { "x": 15, "y": 45, "z": 30 },
      "owner": "ui-arm",
      "lastTouchedBy": "ui-arm",
      "lastTouchedAt": "2024-01-15T10:30:00Z",
      "conflictZone": false
    }
  ]
}
http
GET /api/garden/tree

Get file tree with ownership markers.

http
GET /api/garden/claims

Get all active file claims.

http
GET /api/garden/conflicts

Get current conflict zones.

http
GET /api/garden/activity

Get recent file touch activity.

json
{
  "activity": [
    {
      "path": "src/api/users.ts",
      "armId": "api-arm",
      "action": "write",
      "timestamp": "2024-01-15T10:30:00Z"
    }
  ]
}

Proposals ​

http
GET /api/proposals

List proposals with optional filters.

Query params:

  • status: "open" | "accepted" | "rejected" | "all"
  • type: Proposal type
  • author: Arm ID
http
POST /api/proposals

Create a new proposal (usually done by arms, but can be human-initiated).

http
GET /api/proposals/:id

Get proposal details including arguments and signals.

http
POST /api/proposals/:id/argue

Add an argument to a proposal.

json
{
  "position": "for",
  "content": "This change improves performance by 40%",
  "evidence": ["benchmark-results.json"]
}
http
POST /api/proposals/:id/signal

Add a signal (support/opposition).

json
{
  "weight": 75,
  "reason": "Looks good, tests pass"
}
http
POST /api/proposals/:id/resolve

Human resolves an undecided proposal.

json
{
  "decision": "accept",
  "reason": "Approving despite mixed signals"
}

Approvals ​

http
GET /api/approvals

List pending human approvals.

http
POST /api/approvals/:id/approve

Approve a pending request.

http
POST /api/approvals/:id/reject

Reject a pending request.

json
{
  "reason": "Not ready for production yet"
}

Deployments ​

http
GET /api/deployments

Get deployment history.

http
POST /api/deployments

Request a deployment.

json
{
  "environment": "staging",
  "ref": "main",
  "reason": "Weekly release"
}
http
GET /api/deployments/:id

Get deployment status.


Notifications ​

http
POST /api/notifications/subscribe

Subscribe to push notifications.

json
{
  "endpoint": "https://fcm.googleapis.com/...",
  "keys": {
    "p256dh": "...",
    "auth": "..."
  }
}
http
DELETE /api/notifications/subscribe

Unsubscribe from push notifications.

http
GET /api/notifications/vapid

Get VAPID public key for push subscription.


Config ​

http
GET /api/config

Get system configuration.

http
PATCH /api/config

Update system configuration.


WebSocket ​

Connect to /ws for real-time updates.

Client → Server Messages ​

typescript
// Subscribe to channels
{
  "type": "subscribe",
  "channels": ["arms", "garden", "proposals"]
}

// Unsubscribe
{
  "type": "unsubscribe", 
  "channels": ["garden"]
}

Server → Client Messages ​

typescript
{
  "channel": "arms",
  "event": "arm.status",
  "data": {
    "armId": "ui-arm",
    "status": "working",
    "task": "Implementing dark mode toggle"
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

Channels ​

ChannelEvents
armsarm.spawned, arm.status, arm.activity, arm.killed, arm.paused
gardengarden.claim, garden.touch, garden.conflict, garden.release
proposalsproposal.new, proposal.argue, proposal.signal, proposal.resolved
activityAll events (firehose)
approvalsapproval.new, approval.resolved
deploydeploy.requested, deploy.consensus, deploy.started, deploy.completed, deploy.failed

Push Notifications ​

Using the Web Push API with VAPID for browser push notifications.

Payload Structure ​

typescript
interface PushPayload {
  title: string;
  body: string;
  icon?: string;
  tag?: string;           // For replacing/grouping
  data?: {
    url?: string;         // URL to open on click
    proposalId?: string;
    approvalId?: string;
  };
}

Push Triggers ​

EventPriorityPayload
Human approval neededHighLink to approval page
Deployment to prod readyHighLink to deployment
Arm misbehavior detectedHighLink to arm details
Proposal stalledMediumLink to proposal
Deployment completedLowStatus summary

Example Push ​

json
{
  "title": "Approval Needed",
  "body": "Deploy to production requires your approval",
  "icon": "/icons/coleo.png",
  "tag": "approval-123",
  "data": {
    "url": "/approvals/123",
    "approvalId": "123"
  }
}

Error Responses ​

All errors follow this format:

json
{
  "error": {
    "code": "ARM_NOT_FOUND",
    "message": "Arm with ID 'xyz' not found",
    "details": {}
  }
}

Error Codes ​

CodeHTTP StatusDescription
UNAUTHORIZED401Invalid or missing API key
FORBIDDEN403Action not permitted
NOT_FOUND404Resource not found
CONFLICT409Resource state conflict
VALIDATION_ERROR422Invalid request body
INTERNAL_ERROR500Server error

Rate Limiting ​

EndpointLimit
/api/*100 requests/minute
/ws1 connection per client
Push notifications10/minute per subscription