Skip to content

@persistent-ai/fireflow-sandbox

Isolated container environments for FireFlow — code execution, testing, and deployment inside ephemeral sandboxes, brokered through DBOS durable workflows.

What It Is

This package gives FireFlow flows and LLM agents a safe place to run untrusted or generated code. Each sandbox is an ephemeral, isolated container provisioned through the OpenSandbox runtime. A flow can create a container, run commands in it, stream output, move files in and out of the VFS, deliver secrets, reach services on container ports over HTTP/WebSocket, and tear the container down — all as durable operations that survive worker crashes.

Sandboxes are owner-bound: every operation is checked against the fireflow_sandbox_instances record, and only the caller who created a sandbox may act on it. Containers carry a TTL and are tracked by a durable watcher workflow that keeps their lifecycle status in sync.

Key properties:

  • Durable — every sandbox operation runs as DBOS steps with automatic crash recovery
  • Owner-only access — ACL check is the first step of every operation
  • Path-scoped — optional deny-first access rules restrict read / readwrite / execute by container path
  • Secret-safe.ffsecret references are resolved late, in the node, via ECDH; plaintext never crosses a DBOS boundary
  • Observable — operations emit events to a DBOS stream; long-running commands stream stdout/stderr to PortStream
  • Composable — nodes use @Passthrough() ports so sandboxId, access rules, and endpoint config chain across a flow

How It Works

Operation pattern

Sandbox operations are exposed to nodes via ISandboxContextService (context.services.sandbox), implemented by SandboxContextService. Every method follows the same three-phase shape, each phase wrapped in its own DBOS step so it is checkpointed and replay-safe:

Step 1: ACL check     — load sandbox_instances row, verify caller is owner,
                        optionally enforce path access rules
Step 2: Operation     — call the OpenSandbox SDK (create / run / pause / sync / ...)
Step 3: Event emit    — update the DB record + DBOS.writeStream(sandbox-events)

Short-lived operations (create, execute, kill, pause, resume, renew, getStatus, resolveEndpoint, httpRequest, writeSecretFile) are awaited inline. Long-running operations (executeStream, syncFromVFS, syncToVFS) return an ISandboxWorkflowHandle ({ workflowId, getResult, getStatus }) so a node can fire-and-forget or await completion. Their output is written to a DBOS stream as it is produced and the stream is closed at workflow level on completion.

The steps run inside the current execution workflow (so they share its DBOS context and stream lifetime). A dedicated fireflow-sandbox WorkflowQueue is created at module level — before DBOS.launch() — so a future deployment can route sandbox operations to specialized sandbox workers. The SandboxWorkflows class provides the same operations as standalone @DBOS.workflow() methods for that queue.

Lifecycle watcher

SandboxContextService.create() starts a SandboxWatcherWorkflow child workflow with the deterministic ID sandbox-watcher:${sandboxId}. It polls the container state every 10s inside a single DBOS step (the loop, including its delays, lives in one step — on restart DBOS re-runs the step fresh rather than replaying each poll) and syncs status / terminatedAt on the sandbox_instances row, stopping on any terminal state or when the container disappears.

Access control

checkSandboxACL() is the first step of every operation: it loads the sandbox record, rejects unknown / non-owner / terminated sandboxes. On top of ownership, checkSandboxPathAccess() enforces optional deny-first path rules with three access levels — read, readwrite, execute. Empty rules mean "allow all" (backward compatible); once any rule is set, access defaults to deny unless an allow rule matches the path prefix and level.

Secret delivery

resolveSecretMappingsToEnvs() resolves an array of .ffsecret VFS references to plaintext environment variables using the same ECDH flow as the vault secret nodes: read the .ffsecret file → request the secret from the vault with a per-batch ECDH public key → vault re-encrypts to that key pair → decrypt locally → extract the field. This must run in node.execute(context), where the ExecutionContext (and its ECDH key pair) exists; the resolved env map is then handed to the sandbox service via an in-memory call, never serialized across a DBOS boundary. Secret scope can be the caller or the flowOwner. SandboxWriteSecretFileNode writes a resolved secret to a file inside the container for credential-file workflows.

Endpoint proxying

resolveEndpoint() and httpRequest() resolve a container port to its OpenSandbox endpoint URL (with routing headers) behind the ACL check — the raw endpoint URL is never exposed to the flow graph. The HTTP/WebSocket nodes use this to talk to services running inside a sandbox. Owner-authenticated browser access to those ports (OAuth/PKCE) is provided separately by the proxy mounted on fireflow-execution-api; this package contributes the SandboxProxyConfig type and the proxy_config DB column that drive it.

Package Exports

The package ships client-safe and server-only entry points. Importing the server entry registers the sandbox nodes via their @Node() decorators.

ExportEnvironmentContents
@persistent-ai/fireflow-sandboxClient + ServerType definitions, access-rule schema, event types, config schemas
@persistent-ai/fireflow-sandbox/serverServer onlyEverything: DB schema, nodes, services, steps, types, workflows (triggers node registration)
@persistent-ai/fireflow-sandbox/nodesServer onlyThe 15 sandbox node classes (importing registers them)
@persistent-ai/fireflow-sandbox/dbServer onlysandboxInstances Drizzle table + SandboxInstanceRow / SandboxInstanceInsert types
@persistent-ai/fireflow-sandbox/typesClient + ServerAll type / schema definitions (re-exported from the root entry)
typescript
// Frontend / shared — types and schemas only
import { SandboxAccessRule, type SandboxEventPayload } from '@persistent-ai/fireflow-sandbox'

// Backend — full surface (services, workflows, steps, nodes)
import { SandboxContextService, SandboxWorkflows, checkSandboxACL } from '@persistent-ai/fireflow-sandbox/server'

// Schema sharing (executor / trpc re-export the table from here)
import { sandboxInstances } from '@persistent-ai/fireflow-sandbox/db'

Nodes

15 nodes in the sandbox category. Importing /server or /nodes registers them in the NodeRegistry; all are marked execution: { dbos: true }.

NodePurpose
SandboxCreateNodeProvision a container (image, resources, users, env, access rules, secrets, network policy, proxy config) → returns sandboxId
SandboxExecuteNodeRun a command and return stdout / stderr / exit code (blocking)
SandboxExecuteStreamNodeRun a long-lived command, streaming stdout/stderr/exit chunks to a PortStream
SandboxKillNodeTerminate the container
SandboxPauseNodePause a running container
SandboxResumeNodeResume a paused container
SandboxRenewNodeExtend the container TTL
SandboxStatusNodeRead-only liveness / state / optional metrics (works on terminated sandboxes too)
SandboxSyncFromVFSNodeTransfer files VFS → container (filters, gitignore, empty-dir handling)
SandboxSyncToVFSNodeTransfer files container → VFS (globs, exclude patterns, mirror mode, optional commit)
SandboxWriteSecretFileNodeResolve a .ffsecret and write it as a file inside the container
SandboxHttpRequestNodeAuthenticated HTTP request to a container port (endpoint resolution hidden)
SandboxHttpStreamNodeStreaming HTTP response from a container port
SandboxWebSocketNodeWebSocket connection to a container port
SandboxStreamSplitterNodeSplit a sandbox output stream into separate stdout / stderr / exit channels

Database

One table, fireflow_sandbox_instances (Drizzle, fireflow_ prefix), defined in src/db/schema.ts. It is the ownership / ACL entity for every sandbox operation.

ColumnTypeNotes
idtext PKSandbox ID (from OpenSandbox)
owner_idtextCaller who created the sandbox — checked on every operation
execution_id / workflow_idtextOriginating execution / workflow
imagetextContainer image
statustextcreating / running / paused / terminated
visibilitytextprivate (default)
workspace_idtextAssociated VFS workspace
access_rulesjsonbSandboxAccessRule[]
sandbox_users / default_uidjsonb / intPer-command uid/gid isolation
metadata / resource_specjsonbTags, CPU/memory spec
proxy_configjsonb{ defaultPort?, exposedPorts? } for the endpoint proxy
created_at / expires_at / terminated_attimestampTTL + lifecycle tracking

Indexes: owner_id, execution_id, status.

Events

Operations write a SandboxEventPayload to the sandbox-events DBOS stream:

SANDBOX_CREATED, SANDBOX_KILLED, SANDBOX_PAUSED, SANDBOX_RESUMED, SANDBOX_RENEWED, COMMAND_COMPLETED, SYNC_FROM_VFS_COMPLETED, SYNC_TO_VFS_COMPLETED.

Integration with Flow Execution

The package plugs into fireflow-executor at three points:

  1. Node registration — the executor imports @persistent-ai/fireflow-sandbox/server (via serialization.ts) so the sandbox node classes register at startup.
  2. Context serviceExecutionWorkflows constructs new SandboxContextService(dbosService, db, callerId, vfs, vfsWrite, sandboxWatcher) and injects it as context.services.sandbox, so sandbox nodes can broker operations using the flow's DBOS context, VFS access, and the caller identity for ACL.
  3. Watcher + queue wiringServiceFactory creates the singleton SandboxWatcherWorkflow (a DBOS ConfiguredInstance) and the executor's queue.ts imports the module-level sandboxQueue, both before DBOS.launch().

The sandbox_instances table is also re-exported through fireflow-trpc's Postgres schema so migrations and the rest of the platform share a single definition.

Configuration

OpenSandbox connection settings are read from the environment by createConnectionConfig() (cached per worker process):

VariableDefaultDescription
OPENSANDBOX_DOMAINlocalhost:8080OpenSandbox API host
OPENSANDBOX_API_KEY(none)API key (optional)
OPENSANDBOX_PROTOCOLhttphttp or https
OPENSANDBOX_USE_SERVER_PROXYfalseRoute SDK calls through the server proxy
OPENSANDBOX_REQUEST_TIMEOUT30Request timeout (seconds)

Sandbox queue concurrency:

VariableDefaultDescription
DBOS_SANDBOX_QUEUE_CONCURRENCY30Global concurrency across workers for the sandbox queue
DBOS_SANDBOX_WORKER_CONCURRENCY3Per-worker concurrency

Directory Structure

src/
├── index.ts        # Client-safe exports (types)
├── server.ts       # Server-only barrel (db, nodes, services, steps, types, workflows)
├── db/             # sandbox_instances Drizzle schema
├── types/          # Access rules, events, config schemas (resource/network/proxy/secrets/...)
├── nodes/          # 15 sandbox node classes
├── services/       # SandboxContextService + OpenSandbox connection config
├── steps/          # SDK calls, ACL, file transfer, glob utils, secret resolution
└── workflows/      # SandboxWorkflows, SandboxWatcherWorkflow, sandbox queue

Development

bash
pnpm --filter @persistent-ai/fireflow-sandbox build       # Compile
pnpm --filter @persistent-ai/fireflow-sandbox typecheck    # Type check
pnpm --filter @persistent-ai/fireflow-sandbox dev          # tsc watch
pnpm --filter @persistent-ai/fireflow-sandbox test         # Vitest
pnpm --filter @persistent-ai/fireflow-sandbox test:coverage

Dependencies

PackagePurpose
@alibaba-group/opensandboxSandbox container runtime SDK
@dbos-inc/dbos-sdkDurable workflow execution
@persistent-ai/fireflow-typesNode base classes, decorators, execution context, secret wrapping
ignore / minimatchGitignore + glob filtering for file sync
nanoidDurable workflow ID generation
turndownHTML → Markdown for HTTP responses
wsWebSocket connections to container ports

Peer dependencies: drizzle-orm, pg, superjson.

License

Business Source License 1.1 (BUSL-1.1) — see LICENSE.txt.


View source on GitHub →

Licensed under BUSL-1.1