import { createServer } from 'node:http'; import type { IncomingMessage, ServerResponse } from 'node:http'; import type { Id } from '@spoon/backend/convex/_generated/dataModel.js'; import { env } from './env'; import { abortWorkspaceAgent, cleanupOrphanedWorkspaces, getWorkerHealth, getWorkspaceAgentStatus, getWorkspaceDiff, listWorkspaceTree, openWorkspacePullRequest, readWorkspaceFile, replyToInteraction, runWorkspaceCommand, sendWorkspaceMessage, stopWorkspace, writeWorkspaceFile, } from './worker'; const sendJson = (response: ServerResponse, status: number, body: unknown) => { response.writeHead(status, { 'content-type': 'application/json' }); response.end(JSON.stringify(body)); }; const readBody = async (request: IncomingMessage) => await new Promise((resolve, reject) => { let body = ''; request.on('data', (chunk: Buffer) => { body += chunk.toString('utf8'); }); request.on('end', () => resolve(body)); request.on('error', reject); }); const parseJson = async (request: IncomingMessage) => { const body = await readBody(request); if (!body.trim()) return {} as T; return JSON.parse(body) as T; }; const requireAuth = (request: IncomingMessage) => { const header = request.headers.authorization; const token = header?.startsWith('Bearer ') ? header.slice(7) : ''; if (!env.internalToken || token !== env.internalToken) { throw new Error('Unauthorized'); } }; const jobRoute = (pathname: string) => { const match = /^\/jobs\/([^/]+)\/(.+)$/.exec(pathname); if (!match?.[1] || !match[2]) return null; return { jobId: decodeURIComponent(match[1]), action: match[2] }; }; export const startWorkerServer = () => { const server = createServer((request, response) => { void (async () => { try { requireAuth(request); const url = new URL( request.url ?? '/', `http://localhost:${env.httpPort}`, ); if (url.pathname === '/health' && request.method === 'GET') { sendJson(response, 200, await getWorkerHealth()); return; } if (url.pathname === '/cleanup' && request.method === 'POST') { sendJson(response, 200, await cleanupOrphanedWorkspaces()); return; } const route = jobRoute(url.pathname); if (!route) { sendJson(response, 404, { error: 'Not found' }); return; } if (request.method === 'GET' && route.action === 'tree') { sendJson(response, 200, { tree: await listWorkspaceTree(route.jobId), }); return; } if (request.method === 'GET' && route.action === 'file') { const filePath = url.searchParams.get('path') ?? ''; sendJson(response, 200, { path: filePath, content: await readWorkspaceFile(route.jobId, filePath), }); return; } if (request.method === 'PUT' && route.action === 'file') { const body = await parseJson<{ path?: string; content?: string }>( request, ); sendJson( response, 200, await writeWorkspaceFile( route.jobId, body.path ?? '', body.content ?? '', ), ); return; } if (request.method === 'GET' && route.action === 'diff') { sendJson(response, 200, { diff: await getWorkspaceDiff(route.jobId), }); return; } if (request.method === 'POST' && route.action === 'message') { const body = await parseJson<{ content?: string }>(request); await sendWorkspaceMessage(route.jobId, body.content ?? ''); sendJson(response, 200, { success: true }); return; } if (request.method === 'GET' && route.action === 'agent/status') { sendJson(response, 200, getWorkspaceAgentStatus(route.jobId)); return; } if (request.method === 'POST' && route.action === 'agent/abort') { sendJson(response, 200, await abortWorkspaceAgent(route.jobId)); return; } const interactionMatch = /^interactions\/([^/]+)\/reply$/.exec(route.action); if (request.method === 'POST' && interactionMatch?.[1]) { const body = await parseJson<{ externalRequestId?: string; response?: string; }>(request); sendJson( response, 200, await replyToInteraction(route.jobId, { interactionId: decodeURIComponent( interactionMatch[1], ) as Id<'agentInteractionRequests'>, externalRequestId: body.externalRequestId ?? '', response: body.response ?? 'once', }), ); return; } if (request.method === 'POST' && route.action === 'run-command') { const body = await parseJson<{ command?: string }>(request); sendJson( response, 200, await runWorkspaceCommand(route.jobId, body.command ?? ''), ); return; } if (request.method === 'POST' && route.action === 'open-pr') { sendJson(response, 200, await openWorkspacePullRequest(route.jobId)); return; } if (request.method === 'POST' && route.action === 'stop') { sendJson(response, 200, await stopWorkspace(route.jobId)); return; } sendJson(response, 404, { error: 'Not found' }); } catch (error) { const message = error instanceof Error ? error.message : String(error); const status = message === 'Unauthorized' ? 401 : message.includes('not supported') ? 409 : 500; sendJson(response, status, { error: message, }); } })(); }); server.listen(env.httpPort, () => { console.log( `Spoon agent worker HTTP server listening on port ${env.httpPort}`, ); }); };