← All @molecule/* packages · App templates
@molecule/api-code-sandbox-flyio-spritesProvider 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-spritesnpm · Source on GitHub · Implements @molecule/api-code-sandbox
@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)
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.tsJSDoc, 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.
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' } })
provider
npm install @molecule/api-code-sandbox-flyio-sprites @fly/sprites @molecule/api-bond @molecule/api-code-sandbox @molecule/api-i18n
ServiceSpecA 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
}
SpriteDirentLikeThe directory-entry slice the provider reads from readdir.
interface SpriteDirentLike {
name: string
isDirectory(): boolean
}
SpriteExecResultLikeThe exec result slice the provider reads (SDK results satisfy it).
interface SpriteExecResultLike {
stdout: string | Buffer
stderr: string | Buffer
exitCode: number
}
SpriteFilesystemLikeThe 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>
}
SpriteLikeThe 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>
}
SpritesClientLikeThe 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>
}
SpritesConfigConfiguration 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
}
SpritesSandboxProviderFly Sprites implementation of the sandbox provider.
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.
providerThe provider implementation.
const provider: SandboxProvider
SPRITES_ACCESS_SHIM_PATHWhere the preload lives inside a sprite; NODE_OPTIONS=--require <path>.
const SPRITES_ACCESS_SHIM_PATH: '/etc/mol/sprites-access-shim.cjs'
SPRITES_ACCESS_SHIM_SOURCECommonJS 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"
Implements @molecule/api-code-sandbox interface.
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)
}
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-code-sandbox ^1.0.1@molecule/api-i18n ^1.0.1SPRITE_TOKEN (required) — Fly Sprites API token
sprite login then sprite org auth -o <org> and copy the printed token (org-slug/org-id/token-id/token-value).molecule-dev/1859631/.../...SPRITES_API_URL (optional) — Sprites API base URL — default: https://api.sprites.dev
https://api.sprites.dev@fly/sprites@molecule/api-bond@molecule/api-code-sandbox@molecule/api-i18nTraps 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.http_port (see
ensureService) — getPreviewUrl(port) cannot vary by port.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..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.readDir on a
missing path throws (ENOENT) per the core contract — [] always means
"exists and is empty"./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).