import { randomUUID } from 'node:crypto'; const TIMEOUT_MS = 10000; const MAX_REQUEST_SIZE = 16384; const MAX_RESPONSE_SIZE = 1024 * 1024; const GENERIC_ERROR_MESSAGE = 'Agent request failed.'; const READ_ONLY_TOOLS = new Set([ 'get_health', 'get_build_status', 'get_state', 'get_telemetry', ]); export class AgentNotConfiguredError extends Error { constructor() { super('Agent integration is not configured.'); this.name = 'AgentNotConfiguredError'; } } function validateMcpUrl(urlString, allowedOrigin) { const url = new URL(urlString); if (url.username || url.password || url.hash) { throw new Error(GENERIC_ERROR_MESSAGE); } if (url.protocol === 'http:') { const isLoopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'; if (!isLoopback) { throw new Error(GENERIC_ERROR_MESSAGE); } return url; } if (url.protocol !== 'https:' || !allowedOrigin) { throw new Error(GENERIC_ERROR_MESSAGE); } const configuredOrigin = new URL(allowedOrigin); if (url.origin !== configuredOrigin.origin) { throw new Error(GENERIC_ERROR_MESSAGE); } return url; } export async function callAgent(tool, args) { const mcpUrl = process.env.OPAX_MCP_URL?.trim(); const mcpSecret = process.env.MCP_SECRET?.trim(); if (!mcpUrl || !mcpSecret) { throw new AgentNotConfiguredError(); } try { const allowedOrigin = process.env.OPAX_MCP_ALLOWED_ORIGIN?.trim(); if (!READ_ONLY_TOOLS.has(tool)) { throw new Error(GENERIC_ERROR_MESSAGE); } const validatedUrl = validateMcpUrl(mcpUrl, allowedOrigin); const requestBody = JSON.stringify({ jsonrpc: '2.0', id: randomUUID(), method: 'tools/call', params: { name: tool, arguments: args, }, }); if (requestBody.length > MAX_REQUEST_SIZE) { throw new Error(GENERIC_ERROR_MESSAGE); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); try { const response = await fetch(validatedUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${mcpSecret}`, }, body: requestBody, signal: controller.signal, }); if (!response.ok) { throw new Error(GENERIC_ERROR_MESSAGE); } const responseText = await response.text(); if (responseText.length > MAX_RESPONSE_SIZE) { throw new Error(GENERIC_ERROR_MESSAGE); } const rpcResponse = JSON.parse(responseText); if (rpcResponse.error) { throw new Error(GENERIC_ERROR_MESSAGE); } const content = rpcResponse.result?.content; if (!Array.isArray(content) || content.length === 0) { throw new Error(GENERIC_ERROR_MESSAGE); } const firstContent = content[0]; if ( firstContent?.type !== 'text' || typeof firstContent.text !== 'string' || firstContent.text.length > MAX_RESPONSE_SIZE ) { throw new Error(GENERIC_ERROR_MESSAGE); } return JSON.parse(firstContent.text); } finally { clearTimeout(timeoutId); } } catch { throw new Error(GENERIC_ERROR_MESSAGE); } }