Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,62 @@

## Unreleased

- Added three cloud variants: `e2b`, `coreweave` and `cloudflare`.

`e2b` runs in a Firecracker microVM through E2B's code interpreter SDK, so it
holds a Jupyter kernel per context — `x = 1` in one call is still there in
the next — and answers with rich display data: a figure comes back as an
image, an HTML repr as HTML. It needs `E2B_API_KEY` and
`pip install code-sandboxes[e2b]`. `set_timeout()` extends the life of a
running sandbox and `get_host(port)` gives the public host of a port inside.

`coreweave` runs a container on CoreWeave's GPU cloud. What the SDK offers is
`exec` — a process at a time — so a namespace is held here instead: one
`python -u -c` session is started with the sandbox and fed JSON lines on
stdin, the same arrangement the `modal` variant uses, and snippets share a
namespace as they do everywhere else. A session that cannot start, or that
goes away, drops back to a process per snippet rather than failing. It needs
`CWSANDBOX_API_KEY` and `pip install code-sandboxes[coreweave]`.

`cloudflare` runs a container on Cloudflare's edge. Cloudflare's own SDK is a
Workers binding written in TypeScript, which a Python process cannot hold, so
this variant drives the SANDBOX BRIDGE — the Worker Cloudflare publishes to
expose the SDK over HTTP. Deploy it once with
`npm create cloudflare -- sandbox-bridge --template=cloudflare/sandbox-sdk/bridge/worker`,
then set `CLOUDFLARE_SANDBOX_API_URL` and `CLOUDFLARE_SANDBOX_API_KEY`. The
bridge gives a started process nothing to write to, so each snippet runs in
one of its own and state does not carry between calls — put what shares state
in one snippet, or keep it in a file, which does persist. Its manager creates,
gets and deletes; it cannot list, because the bridge has no endpoint that
enumerates sandboxes, and says so rather than answering with an empty list.

- Hardened the three new variants against silently doing something other than
what was asked. `cloudflare` now carries `SandboxConfig.env_vars` into every
snippet — the bridge takes no environment when it creates a sandbox, so they
had been accepted and dropped — refuses a `network_policy` it cannot apply
rather than leaving a sandbox believed to be cut off connected, refuses
`get_variable` with the reason instead of answering the misleading "no such
variable", and serves `files.read`/`files.write` through the bridge's own
file endpoints so they need no session at all. `coreweave` refuses the
variable APIs when there is no session process — under `stateful=False`, or
after one was lost — rather than reporting a successful set that vanishes
with the process, and a snippet that runs past its timeout now has its
session STOPPED rather than left running and changing the namespace behind a
call that already returned.

- A GPU asked of a variant that has none is now REFUSED rather than dropped.
`--gpu` reaches `coreweave`, `datalayer`, `daytona`, `kaggle` and `modal`,
and `code-sandboxes exec -v e2b --gpu H100` says which variants can give one
instead of running on a CPU as though nothing had been asked — a sandbox that
looks as though it asked for an H100 and did not is one whose timings mean
nothing. `--gpu` was previously accepted and silently ignored for every other
variant.

- Corrected the module docstring of the `modal` variant, which still described
the process-per-snippet behaviour that the session process replaced: `modal`
keeps a namespace between snippets, and falls back to a process per snippet
only when the session cannot be held.

- Added GPU support to the `daytona` variant. `gpu=` takes Daytona's own
flavors, `gpu_count=` how many, and several names comma-separated are an
ordered list of preferences Daytona falls back along — `gpu="H100,H200"`
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ Code Sandboxes (`code_sandboxes`) is a Python package for running code in isolat

Canonical variant names:

- `cloudflare`
- `coreweave`
- `datalayer`
- `daytona`
- `docker`
- `e2b`
- `eval`
- `google-colab`
- `jupyter-server`
Expand Down
9 changes: 9 additions & 0 deletions code_sandboxes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
Cloud container sandboxes:
- ModalSandbox: Modal cloud containers, per-snippet process execution
- DaytonaSandbox: Daytona cloud sandboxes, stateful Python interpreter
- E2BSandbox: E2B microVMs, stateful Python kernel and rich outputs
- CoreWeaveSandbox: CoreWeave containers, stateful Python session
- CloudflareSandbox: Cloudflare containers, through a sandbox bridge Worker

Features:
- Code execution with streaming support
Expand Down Expand Up @@ -60,6 +63,7 @@

from .base import Sandbox
from .client import CodeExecutionOutcome, CodeSandboxClient, execution_result_to_reply
from .cloudflare_sandbox import CloudflareSandbox
from .commands import CommandResult, ProcessHandle, SandboxCommands
from .console import (
EXIT_COMMANDS,
Expand All @@ -69,9 +73,11 @@
show_code,
show_result,
)
from .coreweave_sandbox import CoreWeaveSandbox
from .datalayer_sandbox import DatalayerSandbox
from .daytona_sandbox import DaytonaSandbox
from .docker_sandbox import DockerSandbox
from .e2b_sandbox import E2BSandbox
from .eval_sandbox import EvalSandbox
from .exceptions import (
ContextNotFoundError,
Expand Down Expand Up @@ -147,15 +153,18 @@
"EXIT_COMMANDS",
"KAGGLE_API_TOKEN_ENV",
"PROVIDERS",
"CloudflareSandbox",
"CodeError",
"CodeExecutionOutcome",
"CodeSandboxClient",
"CommandResult",
"Context",
"ContextNotFoundError",
"CoreWeaveSandbox",
"DatalayerSandbox",
"DaytonaSandbox",
"DockerSandbox",
"E2BSandbox",
"EvalSandbox",
"ExecutionResult",
"FileInfo",
Expand Down
2 changes: 1 addition & 1 deletion code_sandboxes/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

"""Code Sandboxes."""

__version__ = "1.1.1"
__version__ = "1.1.2"
26 changes: 25 additions & 1 deletion code_sandboxes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,18 @@ def create( # noqa: C901
from .daytona_sandbox import DaytonaSandbox

sandbox = DaytonaSandbox(config=config, **kwargs)
elif variant_value == "e2b":
from .e2b_sandbox import E2BSandbox

sandbox = E2BSandbox(config=config, **kwargs)
elif variant_value == "coreweave":
from .coreweave_sandbox import CoreWeaveSandbox

sandbox = CoreWeaveSandbox(config=config, **kwargs)
elif variant_value == "cloudflare":
from .cloudflare_sandbox import CloudflareSandbox

sandbox = CloudflareSandbox(config=config, **kwargs)
else:
raise ValueError(
f"Unknown sandbox variant: {variant}. "
Expand Down Expand Up @@ -358,7 +370,7 @@ def from_id(cls, sandbox_id: str, **kwargs) -> Sandbox:
return DatalayerSandbox.from_id(sandbox_id, **kwargs)

@classmethod
def list_environments(
def list_environments( # noqa: C901
cls,
variant: SandboxVariant | str = SandboxVariant.DATALAYER,
**kwargs,
Expand Down Expand Up @@ -398,6 +410,18 @@ def list_environments(
from .daytona_sandbox import DaytonaSandbox

return DaytonaSandbox.list_environments()
if variant_value == "e2b":
from .e2b_sandbox import E2BSandbox

return E2BSandbox.list_environments()
if variant_value == "coreweave":
from .coreweave_sandbox import CoreWeaveSandbox

return CoreWeaveSandbox.list_environments()
if variant_value == "cloudflare":
from .cloudflare_sandbox import CloudflareSandbox

return CloudflareSandbox.list_environments()
if variant_value == "kaggle":
from .kaggle_sandbox import KaggleSandbox

Expand Down
28 changes: 22 additions & 6 deletions code_sandboxes/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
#: brings it back to one of these.
_SUPPORTED_RUN_VARIANTS = frozenset(
{
"cloudflare",
"coreweave",
"datalayer",
"daytona",
"docker",
"e2b",
"eval",
"google-colab",
"jupyter-server",
Expand All @@ -44,6 +47,11 @@
)


#: The variants a GPU can be asked of. The others have none at all, and say so
#: rather than running on a CPU as though nothing had been asked.
_GPU_VARIANTS = frozenset({"coreweave", "datalayer", "daytona", "kaggle", "modal"})


@app.callback(invoke_without_command=True)
def _root(ctx: typer.Context) -> None:
"""Code sandboxes CLI."""
Expand Down Expand Up @@ -103,7 +111,7 @@ def _kaggle_kwargs(
return kwargs


def _resolve_variant_kwargs(
def _resolve_variant_kwargs( # noqa: C901
variant: str,
server_url: str | None,
kernel_id: str | None,
Expand Down Expand Up @@ -131,7 +139,15 @@ def _resolve_variant_kwargs(
if run_url:
kwargs["run_url"] = run_url

if variant in {"modal", "daytona", "datalayer", "kaggle"} and gpu:
if gpu:
# A GPU reaches the variants that can give one, and is REFUSED by the
# rest rather than dropped: a sandbox that looks as though it asked
# for an H100 and did not is one whose timings mean nothing.
if variant not in _GPU_VARIANTS:
raise typer.BadParameter(
f"--gpu is not something {variant} can give. The variants with "
"a GPU are: " + ", ".join(sorted(_GPU_VARIANTS)) + "."
)
kwargs["gpu"] = gpu

if spot:
Expand All @@ -149,10 +165,10 @@ def _resolve_variant_kwargs(
None,
"--variant",
"-v",
help=(
"Sandbox variant (datalayer, daytona, docker, eval, "
"google-colab, jupyter-server, kaggle, modal, monty)."
),
# Built from the set itself, the way the management commands build theirs:
# a hand-written list is one more place to forget a variant, and it had
# already fallen behind twice.
help="Sandbox variant (" + ", ".join(sorted(_SUPPORTED_RUN_VARIANTS)) + ").",
)
_RUN_TIMEOUT_OPTION = typer.Option(60.0, help="Code execution timeout (seconds).")
_RUN_ENVIRONMENT_OPTION = typer.Option(
Expand Down
Loading