feat: multi-agent gateway with separate-origin widget host (experimental)#769
feat: multi-agent gateway with separate-origin widget host (experimental)#769arpitjalan wants to merge 1 commit into
Conversation
Add 'term-llm serve-gateway', a thin reverse proxy that fronts the per-agent web serves of several contain workspaces under one origin, and (optionally) serves each agent's widgets from a dedicated, isolated origin. A term-llm serve binds one agent at startup, so multi-agent access is achieved by fronting many serves; the gateway reads each agent's port and bearer token from its workspace .env and injects the token server-side, so the per-agent token never reaches the browser. Agent proxy: GET /agents (JSON, no tokens), an HTML landing page, and ANY /agent/<name>/... reverse-proxied to that agent's serve. Injects Authorization: Bearer, strips client Authorization/Cookie/X-Api-Key and spoofable X-Forwarded-*; rebases the agent's baked-in base prefix onto /agent/<name> so the SPA re-homes onto the proxy with zero agent changes. Widget host (--widget-host, default widgets.localhost): serves ONLY /w/<agent>/<mount>/... on a dedicated origin (every other path 404s there), so an embedded widget granted allow-same-origin is isolated to a throwaway origin. Routes by Host header; widget responses pass through unrebased (widgets resolve via relative URLs). Landing page lists each agent's widgets, grouped per agent, linking to the widget origin. Discovery: new 'contain port' / 'contain token' subcommands and contain.ReadWebConfig (single source for a workspace's web config), built on a shared contain.ReadEnvFile. Built on the current upstream/main and integrated with its new code: - Reuse webport.go's webPortBase as ReadWebConfig's port fallback, and collapse webport.go's readEnvWebPort into the shared ReadEnvFile parser (one .env parser, not two). - Normalize WEB_BASE_PATH trailing slashes in ReadWebConfig to match the serve's normalizeBasePath, so the rebase needles always match. - Reuse widgets.WidgetStatus for the landing-page widget list and route the mount charset through the new exported widgets.ValidMount. - Drop cmd/contain.go's local readContainEnvFile; route printContainWebUIInfo and printContainAuthSeedHints through contain.ReadWebConfig/ReadEnvFile. - A regression test feeds the real serve-rendered index through the rebase needles so any serveui/buildIndexHTML shape drift fails the build. Hardening: refuses non-loopback binds (no gateway auth yet); validates WEB_PORT; rejects a loopback-literal --widget-host; warns if the widget host doesn't resolve; bounded dial/response-header timeouts that keep SSE open; per-agent web-config cache invalidated by .env mtime; no env proxy on the backend transport. Experimental and loopback-only; the HMAC widget access grant is the next step. Includes a docs guide.
815a46c to
6fe1a2e
Compare
|
give me a bit arpit, I want to think this one through |
|
No rush, thanks Sam! |
|
Also, here's some context on scope and the use case driving this: Why: it comes out of the second-brain Discourse plugin, where widgets are currently proxied through Discourse's own origin and iframed with allow-same-origin - so a prompt-injected widget can run as Discourse (read PMs, act as the user). The fix is to serve widgets from a dedicated, isolated origin, which is what this PR adds (and it doubles as the mechanism for publishing widgets later). This PR is the foundation: a thin multi-agent reverse proxy + the separate-origin widget host, loopback-only (it refuses non-loopback binds since the agent proxy has no auth yet). Merging it exposes nothing. Kept intentionally scoped: the widget origin's access control - a short-lived signed grant the embedding app mints and the host verifies, so the Discourse session never reaches the widget origin - is the natural next step, left out here to keep this PR focused on the proxy/isolation foundation. |
Summary
Adds
term-llm serve-gateway— a thin reverse proxy that fronts the per-agent web serves of several contain workspaces under one origin, and (optionally) serves each agent's widgets from a dedicated, isolated origin.A
term-llm servebinds a single agent at startup, so multi-agent access is achieved by fronting many serves rather than routing agents inside one process (already the production shape: one container per agent). The gateway discovers each agent's published port + bearer token from its workspace.envand injects the token server-side, so the per-agent token never reaches the browser.This is the foundation for two things: reaching many agents from one place, and the separate-origin widget host that future work (a signed access grant) will use to safely embed/publish widgets.
What's included
Agent proxy
GET /agents(JSON, never includes tokens), an HTML landing page, andANY /agent/<name>/...reverse-proxied to that agent's serve.Authorization: Bearer <token>; strips clientAuthorization,Cookie,X-Api-Key, and spoofableX-Forwarded-*./chatprefix onto/agent/<name>so the SPA's API calls, service worker, and subresources all route back through the proxy — zero changes to the agent.Separate-origin widget host (
--widget-host, defaultwidgets.localhost)/w/<agent>/<mount>/...on a dedicated origin; every other path 404s there. That emptiness is the isolation property: a widget iframe grantedallow-same-originis confined to a throwaway origin that hosts nothing sensitive.Hostheader; the agent origin never exposes/w/, and the widget origin never exposes/agents,/agent/*, or the landing page.Discovery + UX
contain port/contain tokensubcommands andcontain.ReadWebConfig, the single source for reading a workspace's web config..html/.cssfiles (matching theserveuiconvention).Hardening
WEB_PORT; rejects a loopback-literal--widget-hostthat would shadow the gateway; warns at startup if the widget host doesn't resolve..envmtime; no environment proxy on the backend transport (avoids leaking the injected token viaHTTP_PROXY).Docs: new
Multi-agent gatewayguide.Security posture
Experimental and loopback-only by design:
/w/<agent>/<mount>/request (loopback only). A signed, expiring HMAC grant for safe cross-origin use is the next step, and the piece that actually closes the widget-iframe XSS in production.An independent multi-agent code review was run over the diff; the top findings were fixed (HEAD/empty-body response handling, the
HTTP_PROXYtoken-leak path, query-string preservation on the widget redirect, and the widget-host/agent-host shadowing guard).