Repository navigation
fix(sandboxcli,gateway): print plain text from run --raw, and serve the e2b surface over :443 - #671
Merged
Merged
Conversation
added 2 commits
October 22, 2026 19:46
…t code
`k8e-sandbox-cli run --raw` is documented as "Stream raw output (no JSON
wrapper)", but it only dropped the CLI's JSON wrapper. What came out was
the server's SSE framing, whose payloads are base64:
$ k8e-sandbox-cli run --raw "printf '\033[36m★\033[0m\n'"
data: {"stdout":"JiozMzZtMOKCrQ=="}
data: {"exit":0}
A user following the docs saw base64, not the command's output. The base64
is deliberate — it is what stops a newline or a half-written ANSI escape
from breaking a frame boundary — but the client has to decode it.
It also always reported success: the exit code came from the terminal
{"exit":N} frame, which nobody was reading, so `run --raw "exit 3"`
reported 0. The JSON mode has always reported it correctly.
The decoding now lives in pkg/sandbox/envdstream, which both the e2b
server and the CLI use, so the two cannot drift apart. The server's
private parseChannelFrame is now a thin wrapper over it.
The frames are decoded as they arrive — the gRPC layer forwards the SSE
body in fixed 4096-byte chunks, so one chunk can hold several frames and a
frame can be split across two, and the decoder buffers the remainder.
- stdout payloads go to stdout, stderr payloads to stderr
- the {"exit":N} frame becomes the command's exit code
- the {"pid":N} start frame is dropped rather than corrupting the output
- a frame that is not an envd output frame (an older sandboxd writing
plain text) is passed through unchanged
Verified against a live cluster:
$ k8e-sandbox-cli run --raw "printf '\033[1;36m★ K8E SANDBOX ★\033[0m\n'"
^[[1;36m★ K8E SANDBOX ★^[[0m
$ k8e-sandbox-cli run --raw "exit 3"; echo $?
3
$ k8e-sandbox-cli run --raw "python3 -c \"print('x'*20000)\"" | wc -c
20001
sandboxd: 61/61 (unchanged). Go: 38/38 packages, 16 new cases.
The e2b Gateway declares three listeners — http (:80), https (:443) and
grpc (:50051) — but both HTTPRoutes were attached to the http listener
only, so the :443 listener had zero routes and every request to it
answered 404. An E2B client configured with an https:// apiUrl could not
talk to the cluster at all.
The routes were bound to :80 alone on purpose: the :443 listener needs a
TLS secret (sandbox-e2b) and reports Invalid without one, and a route
attached to an invalid listener is rejected with NotAllowedByListeners.
The original comment feared that attaching both listeners to one route
would take the whole route — and with it the working :80 path — down.
The fix is to give each listener its own route object:
e2b-control-plane (http) — unchanged
e2b-envd (http) — unchanged
e2b-control-plane-tls (https) — new
e2b-envd-tls (https) — new
A rejected attachment now fails only the HTTPS route; the :80 routes keep
serving. When the TLS secret is present the e2b surface is reachable over
HTTPS as well:
apiUrl = https://<gateway> (control plane)
sandboxUrl = https://<gateway>/e2b/envd (envd data plane)
The client has to trust the sandbox CA — the gateway presents the same
private CA the sandbox gRPC gateway uses — so install it in the client's
OS trust store (the E2B SDK's transport reads the system store; the
SSL_CERT_FILE / REQUESTS_CA_BUNDLE environment variables are ignored).
Verified against a live cluster with the official SDK 2.56.0:
$ kubectl get gateway e2b -o jsonpath='{...attachedRoutes}'
http attachedRoutes=2 https attachedRoutes=2 grpc attachedRoutes=1
$ curl -sk https://<gateway>/health -> 200
$ curl -sk -X POST -H "X-API-KEY: $KEY" https://<gateway>/v2/sandboxes
-> 201
$ E2B_API_URL=https://<gateway> ... python (e2b 2.56.0)
stdout: 42 | exit: 0
read : hello over https
guide Level 1-5 over HTTPS: 6/6
HTTP and HTTPS serve the same surface at the same time.
Contributor
The e2b-gateway.yaml addon is embedded into the binary as compressed bindata, so the manifest edit only reaches the cluster if the bindata is regenerated. Without this the released binary would still stage the pre-#671 manifest and the :443 routes would never be created. `go run pkg/codegen` (what `make generate` runs) produced this. Verified: the staged e2b-gateway.yaml now carries e2b-control-plane-tls and e2b-envd-tls, and the deploy manifest checks pass.
|
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.



Two independent fixes, each verified against a live cluster.
1.
k8e-sandbox-cli run --rawprints plain text — and the exit code--rawis documented as "Stream raw output (no JSON wrapper)". It onlydropped the CLI's JSON wrapper; what came out was the server's SSE
framing, whose payloads are base64:
The base64 is deliberate — it is what stops a newline or a half-written
ANSI escape from breaking a frame boundary — but the client has to decode
it. It also always reported success: the exit code lives in the terminal
{"exit":N}frame, which nobody was reading, sorun --raw "exit 3"reported
0.The decoding now lives in
pkg/sandbox/envdstream, shared by the e2bserver and the CLI so the two cannot drift apart; the server's private
parseChannelFramebecame a thin wrapper over it.Frames are decoded as they arrive. The gRPC layer forwards the SSE body in
fixed 4096-byte chunks, so one chunk can hold several frames and a frame
can be split across two — the decoder buffers the remainder.
{"exit":N}frame → the command's exit code{"pid":N}start frame → dropped, so it cannot corrupt the output2. The e2b surface over the Gateway's :443 listener
The e2b Gateway declares three listeners — http (:80), https (:443) and
grpc (:50051) — but both HTTPRoutes were attached to the http listener
only. The :443 listener had zero routes and answered
404, so an E2Bclient configured with an
https://apiUrl could not talk to the clusterat all.
The routes were bound to :80 alone on purpose: the :443 listener needs a
TLS secret (
sandbox-e2b) and reportsInvalidwithout one, and a routeattached to an invalid listener is rejected with
NotAllowedByListeners.The original comment feared that attaching both listeners to one route
would take the working :80 path down with it.
Each listener now gets its own route object, so a rejected attachment
fails only the HTTPS route:
e2b-control-planee2b-envde2b-control-plane-tlse2b-envd-tlsThe client must trust the sandbox CA — the gateway presents the same
private CA the sandbox gRPC gateway uses. Install it in the client's OS
trust store: the E2B SDK's transport reads the system store, and the
SSL_CERT_FILE/REQUESTS_CA_BUNDLEenvironment variables are ignoredby it.
Verified against a live cluster with the official SDK 2.56.0:
HTTP and HTTPS serve the same surface at the same time.
Also in this PR
pkg/deploy/zz_generated_bindata.gois regenerated: the e2b-gatewayaddon is embedded into the binary as compressed bindata, so the manifest
edit only reaches the cluster if the bindata is regenerated. Without it a
released binary would still stage the pre-#671 manifest and the :443
routes would never be created.
Tests
pkg/sandbox/envdstream: 12 new cases — frame parsing, both prefixforms, a frame split across chunks, several frames in one chunk,
unknown frames passed through, control characters surviving the round
trip
pkg/sandboxcli: 5 new cases — stdout/stderr separation, ANSIpreserved, exit code captured, pid frame not printed, unknown frame
passed through
sandboxd: 61/61 (unchanged). Go: 38/38 packages.