Skip to content

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

Open
TheodoreSpeaks wants to merge 7 commits into
stagingfrom
feat/e2b-alternatives
Open

feat(sandbox): add Daytona as a manual-flip failover for E2B#5860
TheodoreSpeaks wants to merge 7 commits 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)

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 23, 2026 9:01pm

Request Review

@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches the core remote execution path (Python/JS-with-imports/shell, doc generation, Pi agents); a provider adapter bug or misconfigured snapshot could break workflows org-wide, though conformance and live parity scripts reduce that risk.

Overview
Replaces the monolithic E2B module with lib/execution/remote-sandbox: shared marker parsing, mounts, exports, and Pi lifecycle sit above E2B and Daytona adapters, chosen once per run via SANDBOX_PROVIDER (default e2b). Daytona needs new env snapshot IDs plus @daytonaio/sdk; E2B behavior when that provider is selected stays gated on E2B_ENABLED and existing template IDs.

Call sites switch imports only — function execute, doc compile/extract/render, Pi cloud backends, copilot payloads, and tests now use executeInSandbox / executeShellInSandbox and isRemoteSandboxEnabled / isDocSandboxEnabled instead of E2B-named flags and helpers. User-facing errors say “remote code sandbox” rather than naming E2B.

Operational/testing additions: provider conformance tests (same scenarios on both providers), verify-sandbox-parity.ts for live checks, build-pi-daytona-snapshot.ts, and pi-sandbox-packages.ts so Pi images stay aligned across providers. The old e2b.ts implementation and its unit tests are removed in favor of the new stack.

Reviewed by Cursor Bugbot for commit 7b693c1. 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

Comment thread apps/sim/lib/execution/remote-sandbox/index.ts
Comment thread apps/sim/lib/execution/remote-sandbox/daytona.ts
Comment thread apps/sim/lib/execution/remote-sandbox/daytona.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Daytona as a manually selected alternative to E2B. The main changes are:

  • A shared remote sandbox interface for E2B and Daytona.
  • Provider-aware gates for code, shell, document, and Pi sandboxes.
  • Daytona adapters for execution, files, commands, and streaming sessions.
  • Provider conformance tests and a live parity verification script.
  • Import migrations for execution routes, document tools, and Pi handlers.

Confidence Score: 5/5

This looks safe to merge.

  • The execution gate now follows the selected provider and its credentials.
  • The document gate checks the same Daytona snapshot used during sandbox creation.
  • The migrated imports expose the symbols required by current callers.
  • No blocking issue was found in the updated fixes.

Important Files Changed

Filename Overview
apps/sim/lib/core/config/env-flags.ts Adds provider-aware availability checks for remote execution and document sandboxes.
apps/sim/app/api/function/execute/route.ts Moves shell, Python, and imported JavaScript execution to the provider-neutral sandbox interface.
apps/sim/lib/execution/remote-sandbox/index.ts Introduces provider selection and shared execution helpers for E2B and Daytona.
apps/sim/lib/execution/remote-sandbox/daytona.ts Implements Daytona sandbox creation, execution, file operations, and snapshot selection.
apps/sim/lib/copilot/tools/server/files/doc-compile.ts Routes document compilation through the selected remote sandbox provider.

Reviews (5): Last reviewed commit: "fix(sandbox): enforce timeout on Daytona..." | Re-trigger Greptile

Comment thread apps/sim/app/api/function/execute/route.ts
Comment thread apps/sim/lib/copilot/tools/server/files/doc-compile.ts
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).
Replaces the boolean sandbox-provider-daytona feature flag with a
SANDBOX_PROVIDER env var naming the provider ('e2b' default, or 'daytona').
A boolean doesn't scale to a third adapter; a keyed registry does.

- PROVIDERS is a Record<SandboxProviderId, SandboxProvider>, so adding an
  adapter is one entry plus one id-union member — an unhandled provider is a
  compile error, not a runtime surprise
- resolveProvider() reads env synchronously and throws on an unknown value
  (fail fast) instead of an async feature-flag lookup
- drops the sandbox-provider-daytona flag and SANDBOX_PROVIDER_DAYTONA fallback

Verified end-to-end: a Python function block through the running app routes to
Daytona (Creating Daytona sandbox, kind: code) with SANDBOX_PROVIDER=daytona.
Addresses the review round on #5860.

- Availability was gated on isE2bEnabled / isE2BDocEnabled, so a Daytona-only
  deployment (E2B_ENABLED unset) had its Python/shell/JS-with-imports and doc
  paths rejected before the provider-neutral sandbox call could run. Replace both
  with provider-aware flags (isRemoteSandboxEnabled / isDocSandboxEnabled) derived
  from the selected SANDBOX_PROVIDER's own credentials + image. E2B behavior is
  unchanged (the E2B branch mirrors the old definitions exactly).
- Make the function-block gate error messages provider-neutral.
- Daytona's streaming runCommand (Pi) returned empty stdout/stderr and delivered
  output only via callbacks, so the Pi cloud flow — which parses markers from
  stdout and formats errors from stderr — saw nothing. Accumulate the streamed
  chunks and return them while still forwarding to the callbacks.

Renames the env-flag exports (and the @sim/testing mock) to match. Adds a
conformance test that the streamed Pi output lands in stdout/stderr.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/execution/remote-sandbox/index.ts
Addresses the round-2 review on #5860. env-flags lowercased SANDBOX_PROVIDER
for the availability gate, but resolveProvider looked up the raw value in a
lowercase-keyed map — so 'Daytona' passed the gate then threw Unknown
SANDBOX_PROVIDER at create. resolveProvider now normalizes casing identically.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit bf6991c. Configure here.

check:utils flagged the inline `error instanceof Error ? error.message : ...`
pattern in the two new scripts. Use getErrorMessage from @sim/utils/errors,
matching the repo convention the check enforces.
Comment thread apps/sim/lib/execution/remote-sandbox/index.ts
Daytona merges both streams into stdout and returns an empty stderr, but the
shell-error, base64-export, and URL-mount error builders read only
result.stderr — so Daytona failures surfaced a generic 'Process exited with
code N' / 'base64 failed' / 'curl exited N' instead of the real command output
that the API and agents rely on. Fall back to stdout before the generic message
(provider-agnostic: E2B still populates stderr). Strengthens the shell-error
conformance test to assert the real output surfaces.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/execution/remote-sandbox/daytona.ts
Comment thread apps/sim/app/api/function/execute/route.ts
… copy

- Daytona's streaming path (Pi) started the command with runAsync:true and then
  awaited getSessionCommandLogs with no bound, so a hung command never timed out
  the way E2B's commands.run({ timeoutMs }) does. Race the log stream against the
  timeout; on expiry return exit 124 with the accumulated output, and the finally's
  deleteSession terminates the still-running command.
- Two user-facing strings still named E2B after the provider-neutral rename (the
  isolated-vm sandboxPath remediation and the disabled-xlsx message). Made both
  provider-neutral.

Adds a streaming-timeout conformance test.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@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 2 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 7b693c1. Configure here.

stdout,
stderr: stderr || `Command timed out after ${options.timeoutMs}ms`,
exitCode: 124,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Orphaned stream promise on timeout

Medium Severity

When the streaming timeout wins Promise.race, the pending getSessionCommandLogs promise is abandoned. deleteSession in finally then tears down the session, which commonly rejects that orphaned promise with no handler, risking an unhandledRejection during Pi timeouts.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7b693c1. Configure here.

const finished = await this.sandbox.process.getSessionCommand(sessionId, commandId)
return { stdout, stderr, exitCode: finished.exitCode ?? 0 }
} catch (error) {
return { stdout, stderr, exitCode: 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.

Streaming errors drop failure detail

Medium Severity

The streaming catch returns accumulated stdout/stderr and exit code 1 but never records the thrown error. Failures before any chunks (env file write, executeSessionCommand, missing cmdId) surface as empty output, so Pi clone/agent errors become opaque unknown error messages.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7b693c1. Configure here.

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