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
JavaScript
/**
* 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