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. |
|
Have not stopped thinking about this... the shape I am coming around to is term-llm serve hub then hub has a registration token and nodes can use that to register on hub, or hub can add nodes it wishes. nodes can live anywhere on docker containers on vms or cloud. hub UI is to provide a directory and then you can click on any agent to get to the actual UI which is proxied... lots of little details here including ... how do you get back to hub (sidebar), but I am liking it. For dv environments this will be great... single pane of glass to see every single dv container, term-llm being so lightweight is a huge advantage here. continuing my brainstorming but I am making lots of progress here thinking. |
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).