You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(sandbox): add Daytona as a manual-flip failover for E2B (#5860)
* feat(sandbox): add Daytona as a manual-flip failover for E2B
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).
* improvement(sandbox): select the provider by SANDBOX_PROVIDER env var
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.
* fix(sandbox): gate remote execution by provider availability, not E2B
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.
* fix(sandbox): resolve SANDBOX_PROVIDER case-insensitively
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.
* fix(sandbox): use getErrorMessage in build/verify scripts
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.
* fix(sandbox): fall back to stdout for Daytona failure text
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.
* fix(sandbox): enforce timeout on Daytona streaming; drop leftover E2B 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.
* fix(sandbox): handle orphaned stream promise + preserve error detail
Two regressions from the previous timeout fix:
- When the timeout won the race, the abandoned getSessionCommandLogs promise
would reject on deleteSession with no handler (unhandledRejection). Attach a
.catch that records the error and yields an 'error' outcome, so a late
rejection is always handled.
- The streaming catch dropped the thrown error, so failures before any chunks
(env write, executeSessionCommand, missing cmdId) surfaced as empty output.
Fall back to getErrorMessage(error) when nothing streamed.
Adds conformance tests for the stream-reject and start-throw paths.
'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.'
1604
+
'JavaScript code with import statements requires a remote code sandbox to be enabled. Please remove the import statements, or contact your administrator to enable it.'
'Large execution values require the JavaScript isolated-vm runtime. Remove imports, select a nested field, or read the value in a JavaScript function without E2B.'
1615
+
'Large execution values require the JavaScript isolated-vm runtime. Remove imports, select a nested field, or read the value in a JavaScript function without a remote sandbox.'
1612
1616
)
1613
1617
}
1614
1618
1615
-
// Sandbox file mounts and sandboxPath exports only exist in the E2B
1616
-
// runtime; isolated-vm has no filesystem. Silently dropping a declared
1619
+
// Sandbox file mounts and sandboxPath exports only exist in the remote
1620
+
// sandbox runtime; isolated-vm has no filesystem. Silently dropping a declared
1617
1621
// sandbox input/output here produced "export succeeded" responses with
1618
1622
// zero bytes written, so refuse the call instead. The remediation depends
1619
1623
// on WHY this call runs in isolated-vm — "switch to python" is a dead end
1620
-
// when E2B is disabled or the call is a custom tool.
? "E2B is not enabled on this deployment, so there is no sandbox filesystem for any language. Pass input data via params and return output as the code's return value with outputs.files[].path (no sandboxPath)."
1624
+
// when no remote sandbox is enabled or the call is a custom tool.
? "No remote code sandbox is enabled on this deployment, so there is no sandbox filesystem for any language. Pass input data via params and return output as the code's return value with outputs.files[].path (no sandboxPath)."
1624
1631
: isCustomTool
1625
1632
? "custom tools always run in the isolated JavaScript VM, which has no sandbox filesystem. Pass input data via params and return output as the code's return value."
1626
-
: 'plain JavaScript runs in the isolated VM, which has no sandbox filesystem. Use language "python" so the code runs in the E2B sandbox, or drop sandboxPath and return the file content as the code\'s return value with outputs.files[].path.'
1633
+
: 'plain JavaScript runs in the isolated VM, which has no sandbox filesystem. Use language "python" so the code runs in the remote sandbox, or drop sandboxPath and return the file content as the code\'s return value with outputs.files[].path.'
0 commit comments