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/connectorsThe 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.
| Concern | Maintained Fabric adapter | Copied project template |
|---|---|---|
| Provider SDK changes | Bounded peer ranges and one shared compatibility table | Each application must detect and port SDK changes |
| Runtime behavior | One SandboxEnv contract across Node, Temporal, Cloudflare, and Databricks | Behavior can drift between generated copies |
| Recovery | Portable references and decoder registration support cross-process reattachment | Reattachment must be designed per project |
| Cancellation | Shared timeout, abort, orphan-settlement, and cleanup semantics | Every copied adapter must preserve the runtime rules itself |
| Security | Credentials stay in the provider client; capabilities and tenant ownership remain enforceable by the runtime | Security depends on each generated copy remaining current |
| Verification | The same conformance runner checks binary files, cwd/env, streaming, timeout, abort, reconnect, and cleanup | Tests and retained evidence are application-owned |
| Customization | Structural interfaces accept provider objects without importing their SDK into Harness core | Direct 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.
| Provider | Compatible SDK range | Contract-tested version |
|---|---|---|
| Daytona | @daytona/sdk >=0.195.0 <1 | 0.195.0 |
| E2B | @e2b/code-interpreter >=2.6.1 <3 | 2.6.1 |
| Modal | modal >=0.9.0 <1 | 0.9.0 |
| Vercel | @vercel/sandbox >=1.10.1 <2 | 1.10.1 |
| Kubernetes | @kubernetes/client-node >=0.21.0 <0.22 | 0.21.0 |
| Cloudflare Sandbox | @cloudflare/sandbox >=0.9.2 <1 | 0.9.2 |
| Cloudflare Shell | @cloudflare/shell >=0.3.7 <0.4 | 0.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().
Modal
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:
- POSIX shell execution and output
- exact binary file round trips
- working directory and environment forwarding
- timeout exit
124and abort exit130 - stdout/stderr callbacks
- JSON-safe portable references and cross-client reconnect
- 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 testConnector recipes
fh add still prints markdown recipes for project-local adapters:
fh add
fh add daytona | claude
fh add https://e2b.dev --category sandbox | claudeRecipes 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, andtimeoutonexec. - 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
cleanupfor temporary sandboxes. - Implement
snapshot/restoreonly when the provider supports it truthfully. - Add a unit test with a fake provider object.
- Add a live test behind an env gate.