FabricFabricHarness
Building Agents

Sandbox connectors

Modular remote sandbox adapters for Daytona, E2B, Modal, and custom providers.

Fabric Harness treats every execution backend as a SandboxEnv: a small interface for shell execution, file IO, path scoping, cleanup, and optional snapshots. Agent/session/runtime code does not need to know whether work is running in the virtual sandbox, local process, Docker, Daytona, E2B, Modal, Cloudflare, Foundry, Kubernetes, or another provider.

Contract

A remote provider adapter implements RemoteSandboxApi and wraps it with createRemoteSandboxEnv:

import { createRemoteSandboxEnv } from '@fabric-harness/sdk';
import type { RemoteSandboxApi } from '@fabric-harness/sdk';

const api: RemoteSandboxApi = {
  async exec(command, options) { /* provider shell call */ },
  async readFile(path) { /* UTF-8 file read */ },
  async readFileBuffer(path) { /* binary file read */ },
  async writeFile(path, content) { /* file write */ },
  async stat(path) { /* file stat */ },
  async readdir(path) { /* list names */ },
  async exists(path) { /* existence check */ },
  async mkdir(path, options) { /* mkdir */ },
  async rm(path, options) { /* remove */ },
};

export const sandbox = createRemoteSandboxEnv(api, { cwd: '/workspace' });

Provider SDK objects and credentials stay in your app code. Fabric only receives file paths, bytes, commands, cwd, env, and timeout values.

Package adapters

@fabric-harness/connectors ships dependency-free, structural adapters. Your app owns the provider SDK dependency and passes an initialized sandbox object into Fabric.

npm install @fabric-harness/connectors

The connector package declares provider SDKs as optional peers, so applications install only the provider they use.

Why maintained adapters instead of copied templates

A project-installed template is a useful starting point, but the generated adapter becomes application code as soon as it is copied. Fabric keeps the common provider mapping in a versioned package while still letting the application own sandbox creation, credentials, resource limits, and lifecycle.

ConcernMaintained Fabric adapterCopied project template
Provider SDK changesBounded peer ranges and one shared compatibility tableEach application must detect and port SDK changes
Runtime behaviorOne SandboxEnv contract across Node, Temporal, Cloudflare, and DatabricksBehavior can drift between generated copies
RecoveryPortable references and decoder registration support cross-process reattachmentReattachment must be designed per project
CancellationShared timeout, abort, orphan-settlement, and cleanup semanticsEvery copied adapter must preserve the runtime rules itself
SecurityCredentials stay in the provider client; capabilities and tenant ownership remain enforceable by the runtimeSecurity depends on each generated copy remaining current
VerificationThe same conformance runner checks binary files, cwd/env, streaming, timeout, abort, reconnect, and cleanupTests and retained evidence are application-owned
CustomizationStructural interfaces accept provider objects without importing their SDK into Harness coreDirect editing is flexible but creates a permanent fork

This does not make provider infrastructure implicit. The application still provisions the sandbox and chooses whether Fabric owns cleanup. Use a project-local remoteSandbox() adapter when an organization needs a provider SDK version or lifecycle policy outside the maintained compatibility range.

ProviderCompatible SDK rangeContract-tested version
Daytona@daytona/sdk >=0.195.0 <10.195.0
E2B@e2b/code-interpreter >=2.6.1 <32.6.1
Modalmodal >=0.9.0 <10.9.0
Vercel@vercel/sandbox >=1.10.1 <21.10.1
Kubernetes@kubernetes/client-node >=0.21.0 <0.220.21.0
Cloudflare Sandbox@cloudflare/sandbox >=0.9.2 <10.9.2
Cloudflare Shell@cloudflare/shell >=0.3.7 <0.40.3.7

Daytona

import { Daytona } from '@daytona/sdk';
import { daytonaSandbox } from '@fabric-harness/connectors';

const client = new Daytona({ apiKey: process.env.DAYTONA_API_KEY });
const remote = await client.create({ image: 'ubuntu:latest' });

const fabric = await init({
  sandbox: daytonaSandbox(remote, { cleanup: true }),
});

The adapter maps Daytona filesystem/process calls to Fabric's SandboxEnv and uses Daytona's workdir when available through daytonaSandboxFactory().

E2B

import { Sandbox } from '@e2b/code-interpreter';
import { e2bSandbox } from '@fabric-harness/connectors';

const remote = await Sandbox.create();
const fabric = await init({
  sandbox: e2bSandbox(remote, { cleanup: true }),
});

If your E2B package exposes a different class, adapt it to the structural E2BSandboxLike shape or wrap it in remoteSandboxEnv().

Fabric maps the native Modal TypeScript SDK sandbox directly. The structural modalSandbox() helper remains available for custom provider handles.

import { ModalClient } from 'modal';
import { modalSdkSandbox } from '@fabric-harness/connectors/modal';

const client = new ModalClient();
const app = await client.apps.fromName('fabric-harness', { createIfMissing: true });
const image = client.images.fromRegistry('node:22-alpine');
const remote = await client.sandboxes.create(app, image, { workdir: '/workspace' });
const fabric = await init({ sandbox: modalSdkSandbox(remote, { cleanup: true }) });

Generic adapter

Use remoteSandbox() to produce a reusable SandboxFactory:

import { remoteSandbox } from '@fabric-harness/connectors';

export function providerSandbox(client): SandboxFactory {
  return remoteSandbox({
    exec: (command, options) => client.exec(command, options),
    readFile: (path) => client.readFile(path),
    readFileBuffer: (path) => client.readFileBuffer(path),
    writeFile: (path, content) => client.writeFile(path, content),
    stat: (path) => client.stat(path),
    readdir: (path) => client.readdir(path),
    exists: (path) => client.exists(path),
    mkdir: (path, options) => client.mkdir(path, options),
    rm: (path, options) => client.rm(path, options),
  }, { workspacePath: '/workspace' });
}

Stream command output

SandboxExecOptions exposes the same output callbacks for local, Docker, and remote sandboxes:

const result = await env.exec('npm test', {
  timeout: 120_000,
  onStdout: (chunk) => process.stdout.write(chunk),
  onStderr: (chunk) => process.stderr.write(chunk),
});

E2B, Vercel, and Kubernetes forward provider output as it arrives. Daytona's compatible command API returns collected output, so the adapter invokes the callbacks immediately before the command promise settles. Custom and Modal adapters emit incremental chunks through the same options.

Resource and egress enforcement belongs in the provider's sandbox creation call. Configure CPU, memory, image, network blocking, and domain allowlists before passing the provider object to Fabric; use Fabric policy for tool, command, filesystem, and application-level network decisions inside the session.

Certification helper

Use the full contract runner before deploying a provider adapter:

import { assertSandboxCertification } from '@fabric-harness/connectors';

const env = daytonaSandbox(remote, { cleanup: true });
const report = await assertSandboxCertification(env, {
  provider: 'daytona',
  sdkPackage: '@daytona/sdk',
  sdkVersion: '0.195.0',
  credentialed: true,
  reconnect: async (ref) => daytonaSandbox(
    await client.get((ref.providerData as { workspaceId: string }).workspaceId),
  ),
  verifyCleanup: async () => { /* assert the workspace was deleted */ },
});

The runner verifies:

  1. POSIX shell execution and output
  2. exact binary file round trips
  3. working directory and environment forwarding
  4. timeout exit 124 and abort exit 130
  5. stdout/stderr callbacks
  6. JSON-safe portable references and cross-client reconnect
  7. provider cleanup verification

The returned SandboxCertificationReport is safe to retain as CI evidence: it records provider, SDK version, timings, and check outcomes without credentials or workspace content.

Verify a provider connection

Live tests are skipped unless enabled:

FABRIC_DAYTONA_TEST=1 DAYTONA_API_KEY=... pnpm --filter @fabric-harness/connectors test
FABRIC_E2B_TEST=1 E2B_API_KEY=... pnpm --filter @fabric-harness/connectors test
FABRIC_MODAL_TEST=1 MODAL_TOKEN_ID=... MODAL_TOKEN_SECRET=... pnpm --filter @fabric-harness/connectors test

Connector recipes

fh add still prints markdown recipes for project-local adapters:

fh add
fh add daytona | claude
fh add https://e2b.dev --category sandbox | claude

Recipes are useful when the provider SDK version or organization conventions require custom code. Package adapters are better when your provider object matches the structural interfaces.

Provider adapter checklist

  • Scope every path to the provider workspace root.
  • Honor cwd, env, and timeout on exec.
  • Convert text and binary content correctly.
  • Keep API keys and provider SDK objects outside model context/history.
  • Enforce provider-specific network/resource limits before launching work.
  • Implement cleanup for temporary sandboxes.
  • Implement snapshot/restore only when the provider supports it truthfully.
  • Add a unit test with a fake provider object.
  • Add a live test behind an env gate.