UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

148 lines 6.81 kB
/** * The device-side of the remote-agent relay (#1067): the two endpoints a daemon exposes so another * daemon (holding this device's token) can run a session here and watch it. They live under * `/_relay`, behind the shared-token guard (#1051) in {@link startDashboard}. The guard admits a matching * `fw_daemon` cookie without the browser-only `?token=` 302, so a daemon-to-daemon call passes with * a cookie and a token-less caller is already 401'd before it reaches here. * * - `POST /_relay/start` starts an ordinary local agent and returns its {@link StartAgentResult}. It * runs in this device's own home checkout (slice 1); which project it targets is a later slice. * - `GET /_relay/events?run=<id>` streams that agent's events as newline-delimited JSON until it * ends or the caller disconnects. * - `GET /_relay/ping` (#1072) a cookie-guarded reachability probe: 200 and an empty body, starts * nothing. The online/offline status the dashboard shows is the local daemon calling this on each * saved device with its token; a token-less caller is already 401'd by the shared-token guard (#1051) above. * - `POST /_relay/rpc` (#1067 slice 2) runs one whitelisted run-scoped RPC (a read/diff/steer/handoff/ * push/PR) against this device's own checkout for the daemon relaying an agent here, answering {result}. */ export const RELAY_PREFIX = '/_relay'; const MAX_START_BODY = 256 * 1024; /** Route a `/_relay/*` request. A host that wired no relay handlers 404s every relay route. */ export async function handleRelayRequest(req, res, pathname, handlers) { // Ping is a pure reachability + auth probe (#1072): it needs no wired handlers and starts nothing, // so it answers even on a host that enabled no relay. Reaching here means the cookie already passed. if (pathname === `${RELAY_PREFIX}/ping`) return handlePing(req, res); if (!handlers) return end(res, 404, 'relay not enabled'); if (pathname === `${RELAY_PREFIX}/start`) return handleStart(req, res, handlers); if (pathname === `${RELAY_PREFIX}/events`) return handleEvents(req, res, handlers); if (pathname === `${RELAY_PREFIX}/rpc`) return handleRpc(req, res, handlers); end(res, 404, 'not found'); } /** `GET /_relay/ping` (#1072): answer 200 with an empty body. Starts nothing; only proves this * daemon is reachable and the caller's cookie is valid (the shared-token guard (#1051) already enforced that). */ function handlePing(req, res) { if (req.method !== 'GET') return end(res, 405, 'method not allowed', { allow: 'GET' }); res.writeHead(200, { 'content-type': 'text/plain' }); res.end(); } /** `POST /_relay/start`: read the agent request, start it locally, and answer with the result JSON. */ async function handleStart(req, res, handlers) { if (req.method !== 'POST') return end(res, 405, 'method not allowed', { allow: 'POST' }); let body; try { body = (await readJsonBody(req, MAX_START_BODY)); } catch { return end(res, 400, 'invalid request body'); } const prompt = typeof body.prompt === 'string' ? body.prompt : ''; const kind = body.kind === 'research' || body.kind === 'prompt' ? body.kind : 'build'; const options = (body.options && typeof body.options === 'object' ? body.options : {}); // Never relay onward from a relayed agent: strip any nested target before starting it here. const { remote: _drop, ...local } = options; let result; try { result = await handlers.start(prompt, kind, local, undefined); } catch (err) { result = { ok: false, error: err instanceof Error ? err.message : String(err) }; } res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(result)); } /** `GET /_relay/events?run=<id>`: stream the agent's events as newline-delimited JSON. */ function handleEvents(req, res, handlers) { if (req.method !== 'GET') return end(res, 405, 'method not allowed', { allow: 'GET' }); const agentId = new URL(req.url ?? '/', 'http://localhost').searchParams.get('run'); if (!agentId) return end(res, 400, 'missing run id'); res.writeHead(200, { 'content-type': 'application/x-ndjson', 'cache-control': 'no-cache' }); const stop = handlers.tailEvents(agentId, event => { // A dropped write (the caller went away mid-line) must not throw out of the tail callback. try { res.write(`${JSON.stringify(event)}\n`); } catch { // the socket is gone; the close handler below tears the tail down } }); const close = () => stop(); res.on('close', close); req.on('close', close); } const MAX_RPC_BODY = 256 * 1024; /** POST /_relay/rpc: run one whitelisted RPC on this device and answer {result}. */ async function handleRpc(req, res, handlers) { if (req.method !== 'POST') return end(res, 405, 'method not allowed', { allow: 'POST' }); if (!handlers.rpc) return end(res, 404, 'relay rpc not enabled'); let body; try { body = (await readJsonBody(req, MAX_RPC_BODY)); } catch { return end(res, 400, 'invalid request body'); } const fn = typeof body.fn === 'string' ? body.fn : ''; const args = Array.isArray(body.args) ? body.args : []; if (!fn) return end(res, 400, 'missing rpc name'); try { const result = await handlers.rpc(fn, args); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ result })); } catch (err) { end(res, 500, err instanceof Error ? err.message : 'rpc failed'); } } /** Read a capped JSON request body, rejecting on overflow or malformed JSON. */ function readJsonBody(req, maxBytes) { return new Promise((resolvePromise, rejectPromise) => { const chunks = []; let bytes = 0; req.on('data', (chunk) => { bytes += chunk.length; if (bytes > maxBytes) { rejectPromise(new Error('payload too large')); req.destroy(); return; } chunks.push(chunk); }); req.on('end', () => { try { resolvePromise(JSON.parse(Buffer.concat(chunks).toString('utf8'))); } catch (err) { rejectPromise(err instanceof Error ? err : new Error('invalid json')); } }); req.on('error', rejectPromise); }); } /** Answer a relay request with a plain-text status. */ function end(res, status, message, headers = {}) { res.writeHead(status, { 'content-type': 'text/plain', ...headers }); res.end(message); } //# sourceMappingURL=relay-endpoints.js.map