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 { 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'); }