Repository navigation
Reach daemons inside an environment: ate-env-api port proxy, NeMo Gym endpoint() - #92
Open
Tomer Glottmann (tomergee) wants to merge 4 commits into
Open
Tomer Glottmann (tomergee) wants to merge 4 commits into
Tomer Glottmann (tomergee) wants to merge 4 commits into
Conversation
… router A daemon that a runtime layer starts next to the guest (an exec server, a code-execution server, OpenSandbox's execd) was reachable only from inside its environment: ate-env-api proxied the guest alone. The atenet router already tunnels an HTTP CONNECT to any port on an actor, naming the actor in ate-target-actor and the port in the authority, and resumes the actor first. internal/portproxy uses that. A plain HTTP request carrying X-Env-Id, X-Env-Port and optionally X-Env-Atespace goes to that port in that environment, whatever its path. The proxy validates the target (DNS labels, 1-65535), strips the routing headers, opens a CONNECT tunnel through the router's CONNECT listener, speaks HTTP/1.1 to the daemon, flushes streamed responses at once, and keeps idle tunnels in one pool per environment and port. A router 404 or 503 reaches the caller as such; other failures are 502, a handshake timeout 504. ate-env-api serves it on its existing listener: requests with X-Env-Port go to the proxy before the gRPC and MCP handlers, so no new port or manifest change. --atenet-connect names the router's CONNECT listener (default atenet-router.ate-system.svc.cluster.local:8081, the stock install); empty turns forwarding off. The fake router answers CONNECT the way the real one does, for tests.
Implements nemo-gym 0.7's SupportsSandboxEndpoint. create() keeps SandboxSpec.ports on the handle; endpoint(handle, port) refuses an undeclared port with ValueError, as Gym's Docker provider does, wakes the sandbox with one guest probe, and returns ate-env-api's port proxy as the URL with the X-Env-Id, X-Env-Atespace and X-Env-Port routing headers. connection.endpoint_url overrides the URL when callers reach ate-env-api at another address than the provider. _compat gains a SandboxEndpoint mirror for nemo-gym 0.5 and the hermetic tests. The README documents the injected-daemon setup: a runtime layer and sidecar on the base template, ports on the spec, endpoint() at call time, and the limits (HTTP/1.1, self-contained daemon, port 80 is the guest's).
jessicaochen
self-requested a review
October 15, 2026 18:15
…without headers
WebSocket clients and browsers cannot set the routing headers. The OpenHands
SDK, for one, opens its event WebSocket at <host>/sockets/events/<id> with
no custom headers, but keeps the host's path. The path form carries the
target in a prefix the proxy strips before forwarding:
/v1alpha/envs/{id}/ports/{port}/... in the default atespace and
/v1alpha/atespaces/{atespace}/envs/{id}/ports/{port}/... otherwise. Both
forms share one validation; escaping after the prefix is kept as sent.
Tests: prefix stripping with query and escaped segments, the bare prefix,
the atespace form, bad ids, ports and atespaces, other /v1alpha paths still
reaching the API, and an HTTP upgrade (a WebSocket handshake) through the
proxy and the tunnel in both forms.
The path form (/v1alpha/envs/{id}/ports/{port}/...) is documented next to
the header form in the README and RUNTIMES.md. WebSocket upgrades pass
through the proxy and the router: verified live with the OpenHands
agent-server's event stream (see the follow-up example). gRPC stays out.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets code outside the cluster talk to a daemon running inside an environment, and wires it into the NeMo Gym provider as nemo-gym 0.7's
endpoint(handle, port).Why
NeMo Gym recipes can have daemon attached to unmodified task images at runtime, the way #70 attaches the ate-env guest. Half of that exists: a runtime layer (
--layer) mounts the daemon's image read-only, the guest starts it (--sidecar) and gates readiness on it (--sidecar-readyz), and every template derived per task image inherits it (RUNTIMES.md). The missing half, listed as future work in DESIGN.md, was reaching the daemon from outside: ate-env-api proxied only the guest. Gym's provider contract models this asSandboxSpec.portsplus the optionalSupportsSandboxEndpoint.endpoint(handle, port) -> SandboxEndpoint(endpoint, headers)(nemo-gym 0.7,sandbox/providers/base.py). The same gaps, binary injection into users' images and sidecar services, came up from the OpenKruise Agents team on the RL sandboxes discussion.What
ate-env-api port proxy (
internal/portproxy). A plain HTTP request to the API's listener carryingX-Env-Id,X-Env-Portand optionallyX-Env-Atespacegoes to that port in that environment, whatever its path. The atenet router already supports this shape of traffic: an HTTPCONNECTto its CONNECT listener (port 8081 on a stock install, enabled and admitted by the default network policy) naming the actor inate-target-actorand the port in the authority; the router resumes the actor first. The proxy:Host: 127.0.0.1:<port>(what an in-sandbox client sends), flushes streamed responses at once;Path form for clients that cannot set headers (WebSocket clients, browsers):
/v1alpha/envs/{id}/ports/{port}/..., or/v1alpha/atespaces/{atespace}/envs/{id}/ports/{port}/...; the prefix is stripped, so it works as a base URL (the OpenHands SDK builds its event WebSocket from it). HTTP upgrades pass through either form.It shares ate-env-api's existing listener: requests with
X-Env-Portgo to the proxy before the gRPC and MCP handlers. No new port, no manifest change.--atenet-connectnames the router's CONNECT listener (defaultatenet-router.ate-system.svc.cluster.local:8081); empty disables it. Trust model unchanged: anyone who can reach ate-env-api can already run commands in any environment through it.NeMo Gym provider.
create()keepsSandboxSpec.portson the handle.endpoint(handle, port)raisesValueErrorfor an undeclared port (as Gym's Docker provider does), wakes the sandbox with one guest probe, and returnsSandboxEndpoint(endpoint=<ate-env-api URL>, headers={X-Env-Id, X-Env-Atespace, X-Env-Port}).connection.endpoint_urloverrides the URL._compatgains aSandboxEndpointmirror for nemo-gym 0.5 and the hermetic tests. The README documents the full setup: daemon layer and sidecar on the base template,template:in the config,portson the spec,endpoint()at call time.Fake router answers
CONNECTlike the real one, for the proxy tests.Verification
go test -race ./...: 13 packages ok (10 new proxy tests incl. path form and an upgrade through the tunnel, run 3 times with-race; a mutation of the pool key fails the isolation test).make verify-gofmt verify-boilerplatepass, staticcheck clean.--port-connect=8081), ate-env-api built from this branch, a base template withbusybox:1.37-muslas the runtime layer andbusybox httpd -p 44772as the sidecar, environments created from unmodifiedpython:3.12-slim:curl -H 'X-Env-Id: …' -H 'X-Env-Port: 44772' <api>/etc/hostnamereturned the file from the daemon (200), query strings and the daemon's own 404 passed through;/healthz);/healthzover the same port-forward (about 0.3 s from a laptop), so the in-cluster overhead is not visible at that resolution;create(SandboxSpec(image=python@sha256:…, ports=(44772,)))in 3.5 s,endpoint()in 0.22 s, a GET with the endpoint's headers reached the daemon,execstill worked, an undeclared port raisedValueError,close()left the sandbox stopped.Limits and follow-ups
python3in the task image.Overlap
cmd/ate-env-api/main.goconflicts with #86 (both add a flag to the same block). Whichever lands second rebases; the change is mechanical. Merges clean with #88 and #91.