npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@molecule/api-code-sandbox

v1.7.1

Published

Code sandbox provider interface for isolated container execution

Readme

@molecule/api-code-sandbox

Code sandbox core interface for molecule.dev.

Defines the SandboxProvider contract for provisioning isolated execution environments (create/get/list/destroy, optional named-volume management) and the Sandbox handle each returns: lifecycle (start/stop/sleep/wake), exec, file I/O, getPreviewUrl, and file watching. Interface-only: bond a provider (e.g. @molecule/api-code-sandbox-docker) at startup; consumers stay provider-agnostic.

Beyond that core, a set of OPTIONAL capabilities covers what a control plane needs to actually operate a fleet: templates (commitTemplate, getTemplate, listTemplates, removeTemplate, publishTemplate, fetchTemplate, SandboxConfig.templateId) for warm starts and restore points, inspection (describe, find, listVolumes) for recovery and reconciliation, hibernation fidelity (hibernate/resume) for knowing whether a woken sandbox's processes survived, setResources for applying an entitlement after creation, exportFiles/importFiles for bulk workspace transfer, readFileBytes/writeFileBytes for exact binary file I/O, and capacity for refusing a boot before it damages the sandboxes already running.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { setProvider, requireProvider } from '@molecule/api-code-sandbox'
import { provider as docker } from '@molecule/api-code-sandbox-docker'

setProvider(docker) // at startup

const sandbox = await requireProvider().create({ projectId: project.id })
const result = await sandbox.exec('npm test', { timeout: 120_000 })
if (result.exitCode !== 0) console.error(result.stderr)
await sandbox.writeFile('notes/README.md', '# Hello')
const url = sandbox.getPreviewUrl(5173) // proxy/iframe target for the running app
await requireProvider().destroy(sandbox.id)

Type

core

Installation

npm install @molecule/api-code-sandbox @molecule/api-bond @molecule/api-i18n

API

Interfaces

SandboxResources

How much of a machine one sandbox may use.

diskMB is advisory: several providers cannot cap a filesystem's size at all (see each bond's @remarks), so a provider that cannot enforce it MUST say so in its documentation rather than silently accepting the number.

interface SandboxResources {
  /** CPU cores. Fractional values are allowed where the provider supports them. */
  cpu: number
  /** Memory ceiling in megabytes. */
  memoryMB: number
  /** Disk ceiling in megabytes. Advisory — not every provider can enforce it. */
  diskMB: number
  /**
   * Maximum number of processes. Bounds fork-bombs, which a memory cap alone does
   * not. Optional because it has no meaning on providers that do not expose a
   * process table to the caller; those ignore it.
   */
  pidsLimit?: number
}

SandboxConfig

Configuration for creating a new sandbox.

interface SandboxConfig {
  projectId: string
  /**
   * Provider region for resources created by THIS sandbox. A caller that has
   * recorded placement passes it here; providers keep their configured default
   * when it is omitted. Later operations address the created sandbox by id.
   */
  region?: string
  image?: string
  env?: Record<string, string>
  /** Docker volume name to mount at /sandbox/project for persistent storage. */
  volumeName?: string
  /**
   * Where inside the sandbox the persistent volume mounts. Providers keep their
   * historical default (the workspace root) when omitted.
   *
   * The caller — not the provider — knows which subtree is durable state worth
   * a volume and which is regenerable tooling that belongs on the (usually much
   * faster) image-backed root filesystem. Mounting the volume over the whole
   * workspace forces node_modules onto volume storage, where small-file IO can
   * be orders of magnitude slower than the rootfs (measured on Fly: a superset
   * npm reify that takes seconds on rootfs takes minutes on the volume).
   * Pointing the mount at just the app directory keeps the durable source on
   * the volume and everything regenerable on the rootfs.
   */
  volumeMountPath?: string
  /**
   * Boot from a template previously captured with
   * {@link SandboxProvider.commitTemplate} instead of the provider's base image.
   *
   * This is the caller-facing half of the template capability: the caller names
   * the template with its OWN identifier and never handles the provider-native
   * reference. Takes precedence over `image` when both are given.
   *
   * A provider MUST FAIL when the named template does not exist. Falling back to
   * the base image would boot a sandbox whose filesystem is not the one the
   * caller asked for — silently, and looking healthy.
   */
  templateId?: string
  /**
   * Ports INSIDE the sandbox that must be reachable from the control plane, to be
   * read back later via {@link SandboxDescriptor.ports}.
   *
   * The caller knows which ports its workload listens on; the provider does not,
   * and guessing produces a sandbox whose dev server is running and unreachable.
   * Providers keep their historical defaults when this is omitted.
   */
  publishPorts?: number[]
  /**
   * Extra provider-level labels merged into the container's labels (additive;
   * does not replace the provider's own `managed`/`projectId`/`volumeName`
   * labels). Lets callers tag containers for out-of-band recovery — e.g. a
   * production runtime applying `molecule.production=<projectId>` so its
   * long-lived containers are distinguishable from dev sandboxes and
   * recoverable across control-plane restarts.
   */
  labels?: Record<string, string>
  /**
   * The sandbox's MAIN process — what runs as pid 1 instead of the image's own
   * entrypoint/command.
   *
   * A sandbox whose processes are started by `exec` after creation is running
   * nothing at all the moment its host restarts it: the image boots, its idle
   * command runs, and every server the caller launched is gone. That is
   * invisible until it happens, because `status` reports the sandbox as running
   * — it IS running, just empty. A long-lived sandbox (a production instance)
   * must therefore describe its own boot, and this is where it says so.
   *
   * Passed as an argv array, not a shell string, so nothing re-parses it. The
   * command must be resolvable inside the sandbox at BOOT time — which usually
   * means it lives on the persistent volume rather than in the caller's memory:
   * a script on the rootfs is gone on the next start.
   *
   * A provider that cannot run a caller-supplied main process MUST throw rather
   * than ignore this, because silently keeping the image's own command produces
   * exactly the sandbox this option exists to prevent, and reports it healthy.
   */
  command?: string[]
  /**
   * What the provider does when the main process exits.
   *
   * Providers default to `'no'`: a sandbox whose process exits stays down for
   * the caller to inspect, which is right for a dev sandbox nobody is serving
   * from. It is wrong for anything long-lived — there `'always'` (restart on any
   * exit) or `'on-failure'` (restart on a non-zero exit) is what keeps the
   * workload up across a crash, an OOM kill, or a host migration without a
   * control plane in the loop.
   *
   * Only meaningful alongside {@link SandboxConfig.command} — restarting a
   * sandbox back into an idle image command restores nothing.
   */
  restartPolicy?: 'no' | 'on-failure' | 'always'
  /**
   * Env the sandbox's workload will run with, but which the CALLER delivers —
   * a file on the persistent volume, a secrets bond, an init script — rather
   * than handing to the provider.
   *
   * Providers must VALIDATE against it exactly as they validate `env` (a host
   * the sandbox cannot route to is unroutable however the value arrives) and
   * must NEVER persist it in their own configuration.
   *
   * That "never persist" is the entire point. A provider's create-time `env` is
   * stored in ITS control plane, not just in the sandbox: Fly returns the whole
   * Machine config — env included, verbatim — to any holder of an org token, so
   * every tenant's database password and API keys are readable in bulk from a
   * single credential, without touching the vault that encrypted them. Values
   * the workload can obtain another way therefore must not be handed over here.
   * Use `env` for the provider's own non-secret configuration, and this for what
   * the workload reads for itself.
   */
  selfDeliveredEnv?: Record<string, string>
  /**
   * Idle policy for THIS sandbox's public service, overriding the provider's
   * configured default.
   *
   * A caller that is about to spend minutes populating the sandbox needs it to
   * stay up, and an idle policy decided by PROXIED traffic cannot see that work:
   * files moved over the provider's own API (or commands run through its exec
   * endpoint) are not proxy traffic, so a Machine created with `suspend` parks
   * itself in the middle of a copy. A deploy therefore creates its instance with
   * `off` and restores the app's real policy with {@link Sandbox.setIdleAction}
   * once it is populated, booted and healthy. See
   * {@link SandboxProvider.defaultIdleAction} for the value to restore.
   *
   * Optional: providers with no proxy in front of their sandboxes (and no idle
   * concept) ignore it.
   */
  idleAction?: SandboxIdleAction
  resources?: SandboxResources
}

ExecResult

Result of executing a command in a sandbox.

interface ExecResult {
  stdout: string
  stderr: string
  exitCode: number
}

ExecOptions

Options for command execution.

interface ExecOptions {
  cwd?: string
  timeout?: number
  env?: Record<string, string>
}

SpawnOptions

Options for spawning a long-running process.

Same working directory / env / timeout meaning as {@link ExecOptions}, plus the one thing a long-lived interactive process needs and a one-shot exec does not: a terminal.

interface SpawnOptions extends ExecOptions {
  /**
   * Allocate a pseudo-terminal of this size for the process, instead of plain
   * pipes.
   *
   * A PTY is not cosmetic. Without one there is no job control, so Ctrl-C
   * (`0x03`) is just a byte on stdin that nothing turns into SIGINT, and no
   * width is negotiated, so tools that format to the terminal disable it. With
   * one, the sandbox side behaves exactly like a real shell — which is why an
   * interactive terminal should ask for it and a language server (JSON-RPC over
   * stdio, which a PTY would corrupt with echo and CR translation) must not.
   *
   * Providers that cannot allocate a PTY must reject the spawn rather than
   * silently return pipes: a caller that asked for a terminal and got pipes has
   * a terminal whose Ctrl-C does nothing, which is the failure this option
   * exists to prevent.
   */
  pty?: { cols: number; rows: number }
}

SpawnHandle

Handle to a spawned long-running process with streaming I/O.

interface SpawnHandle {
  /** Write data to the process's stdin. */
  write(data: string): void
  /** Register a callback for stdout data. */
  onStdout(cb: (data: string) => void): void
  /** Register a callback for stderr data. */
  onStderr(cb: (data: string) => void): void
  /** Register a callback for when the process exits. */
  onClose(cb: () => void): void
  /** Kill the spawned process. */
  kill(): void
  /**
   * Resize the process's terminal.
   *
   * Only meaningful when the spawn requested {@link SpawnOptions.pty}; present
   * only on handles that can actually do it, so a caller feature-detects
   * (`handle.resize?.(size)`) rather than trusting that a resize happened.
   *
   * @param size - The new terminal size in character cells.
   */
  resize?(size: { cols: number; rows: number }): void
}

DirEntry

Directory entry from readDir.

interface DirEntry {
  name: string
  type: 'file' | 'directory'
  size?: number
  /** If this entry is a symlink, the target path it points to. */
  symlinkTarget?: string
}

FileChangeEvent

File change event from watching.

interface FileChangeEvent {
  type: 'create' | 'modify' | 'delete'
  path: string
}

HibernationOutcome

What a sleep/wake cycle actually did to the sandbox.

The single fact a caller needs and cannot infer: did the running process tree survive? Every provider can suspend a sandbox, but they differ on whether the processes come back — a memory-snapshot suspend restores them, a plain stop does not — and a provider commonly does BOTH, choosing per call depending on whether the snapshot mechanism is available on that host at that moment.

Consumers must act on the difference. A sandbox whose dev servers were started as detached processes comes back from a stop-style wake alive but serving nothing: the container runs, the preview never answers, and nothing in status says so. molecule.dev learned this by shipping it — the fix is a relaunch after a wake that did not preserve processes, which is impossible to write against a Promise<void>.

interface HibernationOutcome {
  /**
   * Whether the process tree that was running before the call is running after
   * it. `false` means the caller must restart anything it had launched.
   */
  processesPreserved: boolean
  /**
   * Which mechanism the provider actually used, for logs and analytics — e.g.
   * `'checkpoint'`, `'stop'`, `'suspend'`. Free-form on purpose: it identifies a
   * provider's own mechanism and callers must not branch on it. Branch on
   * `processesPreserved`.
   */
  mechanism: string
  /** Human-readable detail, especially when a preferred mechanism was unavailable. */
  detail?: string
}

Sandbox

A running sandbox instance.

interface Sandbox {
  id: string
  status: 'creating' | 'running' | 'sleeping' | 'stopped'
  previewUrl: string

  start(): Promise<void>
  stop(): Promise<void>
  sleep(): Promise<void>
  wake(): Promise<void>

  exec(command: string, opts?: ExecOptions): Promise<ExecResult>

  /** Spawn a persistent process with streaming I/O. */
  spawn?(command: string, opts?: SpawnOptions): Promise<SpawnHandle>

  readFile(path: string): Promise<string>
  writeFile(path: string, content: string): Promise<void>

  /** Read a file's exact bytes through the provider's FILE API. */
  readFileBytes?(path: string): Promise<Uint8Array>

  /** Write exact bytes to a file through the provider's FILE API (the inverse of {@link Sandbox.readFileBytes}), creating parent directories as needed and replacing… */
  writeFileBytes?(path: string, data: Uint8Array): Promise<void>

  /** List a directory. */
  readDir(path: string): Promise<DirEntry[]>
  deleteFile(path: string): Promise<void>

  getPreviewUrl(port?: number): string
  onFileChange(cb: (event: FileChangeEvent) => void): () => void

  /** Suspend the sandbox and REPORT what that cost. */
  hibernate?(): Promise<HibernationOutcome>

  /** Resume a suspended sandbox and REPORT whether its processes came back. */
  resume?(): Promise<HibernationOutcome>

  /** Change this sandbox's resource ceilings while it runs. */
  setResources?(resources: Partial<SandboxResources>): Promise<void>

  /** Change the idle policy applied to this sandbox's public service. */
  setIdleAction?(action: SandboxIdleAction): Promise<void>

  /** Stream a directory tree out of the sandbox as a POSIX tar archive. */
  exportFiles?(path: string): Promise<AsyncIterable<Uint8Array>>

  /** Stream a POSIX tar archive into the sandbox, extracting it at `path`. */
  importFiles?(path: string, archive: AsyncIterable<Uint8Array>): Promise<void>
}

(Member comments are shortened to their first sentence to fit; the full declaration is in node_modules/@molecule/api-code-sandbox/dist/types.d.ts.)

EgressVerdict

The result of proving a sandbox's egress restrictions.

interface EgressVerdict {
  state: EgressFilterState
  /** Human-readable explanation — shown to operators, so name the mechanism checked. */
  detail: string
  /** Provider-specific remediation, when the verdict is not `filtered`. */
  remediation?: string
}

SandboxTemplate

A filesystem captured from a sandbox, which later sandboxes can boot from.

One primitive, deliberately not two. A "warm start template" shared by every project with the same dependency set and a "restore point" belonging to one user are the same mechanism — capture a filesystem, boot a new sandbox from it — differing only in who may use it and how long it is kept. Those are the CALLER's policy. Splitting them here would mean two near-identical method families and a provider forced to guess which retention rules apply.

interface SandboxTemplate {
  /** The caller's own identifier, exactly as passed to `commitTemplate`. */
  id: string
  /**
   * The provider-native reference (an image tag, a snapshot id, a URL).
   * OPAQUE — surface it in operator output, never parse or construct it.
   */
  ref: string
  /** When the template was captured, ISO 8601. */
  createdAt: string
  /** Size on the provider's storage, or `null` when the provider cannot report it. */
  sizeBytes: number | null
  /** Free-form label recorded at capture time. */
  label?: string
  /**
   * Whether at least one sandbox is currently backed by this template.
   *
   * This exists for exactly one reason: callers evict templates on a budget, and
   * deleting the filesystem a live sandbox boots from destroys that sandbox. The
   * provider is the only party that can see the reference, so it must report it —
   * and a provider that cannot determine it MUST report `true` (assume in use)
   * rather than `false`, because a wrong `false` here is data loss.
   */
  inUse: boolean
}

CommitTemplateOptions

What to capture into a template, and what to call it.

interface CommitTemplateOptions {
  /** The sandbox whose filesystem is captured. */
  sandboxId: string
  /**
   * The caller's identifier for the resulting template. Re-committing the same
   * id replaces it.
   */
  templateId: string
  /**
   * Absolute paths inside the sandbox that MUST end up in the template even when
   * they live on storage the provider's native capture excludes — typically a
   * mounted volume, which is exactly where a project's files are.
   *
   * The caller is TOLD these paths rather than the provider hunting for them. A
   * provider that guessed would have no way to distinguish "this sandbox has no
   * project files" from "I looked in the wrong place", and would commit an empty
   * template that boots and appears to work.
   */
  capturePaths?: string[]
  /** Free-form label recorded with the template. */
  label?: string
}

ListTemplatesOptions

Narrowing for {@link SandboxProvider.listTemplates}.

interface ListTemplatesOptions {
  /** Only templates whose id starts with this prefix. */
  idPrefix?: string
}

PortMapping

How a port inside a sandbox is reached from the control plane.

Providers assign the outside half dynamically, so it can only be read back after the sandbox is running. getPreviewUrl renders a configured template and cannot answer this.

interface PortMapping {
  /** The port the process inside the sandbox listens on. */
  sandboxPort: number
  /** Host or address the control plane connects to. */
  host: string
  /** Port on `host` that forwards to `sandboxPort`. */
  port: number
}

SandboxDescriptor

Everything the control plane needs to know about an existing sandbox WITHOUT obtaining a handle to it.

A Sandbox is a live handle for doing things; a descriptor is a record for deciding things — which sandbox is stuck, which is orphaned, which volume it pins, which port it answers on. Recovery and reconciliation loops read many and act on few, and building a handle per candidate is both wasteful and, for providers that validate on handle construction, impossible for the broken ones a recovery loop exists to find.

interface SandboxDescriptor {
  id: string
  /** The `projectId` this sandbox was created for, or `null` if it carries none. */
  projectId: string | null
  /** Lifecycle state, using the same coarse mapping as {@link SandboxProvider.get}. */
  status: Sandbox['status']
  /** Every label the provider holds for this sandbox, including caller-supplied ones. */
  labels: Record<string, string>
  /** When the sandbox was created, ISO 8601, or `null` when unavailable. */
  createdAt: string | null
  /**
   * When the sandbox was first started, ISO 8601, or `null` if it has NEVER been
   * started.
   *
   * The null case is the point: a sandbox that was created and never started is
   * not "stopped", it is wreckage from an interrupted creation — it holds its
   * storage, it will never run, and nothing else distinguishes it. molecule.dev
   * filled a host's disk with exactly these.
   */
  startedAt: string | null
  /** The template or image reference this sandbox booted from, or `null`. */
  templateRef: string | null
  /** The named volume attached to this sandbox, or `null` if it has none. */
  volumeName: string | null
  /** Reachable port mappings, empty when the sandbox is not running. */
  ports: PortMapping[]
}

SandboxQuery

Narrowing for {@link SandboxProvider.find}.

Every field is something the CALLER knows because it set it. There is deliberately no free-form query language: a provider that had to interpret one would answer differently per backend, and callers would encode one backend's dialect.

interface SandboxQuery {
  /** Labels that must all be present with these exact values. */
  labels?: Record<string, string>
  /** Restrict to these lifecycle states. Omitted means every state. */
  statuses?: Array<Sandbox['status']>
  /** Only sandboxes created for this project. */
  projectId?: string
}

VolumeInfo

A named volume and what the provider observes about it.

interface VolumeInfo {
  name: string
  /**
   * Whether any sandbox currently has this volume attached — including stopped
   * ones, which still own their data.
   *
   * The reason to list volumes at all is to reclaim leaked ones, and a wrong
   * `false` deletes a user's only copy of their work. A provider that cannot
   * determine attachment MUST report `true`.
   */
  attached: boolean
  /** When the volume was created, ISO 8601, or `null` when unavailable. */
  createdAt: string | null
  /** Size on disk, or `null` when the provider cannot report it. */
  sizeBytes: number | null
}

ListVolumesOptions

Narrowing for {@link SandboxProvider.listVolumes}.

interface ListVolumesOptions {
  /** Only volumes whose name starts with this prefix. */
  namePrefix?: string
  /** `false` for unattached volumes only, `true` for attached only, omitted for both. */
  attached?: boolean
}

SandboxCapacity

What the provider can still fit, so the caller can refuse a boot BEFORE it damages the sandboxes already running.

Policy stays with the caller: the floors, the error copy, and the decision to shed load are its business. Only the measurement is here, because only a provider knows what backs its sandboxes — a filesystem and a page cache on one, an account quota on another.

Missing is not zero. An absent headroom field means the provider could not measure that resource, and a caller must not read it as exhausted; that conflation is the same one {@link EgressFilterState} exists to prevent.

interface SandboxCapacity {
  /**
   * The provider's own verdict when it has one — e.g. an account quota it knows
   * is exhausted. `null` means it has no opinion and the caller should decide
   * from `headroom`.
   */
  admits: boolean | null
  /** Human-readable explanation, including what could NOT be measured and why. */
  detail: string
  /** Measured headroom. Every field is absent when unmeasurable. */
  headroom: {
    /** Free bytes on the storage that backs new sandboxes. */
    diskBytes?: number
    /**
     * Free inodes on that storage.
     *
     * Separate from `diskBytes` because they run out separately: a sandbox
     * populates a dependency tree of hundreds of thousands of tiny files, which
     * can exhaust a fixed inode table while free space still looks ample. Every
     * file creation on the host then fails while `df` reports gigabytes free.
     */
    inodes?: number
    /** Memory available for a new sandbox without swapping. */
    memoryBytes?: number
  }
}

ProductionHostingCapabilities

Whether a provider can fill a LONG-LIVED PUBLIC HOSTING role — running a deployed app that the public reaches, not an ephemeral dev sandbox.

The code-sandbox category serves both roles with one interface, and the interface alone cannot tell them apart: a provider can implement every method and still be unfit to host (sandboxes with a bounded lifetime, or no address a front can route to). Before this type, that mismatch surfaced only at the health gate of a real deploy — the failure class this declaration exists to move to boot, where wiring it is still a choice someone can read.

ABSENT MEANS NOT DECLARED, and an absent declaration MUST be read as "cannot" — the same convention as the optional methods below. A platform filling a hosting role refuses an undeclared provider rather than assuming fitness; a provider that is fit SAYS SO.

interface ProductionHostingCapabilities {
  /**
   * Instances live until they are destroyed — the provider never reaps one on
   * a schedule or a lifetime bound of its own. A platform's own idle parking
   * (`idleAction`) does not count against this: parking is reversible and
   * caller-driven, a bounded lifetime is not.
   */
  longLived: boolean
  /**
   * A running instance is reachable at an address the provider itself reports
   * (`describe()`'s port mapping or `getPreviewUrl()`), from wherever the
   * platform's front runs — not only from inside the provider's own network.
   */
  routable: boolean
}

SandboxProvider

Sandbox provider interface.

Each bond package (Docker, Firecracker, etc.) implements this interface to manage sandbox lifecycle.

interface SandboxProvider {
  readonly name: string

  /** Whether this provider can host long-lived, publicly routable instances (deployed apps). */
  readonly hostingCapabilities?: Readonly<ProductionHostingCapabilities>

  /** The idle policy this provider applies to a sandbox the caller does not override with {@link SandboxConfig.idleAction}. */
  readonly defaultIdleAction?: SandboxIdleAction

  /** Atomically install a prepared sandbox's startup command and steady idle policy, then start it and wait until it is running. */
  activate?(
    id: string,
    options: { command: string[]; idleAction: SandboxIdleAction },
  ): Promise<void>

  create(config: SandboxConfig): Promise<Sandbox>
  /** Obtain a usable handle. */
  get(id: string): Promise<Sandbox | null>

  /** Passive file access to an already-running sandbox. */
  readOnly?(id: string): Promise<Pick<Sandbox, 'readFileBytes' | 'exportFiles'> | null>
  list(userId: string): Promise<Sandbox[]>
  destroy(id: string): Promise<void>

  /** Create a named volume for persistent sandbox storage. */
  createVolume?(name: string): Promise<void>
  /** Remove a named volume. */
  removeVolume?(name: string): Promise<void>
  /** Check if a named volume exists. */
  volumeExists?(name: string): Promise<boolean>

  /** Enumerate named volumes, so a caller can find the ones it has lost track of. */
  listVolumes?(options?: ListVolumesOptions): Promise<VolumeInfo[]>

  /** Capture a sandbox's filesystem as a reusable template. */
  commitTemplate?(options: CommitTemplateOptions): Promise<SandboxTemplate>

  /** Look up one template by the caller's identifier. */
  getTemplate?(templateId: string): Promise<SandboxTemplate | null>

  /** Enumerate templates so the caller can enforce its retention policy. */
  listTemplates?(options?: ListTemplatesOptions): Promise<SandboxTemplate[]>

  /** Delete a template. */
  removeTemplate?(templateId: string): Promise<void>

  /** Copy a local template to the provider's shared store, so other hosts can use it. */
  publishTemplate?(templateId: string): Promise<void>

  /** Retrieve a template from the shared store onto this host, if it is there. */
  fetchTemplate?(templateId: string): Promise<SandboxTemplate | null>

  /** Describe one existing sandbox without building a handle for it. */
  describe?(id: string): Promise<SandboxDescriptor | null>

  /** Find existing sandboxes by what the caller labelled them. */
  find?(query?: SandboxQuery): Promise<SandboxDescriptor[]>

  /** Report what the provider can still fit. */
  capacity?(): Promise<SandboxCapacity>

  /** PROVE that untrusted code in a sandbox cannot reach the network freely. */
  verifyEgress?(): Promise<EgressVerdict>
}

(Member comments are shortened to their first sentence to fit; the full declaration is in node_modules/@molecule/api-code-sandbox/dist/types.d.ts.)

Types

SandboxIdleAction

What a provider does to a sandbox's public service when nothing is using it.

The words are Fly's (autostop on a Machine's service), because Fly is the provider whose idle policy is the reason this exists: suspend snapshots the Machine and resumes it from memory on the next request, stop shuts it down for a cold boot on the next request, and off never parks it. Providers whose sandbox has no proxy in front of it (Docker) have no idle policy at all — they leave the methods that take this type unimplemented rather than pretending.

type SandboxIdleAction = 'off' | 'stop' | 'suspend'

EgressFilterState

Whether a provider can prove that sandbox egress is actually restricted.

filtered and open are OBSERVATIONS, not configuration state — the whole point is that a provider must go and check rather than report back what it was told. inconclusive is for when the check itself could not run (no API credentials, a probe image missing); it must never be conflated with filtered, because "I could not look" and "I looked and it is safe" are the distinction a caller's security decision rests on.

type EgressFilterState = 'filtered' | 'open' | 'inconclusive'

SandboxProviderStaysBackwardCompatible

Proof that every bond written against the smaller interface still satisfies this one.

Making a capability REQUIRED breaks every existing implementation in the fleet, and it does so at THEIR build, far from the edit that caused it. This turns that into a type error in the file where the mistake is made. It is exported so it is a checked part of the package's surface rather than dead code a linter can strip.

type SandboxProviderStaysBackwardCompatible = Assert<
  OriginalSandboxProvider extends SandboxProvider ? true : false
>

SandboxStaysBackwardCompatible

The same proof for the sandbox handle. See {@link SandboxProviderStaysBackwardCompatible}.

type SandboxStaysBackwardCompatible = Assert<OriginalSandbox extends Sandbox ? true : false>

Functions

setProvider(provider, name)

Registers a sandbox provider. Called by bond packages (or the application's own wiring) during startup. Without name the provider becomes the active singleton; with name it is bonded as a named provider under the same category, alongside the singleton.

function setProvider(provider: SandboxProvider, name?: string): void
  • provider — The sandbox provider implementation to bond.
  • name — Optional named-provider slot (e.g. 'production').

getProvider(name)

Retrieves the bonded sandbox provider, or null if none is bonded.

function getProvider(name?: string): SandboxProvider | null
  • name — Optional named-provider slot; omitted reads the singleton.

Returns: The bonded sandbox provider, or null.

hasProvider(name)

Checks whether a sandbox provider is currently bonded.

function hasProvider(name?: string): boolean
  • name — Optional named-provider slot; omitted checks the singleton.

Returns: true if a sandbox provider is bonded.

requireProvider(name)

Retrieves the bonded sandbox provider, throwing if none is configured.

function requireProvider(name?: string): SandboxProvider
  • name — Optional named-provider slot; omitted reads the singleton.

Returns: The bonded sandbox provider.

Available Providers

| Provider | Package | | --------------- | ------------------------------------------ | | Docker | @molecule/api-code-sandbox-docker | | E2B | @molecule/api-code-sandbox-e2b | | Fly.io Machines | @molecule/api-code-sandbox-flyio | | Fly Sprites | @molecule/api-code-sandbox-flyio-sprites |

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-bond ^1.0.1
  • @molecule/api-i18n ^1.0.1

Runtime Dependencies

  • @molecule/api-bond

  • @molecule/api-i18n

  • Treat everything inside a sandbox as untrusted. Files and exec output are user/model-controlled: never eval them on the host, always cap exec with timeout, and pass secrets in via env only when the workload truly needs them.

  • Sandbox handles are not durable. Persist your projectId → sandbox id mapping and re-get(id) after restarts. Files that must outlive the container need a named volume (createVolume + SandboxConfig.volumeName) — the volume APIs are optional, so feature-detect (if (provider.createVolume)).

  • exec returns a result, it doesn't throw on failure. A non-zero exitCode is data — check it and read stderr. Long-running processes (dev servers) need spawn (optional — feature-detect), not exec.

  • readDir() THROWS when the path doesn't exist — an empty array means "exists and is empty", never "missing". The enumerating capabilities (find, listVolumes, listTemplates) follow the same rule: they throw rather than return [], because a caller that DELETES what these return must never be told "nothing is orphaned" by a failed query.

  • Feature-detect every optional capability (if (provider.commitTemplate), sandbox.hibernate?.()). An absent method means the provider cannot do it; a rejected call means it can and this attempt failed. Providers must never collapse the two by stubbing a capability they lack.

  • wake() does not tell you whether your processes survived — a checkpoint-style resume brings them back and a plain start does not, and the sandbox looks identical either way. Use hibernate/resume (optional) when you launched anything the sandbox needs to still be running.

  • A sandbox that must survive its own host restarting has to describe its own boot. Processes started with exec after creation do not come back when the host restarts, migrates or OOM-kills the sandbox — the image's idle command runs instead and status still reports running, because it is: running and empty. Give such a sandbox SandboxConfig.command (its main process, pointing at something on the persistent VOLUME — a script on the rootfs is gone by then) plus SandboxConfig.restartPolicy. Dev sandboxes, which nobody serves from, are fine with the 'no' default.

  • Wire with setProvider(provider) (or bond('code-sandbox', provider) — this core reads the bond registry).

  • exportFiles(path) archives are rooted at the last segment of path (exportFiles('/workspace/my-app') → my-app/…, Docker's archive shape), and the inverse is importFiles(dirname(path), archive). Never assume ./… rooting and never extract at / unless you exported a top-level directory — a differently rooted archive lands files in the wrong place with no error.

  • Never move binary data through exec stdout. Command output is not a binary channel: a provider may truncate or re-encode a large output (E2B's envd cut a 256 KB chunk's base64 to 60 KB) and exec still reports success. Read and write bytes with readFileBytes/writeFileBytes (optional — feature-detect); to bound memory, cut the file into pieces inside the sandbox (dd … of=<part>) and read each piece.

Translations

Translation strings are provided by @molecule/api-locales-code-sandbox.