← All @molecule/* packages · App templates

@molecule/api-code-sandbox-flyio-sprites

Provider bond · code-sandbox · API (Node) · v1.0.1 · Apache-2.0

Fly Sprites (persistent hardware-isolated microVM) code sandbox provider

npm install @molecule/api-code-sandbox-flyio-sprites

npm · Source on GitHub · Implements @molecule/api-code-sandbox

How it works

@molecule/api-code-sandbox-flyio-sprites is a provider bond on the API (Node) side: it implements the code-sandbox core interface (@molecule/api-code-sandbox) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { bond } from '@molecule/api-bond'
import { provider } from '@molecule/api-code-sandbox-flyio-sprites'

bond('codeSandbox', provider)
// Requires SPRITE_TOKEN in the environment (see `sprite org auth`).

Works with: @molecule/api-bond, @molecule/api-i18n

Secrets: SPRITE_TOKEN, SPRITES_API_URL (optional)

Reference

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.

Fly Sprites (sprites.dev) code sandbox provider.

Sprites are persistent, hardware-isolated Linux environments purpose-built for agent/dev workloads: the filesystem persists for the sprite's lifetime (NVMe while running, object storage while cold), sleeping sprites wake automatically on the next request, every sprite gets its own HTTPS URL, and outbound traffic is governed by a DNS-based network policy. This bond maps that platform onto the @molecule/api-code-sandbox contract through the official @fly/sprites SDK.

Quick Start

import { bond } from '@molecule/api-bond'
import { provider } from '@molecule/api-code-sandbox-flyio-sprites'

bond('codeSandbox', provider)
// Requires SPRITE_TOKEN in the environment (see `sprite org auth`).
import { createProvider, ensureService } from '@molecule/api-code-sandbox-flyio-sprites'

const provider = createProvider({
  namePrefix: 'mol-',
  urlAuth: 'public',
  defaultNetworkRules: [
    { domain: 'registry.npmjs.org', action: 'allow' },
    { domain: 'github.com', action: 'allow' },
  ],
})
const sandbox = await provider.create({ projectId, env: { NODE_ENV: 'development' } })

Type

provider

Installation

npm install @molecule/api-code-sandbox-flyio-sprites @fly/sprites @molecule/api-bond @molecule/api-code-sandbox @molecule/api-i18n

API

Interfaces

ServiceSpec

A service definition the caller wants live.

interface ServiceSpec {
  /** Service name (e.g. `vite`). */
  name: string
  /** Executable. */
  cmd: string
  /** Arguments. */
  args: string[]
  /** Working directory. */
  dir?: string
  /** Environment variables. */
  env?: Record<string, string>
  /** Port the sprite URL should route to. */
  httpPort?: number
}

SpriteDirentLike

The directory-entry slice the provider reads from readdir.

interface SpriteDirentLike {
  name: string
  isDirectory(): boolean
}

SpriteExecResultLike

The exec result slice the provider reads (SDK results satisfy it).

interface SpriteExecResultLike {
  stdout: string | Buffer
  stderr: string | Buffer
  exitCode: number
}

SpriteFilesystemLike

The filesystem slice the provider consumes (SDK SpriteFilesystem satisfies it).

interface SpriteFilesystemLike {
  readFile(path: string, encoding: 'utf8'): Promise<string>
  /** The SDK PUTs `data` as the raw body, so a Buffer uploads binary in one call. */
  writeFile(path: string, data: string | Uint8Array): Promise<void>
  readdir(path: string, options: { withFileTypes: true }): Promise<SpriteDirentLike[]>
  rm(path: string): Promise<void>
}

SpriteLike

The sprite slice the provider (and ensureService) consume, structurally. The SDK's Sprite class satisfies it; tests inject plain fakes.

interface SpriteLike {
  readonly name: string
  url?: string
  status?: string
  /**
   * ONLY `execFile` is used by this bond. The SDK's string `exec(command)`
   * naive-splits on whitespace and runs the first token as the BINARY — no
   * shell — so `mkdir -p x && y` becomes `mkdir` with `&&` as an argument
   * (observed: uutils mkdir rejecting `-d` from a piped `base64 -d`). Every
   * molecule caller passes shell syntax, so commands go through
   * `execFile('sh', ['-c', command])`.
   */
  execFile(
    file: string,
    args: string[],
    options?: { cwd?: string; env?: Record<string, string>; timeout?: number },
  ): Promise<SpriteExecResultLike>
  filesystem(workingDir?: string): SpriteFilesystemLike
  updateNetworkPolicy(policy: { rules: PolicyRule[] }): Promise<void>
  updateResourcesPolicy(policy: {
    memory?: { limitMB: number; autoscale?: boolean }
  }): Promise<void>
  getService(serviceName: string): Promise<unknown>
  createService(serviceName: string, config: unknown, duration?: string): Promise<unknown>
  stopService(serviceName: string, timeout?: string): Promise<unknown>
  deleteService(serviceName: string): Promise<void>
}

SpritesClientLike

The slice of the @fly/sprites SDK client the provider consumes, structurally. Tests inject a plain object; production wraps a real {@link SpritesClient}.

interface SpritesClientLike {
  sprite(name: string): SpriteLike
  createSprite(
    name: string,
    options?: {
      config?: { ramMB?: number; cpus?: number; region?: string; storageGB?: number }
      environment?: Record<string, string>
      urlSettings?: { auth?: string }
      waitForCapacity?: boolean
    },
  ): Promise<SpriteLike>
  getSprite(name: string): Promise<SpriteLike>
  listSprites(options?: {
    prefix?: string
    maxResults?: number
    continuationToken?: string
  }): Promise<{
    sprites: { name: string; status?: string }[]
    hasMore: boolean
    nextContinuationToken?: string
  }>
  deleteSprite(name: string): Promise<void>
}

SpritesConfig

Configuration for the Fly Sprites sandbox provider.

interface SpritesConfig {
  /**
   * Sprites API token. Falls back to `SPRITE_TOKEN`. Generate one with
   * `sprite org auth` (interactive) or `SpritesClient.createToken()` from a
   * Fly.io macaroon.
   */
  token?: string
  /** Sprites API base URL. Falls back to `SPRITES_API_URL`, then `https://api.sprites.dev`. */
  baseUrl?: string
  /**
   * Prefix for every sprite name this provider owns. Defaults to `mol-`.
   * Listing and adoption are scoped to this prefix, so two deployments sharing
   * one Sprites organization must use distinct prefixes.
   */
  namePrefix?: string
  /**
   * URL auth mode applied to created sprites: `public` (anyone with the URL —
   * required for anonymous browser previews) or `sprite` (Bearer token
   * required). Defaults to `public`.
   */
  urlAuth?: 'public' | 'sprite'
  /** Per-request timeout for Sprites API calls, in ms. Defaults to 30000. */
  requestTimeoutMs?: number
  /**
   * DNS-based egress rules applied to every sprite at creation via the Sprites
   * network Policy API. Unset means NO policy is applied and sprite egress is
   * whatever the platform default allows (observed: unrestricted) —
   * `verifyEgress()` will then report `open`, and molecule.dev's production
   * boot refuses a non-`filtered` verdict. Example:
   * `[{ domain: 'registry.npmjs.org', action: 'allow' }, { domain: 'github.com', action: 'allow' }]`.
   */
  defaultNetworkRules?: PolicyRule[]
  /**
   * Extra hostnames appended to the scaffolded Vite dev server's
   * `VITE_ALLOWED_HOSTS` (written into `/etc/mol/env` at creation).
   * `.sprites.app` is always included — without it Vite 403s every request
   * arriving through the sprite's public URL.
   */
  extraViteAllowedHosts?: string[]
  /**
   * How long `verifyEgress()` waits for a just-applied network policy to
   * propagate before concluding the canary is genuinely reachable. Defaults to
   * 30000ms. A policy is not enforced the instant `updateNetworkPolicy`
   * returns, so a one-shot canary probe can race propagation and falsely
   * report `open` (observed in production). Tests set this to 0.
   */
  egressPropagationMs?: number
}

Classes

SpritesSandboxProvider

Fly Sprites implementation of the sandbox provider.

Functions

createProvider(config, client)

Creates a Fly Sprites sandbox provider.

function createProvider(config?: SpritesConfig, client?: SpritesClientLike): SandboxProvider
  • config — Provider configuration.
  • client — SDK client override, primarily for tests.

Returns: The provider.

ensureService(sprite, spec)

Ensures a service exists with EXACTLY this definition and is running.

Existing service with a different definition: stopped, redefined, restarted, then read back — a definition that did not take throws instead of leaving a stale server behind a fresh-looking success.

function ensureService(sprite: SpriteLike, spec: ServiceSpec): Promise<void>
  • sprite — The sprite.
  • spec — The desired service.

mapSpriteStatus(status)

Maps a sprite status string onto the core's four-state union.

LOSSY BY DESIGN: sprites report richer states (running, warm, cold, transitional strings). warm maps to sleeping and cold to stopped, but BOTH auto-wake on the next request — a stopped sprite is not dead the way a stopped container is, just slower to resume (its filesystem restores from object storage). Unknown transitional states map to creating so pollers keep polling rather than giving up.

function mapSpriteStatus(
  status: string | undefined,
): 'creating' | 'running' | 'sleeping' | 'stopped'
  • status — The sprite's reported status.

Returns: The core sandbox status.

renderPlatformEnv(env)

Renders env vars as the export K='v' lines molecule platform layers source from /etc/mol/env. Single quotes with embedded-quote escaping; CR/LF are stripped because a value with a newline would smuggle extra shell lines.

function renderPlatformEnv(env: Record<string, string>): string
  • env — The variables.

Returns: The file content.

spriteNameFor(prefix, projectId)

Builds the sprite name for a project id under the given prefix.

Lowercases and collapses everything outside [a-z0-9-] to - so a UUID (or any caller-supplied id) always yields a valid DNS-label-shaped name, then bounds the length.

function spriteNameFor(prefix: string, projectId: string): string
  • prefix — The provider's name prefix (e.g. mol-).
  • projectId — The caller's project id.

Returns: The sprite name.

Constants

provider

The provider implementation.

const provider: SandboxProvider

SPRITES_ACCESS_SHIM_PATH

Where the preload lives inside a sprite; NODE_OPTIONS=--require <path>.

const SPRITES_ACCESS_SHIM_PATH: '/etc/mol/sprites-access-shim.cjs'

SPRITES_ACCESS_SHIM_SOURCE

CommonJS source of the preload. Rewrites an EACCES answer from the access(2) family into the stat(2) answer: when stat succeeds the path exists, so existence-style checks succeed. For R_OK/W_OK/X_OK requests the real answer is unknowable while the platform bug stands — stat-success is the lesser error, because reads DO work on the affected paths and a false EACCES hard-breaks the sandbox.

const SPRITES_ACCESS_SHIM_SOURCE: "// Sprites access(2) EACCES workaround — REMOVE when Fly fixes cold-restore.\n//\n// Platform bug (docs/sprites-cold-restore-bug-report.md, molecule-workspace):\n// after a sprite rehydrates from cold storage, access(2) returns EACCES for\n// paths written before the sleep while stat/read/readdir all succeed. Node's\n// fs.existsSync/fs.accessSync sit on access(2), so Vite's boot check (and any\n// existsSync-gated tool) dies on every rehydrated sandbox. This preload falls\n// back to stat(2) whenever access(2) says EACCES: stat-success means the path\n// exists and is, in practice, readable (read(2) works on the affected paths).\n// For R_OK/W_OK/X_OK the true answer is unknowable while the bug stands —\n// stat-success is the lesser error than a false EACCES.\n'use strict';\nconst fs = require('fs');\n\nconst origAccessSync = fs.accessSync;\nconst origAccess = fs.access;\nconst origPromisesAccess = fs.promises.access;\nconst origExistsSync = fs.existsSync;\n\nconst isBuggedEacces = (err) => !!err && err.code === 'EACCES';\n\nfs.accessSync = function accessSync(path, mode) {\n  try {\n    return origAccessSync.call(fs, path, mode);\n  } catch (err) {\n    if (!isBuggedEacces(err)) throw err;\n    fs.statSync(path); // throws (e.g. ENOENT) when the path is genuinely absent\n    return undefined;\n  }\n};\n\nfs.access = function access(path, mode, callback) {\n  if (typeof mode === 'function') {\n    callback = mode;\n    mode = undefined;\n  }\n  origAccess.call(fs, path, mode, (err) => {\n    if (!isBuggedEacces(err)) return callback(err);\n    fs.stat(path, (statErr) => callback(statErr ? err : null));\n  });\n};\n\nfs.promises.access = async function access(path, mode) {\n  try {\n    return await origPromisesAccess.call(fs.promises, path, mode);\n  } catch (err) {\n    if (!isBuggedEacces(err)) throw err;\n    await fs.promises.stat(path); // rethrows when genuinely absent\n    return undefined;\n  }\n};\n\nfs.existsSync = function existsSync(path) {\n  // existsSync swallows the bugged EACCES into a plain false — double-check\n  // with stat before agreeing that the path is missing.\n  if (origExistsSync.call(fs, path)) return true;\n  try {\n    fs.statSync(path);\n    return true;\n  } catch {\n    return false;\n  }\n};\n"

Core Interface

Implements @molecule/api-code-sandbox interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-code-sandbox'
import { provider } from '@molecule/api-code-sandbox-flyio-sprites'

export function setupCodeSandboxFlyioSprites(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

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

Environment Variables

  • SPRITE_TOKEN (required) — Fly Sprites API token
    • Setup: Run sprite login then sprite org auth -o <org> and copy the printed token (org-slug/org-id/token-id/token-value).
    • Get it here: https://docs.sprites.dev/
    • Example: molecule-dev/1859631/.../...
  • SPRITES_API_URL (optional) — Sprites API base URL — default: https://api.sprites.dev
    • Setup: Override the Sprites API endpoint.
    • Example: https://api.sprites.dev

Runtime Dependencies

  • @fly/sprites
  • @molecule/api-bond
  • @molecule/api-code-sandbox
  • @molecule/api-i18n

Traps a consumer must know, each observed against the real platform:

  • templateId THROWS. Sprite checkpoints are per-sprite overlay snapshots and cannot seed a different sprite, so there is no cross-sprite template mechanism. Fast cold starts come from a pre-warmed sprite pool (create + install ahead of demand, adopt on project create), not from templates.
  • start/stop/sleep/wake are no-ops. Sprites sleep and wake themselves; a stopped (cold) sprite still auto-wakes on the next request — it is not dead, just slower to resume.
  • volumeName/volumeMountPath are ignored. The sprite filesystem is already persistent; there is no separate volume to mount.
  • The preview URL routes to the service that claims http_port (see ensureService) — getPreviewUrl(port) cannot vary by port.
  • A PUT on a RUNNING service is silently ignored (Service already running with that command), and the public /restart path 404s. Use ensureService, which stops/redefines/verifies, instead of raw service calls.
  • Vite must allow .sprites.app. The provider writes VITE_ALLOWED_HOSTS=.sprites.app (plus extraViteAllowedHosts) into /etc/mol/env; a scaffold that does not read that env var will 403 every preview request.
  • urlAuth: 'public' is the default so anonymous browsers can load previews; switch to 'sprite' only if every preview request can carry a Bearer token.
  • setResources only bounds memory, and the limit was NOT observed to apply to the live cgroup of a running sprite — treat it as binding on the next wake. CPU/disk changes throw.
  • File ops use the Sprites Filesystem API (HTTP), so readDir on a missing path throws (ENOENT) per the core contract — [] always means "exists and is empty".
  • WORKAROUND (remove when Fly fixes cold-restore): every sprite gets a preload at /etc/mol/sprites-access-shim.cjs, wired via NODE_OPTIONS in /etc/mol/env, that falls back to stat(2) when access(2) answers EACCES. A Sprites platform bug (see docs/sprites-cold-restore-bug-report.md in the molecule workspace) makes access(2) return EACCES on paths written before a COLD sleep/restore cycle while stat/read/readdir still work — which breaks fs.existsSync consumers like Vite's boot check. Caller-supplied NODE_OPTIONS are preserved (shim appended).