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

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().

Modal SDK shapes vary by deployment. Fabric therefore supports two paths:

  1. Pass any object that already implements RemoteSandboxApi to modalSandbox().
  2. Export a project-local factory for live tests.
import { modalSandbox } from '@fabric-harness/connectors';

const fabric = await init({
  sandbox: modalSandbox(myModalRemoteSandbox, { cwd: '/workspace' }),
});

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' });
}

Validation helper

Every provider adapter should pass the Fabric smoke contract:

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

const env = daytonaSandbox(remote, { cleanup: true });
await validateSandboxAdapter(env);

The validator performs:

  1. mkdir
  2. writeFile
  3. readFile
  4. exec
  5. rm

This does not prove every provider feature works, but it catches the common adapter failures: bad cwd scoping, text/binary conversion bugs, command execution mismatch, and cleanup issues.

Live tests

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 FABRIC_MODAL_ADAPTER_MODULE=file:///abs/path/modal-adapter.mjs pnpm --filter @fabric-harness/connectors test

For Modal, the module should export either createModalSandboxEnv() or createSandbox() and return a SandboxEnv.

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.

What is still left

The package adapters are functional and testable. Remaining production work is provider certification:

  • run live-gated tests in CI environments with real credentials;
  • document known SDK version compatibility for each provider;
  • add provider-specific streaming process/log support where SDKs expose it;
  • add richer resource/network policy mapping per provider.