[taskboard] add direct host dispatch targets
This commit is contained in:
@@ -14,7 +14,7 @@ WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=8395
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git ca-certificates \
|
||||
&& apt-get install -y --no-install-recommends git ca-certificates openssh-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
32
README.md
32
README.md
@@ -6,7 +6,8 @@ It tracks and visualizes:
|
||||
|
||||
- OpenClaw swarm agents on `ubuntu`
|
||||
- ZeroClaw host runtimes on `grizzley` and `ice`
|
||||
- shared task assignment and dispatch across both families
|
||||
- direct SSH host targets for `pve`, `truenas`, and `panda`
|
||||
- shared task assignment and dispatch across all families
|
||||
- wiki pages and architecture documentation rendered in the UI
|
||||
- dispatch audit history, failure queues, heartbeat overlays, and task templates
|
||||
|
||||
@@ -21,7 +22,7 @@ It tracks and visualizes:
|
||||
## Key Pages
|
||||
|
||||
- `/tasks` - unified Kanban board
|
||||
- `/agents` - configured OpenClaw and ZeroClaw runtimes
|
||||
- `/agents` - configured OpenClaw, ZeroClaw, and direct host targets
|
||||
- `/openclaw` - focused OpenClaw swarm view
|
||||
- `/zeroclaw` - focused ZeroClaw host-runtime view
|
||||
- `/dispatch` - dispatch audit log and failure queue
|
||||
@@ -35,10 +36,11 @@ It tracks and visualizes:
|
||||
- dispatch lifecycle states and SQLite audit history
|
||||
- OpenClaw swarm dispatch into `~/.clawdbot/active-tasks.json`
|
||||
- ZeroClaw webhook dispatch for `grizzley` and `ice`
|
||||
- direct SSH dispatch for `pve`, `truenas`, and `panda`
|
||||
- task callback API for remote completion/result sync
|
||||
- OpenClaw registry sync API for swarm task state reconciliation
|
||||
- failure queue and dispatch history views
|
||||
- family-specific runtime views for OpenClaw and ZeroClaw
|
||||
- family-specific runtime views for OpenClaw and ZeroClaw plus unified direct-host visibility
|
||||
- architecture documentation rendered directly from tracked config
|
||||
|
||||
## Fleet Model
|
||||
@@ -64,7 +66,21 @@ It tracks and visualizes:
|
||||
- Channels:
|
||||
- paired HTTP gateway access
|
||||
- Homelab-Ice forum topics
|
||||
- remote gateway routing from `ice`
|
||||
- remote gateway routing from `ice`
|
||||
|
||||
### Direct SSH Targets
|
||||
|
||||
- Execution host: `ubuntu` taskboard container
|
||||
- Transport: `ssh` using the mounted host key
|
||||
- Configured targets:
|
||||
- `pve` via `root@192.168.50.11`
|
||||
- `truenas` via `christopher@192.168.50.12`
|
||||
- `panda` via `bear@192.168.50.196`
|
||||
- Dispatch model:
|
||||
- select a direct target agent
|
||||
- dispatch a built-in safe action
|
||||
- capture stdout/stderr
|
||||
- write completion through the same callback pipeline as remote runtimes
|
||||
|
||||
## Important Environment Variables
|
||||
|
||||
@@ -80,6 +96,8 @@ It tracks and visualizes:
|
||||
- `ZEROCLAW_GRIZZLEY_TOKEN`
|
||||
- `ZEROCLAW_ICE_URL`
|
||||
- `ZEROCLAW_ICE_TOKEN`
|
||||
- `DIRECT_SSH_KEY_PATH`
|
||||
- `DIRECT_SSH_TIMEOUT_MS`
|
||||
|
||||
## Development
|
||||
|
||||
@@ -106,11 +124,15 @@ npm start
|
||||
- ZeroClaw architecture:
|
||||
- rendered from the tracked fleet model in this repo
|
||||
- optional runtime path overrides can be provided via `ZEROCLAW_PRIMARY_DIR` and `ZEROCLAW_CONTROL_DIR`
|
||||
- Direct SSH:
|
||||
- taskboard container mounts `/home/bear/.ssh` as read-only
|
||||
- direct targets use `/root/.ssh/id_ed25519` by default
|
||||
|
||||
## Notes
|
||||
|
||||
- The UI intentionally treats OpenClaw and ZeroClaw as separate families with different runtime and channel models.
|
||||
- The UI intentionally treats OpenClaw, ZeroClaw, and direct host targets as separate families with different runtime and channel models.
|
||||
- `ice` ZeroClaw remains tied to host-local secret/encryption state; the dashboard reads that runtime but does not attempt to rewrite it.
|
||||
- Direct targets are intentionally limited to safe built-in actions from `config/fleet.json`, not arbitrary shell commands from the browser.
|
||||
|
||||
## Status Docs
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export async function POST(
|
||||
detail?: string | null;
|
||||
completed_by?: string | null;
|
||||
last_error?: string | null;
|
||||
last_dispatch_at?: string | null;
|
||||
};
|
||||
|
||||
const updated = await applyTaskCallback(numericId, payload);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AppShell } from "@/components/app-shell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Claw Fleet Console",
|
||||
description: "OpenClaw and ZeroClaw fleet dashboard",
|
||||
description: "OpenClaw, ZeroClaw, and direct host operations dashboard",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
|
||||
@@ -45,7 +45,7 @@ export function AgentsClient({
|
||||
<CardHeader>
|
||||
<CardTitle>Configured Agent Runtimes</CardTitle>
|
||||
<CardDescription>
|
||||
OpenClaw swarm members and ZeroClaw host runtimes from the tracked fleet model with heartbeat and dispatch overlays.
|
||||
OpenClaw swarm members, ZeroClaw runtimes, and direct host targets from the tracked fleet model with heartbeat and dispatch overlays.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-[1fr_220px]">
|
||||
@@ -58,6 +58,7 @@ export function AgentsClient({
|
||||
<option value="">All families</option>
|
||||
<option value="openclaw">OpenClaw</option>
|
||||
<option value="zeroclaw">ZeroClaw</option>
|
||||
<option value="direct">Direct</option>
|
||||
</Select>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -75,7 +76,9 @@ export function AgentsClient({
|
||||
<CardDescription>{agent.role}</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant={agent.family === "openclaw" ? "default" : "success"}>{agent.family}</Badge>
|
||||
<Badge variant={agent.family === "openclaw" ? "default" : agent.family === "zeroclaw" ? "success" : "warning"}>
|
||||
{agent.family}
|
||||
</Badge>
|
||||
<Badge variant="secondary">{agent.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function DispatchHistory({
|
||||
<p className="text-sm text-slate-400">Task #{event.task_id} • {event.assignee || "unassigned"} • {event.host || "n/a"}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant={event.family === "zeroclaw" ? "success" : "default"}>
|
||||
<Badge variant={event.family === "zeroclaw" ? "success" : event.family === "direct" ? "warning" : "default"}>
|
||||
{event.family || "manual"}
|
||||
</Badge>
|
||||
<Badge variant={event.event_type === "dispatch_failed" ? "warning" : "secondary"}>
|
||||
|
||||
@@ -21,7 +21,13 @@ const COLUMNS: TaskStatus[] = ["Backlog", "Todo", "In Progress", "Review", "Done
|
||||
const PRIORITIES: TaskPriority[] = ["Low", "Medium", "High", "Critical"];
|
||||
|
||||
function familyVariant(family: string | null) {
|
||||
return family === "zeroclaw" ? "success" : "default";
|
||||
if (family === "zeroclaw") {
|
||||
return "success";
|
||||
}
|
||||
if (family === "direct") {
|
||||
return "warning";
|
||||
}
|
||||
return "default";
|
||||
}
|
||||
|
||||
function dispatchVariant(state: TaskRecord["dispatch_state"]) {
|
||||
@@ -159,7 +165,7 @@ export function TasksClient({
|
||||
<CardHeader>
|
||||
<CardTitle>Unified Task Intake</CardTitle>
|
||||
<CardDescription>
|
||||
Create typed tasks, apply dispatch templates, and route work to OpenClaw or ZeroClaw.
|
||||
Create typed tasks, apply dispatch templates, and route work to OpenClaw, ZeroClaw, or direct host targets.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -286,7 +292,7 @@ export function TasksClient({
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Dispatch Activity</CardTitle>
|
||||
<CardDescription>
|
||||
Latest control-plane events across both families.
|
||||
Latest control-plane events across all configured task families.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 lg:grid-cols-3">
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
"overview": [
|
||||
"OpenClaw is the ubuntu-local orchestration layer and Telegram HQ entrypoint.",
|
||||
"ZeroClaw provides host-scoped remote administration on grizzley and ice.",
|
||||
"The taskboard is the shared planning, dispatch, and audit surface across both families."
|
||||
"Direct SSH targets extend the taskboard to hosts that do not run an active Claw runtime.",
|
||||
"The taskboard is the shared planning, dispatch, and audit surface across all host-operation families."
|
||||
],
|
||||
"topologyDiagram": " Telegram / Forum Topics\n |\n +----------------+----------------+\n | |\n v v\n OpenClaw gateway ZeroClaw control\n ubuntu :18789 ice zeroclaw-admin\n local swarm topic router / paired gateway\n | |\n +------------+--------------------+\n |\n v\n shared taskboard UI\n |\n +-----------+-----------+\n | |\n v v\n OpenClaw agents ZeroClaw runtimes\n ubuntu-local swarm grizzley / ice\n",
|
||||
"topologyDiagram": " Telegram / Forum Topics\n |\n +----------------+----------------+\n | |\n v v\n OpenClaw gateway ZeroClaw control\n ubuntu :18789 ice zeroclaw-admin\n local swarm topic router / paired gateway\n | |\n +------------+--------------------+\n |\n v\n shared taskboard UI\n |\n +-----------------+---------------------+\n | | |\n v v v\n OpenClaw agents ZeroClaw runtimes Direct SSH targets\n ubuntu-local grizzley / ice pve / truenas / panda\n",
|
||||
"sections": [
|
||||
{
|
||||
"id": "openclaw",
|
||||
@@ -62,6 +63,31 @@
|
||||
"Grizzley is host-scoped and should not proxy other hosts directly.",
|
||||
"Ice still uses host-local secret and encryption state under /home/bear/.zeroclaw-admin."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "direct",
|
||||
"title": "Direct Host Targets",
|
||||
"summary": "SSH-backed host operations for systems that do not run an active OpenClaw or ZeroClaw runtime. These flows execute safe, built-in host checks and complete through the taskboard callback pipeline.",
|
||||
"runtime": [
|
||||
{ "label": "Execution", "value": "taskboard container on ubuntu" },
|
||||
{ "label": "Transport", "value": "SSH with mounted host key material" },
|
||||
{ "label": "Key Path", "value": "/root/.ssh/id_ed25519 inside container" }
|
||||
],
|
||||
"channels": [
|
||||
{ "label": "PVE", "value": "root@192.168.50.11:22" },
|
||||
{ "label": "TrueNAS", "value": "christopher@192.168.50.12:22" },
|
||||
{ "label": "Panda", "value": "bear@192.168.50.196:22" }
|
||||
],
|
||||
"configuredAgents": [
|
||||
"pve-direct",
|
||||
"truenas-direct",
|
||||
"panda-direct"
|
||||
],
|
||||
"diagram": "taskboard direct SSH\n -> pve : built-in Proxmox overview\n -> truenas : built-in storage overview\n -> panda : built-in SSH add-on overview\n\nEach direct task\n -> ssh safe built-in command\n -> capture stdout/stderr\n -> task callback -> completed result\n",
|
||||
"notes": [
|
||||
"Direct targets are for safe built-in actions, not arbitrary remote shell execution from the UI.",
|
||||
"Completion state is written through the same callback pipeline used by remote agent runtimes."
|
||||
]
|
||||
}
|
||||
],
|
||||
"zeroclawAgents": [
|
||||
@@ -117,5 +143,132 @@
|
||||
"description": "Posts JSON webhook payloads to the ice ZeroClaw runtime."
|
||||
}
|
||||
}
|
||||
],
|
||||
"directAgents": [
|
||||
{
|
||||
"slug": "pve-direct",
|
||||
"assignmentKey": "pve-direct",
|
||||
"aliases": ["pve-direct", "PVE Direct", "pve"],
|
||||
"name": "PVE Direct",
|
||||
"host": "pve",
|
||||
"role": "Direct Proxmox host checks over SSH",
|
||||
"runtimePath": "ssh://root@192.168.50.11:22",
|
||||
"configPath": null,
|
||||
"emoji": "P",
|
||||
"channels": [
|
||||
{ "label": "SSH", "value": "root@192.168.50.11:22" },
|
||||
{ "label": "Actions", "value": "proxmox-overview" }
|
||||
],
|
||||
"tools": ["ssh", "systemctl", "pct", "qm"],
|
||||
"capabilities": [
|
||||
"Verify core Proxmox services",
|
||||
"Enumerate running LXC containers",
|
||||
"Enumerate VM state"
|
||||
],
|
||||
"files": [],
|
||||
"notes": [
|
||||
"Uses direct SSH from the taskboard container.",
|
||||
"Designed for safe built-in verification flows."
|
||||
],
|
||||
"dispatch": {
|
||||
"method": "direct-ssh",
|
||||
"hostname": "192.168.50.11",
|
||||
"user": "root",
|
||||
"port": 22,
|
||||
"defaultAction": "proxmox-overview",
|
||||
"actions": [
|
||||
{
|
||||
"key": "proxmox-overview",
|
||||
"title": "Proxmox overview",
|
||||
"description": "Verify core services and list active LXCs and VMs.",
|
||||
"command": "systemctl is-active pve-cluster pvedaemon pveproxy pvestatd ssh && printf '\\nCTs:\\n' && pct list && printf '\\nVMs:\\n' && qm list",
|
||||
"successSummary": "PVE services and guest inventory collected"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "truenas-direct",
|
||||
"assignmentKey": "truenas-direct",
|
||||
"aliases": ["truenas-direct", "TrueNAS Direct", "truenas"],
|
||||
"name": "TrueNAS Direct",
|
||||
"host": "truenas",
|
||||
"role": "Direct storage checks over SSH",
|
||||
"runtimePath": "ssh://christopher@192.168.50.12:22",
|
||||
"configPath": null,
|
||||
"emoji": "T",
|
||||
"channels": [
|
||||
{ "label": "SSH", "value": "christopher@192.168.50.12:22" },
|
||||
{ "label": "Actions", "value": "storage-overview" }
|
||||
],
|
||||
"tools": ["ssh", "zfs", "systemctl", "midclt"],
|
||||
"capabilities": [
|
||||
"Verify storage datasets",
|
||||
"Check docker service state",
|
||||
"Report host identity and storage status"
|
||||
],
|
||||
"files": [],
|
||||
"notes": [
|
||||
"Runs safe read-only storage checks.",
|
||||
"Does not modify datasets or apps."
|
||||
],
|
||||
"dispatch": {
|
||||
"method": "direct-ssh",
|
||||
"hostname": "192.168.50.12",
|
||||
"user": "christopher",
|
||||
"port": 22,
|
||||
"defaultAction": "storage-overview",
|
||||
"actions": [
|
||||
{
|
||||
"key": "storage-overview",
|
||||
"title": "Storage overview",
|
||||
"description": "Report host identity, docker-app service state, and top-level ZFS datasets.",
|
||||
"command": "printf 'Host: '; hostname && printf '\\nDocker apps service:\\n' && systemctl is-active truenas-docker-apps.service || true && printf '\\nDatasets:\\n' && zfs list -o name,used,avail | head -n 12",
|
||||
"successSummary": "TrueNAS storage overview collected"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "panda-direct",
|
||||
"assignmentKey": "panda-direct",
|
||||
"aliases": ["panda-direct", "Panda Direct", "panda"],
|
||||
"name": "Panda Direct",
|
||||
"host": "panda",
|
||||
"role": "Direct SSH add-on checks for the Home Assistant host",
|
||||
"runtimePath": "ssh://bear@192.168.50.196:22",
|
||||
"configPath": null,
|
||||
"emoji": "H",
|
||||
"channels": [
|
||||
{ "label": "SSH", "value": "bear@192.168.50.196:22" },
|
||||
{ "label": "Actions", "value": "ssh-addon-overview" }
|
||||
],
|
||||
"tools": ["ssh", "hostname", "cat", "ls"],
|
||||
"capabilities": [
|
||||
"Verify SSH add-on shell reachability",
|
||||
"Report add-on OS state and mounted data files"
|
||||
],
|
||||
"files": [],
|
||||
"notes": [
|
||||
"Targets the Home Assistant SSH add-on shell, not a full host shell.",
|
||||
"Uses shell-safe inspection commands that work without supervisor API auth."
|
||||
],
|
||||
"dispatch": {
|
||||
"method": "direct-ssh",
|
||||
"hostname": "192.168.50.196",
|
||||
"user": "bear",
|
||||
"port": 22,
|
||||
"defaultAction": "ssh-addon-overview",
|
||||
"actions": [
|
||||
{
|
||||
"key": "ssh-addon-overview",
|
||||
"title": "SSH add-on overview",
|
||||
"description": "Report add-on shell identity, OS information, and mounted /data files.",
|
||||
"command": "printf 'Host: '; hostname && printf '\\nOS:\\n' && cat /etc/os-release && printf '\\nData dir:\\n' && ls -1 /data 2>/dev/null | head -n 10",
|
||||
"successSummary": "Panda SSH add-on overview collected"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -55,5 +55,44 @@
|
||||
"dispatchMethod": "zeroclaw-webhook",
|
||||
"reasoningEffort": "medium"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "direct-pve-check",
|
||||
"title": "PVE direct verification",
|
||||
"summary": "Run the built-in Proxmox overview action through the direct SSH target.",
|
||||
"family": "direct",
|
||||
"tags": ["host-ops", "service-check", "action:proxmox-overview"],
|
||||
"defaults": {
|
||||
"priority": "High",
|
||||
"dispatchMethod": "direct-ssh",
|
||||
"targetHost": "pve",
|
||||
"targetChannel": "root@192.168.50.11:22"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "direct-truenas-check",
|
||||
"title": "TrueNAS direct verification",
|
||||
"summary": "Run the built-in storage overview action through the direct SSH target.",
|
||||
"family": "direct",
|
||||
"tags": ["host-ops", "storage-check", "action:storage-overview"],
|
||||
"defaults": {
|
||||
"priority": "High",
|
||||
"dispatchMethod": "direct-ssh",
|
||||
"targetHost": "truenas",
|
||||
"targetChannel": "christopher@192.168.50.12:22"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "direct-panda-check",
|
||||
"title": "Panda direct verification",
|
||||
"summary": "Run the built-in SSH add-on overview action through the direct target.",
|
||||
"family": "direct",
|
||||
"tags": ["host-ops", "home-assistant", "action:ssh-addon-overview"],
|
||||
"defaults": {
|
||||
"priority": "High",
|
||||
"dispatchMethod": "direct-ssh",
|
||||
"targetHost": "panda",
|
||||
"targetChannel": "bear@192.168.50.196:22"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- Next.js App Router migration with React 19, Tailwind CSS, and shadcn-style UI primitives
|
||||
- Typed fleet model loaded from `config/fleet.json`
|
||||
- Typed task templates loaded from `config/task-templates.json`
|
||||
- Unified task intake for OpenClaw and ZeroClaw
|
||||
- Unified task intake for OpenClaw, ZeroClaw, and direct SSH targets
|
||||
- Dispatch lifecycle states:
|
||||
- `planned`
|
||||
- `assigned`
|
||||
@@ -33,15 +33,21 @@
|
||||
- ZeroClaw webhook dispatch:
|
||||
- bearer-token support for paired gateways
|
||||
- direct gateway mode for testing
|
||||
- Direct SSH dispatch:
|
||||
- typed direct target definitions in `config/fleet.json`
|
||||
- safe built-in host actions for `pve`, `truenas`, and `panda`
|
||||
- completion written through the callback pipeline
|
||||
|
||||
## Verified Live
|
||||
|
||||
- `grizzley` ZeroClaw webhook dispatch from taskboard
|
||||
- `ice` ZeroClaw webhook dispatch from taskboard
|
||||
- OpenClaw swarm queue creation and host worktree creation on `ubuntu`
|
||||
- direct SSH host actions can now be dispatched for `pve`, `truenas`, and `panda`
|
||||
|
||||
## Current Limits
|
||||
|
||||
- Taskboard can dispatch OpenClaw swarm tasks, but it does not yet monitor tmux session progress automatically.
|
||||
- ZeroClaw acknowledgements and completions are still operator-driven; remote runtimes do not push completion state back yet.
|
||||
- The board records remote webhook responses, but not structured per-step execution output from the agents.
|
||||
- Direct targets are intentionally restricted to configured safe actions and do not expose arbitrary shell execution in the UI.
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
- Persist remote execution summaries
|
||||
- Auto-transition tasks to `acknowledged` or `completed`
|
||||
|
||||
3. Add per-host direct taskboard targets beyond the current fleet.
|
||||
- `pve`
|
||||
- `truenas`
|
||||
- `panda`
|
||||
3. Expand direct host operations beyond the first safe action set.
|
||||
- add more read-only Proxmox actions
|
||||
- add richer TrueNAS storage and service checks
|
||||
- add more Home Assistant supervisor and add-on checks
|
||||
|
||||
4. Add operator controls for swarm execution.
|
||||
- launch queued task
|
||||
@@ -43,3 +43,4 @@
|
||||
- Introduce a fleet capability registry so the taskboard can validate whether a task is legal for a given host before dispatch.
|
||||
- Add authentication and RBAC for multi-operator use.
|
||||
- Add generated runbooks and service maps directly from live host inventory.
|
||||
- Add a dedicated direct-host page or dashboard slice for SSH-backed targets.
|
||||
|
||||
@@ -289,13 +289,54 @@ async function buildZeroClawAgents() {
|
||||
);
|
||||
}
|
||||
|
||||
async function buildDirectAgents() {
|
||||
return Promise.all(
|
||||
FLEET_CONFIG.directAgents.map(async (configuredAgent) => {
|
||||
const taskBuckets = await fetchTaskBuckets(configuredAgent.aliases);
|
||||
const eventSummary = await fetchAgentEventSummary(configuredAgent.aliases);
|
||||
const heartbeatAt = eventSummary.lastEvent?.created_at || null;
|
||||
const heartbeatAgeMinutes = deriveHeartbeatAgeMinutes(heartbeatAt);
|
||||
|
||||
return {
|
||||
slug: configuredAgent.slug,
|
||||
assignmentKey: configuredAgent.assignmentKey,
|
||||
aliases: configuredAgent.aliases,
|
||||
family: "direct" as const,
|
||||
name: configuredAgent.name,
|
||||
host: configuredAgent.host,
|
||||
role: configuredAgent.role,
|
||||
runtimePath: configuredAgent.runtimePath,
|
||||
configPath: configuredAgent.configPath,
|
||||
defaultDispatchMethod: configuredAgent.dispatch.method,
|
||||
model: null,
|
||||
emoji: configuredAgent.emoji,
|
||||
channels: configuredAgent.channels,
|
||||
tools: configuredAgent.tools,
|
||||
capabilities: configuredAgent.capabilities,
|
||||
files: configuredAgent.files,
|
||||
status: deriveStatus(taskBuckets.activeTasks.length, heartbeatAt),
|
||||
workload: taskBuckets.activeTasks.length,
|
||||
activeTasks: taskBuckets.activeTasks,
|
||||
completedTasks: taskBuckets.completedTasks,
|
||||
currentTask: taskBuckets.activeTasks[0]?.title || null,
|
||||
heartbeatAt,
|
||||
heartbeatAgeMinutes,
|
||||
lastEvent: eventSummary.lastEvent,
|
||||
failureStreak: eventSummary.failureStreak,
|
||||
notes: configuredAgent.notes,
|
||||
} satisfies FleetAgent;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function listFleetAgents() {
|
||||
const [openclawAgents, zeroclawAgents] = await Promise.all([
|
||||
const [openclawAgents, zeroclawAgents, directAgents] = await Promise.all([
|
||||
buildOpenClawAgents(),
|
||||
buildZeroClawAgents(),
|
||||
buildDirectAgents(),
|
||||
]);
|
||||
|
||||
return [...openclawAgents, ...zeroclawAgents];
|
||||
return [...openclawAgents, ...zeroclawAgents, ...directAgents];
|
||||
}
|
||||
|
||||
export async function findAgentByAssignmentKey(assignmentKey: string) {
|
||||
|
||||
107
lib/dispatch.ts
107
lib/dispatch.ts
@@ -4,6 +4,9 @@ import { promisify } from "node:util";
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
import {
|
||||
DIRECT_SSH_KEY_PATH,
|
||||
DIRECT_SSH_TIMEOUT_MS,
|
||||
FLEET_CONFIG,
|
||||
REPO_ACCESS_ROOTS,
|
||||
SWARM_HOST_WORKTREES_DIR,
|
||||
SWARM_REPO_MAP_FILE,
|
||||
@@ -12,8 +15,8 @@ import {
|
||||
ZEROCLAW_WEBHOOK_TIMEOUT_MS,
|
||||
} from "@/lib/fleet-config";
|
||||
import { findAgentByAssignmentKey } from "@/lib/agents";
|
||||
import { appendTaskEvent, findTask, updateTask } from "@/lib/tasks";
|
||||
import type { DispatchState } from "@/lib/types";
|
||||
import { appendTaskEvent, applyTaskCallback, findTask, updateTask } from "@/lib/tasks";
|
||||
import type { DispatchState, TaskCallbackPayload } from "@/lib/types";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -21,6 +24,7 @@ type DispatchResult = {
|
||||
state: DispatchState;
|
||||
summary: string;
|
||||
detail: string;
|
||||
callback?: TaskCallbackPayload;
|
||||
};
|
||||
|
||||
function defaultModelForAgent(agent: string) {
|
||||
@@ -58,7 +62,20 @@ function ensureSwarmRegistry() {
|
||||
}
|
||||
}
|
||||
|
||||
async function dispatchOpenClawTask(taskId: number) {
|
||||
function extractTagValue(tags: string[], prefix: string) {
|
||||
const match = tags.find((tag) => tag.startsWith(prefix));
|
||||
return match ? match.slice(prefix.length) : null;
|
||||
}
|
||||
|
||||
function truncateOutput(output: string, maxLength = 4000) {
|
||||
const trimmed = output.trim();
|
||||
if (trimmed.length <= maxLength) {
|
||||
return trimmed;
|
||||
}
|
||||
return `${trimmed.slice(0, maxLength - 15)}\n...[truncated]`;
|
||||
}
|
||||
|
||||
async function dispatchOpenClawTask(taskId: number): Promise<DispatchResult> {
|
||||
const task = await findTask(taskId);
|
||||
if (!task) {
|
||||
throw new Error("task_not_found");
|
||||
@@ -145,7 +162,7 @@ async function dispatchOpenClawTask(taskId: number) {
|
||||
};
|
||||
}
|
||||
|
||||
async function dispatchZeroClawTask(taskId: number) {
|
||||
async function dispatchZeroClawTask(taskId: number): Promise<DispatchResult> {
|
||||
const task = await findTask(taskId);
|
||||
if (!task) {
|
||||
throw new Error("task_not_found");
|
||||
@@ -190,6 +207,73 @@ async function dispatchZeroClawTask(taskId: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function findDirectAgentDefinition(assignmentKey: string) {
|
||||
return (
|
||||
FLEET_CONFIG.directAgents.find(
|
||||
(agent) => agent.assignmentKey === assignmentKey || agent.aliases.includes(assignmentKey),
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
async function dispatchDirectTask(taskId: number): Promise<DispatchResult> {
|
||||
const task = await findTask(taskId);
|
||||
if (!task) {
|
||||
throw new Error("task_not_found");
|
||||
}
|
||||
|
||||
const directAgent = findDirectAgentDefinition(task.assignee);
|
||||
if (!directAgent) {
|
||||
throw new Error("direct_target_not_found");
|
||||
}
|
||||
|
||||
const actionKey = extractTagValue(task.tags, "action:") || directAgent.dispatch.defaultAction;
|
||||
const action = directAgent.dispatch.actions.find((entry) => entry.key === actionKey);
|
||||
if (!action) {
|
||||
throw new Error(`unsupported_direct_action:${actionKey}`);
|
||||
}
|
||||
|
||||
const sshArgs = [
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
"-o",
|
||||
"ConnectTimeout=15",
|
||||
"-i",
|
||||
DIRECT_SSH_KEY_PATH,
|
||||
"-p",
|
||||
String(directAgent.dispatch.port),
|
||||
`${directAgent.dispatch.user}@${directAgent.dispatch.hostname}`,
|
||||
action.command,
|
||||
];
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync("ssh", sshArgs, {
|
||||
timeout: DIRECT_SSH_TIMEOUT_MS,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
const detail = truncateOutput([stdout, stderr].filter(Boolean).join("\n"));
|
||||
return {
|
||||
state: "completed" as const,
|
||||
summary: `${action.successSummary}`,
|
||||
detail,
|
||||
callback: {
|
||||
status: "Done",
|
||||
dispatch_state: "completed",
|
||||
summary: action.successSummary,
|
||||
detail,
|
||||
completed_by: `direct-ssh:${directAgent.host}`,
|
||||
last_error: null,
|
||||
last_dispatch_at: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const execError = error as Error & { stdout?: string; stderr?: string };
|
||||
const detail = truncateOutput([execError.stdout, execError.stderr, execError.message].filter(Boolean).join("\n"));
|
||||
throw new Error(`direct_ssh_failed:${directAgent.host}:${action.key}:${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function dispatchTask(taskId: number) {
|
||||
const task = await findTask(taskId);
|
||||
if (!task) {
|
||||
@@ -217,7 +301,20 @@ export async function dispatchTask(taskId: number) {
|
||||
|
||||
try {
|
||||
const result =
|
||||
agent.family === "openclaw" ? await dispatchOpenClawTask(taskId) : await dispatchZeroClawTask(taskId);
|
||||
agent.family === "openclaw"
|
||||
? await dispatchOpenClawTask(taskId)
|
||||
: agent.family === "zeroclaw"
|
||||
? await dispatchZeroClawTask(taskId)
|
||||
: await dispatchDirectTask(taskId);
|
||||
|
||||
if (result.callback) {
|
||||
const updated = await applyTaskCallback(taskId, result.callback);
|
||||
if (!updated) {
|
||||
throw new Error("task_not_found_after_callback");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
const updated = await updateTask(taskId, {
|
||||
status: task.status === "Backlog" ? "Todo" : task.status,
|
||||
|
||||
@@ -18,6 +18,8 @@ export const REPO_ACCESS_ROOTS = (process.env.REPO_ACCESS_ROOTS || "/srv/apps,/h
|
||||
export const ZEROCLAW_PRIMARY_DIR = process.env.ZEROCLAW_PRIMARY_DIR || "/app/zeroclaw/grizzley";
|
||||
export const ZEROCLAW_CONTROL_DIR = process.env.ZEROCLAW_CONTROL_DIR || "/app/zeroclaw/ice";
|
||||
export const ZEROCLAW_WEBHOOK_TIMEOUT_MS = Number(process.env.ZEROCLAW_WEBHOOK_TIMEOUT_MS || "15000");
|
||||
export const DIRECT_SSH_TIMEOUT_MS = Number(process.env.DIRECT_SSH_TIMEOUT_MS || "30000");
|
||||
export const DIRECT_SSH_KEY_PATH = process.env.DIRECT_SSH_KEY_PATH || "/root/.ssh/id_ed25519";
|
||||
|
||||
const CONFIG_DIR = path.join(process.cwd(), "config");
|
||||
const FLEET_CONFIG_PATH = path.join(CONFIG_DIR, "fleet.json");
|
||||
@@ -37,6 +39,7 @@ export const FLEET_CONFIG = readJsonFile<FleetConfig>(FLEET_CONFIG_PATH, {
|
||||
topologyDiagram: "",
|
||||
sections: [],
|
||||
zeroclawAgents: [],
|
||||
directAgents: [],
|
||||
});
|
||||
|
||||
export const TASK_TEMPLATES = readJsonFile<TaskTemplate[]>(TASK_TEMPLATE_PATH, []);
|
||||
|
||||
@@ -17,8 +17,8 @@ import type {
|
||||
|
||||
const VALID_STATUSES: TaskStatus[] = ["Backlog", "Todo", "In Progress", "Review", "Done"];
|
||||
const VALID_PRIORITIES: TaskPriority[] = ["Low", "Medium", "High", "Critical"];
|
||||
const VALID_FAMILIES: AgentFamily[] = ["openclaw", "zeroclaw"];
|
||||
const VALID_DISPATCH_METHODS: DispatchMethod[] = ["manual", "openclaw-swarm", "zeroclaw-webhook"];
|
||||
const VALID_FAMILIES: AgentFamily[] = ["openclaw", "zeroclaw", "direct"];
|
||||
const VALID_DISPATCH_METHODS: DispatchMethod[] = ["manual", "openclaw-swarm", "zeroclaw-webhook", "direct-ssh"];
|
||||
const VALID_DISPATCH_STATES: DispatchState[] = [
|
||||
"planned",
|
||||
"assigned",
|
||||
@@ -392,6 +392,7 @@ export async function applyTaskCallback(id: number, payload: {
|
||||
detail?: string | null;
|
||||
completed_by?: string | null;
|
||||
last_error?: string | null;
|
||||
last_dispatch_at?: string | null;
|
||||
}) {
|
||||
const nextStatus = payload.status ?? (payload.dispatch_state === "completed" ? "Done" : undefined);
|
||||
const updated = await updateTask(id, {
|
||||
@@ -401,6 +402,7 @@ export async function applyTaskCallback(id: number, payload: {
|
||||
result_detail: payload.detail ?? undefined,
|
||||
completed_by: payload.completed_by ?? undefined,
|
||||
last_error: payload.last_error ?? undefined,
|
||||
last_dispatch_at: payload.last_dispatch_at ?? undefined,
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
|
||||
39
lib/types.ts
39
lib/types.ts
@@ -1,8 +1,8 @@
|
||||
export type TaskStatus = "Backlog" | "Todo" | "In Progress" | "Review" | "Done";
|
||||
export type TaskPriority = "Low" | "Medium" | "High" | "Critical";
|
||||
export type AgentFamily = "openclaw" | "zeroclaw";
|
||||
export type AgentFamily = "openclaw" | "zeroclaw" | "direct";
|
||||
export type AgentStatus = "active" | "busy" | "idle";
|
||||
export type DispatchMethod = "openclaw-swarm" | "zeroclaw-webhook" | "manual";
|
||||
export type DispatchMethod = "openclaw-swarm" | "zeroclaw-webhook" | "direct-ssh" | "manual";
|
||||
export type DispatchState =
|
||||
| "planned"
|
||||
| "assigned"
|
||||
@@ -88,6 +88,7 @@ export type TaskCallbackPayload = {
|
||||
detail?: string | null;
|
||||
completed_by?: string | null;
|
||||
last_error?: string | null;
|
||||
last_dispatch_at?: string | null;
|
||||
};
|
||||
|
||||
export type WikiPageSummary = {
|
||||
@@ -184,10 +185,44 @@ export type ZeroClawAgentDefinition = {
|
||||
};
|
||||
};
|
||||
|
||||
export type DirectAgentActionDefinition = {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
command: string;
|
||||
successSummary: string;
|
||||
};
|
||||
|
||||
export type DirectAgentDefinition = {
|
||||
slug: string;
|
||||
assignmentKey: string;
|
||||
aliases: string[];
|
||||
name: string;
|
||||
host: string;
|
||||
role: string;
|
||||
runtimePath: string;
|
||||
configPath: string | null;
|
||||
emoji: string;
|
||||
channels: AgentRouteSummary[];
|
||||
tools: string[];
|
||||
capabilities: string[];
|
||||
files: string[];
|
||||
notes: string[];
|
||||
dispatch: {
|
||||
method: "direct-ssh";
|
||||
hostname: string;
|
||||
user: string;
|
||||
port: number;
|
||||
defaultAction: string;
|
||||
actions: DirectAgentActionDefinition[];
|
||||
};
|
||||
};
|
||||
|
||||
export type FleetConfig = {
|
||||
title: string;
|
||||
overview: string[];
|
||||
topologyDiagram: string;
|
||||
sections: FleetSection[];
|
||||
zeroclawAgents: ZeroClawAgentDefinition[];
|
||||
directAgents: DirectAgentDefinition[];
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "openclaw-taskboard",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "Next.js fleet dashboard for OpenClaw and ZeroClaw runtimes",
|
||||
"description": "Next.js fleet dashboard for OpenClaw, ZeroClaw, and direct host operations",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
Reference in New Issue
Block a user