(rec, { instanceId, initialTab, sendPrompt, runTurn, log })
| 218 | * exercised without binding a socket. |
| 219 | */ |
| 220 | export function makeHandler(rec, { instanceId, initialTab, sendPrompt, runTurn, log }) { |
| 221 | return async function handler(req, res) { |
| 222 | const url = new URL(req.url, "http://127.0.0.1"); |
| 223 | try { |
| 224 | // Per-instance secret: real instances (createInstanceServer) mint a token |
| 225 | // that the host embeds in the iframe URL. Reject any loopback request that |
| 226 | // doesn't present it, so other local processes can't read repo state or |
| 227 | // dispatch agent actions just by guessing the random port. When no token is |
| 228 | // set (direct makeHandler unit tests) the guard is a no-op. |
| 229 | if (rec.token) { |
| 230 | const provided = url.searchParams.get("token"); |
| 231 | if (provided !== rec.token) { |
| 232 | res.statusCode = 403; |
| 233 | res.setHeader("Content-Type", "application/json"); |
| 234 | res.end(JSON.stringify({ ok: false, error: "forbidden" })); |
| 235 | return; |
| 236 | } |
| 237 | } |
| 238 | if (req.method === "GET" && url.pathname === "/") { |
| 239 | res.setHeader("Content-Type", "text/html; charset=utf-8"); |
| 240 | res.end(renderHtml({ instanceId, initialTab, token: rec.token })); |
| 241 | return; |
| 242 | } |
| 243 | if (req.method === "GET" && url.pathname === "/state") { |
| 244 | const state = await buildState(rec); |
| 245 | res.setHeader("Content-Type", "application/json"); |
| 246 | res.end(JSON.stringify(state)); |
| 247 | return; |
| 248 | } |
| 249 | if (req.method === "GET" && url.pathname === "/events") { |
| 250 | res.writeHead(200, { |
| 251 | "Content-Type": "text/event-stream", |
| 252 | "Cache-Control": "no-cache", |
| 253 | Connection: "keep-alive", |
| 254 | }); |
| 255 | res.write("retry: 3000\n\n"); |
| 256 | rec.sseClients.add(res); |
| 257 | req.on("close", () => rec.sseClients.delete(res)); |
| 258 | return; |
| 259 | } |
| 260 | if (req.method === "POST" && url.pathname === "/action") { |
| 261 | const { body, tooLarge } = await readBody(req); |
| 262 | if (tooLarge) { |
| 263 | res.statusCode = 413; |
| 264 | res.setHeader("Content-Type", "application/json"); |
| 265 | res.end(JSON.stringify({ ok: false, error: "request body too large" })); |
| 266 | return; |
| 267 | } |
| 268 | let parsed; |
| 269 | try { |
| 270 | parsed = JSON.parse(body || "{}"); |
| 271 | } catch { |
| 272 | res.statusCode = 400; |
| 273 | res.setHeader("Content-Type", "application/json"); |
| 274 | res.end(JSON.stringify({ ok: false, error: "invalid JSON body" })); |
| 275 | return; |
| 276 | } |
| 277 | if (!parsed || typeof parsed.kind !== "string" || !parsed.kind) { |
no test coverage detected