- 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)
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:
agent/library.yamldefines all skills with invoke patterns and compact summaries.- The control plane injects the compact skill index via
--append-system-promptwhen spawning pi (with--no-skillsto disable pi's built-in discovery). - Full skill content is served on-demand through the
skills_searchextension 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=aiderCONTROLPLANE_AIDER_BIN=aiderCONTROLPLANE_AIDER_FLAGS="--no-check-update --no-gitignore --no-auto-commits --no-dirty-commits"CONTROLPLANE_AIDER_TMUX_SESSION=clawdie-controlplaneCONTROLPLANE_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 architecturedoc/CONTROLPLANE-AGENT-ROLES.md— role definitionsSOUL.md,.agent/identities/SYSADMIN.md,.agent/identities/DB_ADMIN.md,.agent/identities/GIT_ADMIN.md— agent identities