Skip to content

Add the Session gateway for model, MCP and generic egress - #347

Merged
SaladDay merged 3 commits into
feature/agent-outside-sandboxfrom
aos/gateway
Oct 1, 2026
Merged

SaladDay merged 3 commits into
feature/agent-outside-sandboxfrom
aos/gateway

Conversation

@SaladDay

@SaladDay SaladDay commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Adds the Session gateway, apps/daemon/internal/gateway. The Harness on the agent host never holds a real credential and has no network route of its own. Inside the Session's loopback-only network namespace, the gateway serves:

  • one listener per frozen model upstream. It relays only the routes declared in internal/modelprovider:
    • an undeclared route gets 404, an undeclared method 405 and an undeclared upgrade 400, each without contacting the upstream;
    • it strips every declared credential header (all values, in any case) and injects the real credential;
    • it keeps the base path through UpstreamPath and the query unchanged;
    • it streams bodies (SSE and WebSocket) without buffering;
    • it never follows a redirect, so a 3xx is returned as is, Location included;
    • TLS to the upstream runs from the agent host.
  • one generic proxy. It accepts CONNECT and absolute-form http:// requests, and every connection goes through sandboxnet.Connect on a new Network stream. It has no direct outbound path, and it rejects any other request form.
  • one listener per MCPHTTPServer binding:
    • ValidateConnectionOrigin runs first;
    • the gateway owns the bearer and the upstream TLS;
    • an environment-origin binding dials through sandboxnet, and a service-origin binding dials from the agent host;
    • the Harness gets http://127.0.0.1:<port> plus the binding's path.

The listeners open on a thread that joins the Session's network namespace, and nothing listens outside it. The gateway logs nothing, and its errors never include credentials or URLs. There is no per-Harness branch.

API for the adapter wiring (a later lane):

  • Plan(cfg) returns Endpoints before the view starts. Ports are fixed inside the private namespace: the proxy on 17100, then the models, then MCP.
  • Start(ctx, SessionNetwork, cfg) is passed as sessionview's Network.Setup.

Tests:

  • Model: the upstream receives the injected key and never the placeholder; an undeclared route never reaches the upstream; SSE streams; a redirect is not followed.
  • Proxy: CONNECT to a TLS server and a plain-HTTP forward, both through sandboxnet over the sandboxlinktest relay.
  • MCP: both origins with the bearer injected; origin admission rejects a mismatched binding.
  • In a Session view (privileged container):
    • the listeners answer inside the namespace;
    • a direct external dial fails with ENETUNREACH but succeeds through the proxy;
    • the listeners are unreachable from the host namespace.

Checks:

  • go test -race
  • the view test in the privileged container
  • vet and gofmt
  • darwin and windows builds

Docs: none in this PR. model-execution.md already holds the credential-gateway rule (#343). The MCP and proxy documentation changes come with the adapter wiring, which changes behaviour.

Part of the agent-outside-sandbox work, milestone M1, lane L6.

Blind review fixes

Two blind review rounds; their fixes are in 3f8061c and 806000f.

  • Credentials in responses: response headers, trailers, 1xx responses and Location that contain the injected key or MCP bearer are removed. Bodies pass unchanged, as model-execution.md declares. The injected value is normalized as the HTTP/1 header writer sends it, and filtered in that form.
  • Credentials in logs: credential-bearing HTTP/1 upstream connections are never left idle, so the transport cannot log unsolicited bytes that echo the key. HTTP/2 reuse is kept.
  • TLS: Config.RootCAs replaces Config.TLS. The server name always comes from the destination.
  • Session end: hijacked and upgraded connections close when the Session ends, and so does every upstream connection handed over with a 101. Mismatched upgrade responses fail with 502.
  • Sandbox dials: a local deadline covers opening the stream plus Connect, and Session cancellation ends every dial.
  • Forward proxy: the raw query is kept byte for byte.

The gateway serves, inside the Session's loopback-only network namespace, one
listener per frozen model upstream, one per MCP HTTP binding and one generic
proxy. Model listeners relay only the routes internal/modelprovider declares,
strip every credential header and inject the upstream credential from the
agent host. MCP listeners admit each binding's connection origin first, then
relay to the server with the bearer injected: environment origin through the
sandbox Network service, service origin from the agent host. The generic proxy
carries CONNECT tunnels and absolute-form plain HTTP, and connects only through
a new sandboxnet stream per connection. Redirects are returned unfollowed and
the gateway logs nothing.

Plan returns the Endpoints before the view exists, so the Harness environment
can be built before sessionview starts; Start opens the listeners from the
launcher's network hook at the same fixed ports.
- Model and MCP relays remove every response header and trailer value that
  contains the injected key or bearer, informational responses included.
  Bodies pass unchanged. model-execution.md declares the rule.
- Config.TLS becomes Config.RootCAs: upstream TLS always verifies the
  destination's hostname.
- The listeners record every accepted connection, so the end of the Session
  aborts hijacked tunnels and upgraded connections that http.Server.Close
  leaves open, including a relay blocked on a Harness that does not read.
- Every upstream dial ends with the Session, and a sandbox open plus Connect
  has a local deadline of the connect timeout plus a margin.
- The forward proxy restores the query that the reverse proxy cleans of
  semicolon parameters.
@SaladDay
SaladDay merged commit f1417a6 into feature/agent-outside-sandbox Oct 1, 2026
1 check passed
@SaladDay
SaladDay deleted the aos/gateway branch October 1, 2026 02:01
Relay transports close each HTTP/1 connection after its response, so
http.Transport never logs bytes an idle connection receives; HTTP/2
connections stay shared. Relays inject and withhold each credential in
the form the header writer sends. ModifyResponse rejects a mismatched
101, and every upgraded upstream connection closes with its request.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant