Agents can now provision their sandbox on a connected fleet node instead of the gateway host. Local stays the strict default, so existing agents are byte-for- byte unaffected until explicitly placed elsewhere. Security parity: the daemon links the REAL cm-sandbox DockerDriver and runs the typed container ops (sb_provision/sb_exec/sb_destroy/sb_health/sb_list) through it — identical hardening (cap-drop ALL, seccomp, no-net, read-only, non-root) to local sandboxes. cm-sandbox spec types are now Serialize/Deserialize so the spec crosses the channel. - cm-api: RemoteDriver (impl SandboxDriver over the node channel) + HubDriverProvider (impl cm_runtime::NodeDriverProvider, hands out a driver only for connected nodes via a sync online set) + NodeHub.call/is_connected. AppState.with_node_hub so the hub is shared with the placement provider. - cm-runtime SandboxManager: driver_for(node_id) routes by the recorded agent_containers.node_id (local default = existing driver, identical path); placement_node() reads the workspace setting and falls back to local if the node is offline; exec/release route accordingly. NodeDriverProvider trait. - DB: 0020_workspace_placement + repo (for_agent/get/set/clear). - main.rs: build the NodeHub first; inject HubDriverProvider into the agent manager + share the hub with AppState. - API+UI: GET/PUT /api/fleet/placement + a "Run agents on: Local / <node>" selector in the Fleet overview. Note: a node must be able to pull the agent image (the daemon docker-pulls it); interactive PTY for agent containers on remote nodes is not wired (Terminal app stays local) — the in-dashboard node shell already covers host access. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
200 lines
6.7 KiB
JSON
200 lines
6.7 KiB
JSON
{
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
"$id": "https://clawmates.work/schemas/events.schema.json",
|
|
"title": "Clawmates Event Taxonomy",
|
|
"description": "Normalized real-time events emitted by the live bridge and consumed by the visualizations. Each SSE frame is `event: <type>` + `data: <object matching the matching definition>`.",
|
|
"type": "object",
|
|
"oneOf": [
|
|
{ "$ref": "#/definitions/agent.status" },
|
|
{ "$ref": "#/definitions/agent.task.update" },
|
|
{ "$ref": "#/definitions/agent.reasoning.delta" },
|
|
{ "$ref": "#/definitions/agent.computer.frame" },
|
|
{ "$ref": "#/definitions/agent.tool.call" },
|
|
{ "$ref": "#/definitions/door.request" },
|
|
{ "$ref": "#/definitions/door.resolve" },
|
|
{ "$ref": "#/definitions/agent.message" },
|
|
{ "$ref": "#/definitions/world.touch" },
|
|
{ "$ref": "#/definitions/node.activity" },
|
|
{ "$ref": "#/definitions/topology.update" },
|
|
{ "$ref": "#/definitions/telemetry" },
|
|
{ "$ref": "#/definitions/routine.update" }
|
|
],
|
|
"definitions": {
|
|
"agent.status": {
|
|
"type": "object",
|
|
"required": ["agentId", "status"],
|
|
"properties": {
|
|
"agentId": { "type": "string" },
|
|
"status": { "enum": ["online", "working", "idle", "offline"] },
|
|
"role": { "type": "string" }
|
|
}
|
|
},
|
|
"agent.task.update": {
|
|
"type": "object",
|
|
"required": ["agentId", "taskId", "title"],
|
|
"properties": {
|
|
"agentId": { "type": "string" },
|
|
"taskId": { "type": "string" },
|
|
"title": { "type": "string" },
|
|
"elapsedMs": { "type": "integer", "minimum": 0 },
|
|
"steps": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["label", "state"],
|
|
"properties": {
|
|
"label": { "type": "string" },
|
|
"state": { "enum": ["done", "active", "pending"] }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"agent.reasoning.delta": {
|
|
"type": "object",
|
|
"required": ["agentId", "text"],
|
|
"properties": {
|
|
"agentId": { "type": "string" },
|
|
"text": { "type": "string", "description": "Token chunk to APPEND to the reasoning stream." },
|
|
"channel": { "enum": ["think", "say", "tool"], "default": "think" }
|
|
}
|
|
},
|
|
"agent.computer.frame": {
|
|
"type": "object",
|
|
"required": ["agentId", "app"],
|
|
"properties": {
|
|
"agentId": { "type": "string" },
|
|
"app": { "enum": ["browser", "terminal", "slack", "claw-chat"] },
|
|
"url": { "type": "string" },
|
|
"lines": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["text"],
|
|
"properties": {
|
|
"text": { "type": "string" },
|
|
"kind": { "enum": ["plain", "add", "del", "ok", "warn", "cursor"], "default": "plain" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"agent.tool.call": {
|
|
"type": "object",
|
|
"required": ["agentId", "tool"],
|
|
"properties": {
|
|
"agentId": { "type": "string" },
|
|
"tool": { "type": "string" },
|
|
"target": { "type": "string" },
|
|
"doorRequired": { "type": "boolean", "default": false }
|
|
}
|
|
},
|
|
"door.request": {
|
|
"type": "object",
|
|
"required": ["doorId", "agentId", "action"],
|
|
"properties": {
|
|
"doorId": { "type": "string" },
|
|
"agentId": { "type": "string" },
|
|
"action": { "type": "string", "description": "e.g. github.review.comment" },
|
|
"target": { "type": "string" },
|
|
"summary": { "type": "string" }
|
|
}
|
|
},
|
|
"door.resolve": {
|
|
"type": "object",
|
|
"required": ["doorId", "decision"],
|
|
"properties": {
|
|
"doorId": { "type": "string" },
|
|
"decision": { "enum": ["approve", "deny"] },
|
|
"by": { "type": "string" }
|
|
}
|
|
},
|
|
"agent.message": {
|
|
"type": "object",
|
|
"required": ["fromAgentId", "toAgentId", "text"],
|
|
"properties": {
|
|
"fromAgentId": { "type": "string" },
|
|
"toAgentId": { "type": "string" },
|
|
"text": { "type": "string" },
|
|
"ts": { "type": "string", "format": "date-time" }
|
|
}
|
|
},
|
|
"world.touch": {
|
|
"type": "object",
|
|
"description": "An agent is converging on a project/service/event node (Gource). Drives the Live canvas retarget + beam.",
|
|
"required": ["agentId", "nodeId"],
|
|
"properties": {
|
|
"agentId": { "type": "string" },
|
|
"nodeId": { "type": "string" },
|
|
"kind": { "enum": ["service", "event"], "default": "service" },
|
|
"weight": { "type": "number", "minimum": 0, "maximum": 1, "default": 1 }
|
|
}
|
|
},
|
|
"node.activity": {
|
|
"type": "object",
|
|
"required": ["nodeId"],
|
|
"properties": {
|
|
"nodeId": { "type": "string" },
|
|
"label": { "type": "string" },
|
|
"kind": { "enum": ["service", "event"] },
|
|
"heat": { "type": "number", "minimum": 0, "maximum": 1 }
|
|
}
|
|
},
|
|
"topology.update": {
|
|
"type": "object",
|
|
"description": "Full or partial re-layout of the world graph.",
|
|
"required": ["formation"],
|
|
"properties": {
|
|
"formation": { "enum": ["hierarchy", "flat", "live"] },
|
|
"nodes": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["id", "tier"],
|
|
"properties": {
|
|
"id": { "type": "string" },
|
|
"tier": { "enum": ["org", "company", "team", "agent", "service", "event"] },
|
|
"label": { "type": "string" },
|
|
"parentId": { "type": "string" }
|
|
}
|
|
}
|
|
},
|
|
"edges": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["from", "to"],
|
|
"properties": {
|
|
"from": { "type": "string" },
|
|
"to": { "type": "string" },
|
|
"kind": { "enum": ["parent", "peer", "flow"], "default": "parent" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"telemetry": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tokensPerMin": { "type": "number" },
|
|
"costPerHr": { "type": "number" },
|
|
"loops": { "type": "integer" },
|
|
"doorsPending": { "type": "integer" }
|
|
}
|
|
},
|
|
"routine.update": {
|
|
"type": "object",
|
|
"required": ["routineId", "name", "kind"],
|
|
"properties": {
|
|
"routineId": { "type": "string" },
|
|
"name": { "type": "string" },
|
|
"kind": { "enum": ["cron", "loop"] },
|
|
"owner": { "type": "string" },
|
|
"schedule": { "type": "string", "description": "cron expr or next-run hint" },
|
|
"progress": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
"state": { "enum": ["running", "scheduled", "done", "failed", "door"] }
|
|
}
|
|
}
|
|
}
|
|
}
|