Skip to content

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

Terminal window
pnpm add @effect-uai/core @effect-uai/deno @deno/sandbox effect

Requirements: 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; only CommonCreateRequest is 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’s defaultRegion, then to the org’s default. Volumes are currently "ord"-only.
  • memory: accepts a byte count, a Memory branded 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 in Sandbox.list and the Deploy dashboard. Keys ≤ 64 bytes, values ≤ 128 bytes.
  • port: auto-expose this internal port at boot; the live sandbox’s url reaches it. Equivalent to calling Sandbox.exposePort after 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 SDK

Max 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 typed DenoSandboxBoundSecret omits the header field at the type level; calls through the generic Sandbox.create surface that set header fail loudly with SandboxUnsupported.

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") // ✗ → SandboxUnsupported

Network.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 runtime
yield * Sandbox.snapshot(sb, "my-snapshot")
// ✓ use the per-provider escape hatch
const 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 for createVolume. To pre-create in ams (when it becomes available), drop down to the SDK’s client.volumes.create directly.
  • Deno mounts volumes read-write only: the readonly: true field on the volumes array fails with SandboxUnsupported.

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

The 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

MarkerShippedNotes
SandboxHostnameAllowlistHostnames + wildcards + literal IPs. No CIDRs.
SandboxSecretInjectionAuthorization: Bearer only; see secrets.
SandboxSnapshotsRead side only. create(from) fails: use snapshotVolume(volumeId).
SandboxVolumesVolumes live in ord at present; read-write mounts only.
SandboxPortExposureRuntime exposeHttp returns a public URL.
SandboxPauseResumenoNo in-place memory-preserving pause.
SandboxCustomImagenoDefault base or snapshots only: no Dockerfile, no OCI registry refs.
SandboxKernelSessionnoNo Jupyter/REPL surface.
SandboxPtynoUse 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 failureMapped to
MissingTokenError / InvalidTokenError / 401 / 403SandboxAuthFailed
429SandboxQuotaExceeded
404 / SANDBOX_ALREADY_TERMINATEDSandboxNotFound
InvalidTimeoutError / InvalidMemoryErrorSandboxInvalidRequest
SandboxCommandError / ConnectionClosedErrorSandboxExecFailed
ImageRef.Registry / DockerfileSandboxUnsupported (image.*)
NetworkPolicy.Allowlist with cidrsSandboxUnsupported (network.cidrs)
Volume mount with readonly: trueSandboxUnsupported (volumes.readonly)
Custom secret header via generic surfaceSandboxUnsupported (BoundSecret.header)
Sandbox.snapshots.create(from) via generic surfaceSandboxUnsupported (snapshots.create)
Empty argv arraySandboxInvalidRequest
Anything else on create / lookupSandboxCreateFailed

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 explicit timeout if you intend to disconnect and reconnect via attach.
  • Volumes only in ord (as of 2026-05). createVolume defaults to ord; multi-region volumes when the platform supports them.