163 lines · 6.6 KB · TypeScript
RawDownload
import { Hono } from "hono";import {  handleAgentAbort,  handleAgentCompact,  handleAgentTurn,  handleExtensionUiResponse,  handleRuntimeEvents,  handleRuntimeSessions,  handleRuntimeStatus,  handleSessionListChanged,  handleSetupChecks,} from "./handlers";import {  handleBrowserEngineSelect,  handleBrowserEngines,  handleBrowserFetch,  handleBrowserFrame,  handleBrowserHistory,  handleBrowserInput,  handleBrowserLocalhosts,  handleBrowserState,  handleBrowserVerb,  handleBrowserViewport,} from "./browser-handlers";import {  handleProviderLogin,  handleProviderLoginCancel,  handleProviderLoginJob,  handleProviderLoginRespond,  handleProviderLogout,  handleProvidersList,} from "./provider-handlers";import {  handleAutomationCreate,  handleAutomationDelete,  handleAutomationPatch,  handleAutomationRun,  handleAutomationsList,  handleGoalDelete,  handleGoalGet,  handleGoalPut,} from "./automation-handlers";import {  handleSubagentGet,  handleSubagentRun,  handleSubagentsList,  handleSubagentStop,} from "./subagent-handlers";import { handlePrGet, handlePrMerge } from "./pr-handlers";import {  handlePtyClose,  handlePtyInput,  handlePtyOpen,  handlePtyResize,  handlePtyStream,} from "./pty-handlers";import { handleAgentModels } from "./model-handlers";import {  handleAllSessions,  handleSessionGet,  handleSessionPatch,  handleSessionsDelete,  handleSessionsList,} from "./session-handlers"; // The runtime binds loopback only, so every legitimate request carries a// loopback Host. A browser tricked by DNS rebinding reaches the socket with// the attacker's hostname in Host — reject those before any route runs.const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]); const isLoopbackHost = (header: string | undefined): boolean => {  if (!header) return false;  const host = header.trim().toLowerCase();  const name = host.startsWith("[")    ? host.replace(/\]:\d+$/, "]")    : host.replace(/:\d+$/, "");  return LOOPBACK_HOSTS.has(name);}; export function createAgentRuntimeApp() {  const app = new Hono();   app.use("*", (c, next) => {    if (!isLoopbackHost(c.req.header("host"))) {      return Promise.resolve(c.json({ error: "Forbidden host" }, 403));    }    return next();  });   app.get("/health", (c) =>    c.json({ ok: true, service: "local-studio-agent-runtime", pid: process.pid }),  );  app.post("/api/agent/turn", (c) => handleAgentTurn(c.req.raw));  app.post("/api/agent/abort", (c) => handleAgentAbort(c.req.raw));  app.post("/api/agent/compact", (c) => handleAgentCompact(c.req.raw));  app.post("/api/agent/runtime/extension-ui", (c) => handleExtensionUiResponse(c.req.raw));  app.get("/api/agent/runtime/sessions", () => handleRuntimeSessions());  app.get("/api/agent/runtime/status", (c) => handleRuntimeStatus(c.req.raw));  app.get("/api/agent/runtime/events", (c) => handleRuntimeEvents(c.req.raw));  app.get("/api/agent/session-list-changed", (c) => handleSessionListChanged(c.req.raw));  app.get("/api/agent/setup-checks", () => handleSetupChecks());  app.get("/api/agent/models", () => handleAgentModels());  app.post("/api/agent/models", (c) => handleAgentModels(c.req.raw));  app.get("/api/agent/sessions", (c) => handleSessionsList(c.req.raw));  app.delete("/api/agent/sessions", () => handleSessionsDelete());  app.get("/api/agent/sessions/all", (c) => handleAllSessions(c.req.raw));  app.get("/api/agent/sessions/:id", (c) => handleSessionGet(c.req.raw, c.req.param("id")));  app.patch("/api/agent/sessions/:id", (c) => handleSessionPatch(c.req.raw, c.req.param("id")));  app.get("/api/agent/automations", () => handleAutomationsList());  app.post("/api/agent/automations", (c) => handleAutomationCreate(c.req.raw));  app.patch("/api/agent/automations/:id", (c) =>    handleAutomationPatch(c.req.raw, c.req.param("id")),  );  app.delete("/api/agent/automations/:id", (c) =>    handleAutomationDelete(c.req.param("id")),  );  app.post("/api/agent/automations/:id/run", (c) => handleAutomationRun(c.req.param("id")));  app.get("/api/agent/pr", (c) => handlePrGet(c.req.raw));  app.post("/api/agent/pr/merge", (c) => handlePrMerge(c.req.raw));  app.get("/api/agent/subagents", (c) => handleSubagentsList(c.req.raw));  app.post("/api/agent/subagents", (c) => handleSubagentRun(c.req.raw));  app.get("/api/agent/subagents/:runId", (c) => handleSubagentGet(c.req.raw, c.req.param("runId")));  app.post("/api/agent/subagents/:runId/stop", (c) =>    handleSubagentStop(c.req.raw, c.req.param("runId")),  );  app.get("/api/agent/goal", (c) => handleGoalGet(c.req.raw));  app.put("/api/agent/goal", (c) => handleGoalPut(c.req.raw));  app.delete("/api/agent/goal", (c) => handleGoalDelete(c.req.raw));  app.get("/api/agent/providers", () => handleProvidersList());  app.get("/api/agent/providers/login/:jobId", (c) =>    handleProviderLoginJob(c.req.raw, c.req.param("jobId")),  );  app.post("/api/agent/providers/login/:jobId/respond", (c) =>    handleProviderLoginRespond(c.req.raw, c.req.param("jobId")),  );  app.post("/api/agent/providers/login/:jobId/cancel", (c) =>    handleProviderLoginCancel(c.req.param("jobId")),  );  app.post("/api/agent/providers/:providerId/login", (c) =>    handleProviderLogin(c.req.raw, c.req.param("providerId")),  );  app.post("/api/agent/providers/:providerId/logout", (c) =>    handleProviderLogout(c.req.param("providerId")),  );  app.post("/api/agent/terminal/pty/open", (c) => handlePtyOpen(c.req.raw));  app.get("/api/agent/terminal/pty/stream", (c) => handlePtyStream(c.req.raw));  app.post("/api/agent/terminal/pty/input", (c) => handlePtyInput(c.req.raw));  app.post("/api/agent/terminal/pty/resize", (c) => handlePtyResize(c.req.raw));  app.post("/api/agent/terminal/pty/close", (c) => handlePtyClose(c.req.raw));  app.get("/api/agent/browser/fetch", (c) => handleBrowserFetch(c.req.raw));  app.get("/api/agent/browser/frame", () => handleBrowserFrame());  app.post("/api/agent/browser/input", (c) => handleBrowserInput(c.req.raw));  app.get("/api/agent/browser/localhosts", (c) => handleBrowserLocalhosts(c.req.raw));  app.get("/api/agent/browser/state", () => handleBrowserState());  app.get("/api/agent/browser/history", (c) => handleBrowserHistory(c.req.raw));  app.get("/api/agent/browser/engines", () => handleBrowserEngines());  app.post("/api/agent/browser/viewport", (c) => handleBrowserViewport(c.req.raw));  // Registered ahead of the :verb catch-all, which would otherwise reject it.  app.post("/api/agent/browser/engine", (c) => handleBrowserEngineSelect(c.req.raw));  app.post("/api/agent/browser/:verb", (c) => handleBrowserVerb(c.req.raw, c.req.param("verb")));   return { app };}