Skip to content

fix(sandboxcli,gateway): print plain text from run --raw, and serve the e2b surface over :443 - #671

Merged
xiaods merged 3 commits into
mainfrom
fix/cli-raw-output-and-gateway-https
Oct 22, 2026
Merged

xiaods merged 3 commits into
mainfrom
fix/cli-raw-output-and-gateway-https

Conversation

@xiaods

@xiaods xiaods commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Two independent fixes, each verified against a live cluster.

1. k8e-sandbox-cli run --raw prints plain text — and the exit code

--raw is documented as "Stream raw output (no JSON wrapper)". 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}

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, so run --raw "exit 3"
reported 0.

The decoding now lives in pkg/sandbox/envdstream, shared by the e2b
server and the CLI so the two cannot drift apart; the server's private
parseChannelFrame became 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.

  • stdout payloads → stdout, stderr payloads → stderr
  • the {"exit":N} frame → the command's exit code
  • the {"pid":N} start frame → dropped, so it cannot corrupt the output
  • a frame that is not an envd output frame → passed through unchanged
$ 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

2. 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 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 working :80 path down with it.

Each listener now gets its own route object, so a rejected attachment
fails only the HTTPS route:

route listener
e2b-control-plane http
e2b-envd http
e2b-control-plane-tls https (new)
e2b-envd-tls https (new)
apiUrl     = https://<gateway>            (control plane)
sandboxUrl = https://<gateway>/e2b/envd   (envd data plane)

The 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_BUNDLE environment variables are ignored
by it.

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.

Also in this PR

pkg/deploy/zz_generated_bindata.go is regenerated: the e2b-gateway
addon 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 prefix
    forms, 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, ANSI
    preserved, exit code captured, pid frame not printed, unknown frame
    passed through

sandboxd: 61/61 (unchanged). Go: 38/38 packages.

k8e-fix 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.
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

1 041 tests  +17   1 014 ✅ +17   7m 6s ⏱️ -9s
  121 suites + 1      27 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 0657aec. ± Comparison against base commit 23afaa2.

♻️ This comment has been updated with latest results.

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.
@sonarqubecloud

Copy link
Copy Markdown

@xiaods
xiaods merged commit c547322 into main Oct 22, 2026
9 checks passed
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