[taskboard] add direct host dispatch targets

This commit is contained in:
2026-03-07 13:09:22 -08:00
parent 73da5ae6d2
commit 85c5ab10b0
17 changed files with 441 additions and 32 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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({

View File

@@ -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>

View File

@@ -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"}>

View File

@@ -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">

View File

@@ -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"
}
]
}
}
]
}

View File

@@ -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"
}
}
]

View File

@@ -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.

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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, []);

View File

@@ -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) {

View File

@@ -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[];
};

View File

@@ -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",