Park the Pi-only control simplification and cross-host run contract so other agents can review before implementation starts. --- Build: pass | Tests: pass — 2456 passed (182 files)
8.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);
Telegram Images (Vision Fallback)
Telegram photo messages are persisted to disk under TMP_DIR and stored in the
conversation as a placeholder:
[Photo saved: /home/clawdie/clawdie-ai/tmp/telegram/photos/<chat>/<id>.jpg]
For screenshot/meme analysis, the runtime can optionally run a vision helper model that OCRs the saved image and injects a bounded block into the prompt:
[Vision OCR] ... [/Vision OCR]
The main chat model must answer using the OCR/summary in that block and should not claim it “can’t see the image”.
Config:
OPENROUTER_API_KEY— required whenVISION_PROVIDER=openrouterVISION_PROVIDER=openrouterVISION_MODEL=nvidia/nemotron-nano-12b-v2-vl:free(default)VISION_MAX_IMAGES(default1)VISION_MAX_CHARS_PER_IMAGE(default4000)VISION_MAX_TOTAL_CHARS(default6000)
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 definitionsdoc/COLIBRI-PI-CONTROL-PLAN.md— planned Pi-only simplification and Colibri event fabricdoc/INTERAGENT-RUN-CONTRACT.md— cross-host run manifest and artifact exchange contractSOUL.md,.agent/identities/SYSADMIN.md,.agent/identities/DB_ADMIN.md,.agent/identities/GIT_ADMIN.md— agent identities