Skip to content

feat(worker): rebuild E2B bridge in Go - #14

Open
Coooolfan wants to merge 12 commits into
mainfrom
feat/worker-bridge-e2b
Open

feat(worker): rebuild E2B bridge in Go#14
Coooolfan wants to merge 12 commits into
mainfrom
feat/worker-bridge-e2b

Conversation

@Coooolfan

Copy link
Copy Markdown
Owner

Summary

  • add a Go-based worker-bridge-e2b that connects to the console worker registry stream
  • implement echo, pythonExec, terminalExec, and terminalResource against E2B Control API and envd
  • support reusable terminal sessions, shared per-session concurrency, monotonic leases, timeout isolation, janitor cleanup, and shutdown cleanup
  • support validate, read, and export resource actions, including configurable worker-proxied or sandbox-direct streaming uploads
  • add environment/TOML configuration, E2B-compatible aliases, cloud-oriented defaults, and complete configuration/tool documentation

Behavior

  • pythonExec uses a one-shot E2B sandbox and always cleans it up
  • terminalExec reuses the E2B sandbox filesystem by session ID while commands run as independent shells
  • terminalExec and terminalResource share the same per-session concurrency limit
  • timed-out work marks the session for destruction without interrupting already-running sibling commands
  • expired sessions are reaped automatically
  • exports default to direct upload from the E2B sandbox so file contents do not traverse the worker

Validation

  • go test -race ./...
  • go vet ./...
  • live E2B integration coverage for all four capabilities
  • live same-session concurrent terminalExec, resource read/export, timeout isolation, janitor cleanup, and sandbox cleanup

@Coooolfan
Coooolfan marked this pull request as ready for review July 30, 2026 06:12
Copilot AI review requested due to automatic review settings July 30, 2026 06:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40fee29b7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +314 to +316
sandbox, err := m.backend.Create(ctx, m.template, timeout)
session.sandbox = sandbox
session.initErr = err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for sandbox creation before closing the manager

When shutdown occurs while a terminal sandbox is being created, Run can call terminalManager.Close() without waiting for detached command goroutines. Close may therefore observe session.sandbox == nil, clear the session map, and return just before this assignment publishes a successfully created sandbox; the subsequent canceled execution cannot find the session in releaseAndDestroySession, so the sandbox is never killed and remains billable until its E2B timeout. Synchronize creation with Close or track and wait for in-flight creations.

Useful? React with 👍 / 👎.

Comment on lines +128 to +131
if err := waitReconnect(ctx, reconnectDelay); err != nil {
return err
}
reconnectDelay = nextReconnectDelay(reconnectDelay)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset reconnect backoff after a successful session

After any connection-failure streak, reconnectDelay is carried through a later successful connect_ack and is only increased here. Consequently, once it reaches 15 seconds, even a connection that remained healthy for a long time waits the maximum delay after its next ordinary disconnect, and all subsequent reconnects continue doing so. Reset the backoff when a session successfully completes its handshake so exponential delay applies to consecutive failures rather than the worker's entire lifetime.

Useful? React with 👍 / 👎.

if sandbox == nil {
return errors.New("E2B sandbox is unavailable")
}
return m.backend.SetTimeout(ctx, sandbox.ID, secondsUntil(expires))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the sandbox alive while a command is running

When a terminal command runs longer than its requested lease—for example, a 120-second command with lease_ttl_sec=60—this sets E2B's sandbox timeout only to the lease expiry before starting the command. Unlike the local janitor, E2B does not account for inflight, so it terminates the sandbox at 60 seconds and the otherwise-valid command fails midway. Set the remote timeout to cover both the lease and the active command deadline, then reduce it to the remaining lease after the command finishes.

Useful? React with 👍 / 👎.

Comment on lines +338 to +341
m.mu.Lock()
expires := session.leaseExpiresAt
sandbox := session.sandbox
m.mu.Unlock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize concurrent sandbox timeout updates

With concurrent calls on one session, a request can snapshot the old shorter leaseExpiresAt, another request can extend the lease and complete its longer SetTimeout first, and then the older network request can complete last and shorten E2B's timeout again. The local lease remains extended while the remote sandbox expires early, causing later calls to lose the session. Serialize timeout updates or reject stale completions so the remote expiry is monotonic with the manager's lease.

Useful? React with 👍 / 👎.

Comment on lines +110 to +111
controlHTTP: &http.Client{Transport: transport.Clone(), Timeout: requestTimeout},
sandboxHTTP: &http.Client{Transport: transport},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the configured request timeout to envd calls

When envd accepts a connection but never returns response headers, WORKER_E2B_REQUEST_TIMEOUT_SEC does not bound the request because sandboxHTTP has neither a client timeout nor a transport response-header timeout. A task with a longer deadline can therefore occupy its capability and session slots for that entire deadline despite the documented E2B request timeout. Apply the configured timeout to envd connection/header establishment without imposing it on the duration of a successfully started streaming command.

Useful? React with 👍 / 👎.

@@ -0,0 +1,21 @@
module github.com/onlyboxes/onlyboxes/worker/worker-bridge-e2b

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the new worker module to CI

The checked .github/workflows/ci.yml path filters and jobs cover console, worker-docker, worker-sys, and worker-boxlite, but never worker/worker-bridge-e2b/**. Because this commit changes only that new module, the repository CI selects no Go build, vet, or test job for it, and future bridge changes can likewise merge without even compiling. Add a bridge path-filter output and check job, and include that job in ci-ok.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99ec500fa9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +172 to +175
if m.exportMaxBytes > 0 && probe.Size > int64(m.exportMaxBytes) {
return terminalResourceRunResult{}, newTerminalExecError(terminalResourceCodeFileTooLarge, "file exceeds export limit")
}
if err := m.exportResource(ctx, session.sandbox, filePath, signedURL, headers); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the export limit while streaming

When WORKER_TERMINAL_EXPORT_MAX_BYTES is nonzero and another permitted same-session command grows or replaces the file after this probe, the check still approves the old size while sandbox export streams the entire new file; worker-mode export is likewise unbounded when envd returns an unknown content length. Because same-session operations are concurrent by default, the configured export cap can be bypassed. Apply the limit to the bytes actually uploaded rather than only to preliminary metadata.

Useful? React with 👍 / 👎.

Comment on lines +5 to +7
// Version is injected at build time via:
// go build -ldflags "-X github.com/onlyboxes/onlyboxes/worker/worker-bridge-e2b/internal/buildinfo.Version=<version>"
var Version = defaultVersion

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Package the E2B worker in release builds

The checked .github/workflows/package-release.yml builds and uploads binaries for worker-docker, worker-sys, and worker-boxlite, and publish-release depends only on those worker jobs; there is no build or asset for this new module. Consequently, published releases contain no runnable E2B bridge, and this documented version injection is never performed by the release pipeline. Add a bridge build/upload job and include it in the release gate.

Useful? React with 👍 / 👎.

Comment on lines +50 to +52
encoded := base64.StdEncoding.EncodeToString([]byte(code))
command := "printf '%s' '" + encoded + "' | base64 -d > /tmp/onlyboxes-pythonexec.py && uv run /tmp/onlyboxes-pythonexec.py"
result, err := r.backend.Run(ctx, sandbox, command, -1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound pythonExec output before buffering it

When submitted Python code emits large or unending stdout/stderr, passing -1 disables both limitedBuffer caps, so the worker retains the entire stream in memory. A single multi-megabyte result can also exceed gRPC's default message limit when the result is sent, interrupting the registry session, while sustained output can OOM the worker; the default concurrency permits 32 such executions. Apply a finite output limit before accumulating the envd stream.

Useful? React with 👍 / 👎.

Comment on lines +190 to +191
command := "python3 -c '" + script + "'"
output, err := m.backend.Run(ctx, session.sandbox, command, 64*1024)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bypass login startup files for resource helpers

When an earlier terminalExec modifies ~/.bash_profile or another login startup file, this resource probe runs through backend.Run, which always invokes /bin/bash -l -c. Even a harmless startup echo prefixes the expected JSON and makes decoding fail, while PATH or command hooks can also prevent later reads and exports. Execute internal resource helpers without user-controlled login-shell initialization.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2ef5e9983

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +242 to +246
if err := stream.Err(); err != nil {
if connect.CodeOf(err) == connect.CodeNotFound {
return CommandResult{}, fmt.Errorf("%w: %v", ErrSandboxNotFound, err)
}
return CommandResult{}, fmt.Errorf("receive E2B command output: %w", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Terminate envd processes when their output stream aborts

When Start has already emitted a PID and the context expires or the HTTP/2 stream fails, this returns without using the protocol's SendSignal RPC, and the receive loop never retains the PID. Since envd processes are independently addressable from their output streams, a transport failure leaves the command running while Execute releases the session normally; on deadline cancellation, sandbox destruction is also deferred whenever a same-session sibling is active. The orphaned command can therefore continue consuming resources or mutating the shared filesystem after its call has failed; retain the start PID and signal the process on premature stream termination.

Useful? React with 👍 / 👎.

return commandErrorResult(commandID, "execution_failed", fmt.Sprintf("terminalExec execution failed: %v", err))
}

resultPayload, err := json.Marshal(execResult)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound encoded terminal results below the gRPC receive limit

When stdout or stderr contains JSON-sensitive bytes such as NULs, the 1 MiB raw-output limit is insufficient because json.Marshal can expand each byte to a six-byte escape; a single stream can therefore exceed 4 MiB, and two quote-heavy streams can also cross that threshold. The console uses an unconfigured grpc.NewServer() at console/internal/grpcserver/server.go:9, so its default receive limit rejects the command result and closes the registry stream instead of returning the truncated output. Apply a limit to the serialized payload or configure a matching receive size on the console.

Useful? React with 👍 / 👎.

Comment on lines +519 to +520
for _, sandbox := range sandboxes {
m.killSandbox(sandbox)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up terminal sandboxes concurrently on shutdown

When the worker receives SIGTERM with many live sessions and E2B delete requests are slow or unreachable, this serial loop can spend the separate 10-second cleanup timeout on every sandbox. Even though the new createWG now waits for creations, a typical supervisor grace period can expire after only a few deletes, terminating the worker before the remaining billable sandboxes receive any kill request. Use bounded concurrent cleanup or a shared shutdown deadline so every sandbox is attempted within the grace window.

Useful? React with 👍 / 👎.

return fmt.Errorf("enqueue heartbeat: %w", err)
}

ackTimer := time.NewTimer(cfg.CallTimeout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Start the heartbeat timeout only after sending the frame

When many individually valid large results are queued—for example, concurrent terminalResource reads near the default 1 MiB limit—the heartbeat can sit behind up to 64 outbound frames, but this timer starts as soon as the channel accepts it rather than when senderLoop actually calls stream.Send. On a slow console connection, the queued base64 results can consume the entire 13-second default timeout before the heartbeat reaches the wire; two such false misses force a reconnect and discard in-flight results. Prioritize heartbeat frames or have the sender acknowledge the actual send before starting the ack timer.

Useful? React with 👍 / 👎.

if err != nil {
return pythonExecRunResult{}, fmt.Errorf("create E2B python sandbox: %w", err)
}
defer r.killSandbox(sandbox.ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return Python results before waiting for sandbox deletion

When the Python process finishes shortly before its dispatch deadline, this deferred cleanup can still block Execute for the independent 10-second delete timeout. If deletion crosses the command deadline, the console unregisters the pending command and reports a timeout even though execution completed successfully; the worker later sends a result that is ignored, and its Python capacity slot was occupied solely by cleanup in the meantime. Track cleanup asynchronously, including during worker shutdown, so remote deletion does not delay the completed command result.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40d4703d68

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
session := m.newSessionLocked(sessionID, leaseTarget)
m.mu.Unlock()
m.killSandbox(expiredSandbox)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid blocking session replacement on stale sandbox cleanup

When create_if_missing=true replaces an expired session and the E2B delete endpoint is slow or unreachable, this synchronous cleanup can consume the independent 10-second cleanup timeout before the replacement sandbox is created. A command with a shorter timeout_ms expires during the delete, so awaitSessionReady subsequently calls Create with an already-canceled context and all callers waiting on the new session share that failure, even though deleting the expired sandbox is not required to create its replacement. Perform this stale-sandbox cleanup asynchronously or otherwise keep it outside the replacement command's critical path.

Useful? React with 👍 / 👎.

Comment on lines +1 to +4
import architectureDarkUrl from '../../../../static/architecture-dark.svg?url'
import architectureUrl from '../../../../static/architecture.svg?url'
import architectureZhCnDarkUrl from '../../../../static/architecture.zh-CN-dark.svg?url'
import architectureZhCnUrl from '../../../../static/architecture.zh-CN.svg?url'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include external architecture assets in the website CI filter

These imports make the website depend on the root static/architecture*.svg files, but the checked website filter in .github/workflows/ci.yml only matches .github/workflows/ci.yml and website/**. A change affecting only these assets—such as accidentally deleting one—therefore skips the website test and build jobs even though the Vite build would have a broken import. Add the imported static asset paths to the website filter.

Useful? React with 👍 / 👎.

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.

2 participants