[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

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