Phase 5 of v2.0. External agent dispatch via SSH to host. ACP dispatch (acp-dispatch.ts): spawns agent via SSH with JSON-RPC stdio pipe. Wraps opencode/goose in ACP mode. Captures structured events (file operations, tool calls) mapped to parts taxonomy. Falls back to PTY if ACP handshake fails. PTY dispatch (pty-dispatch.ts): raw SSH spawn for agents without ACP support (claude, pi). Captures stdout/stderr as plain text. Simpler but less structured than ACP. SSH helper (ssh.ts): shared spawn wrapper for SSH commands to samkintop@100.114.205.53 (Tailscale IP, same as booterm). Uses openssh-client installed in the runtime Dockerfile stage. Worktree management (worktrees.ts): createWorktree (git worktree add via SSH), diffWorktree (git diff HEAD...task-branch), cleanupWorktree (git worktree remove --force). One worktree per task at /tmp/booworktrees/<taskId>. Dispatcher updated: checks available_agents.supports_acp to pick transport. Path B flow: create worktree → dispatch agent → diff worktree → queue diff into pending_changes → cleanup worktree → mark task complete. Agent probe updated: probes via SSH to find host-installed agents (which opencode && opencode --version over SSH). Dockerfile: openssh-client added to runtime stage. Config: SSH_HOST env var (default 100.114.205.53).
71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
import type { Sql } from '../db.js';
|
|
import type { FastifyBaseLogger } from 'fastify';
|
|
import { sshExec } from './ssh.js';
|
|
|
|
const KNOWN_AGENTS: Array<{ name: string; supportsAcp: boolean }> = [
|
|
{ name: 'opencode', supportsAcp: true },
|
|
{ name: 'goose', supportsAcp: true },
|
|
{ name: 'claude', supportsAcp: false },
|
|
{ name: 'pi', supportsAcp: false },
|
|
];
|
|
|
|
/**
|
|
* Probe for available agents on the HOST via SSH.
|
|
*
|
|
* The boocoder container can't run agents locally — they live on the host.
|
|
* We SSH to the host (same mechanism BooTerm uses) and check which agent
|
|
* binaries are on PATH.
|
|
*/
|
|
export async function probeAgents(sql: Sql, log: FastifyBaseLogger): Promise<void> {
|
|
log.info('agent-probe: scanning HOST for known agents via SSH');
|
|
|
|
for (const agent of KNOWN_AGENTS) {
|
|
try {
|
|
// Check if the agent binary is on the host's PATH
|
|
const whichResult = await sshExec(`which ${agent.name}`, { timeoutMs: 10_000 });
|
|
const installPath = whichResult.stdout.trim();
|
|
if (whichResult.exitCode !== 0 || !installPath) continue;
|
|
|
|
// Get version
|
|
let version: string | null = null;
|
|
try {
|
|
const verResult = await sshExec(`${agent.name} --version`, { timeoutMs: 15_000 });
|
|
if (verResult.exitCode === 0) {
|
|
version = verResult.stdout.trim().slice(0, 100);
|
|
}
|
|
} catch {
|
|
// Some agents may not support --version — that's fine
|
|
}
|
|
|
|
// For ACP-capable agents, verify ACP mode actually works
|
|
let supportsAcp = agent.supportsAcp;
|
|
if (supportsAcp) {
|
|
try {
|
|
const acpCheck = await sshExec(`${agent.name} acp --help`, { timeoutMs: 10_000 });
|
|
supportsAcp = acpCheck.exitCode === 0;
|
|
} catch {
|
|
supportsAcp = false;
|
|
}
|
|
}
|
|
|
|
// UPSERT into available_agents
|
|
await sql`
|
|
INSERT INTO available_agents (name, install_path, version, supports_acp, last_probed_at)
|
|
VALUES (${agent.name}, ${installPath}, ${version}, ${supportsAcp}, clock_timestamp())
|
|
ON CONFLICT (name) DO UPDATE SET
|
|
install_path = EXCLUDED.install_path,
|
|
version = EXCLUDED.version,
|
|
supports_acp = EXCLUDED.supports_acp,
|
|
last_probed_at = EXCLUDED.last_probed_at
|
|
`;
|
|
log.info({ agent: agent.name, version, installPath, supportsAcp }, 'agent-probe: found on host');
|
|
} catch (err) {
|
|
// SSH failed or agent not found — skip silently
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
log.debug({ agent: agent.name, err: msg }, 'agent-probe: not found or SSH failed');
|
|
}
|
|
}
|
|
|
|
log.info('agent-probe: scan complete');
|
|
}
|