Skip to content

feat(sandbox): add Daytona as a manual-flip failover for E2B#5860

Open
TheodoreSpeaks wants to merge 1 commit into
stagingfrom
feat/e2b-alternatives
Open

feat(sandbox): add Daytona as a manual-flip failover for E2B#5860
TheodoreSpeaks wants to merge 1 commit into
stagingfrom
feat/e2b-alternatives

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • E2B was a hard single point of failure — lib/execution/e2b.ts had no retry and no fallback, so one failed Sandbox.create() killed Python function blocks, JS-with-imports, shell, doc generation, and the Pi cloud agent
  • Extracts a SandboxRunner boundary (lib/execution/remote-sandbox) with an E2B runner and a Daytona runner, selected by the sandbox-provider-daytona AppConfig flag — flip it during an E2B outage, no redeploy
  • Everything above the provider boundary (marker parsing, mount materialization, file export, corruption handling) is unchanged; all 8 call sites are import-only updates
  • Adds scripts/verify-sandbox-parity.ts — 8 live executions against whichever provider the flag selects, as the pre-flip confidence check
  • Drops the dead E2BExecutionResult.images field (populated, never consumed)

Companion: simstudioai/mothership#369 (builds the shell + doc images on Daytona)

Notes

Selection resolves once before create() and never mid-execution, since user code has side effects. Every sandbox kind fails closed when its snapshot id is unset.

Three Daytona quirks the adapter absorbs:

  • language binds at create(), not per call — Daytona applies it as a sandbox label and silently runs JS through Python if passed to codeRun
  • Python routes via CodeInterpreter for its {name,value,traceback} error shape, which matches E2B's and keeps formatE2BError's line offsets correct
  • the streaming path (Pi) delivers env via the filesystem API, since SessionExecuteRequest has no env field and secrets must not reach a command line

Known caveat: Daytona cold-starts a sandbox in ~45s when it lands on a runner without the image cached (warm is ~800ms vs E2B's ~200ms). It costs wall-clock, not correctness — timeoutMs goes to runCode, not create, and plan sync budgets are 300s/3000s.

Type of Change

  • New feature

Testing

  • New conformance suite runs 11 scenarios twice, once per provider, asserting identical results — 25/25 pass
  • verify-sandbox-parity.ts against both providers with live sandboxes: E2B 8/8, Daytona 8/8 (marker round-trip, data-science imports, structured errors, outbound network, JS-under-node, shell envs, file mount + text export, xlsx compile + base64 binary export)
  • bun run lint and bun run check:api-validation:strict pass; tsc --noEmit clean
  • Affected suites 192/193 — the one failure is cloud-review-tools.test.ts spawning Python that needs a real rg binary, absent locally

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

E2B was a hard single point of failure: lib/execution/e2b.ts had no retry
and no fallback, so a failed Sandbox.create() killed Python function blocks,
JS-with-imports, shell, doc generation and the Pi cloud agent outright.

Extract a SandboxRunner boundary (lib/execution/remote-sandbox) with an E2B
runner and a Daytona runner, selected once per execution by the
sandbox-provider-daytona AppConfig flag. Everything above the provider
boundary — marker parsing, mount materialization, file export, corruption
handling — is unchanged.

Selection resolves before create() and never mid-execution, since user code
has side effects. Each sandbox kind fails closed when its snapshot id is unset.

Notes on the Daytona adapter:
- language binds at create(), not per call: Daytona applies it as a sandbox
  label and silently runs JS through Python if passed to codeRun
- Python routes via CodeInterpreter for its {name,value,traceback} error shape,
  which matches E2B's and keeps formatE2BError's line offsets correct
- timeouts convert ms to seconds
- the streaming path delivers env via the filesystem API, as
  SessionExecuteRequest has no env field and secrets must not reach a command line

Drops the dead E2BExecutionResult.images field (populated, never consumed).
@TheodoreSpeaks
TheodoreSpeaks requested a review from a team as a code owner July 22, 2026 21:36
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jul 22, 2026 9:41pm

Request Review

@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches all remote code execution paths (function blocks, shell, docs, Pi) and introduces a second provider whose SDK quirks (language at create time, streaming env delivery) must stay behavior-identical to E2B for safe failover.

Overview
Replaces the monolithic lib/execution/e2b module with lib/execution/remote-sandbox, a provider-agnostic layer that still exposes executeInSandbox, executeShellInSandbox, and withPiSandbox. E2B and Daytona sit behind a shared SandboxHandle API; the sandbox-provider-daytona feature flag (with SANDBOX_PROVIDER_DAYTONA fallback) picks the backend once per run before sandbox creation so executions are not split across providers mid-flight.

Call sites (function execute route, doc compile/extract/render/recalc, Pi cloud backends) are import renames only—behavior above the boundary (mounts, __SIM_RESULT__ parsing, exports) is intended to stay the same. New DAYTONA_* snapshot env vars mirror the existing E2B template ids; each sandbox kind still fails closed if its image id is missing.

Adds @daytonaio/sdk, a dual-provider conformance test suite, verify-sandbox-parity.ts for live pre-flip checks, shared pi-sandbox-packages for E2B/Daytona Pi image builds, and a Daytona Pi snapshot build script. Removes the unused images field from sandbox execution results and deletes the old e2b.test.ts in favor of provider conformance coverage.

Reviewed by Cursor Bugbot for commit e201d6c. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions github-actions Bot added the requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep label Jul 22, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Cross-repo companion check

One or more companion PRs aren't merged into staging yet. Merging this without them will leave copilot and sim out of sync — merge them in lockstep.

  • simstudioai/mothership#369OPEN, not merged (targets staging) — feat(sandbox): build the mothership sandbox images on Daytona too

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

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e201d6c. Configure here.

rootUser: true,
onStdout: options.onStdout,
onStderr: options.onStderr,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pi sandbox PATH not merged

High Severity

withPiSandbox forwards runCommand envs without merging the standard Pi PATH that included /root/.local/bin. The removed e2b.ts wrapper always appended that path so the global pi CLI was found; without it, Create PR / Review git steps can fail with “command not found” depending on the image default PATH.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e201d6c. Configure here.

// Daytona merges the two streams into `result`; splitting them back out is
// not possible, so stdout carries everything and callers that join the two
// (every caller today) are unaffected.
return { stdout: result.result ?? '', stderr: '', exitCode: result.exitCode ?? 0 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Daytona stderr empty on failures

Medium Severity

Daytona runCommand puts merged command output in stdout and always leaves stderr empty. Shared shell handling and review-tool failures still treat stderr as the error text, so non-zero exits on Daytona often surface as generic exit-code messages instead of the real stderr content E2B returned separately.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e201d6c. Configure here.

(chunk: string) => options.onStderr?.(chunk)
)
const finished = await this.sandbox.process.getSessionCommand(sessionId, commandId)
return { stdout: '', stderr: '', exitCode: finished.exitCode ?? 0 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Streaming Pi run empty streams

Medium Severity

Daytona’s streaming runCommand path delivers output only via callbacks and returns empty stdout and stderr. When the Pi agent exits non-zero, cloud backends format errors with piRun.stderr || piRun.stdout, which are both blank on Daytona even though logs streamed correctly.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e201d6c. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Daytona as a manually selected remote-sandbox provider. The main changes are:

  • A shared sandbox runner for E2B and Daytona.
  • Daytona adapters for code, shell, files, and Pi sessions.
  • Provider-neutral imports across function, document, and Pi execution.
  • Conformance tests, snapshot builders, and a live parity script.

Confidence Score: 4/5

Daytona-only failover remains blocked in function and document execution paths.

  • Provider selection and adapter boundaries are implemented consistently.
  • Function execution still requires the legacy E2B enablement flag.
  • Document availability still requires E2B credentials and an E2B template.

apps/sim/app/api/function/execute/route.ts and the provider availability logic used by document compilation.

Important Files Changed

Filename Overview
apps/sim/lib/execution/remote-sandbox/index.ts Adds provider selection and shared execution, file-export, marker-parsing, and Pi lifecycle logic.
apps/sim/lib/execution/remote-sandbox/daytona.ts Implements Daytona sandbox creation, code and command execution, filesystem access, streaming sessions, and cleanup.
apps/sim/lib/execution/remote-sandbox/e2b.ts Moves the existing E2B implementation behind the shared sandbox provider interface.
apps/sim/app/api/function/execute/route.ts Uses provider-neutral execution calls, but legacy E2B enablement still blocks Daytona-only function execution.
apps/sim/lib/copilot/tools/server/files/doc-compile.ts Uses provider-neutral document execution, but availability remains tied to E2B-specific configuration.
apps/sim/lib/core/config/feature-flags.ts Registers the global Daytona provider flag and its environment fallback.
apps/sim/next.config.ts Externalizes the Daytona SDK for server-side runtime loading.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Remote sandbox request] --> B{Legacy E2B gate allows path?}
    B -->|No| C[Request rejected before provider selection]
    B -->|Yes| D{Daytona feature flag enabled?}
    D -->|No| E[E2B provider]
    D -->|Yes| F[Daytona provider]
    E --> G[Shared execution and export flow]
    F --> G
Loading

Reviews (1): Last reviewed commit: "feat(sandbox): add Daytona as a manual-f..." | Re-trigger Greptile

exportedFileContent,
exportedFiles,
} = await executeShellInE2B({
} = await executeShellInSandbox({

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.

P1 Legacy Gate Blocks Daytona Execution

When Daytona is configured but E2B_ENABLED is false, the existing shell, Python, and JavaScript gates reject the request before this provider-neutral call can resolve Daytona. A Daytona-only failover configuration therefore cannot run function blocks; entry into the remote-sandbox path must depend on the selected provider's availability.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@@ -259,7 +263,7 @@ async function compileDocViaE2BPython(
// unaffected. Runs only after the user's script succeeds.

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.

P1 E2B Gate Hides Daytona Documents

Document compilation still enters this path through isE2BDocEnabled, which requires the E2B API key and E2B document template. With Daytona selected and DAYTONA_DOC_SNAPSHOT_ID configured, removing the unavailable E2B configuration marks the compiler unavailable before this call runs, so document generation fails during the intended failover.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant