Deno Sandbox
Deno Sandbox provisions Firecracker
microVMs on the Deno Deploy edge: sub-second boot, hostname-level
egress allowlist, proxy-injected secrets, and one-call public preview
URLs. This adapter wraps the
@deno/sandbox Node
SDK and maps it onto the core SandboxService shape. The SDK runs
fine from plain Node. You do not need to use the Deno runtime to
use this provider.
Good fit for: agent code-exec with a clean secret-injection story, preview-URL workflows (“the model built a server, share the URL”), no-local-infra deployments. Less good fit for: workloads that need custom OCI images (Deno only takes default base + snapshots) or sandboxes that must run longer than 30 minutes.
Install
pnpm add @effect-uai/core @effect-uai/deno @deno/sandbox effectRequirements: Node ≥ 22 (the SDK uses await using /
Symbol.asyncDispose). A Deno Deploy account at
https://console.deno.com and an access token from Settings →
Organization tokens (ddo_…, works on its own) or
Personal tokens (ddp_…, also needs DENO_DEPLOY_ORG).
Wire it up
Set DENO_DEPLOY_TOKEN in env and the SDK picks it up automatically:
import { Effect, Layer } from "effect"import { layer as denoLayer } from "@effect-uai/deno/DenoSandbox"
const provider = denoLayer({ defaultRegion: "ord", // optional, applied when a request omits `region`})Or pass the token explicitly via the layer config (useful if you read it from a secret manager rather than env):
import { Redacted } from "effect"
const provider = denoLayer({ token: Redacted.make(process.env.MY_DEPLOY_TOKEN!), org: "my-org-slug", // required for personal `ddp_` tokens defaultRegion: "ord",})denoLayer registers two service tags and five capability markers
from one underlying implementation:
DenoSandbox: the typed tag. Yield this for the narrowed request shape (region,memory,labels,port, …).Sandbox: the generic tag. Yield this for provider-portable code; onlyCommonCreateRequestis accepted at the call site.- Capability markers shipped:
SandboxHostnameAllowlist,SandboxSecretInjection,SandboxSnapshots(read-side only),SandboxVolumes,SandboxPortExposure. See capabilities below.
Config
interface DenoSandboxConfig { readonly token?: Redacted.Redacted<string> // overrides DENO_DEPLOY_TOKEN readonly org?: string // overrides DENO_DEPLOY_ORG readonly apiEndpoint?: string // overrides DENO_DEPLOY_ENDPOINT readonly defaultRegion?: "ord" | "ams"}All four fields are optional. The SDK reads DENO_DEPLOY_TOKEN /
DENO_DEPLOY_ORG / DENO_DEPLOY_ENDPOINT from env by default; pass
the corresponding config field to override.
Request shape
interface DenoSandboxCreateRequest extends Omit<CommonCreateRequest, "secrets"> { readonly secrets?: ReadonlyArray<DenoSandboxBoundSecret> // no `header` field readonly region?: "ord" | "ams" readonly memory?: Memory.Input // bytes, branded Memory, or "1280 MiB" readonly labels?: Readonly<Record<string, string>> readonly port?: number // auto-exposed at boot; `sandbox.url` reaches it}On top of CommonCreateRequest
(image, timeout, env, network, volumes):
region:"ord"(Chicago) or"ams"(Amsterdam). Falls back to the layer’sdefaultRegion, then to the org’s default. Volumes are currently"ord"-only.memory: accepts a byte count, aMemorybranded value, or a human string like"1280 MiB"/"1 GiB". Deno’s accepted range at GA is 768 MiB – 4 GiB (default ~1.2 GiB).labels: up to 5 key/value pairs for filtering inSandbox.listand the Deploy dashboard. Keys ≤ 64 bytes, values ≤ 128 bytes.port: auto-expose this internal port at boot; the live sandbox’surlreaches it. Equivalent to callingSandbox.exposePortafter the fact, but available immediately without an extra round-trip.
Timeouts
Deno’s timeout accepts "session" (lives only while the SDK is
connected, the default) or a duration like "5m" / "300s". The
adapter normalizes CommonCreateRequest.timeout (Duration.Input)
to whole seconds before handing off:
const sb = yield * Sandbox.create({ timeout: "10 minutes" })// → mapped to `timeout: "600s"` on the SDKMax lifetime is 30 minutes; extend a live sandbox via the SDK’s
extendTimeout if you need longer.
Secrets
See the secrets section in the overview
for what BoundSecret is and why it exists.
const sb = yield * Sandbox.create({ secrets: [ { name: "OPENAI_API_KEY", value: Redacted.make("sk-..."), hosts: ["api.openai.com"], }, ], })Deno-specific notes:
- Placeholder shape: secrets surface inside the guest as opaque
placeholders: the real value materializes only on outbound HTTPS to
a host in
hosts. The exact placeholder format is provider-internal; don’t try to parse or interpolate it. - Header fixed to
Authorization: Bearer <value>. No per-secret custom header. The typedDenoSandboxBoundSecretomits theheaderfield at the type level; calls through the genericSandbox.createsurface that setheaderfail loudly withSandboxUnsupported.
Network policy
Deno accepts hostnames (with wildcards) and literal IPv4 / IPv6, but
no CIDR ranges. The adapter rejects cidrs at decode time rather
than silently truncating:
import * as Network from "@effect-uai/core/SandboxNetwork"
Network.allowHosts("api.openai.com", "*.anthropic.com") // ✓Network.allowCidrs("10.0.0.0/8") // ✗ → SandboxUnsupportedNetwork.blocked (no egress at all) and Network.open (provider
defaults) both work.
Snapshots
Deno snapshots are derived from volumes, not from running
sandboxes. You create a bootable volume, install software into a
sandbox booted from it, then snapshot the volume. The generic
Sandbox.snapshot(from, name)
helper doesn’t fit this shape and fails with SandboxUnsupported:
// ✗ fails with SandboxUnsupported at runtimeyield * Sandbox.snapshot(sb, "my-snapshot")
// ✓ use the per-provider escape hatchconst deno = yield * DenoSandbox.asEffect()const snapId = yield * deno.snapshotVolume(volumeId, "my-snapshot")The read-side helpers (listSnapshots, destroySnapshot) work as
usual. Boot a sandbox from a snapshot via Image.snapshot(slug) (the
SDK accepts snapshot slugs as the root field).
Volumes
Standard volume API works: createVolume(name) / destroyVolume(id)
/ listVolumes, mount via volumes: [{ id, mountPath }] on create.
A couple of Deno-specific constraints:
- Volumes are currently
ord-only; the adapter defaults to that region forcreateVolume. To pre-create inams(when it becomes available), drop down to the SDK’sclient.volumes.createdirectly. - Deno mounts volumes read-write only: the
readonly: truefield on thevolumesarray fails withSandboxUnsupported.
Expose ports
Sandbox.exposePort(sb, port) returns a real public HTTPS URL under
*.sandbox.deno.net, no extra config:
yield * sb.spawn({ cmd: ["deno", "run", "-NE", "/tmp/server.ts"] })const { url } = yield * Sandbox.exposePort(sb, 8000)// → https://<hash>.sandbox.deno.netThe URL stays live for the sandbox lifetime. Public, unauthenticated:
anyone with the URL can hit the service. Use the
port: 8000 field on create to auto-expose at boot if you don’t want
the extra round-trip.
Capabilities
| Marker | Shipped | Notes |
|---|---|---|
SandboxHostnameAllowlist | ✓ | Hostnames + wildcards + literal IPs. No CIDRs. |
SandboxSecretInjection | ✓ | Authorization: Bearer only; see secrets. |
SandboxSnapshots | ◐ | Read side only. create(from) fails: use snapshotVolume(volumeId). |
SandboxVolumes | ✓ | Volumes live in ord at present; read-write mounts only. |
SandboxPortExposure | ✓ | Runtime exposeHttp returns a public URL. |
SandboxPauseResume | no | No in-place memory-preserving pause. |
SandboxCustomImage | no | Default base or snapshots only: no Dockerfile, no OCI registry refs. |
SandboxKernelSession | no | No Jupyter/REPL surface. |
SandboxPty | no | Use execStream for byte-oriented IO. |
Calling snapshot(from) or exposePort(instance, port) against an
unmarked layer is a compile-time error, not a runtime
SandboxUnsupported.
Errors
| SDK / runtime failure | Mapped to |
|---|---|
MissingTokenError / InvalidTokenError / 401 / 403 | SandboxAuthFailed |
| 429 | SandboxQuotaExceeded |
404 / SANDBOX_ALREADY_TERMINATED | SandboxNotFound |
InvalidTimeoutError / InvalidMemoryError | SandboxInvalidRequest |
SandboxCommandError / ConnectionClosedError | SandboxExecFailed |
ImageRef.Registry / Dockerfile | SandboxUnsupported (image.*) |
NetworkPolicy.Allowlist with cidrs | SandboxUnsupported (network.cidrs) |
Volume mount with readonly: true | SandboxUnsupported (volumes.readonly) |
Custom secret header via generic surface | SandboxUnsupported (BoundSecret.header) |
Sandbox.snapshots.create(from) via generic surface | SandboxUnsupported (snapshots.create) |
| Empty argv array | SandboxInvalidRequest |
| Anything else on create / lookup | SandboxCreateFailed |
Recover per-tag with Effect.catchTag / Stream.catchTag. The
transient WebSocket-handshake 500 the Deploy edge occasionally
returns on spawn (SANDBOX_WEBSOCKET_HANDSHAKE_ERROR) is handled
inside the adapter via exponential-backoff retry (3 attempts): you
don’t see it.
Known quirks
- Pre-release concurrency cap. Deno Deploy currently allows up to 5 sandboxes per org concurrently. Long-running test suites should serialize creates or fan in/out carefully. Will lift.
- No custom images. Snapshots are the only “pre-installed deps”
story: install once into a volume, snapshot, then boot fresh
sandboxes from the snapshot. No equivalent of
Image.registry("python:3.12"). timeout: "session"is the default. The sandbox lives only as long as the SDK keeps its connection open. Pass an explicittimeoutif you intend to disconnect and reconnect viaattach.- Volumes only in
ord(as of 2026-05).createVolumedefaults toord; multi-region volumes when the platform supports them.