clawdie-ai/doc/CONTROLPLANE-MESSAGE-CONTRACT.md
Mevy Assistant c633fdcc49 Remove legacy agent IDs + tighten task API
- Canonicalize controlplane agent IDs/roles to: sysadmin, db-admin, git-admin (drop *_agent variants).

- Add DB migration to rewrite existing *_agent rows and references to canonical IDs.

- Tighten POST /api/controlplane/tasks contract: require assigned_to (remove agent_id alias).

- Update tests and docs to match canonical IDs.

---

Build: pass (just typecheck)

Tests: pass — 1536 passed (92 files) (just test)
2026-04-19 06:54:28 +00:00

7.5 KiB

Control Plane Message Contract

Overview

Agents query the control plane HTTP API for governance (tasks, budgets, approvals) and local resources for operations (sessions, skills). This is the dual-layer decision model.

Agent Heartbeat:
  1. GET /api/controlplane/state        → "What's my budget? Am I active?"
  2. GET /api/controlplane/tasks?role=X  → "What's assigned to me?"
  3. Read local data/sessions/{name}.jsonl → "What did I do last time?"
  4. Execute skill or escalate
  5. POST /api/controlplane/activity     → "Here's what I did"

Control Plane API Queries

1. Get State

GET /api/controlplane/state
Authorization: Bearer {CONTROLPLANE_SHARED_SECRET}

Response:

{
  "agents": [
    { "id": "clawdie", "role": "orchestrator", "heartbeat_enabled": false },
    {
      "id": "sysadmin",
      "role": "sysadmin",
      "heartbeat_enabled": true
    },
    {
      "id": "db-admin",
      "role": "db-admin",
      "heartbeat_enabled": false
    },
    {
      "id": "git-admin",
      "role": "git-admin",
      "heartbeat_enabled": false
    }
  ],
  "budget": {
    "daily_tokens": 100000,
    "spent_today": 25000,
    "remaining": 75000,
    "hard_limit_exceeded": false,
    "allocation": {
      "orchestrator": 80000,
      "sysadmin": 10000,
      "db-admin": 5000,
      "git-admin": 5000
    }
  }
}

2. Get Task Queue

GET /api/controlplane/tasks?role={agent_role}
Authorization: Bearer {CONTROLPLANE_SHARED_SECRET}

Response:

{
  "tasks": [
    {
      "task_id": "TASK-001",
      "title": "Check if db jail is running",
      "description": "Verify clawdie-db is up and healthy",
      "assigned_to": "sysadmin",
      "priority": "medium",
      "status": "pending",
      "created_at": "2026-04-07T10:30:00Z",
      "context": { "jail_name": "clawdie-db" }
    }
  ]
}

3. Get Approvals

GET /api/controlplane/approvals?agent_id={agent_id}
Authorization: Bearer {CONTROLPLANE_SHARED_SECRET}

Response:

{
  "pending": [
    {
      "approval_id": "APPR-042",
      "task_id": "TASK-002",
      "operation": "merge PR with conflict resolution",
      "estimated_tokens": 8500,
      "operator_approved": false
    }
  ],
  "approved": [
    {
      "approval_id": "APPR-041",
      "operation": "backup database",
      "operator_approved": true,
      "approved_at": "2026-04-07T10:45:00Z"
    }
  ]
}

6. Proxy hostd Operation (jail agents)

Jail agents use this endpoint to execute privileged host operations (bastille, zfs, pf) through the controlplane API instead of direct Unix socket access.

POST /api/controlplane/hostd
Authorization: Bearer {CONTROLPLANE_SHARED_SECRET}

{
  "op": "bastille-list",
  "params": {}
}

Response:

{
  "ok": true,
  "output": "JID  IP Address      Hostname  Path",
  "exitCode": 0
}

The API proxies the request to the hostd daemon on the host. Available ops match those in src/hostd/privileged-commands.ts (bastille-list, bastille-cmd, zfs-snapshot, etc.).


Agent Posts

1. Task Completion

POST /api/controlplane/activity
Authorization: Bearer {CONTROLPLANE_SHARED_SECRET}

{
  "event_type": "task_completed",
  "task_id": "TASK-001",
  "agent_id": "sysadmin",
  "skill_executed": "jail-status",
  "result": {
    "status": "success",
    "output": "Jail clawdie-db running, uptime 5d 3h, CPU 2.1%",
    "tokens_used": 420
  }
}

2. Approval Request

POST /api/controlplane/activity
Authorization: Bearer {CONTROLPLANE_SHARED_SECRET}

{
  "event_type": "approval_request",
  "agent_id": "git-admin",
  "operation": "Merge PR #42 with conflict resolution",
  "reasoning": "Conflict detected in src/index.ts",
  "estimated_tokens": 8500
}

3. Error / Escalation

POST /api/controlplane/activity
Authorization: Bearer {CONTROLPLANE_SHARED_SECRET}

{
  "event_type": "error",
  "agent_id": "db-admin",
  "error_message": "Vacuum failed: database locked",
  "action_taken": "Escalated to orchestrator",
  "tokens_used": 1200
}

Local Resources (No API)

Session History

const sessionPath = `${process.env.CONTROLPLANE_SESSION_CWD}/${agentName}.jsonl`;

JSONL format, one entry per line:

{
  "timestamp": "2026-04-06T10:00:00Z",
  "task": "Check jail status",
  "skill": "jail-status",
  "outcome": "running",
  "tokens_used": 420
}

Skills Catalog

Skills are not scanned from a directory at runtime. Instead:

  1. agent/library.yaml defines all skills with invoke patterns and compact summaries.
  2. The control plane injects the compact skill index via --append-system-prompt when spawning pi (with --no-skills to disable pi's built-in discovery).
  3. Full skill content is served on-demand through the skills_search extension tool.
import { getAgentSkillIndex } from './skill-library';
const index = getAgentSkillIndex(agentId);

The Loop

[CONTROL PLANE API]           [LOCAL RESOURCES]         [AGENT]
       |
       |<-- GET /api/controlplane/state -----|
       |<-- GET /api/controlplane/tasks ------|
       |                                      |-- Read session JSONL
       |                                      |-- Load skills catalog
       |                                      |-- Pattern match → execute skill
       |
       |<-- POST /api/controlplane/activity --|
       |                                               [Done]

Most work (skill execution) happens locally. API is coordination + audit.


Implementation Mapping

src/index.ts

app.get('/api/controlplane/state', requireAuth, async (req, res) => { ... });
app.get('/api/controlplane/tasks', requireAuth, async (req, res) => { ... });
app.post('/api/controlplane/activity', requireAuth, async (req, res) => { ... });

src/controlplane-runner.ts

const agentEnv = {
  CONTROLPLANE_AGENT_ID: agent.id,
  CONTROLPLANE_API_URL: `http://localhost:${process.env.CONTROLPLANE_API_PORT || 3100}`,
  CONTROLPLANE_API_KEY: agent.apiKey,
  CONTROLPLANE_TASK_ID: task.id,
  CONTROLPLANE_WORKSPACE_CWD: '/home/clawdie/clawdie-ai',
  CONTROLPLANE_SESSION_CWD: '/home/clawdie/clawdie-ai/data/sessions',
};

Runner Modes (pi vs aider)

Control plane tasks are executed by a runner. Default is pi, but an Aider runner can be enabled for multi-agent orchestration with tmux glass-pane visibility.

Environment switches

  • CONTROLPLANE_RUNNER=pi (default)
  • CONTROLPLANE_RUNNER=aider
  • CONTROLPLANE_AIDER_BIN=aider
  • CONTROLPLANE_AIDER_FLAGS="--no-check-update --no-gitignore --no-auto-commits --no-dirty-commits"
  • CONTROLPLANE_AIDER_TMUX_SESSION=clawdie-controlplane
  • CONTROLPLANE_AIDER_LOG_DIR=/home/clawdie/clawdie-ai/tmp/controlplane/aider

tmux glass-pane

When CONTROLPLANE_RUNNER=aider, each agent streams output to: CONTROLPLANE_AIDER_LOG_DIR/{agentId}.log.

If you already have a tmux session named the same as CONTROLPLANE_AIDER_TMUX_SESSION and its window indices are constrained by a custom config, tmux may reject new-window with an “index in use” error. Use an empty session name or delete stale windows before running the controlplane to avoid this edge case.

Attach:

tmux attach -t clawdie-controlplane

References

  • doc/CONTROLPLANE-ARCHITECTURE.md — service architecture
  • doc/CONTROLPLANE-AGENT-ROLES.md — role definitions
  • SOUL.md, .agent/identities/SYSADMIN.md, .agent/identities/DB_ADMIN.md, .agent/identities/GIT_ADMIN.md — agent identities