/** * Gateway → assistant reverse IPC client. * * Connects to the assistant's Unix domain socket (assistant.sock) to make * one-shot JSON-RPC calls from the gateway to the assistant daemon. * * Protocol: newline-delimited JSON over the Unix domain socket: * - Request: `{ "id": string, "method": string, "params"?: object }` * - Response: `{ "id": string, "result"?: unknown, "error"?: string }` * * This reverse client stays inline because @vellumai/gateway-client models * assistant-to-gateway calls; this path calls the assistant from the gateway. */ import { connect, type Socket } from "node:net"; import type { ScopeOption, DirectoryScopeOption } from "../risk/risk-types.js"; import { resolveIpcSocketPath } from "./socket-path.js"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const CALL_TIMEOUT_MS = 30_000; // 30s to accommodate LLM latency const CONNECT_TIMEOUT_MS = 3_000; // --------------------------------------------------------------------------- // Internal types // --------------------------------------------------------------------------- interface IpcRequest { id: string; method: string; params?: Record; } interface IpcResponse { id: string; result?: unknown; error?: string; statusCode?: number; errorCode?: string; } // --------------------------------------------------------------------------- // Structured IPC errors (used by the gateway IPC proxy) // --------------------------------------------------------------------------- /** * Error thrown by {@link ipcCallAssistant} when the daemon returns a * handler-level error (e.g. a RouteError with statusCode). */ export class IpcHandlerError extends Error { readonly statusCode: number; readonly code: string; constructor(message: string, statusCode: number, code: string) { super(message); this.name = "IpcHandlerError"; this.statusCode = statusCode; this.code = code; } } /** * Error thrown by {@link ipcCallAssistant} when the daemon is unreachable * (socket error, timeout, closed before response). */ export class IpcTransportError extends Error { constructor(message: string) { super(message); this.name = "IpcTransportError"; } } // --------------------------------------------------------------------------- // Socket path // --------------------------------------------------------------------------- function getAssistantSocketPath(): string { return resolveIpcSocketPath("assistant").path; } // --------------------------------------------------------------------------- // One-shot IPC call to the assistant // --------------------------------------------------------------------------- /** * One-shot IPC helper: connect to assistant.sock, call a method, disconnect. * * - On success: resolves with the result value. * - On handler error (assistant RouteError): throws {@link IpcHandlerError} * with statusCode and code. * - On transport failure (socket not found, timeout, parse error, closed * before response): throws {@link IpcTransportError}. * * `opts.timeoutMs` bounds the whole call (connect + response) and tears the * socket down on expiry; defaults to 30 seconds to accommodate LLM latency * on the assistant side. */ export async function ipcCallAssistant( method: string, params?: Record, opts?: { timeoutMs?: number }, ): Promise { const socketPath = getAssistantSocketPath(); const callTimeoutMs = opts?.timeoutMs ?? CALL_TIMEOUT_MS; return new Promise((resolve, reject) => { let settled = false; const finish = (value: unknown, error?: Error) => { if (settled) return; settled = true; clearTimeout(connectTimer); clearTimeout(callTimer); socket.destroy(); if (error) { reject(error); } else { resolve(value); } }; const connectTimer = setTimeout(() => { finish( undefined, new IpcTransportError( `Connect timed out after ${CONNECT_TIMEOUT_MS}ms`, ), ); }, CONNECT_TIMEOUT_MS); const callTimer = setTimeout(() => { finish( undefined, new IpcTransportError(`Call timed out after ${callTimeoutMs}ms`), ); }, callTimeoutMs); const socket: Socket = connect(socketPath); socket.unref(); let buffer = ""; const reqId = crypto.randomUUID(); socket.on("connect", () => { clearTimeout(connectTimer); const req: IpcRequest = { id: reqId, method, params }; socket.write(JSON.stringify(req) + "\n"); socket.on("data", (chunk) => { buffer += chunk.toString(); let newlineIdx: number; while ((newlineIdx = buffer.indexOf("\n")) !== -1) { const line = buffer.slice(0, newlineIdx).trim(); buffer = buffer.slice(newlineIdx + 1); if (!line) continue; try { const msg = JSON.parse(line) as IpcResponse; if (msg.id === reqId) { if (msg.error) { if (msg.statusCode) { finish( undefined, new IpcHandlerError( msg.error, msg.statusCode, msg.errorCode ?? "UNKNOWN", ), ); } else { finish(undefined, new IpcTransportError(msg.error)); } } else { finish(msg.result); } return; } } catch { // Ignore malformed lines } } }); }); socket.on("error", (err) => { finish( undefined, new IpcTransportError(err instanceof Error ? err.message : String(err)), ); }); socket.on("close", () => { if (!settled) { finish( undefined, new IpcTransportError("Socket closed before response"), ); } }); }); } // --------------------------------------------------------------------------- // Typed helpers // --------------------------------------------------------------------------- export interface SuggestTrustRuleRequest { tool: string; command: string; riskAssessment: { risk: string; reasoning: string; reasonDescription: string; }; scopeOptions: ScopeOption[]; directoryScopeOptions?: DirectoryScopeOption[]; currentThreshold: string; // "low" | "medium" | "high" intent: "auto_approve" | "escalate"; existingRule?: { id: string; pattern: string; risk: string; }; } export interface SuggestTrustRuleResponse { pattern: string; risk: string; // "low" | "medium" | "high" scope?: string; description: string; scopeOptions: ScopeOption[]; directoryScopeOptions?: DirectoryScopeOption[]; } /** * Ask the assistant daemon to suggest a trust rule for a command invocation. * * Throws if the assistant returns an error or an unexpected response shape. */ export async function ipcSuggestTrustRule( params: SuggestTrustRuleRequest, ): Promise { const result = await ipcCallAssistant("suggest_trust_rule", { body: params, } as unknown as Record); if (!result || typeof result !== "object" || Array.isArray(result)) { throw new Error("ipcSuggestTrustRule: unexpected response shape"); } return result as SuggestTrustRuleResponse; }