<!--
  Canonical spec for fabric-harness sandbox connectors.
  Shipped with @fabric-harness/sdk so it's available offline.

  Mirrored at the docs site:
    https://harness.fabric.pro/docs/building/sandbox-connectors

  Raw GitHub URL (must remain valid):
    https://raw.githubusercontent.com/Fabric-Pro/fabric-harness/main/packages/sdk/connector-spec/sandbox.md

  If you move this file, update `packages/cli/bin/fabric-harness.ts` (`fh add`)
  and `apps/docs/content/docs/building/sandbox-connectors.mdx` to match.
-->

# fabric-harness Sandbox Connector Spec

This document is the contract for building a fabric-harness sandbox connector. A sandbox connector adapts a third-party sandbox provider's SDK (Daytona, E2B, Modal, Vercel Sandbox, Cloudflare Containers, your in-house infra, etc.) into fabric-harness's `RemoteSandboxApi` interface so that agents can run shell commands and read/write files inside that sandbox.

If you are an AI coding agent reading this to build a connector for a user, follow this document literally and produce a single TypeScript file that exports a factory function returning a `SandboxEnv`.

---

## High-Level Shape

A connector is one TypeScript file. It exports a factory function that takes an already-initialized provider sandbox plus options, and returns a `SandboxEnv` via `createRemoteSandboxEnv`.

```ts
// .fabricharness/connectors/<provider>.ts (or ./connectors/<provider>.ts)
import { createRemoteSandboxEnv } from '@fabric-harness/sdk';
import type {
  RemoteSandboxApi,
  RemoteSandboxOptions,
  SandboxEnv,
  SandboxExecOptions,
  FileStat,
  ShellResult,
} from '@fabric-harness/sdk';
import type { Sandbox as ProviderSandbox } from '<provider-sdk>';

class ProviderSandboxApi implements RemoteSandboxApi {
  constructor(private sandbox: ProviderSandbox) {}
  // ... implement every required method (see "Required RemoteSandboxApi Methods" below)
}

export interface ProviderSandboxOptions extends RemoteSandboxOptions {
  // Provider-specific knobs (regions, image tags, etc.) go here.
}

export function provider(
  sandbox: ProviderSandbox,
  options: ProviderSandboxOptions = {},
): SandboxEnv {
  const api = new ProviderSandboxApi(sandbox);
  return createRemoteSandboxEnv(api, options);
}
```

Connectors are pure adapters. They map a provider sandbox to a `SandboxEnv` and stop there. They do not manage the sandbox's lifetime — the user owns what they create. If the provider exposes a teardown call, surface it via the `cleanup` option (see below).

---

## Imports You Will Use

All from `@fabric-harness/sdk`:

- `createRemoteSandboxEnv(api, options)` — wraps your `RemoteSandboxApi` into a `SandboxEnv` that fabric-harness can drive.
- `RemoteSandboxApi` — the interface you implement.
- `RemoteSandboxOptions` — options accepted by `createRemoteSandboxEnv`.
- `SandboxEnv` — what `createRemoteSandboxEnv` returns. You don't construct this yourself.
- `SandboxExecOptions`, `FileStat`, `ShellResult` — types used in method signatures.

---

## Required `RemoteSandboxApi` Methods

Every method below MUST be implemented unless explicitly marked optional.

| Method | Signature | Notes |
|---|---|---|
| `exec` | `(command: string, options?: SandboxExecOptions) => Promise<ShellResult>` | Run a shell command. Honor `options.cwd`, `options.timeoutMs`, `options.env`, `options.signal`. Return `{ command, exitCode, stdout, stderr, durationMs?, signal? }`. |
| `readFile` | `(path: string) => Promise<string>` | UTF-8 text. Throw if the file is binary or missing. |
| `readFileBuffer` | `(path: string) => Promise<Uint8Array>` | Binary read. Required for artifact handling. |
| `writeFile` | `(path: string, content: string \| Uint8Array) => Promise<void>` | Create parent dirs as needed. |
| `stat` | `(path: string) => Promise<FileStat>` | `{ size, isDirectory, isFile, mtimeMs }`. Throw on missing. |
| `readdir` | `(path: string) => Promise<string[]>` | Names only (not full paths). |
| `exists` | `(path: string) => Promise<boolean>` | Cheap probe. |
| `mkdir` | `(path: string, options?: { recursive?: boolean }) => Promise<void>` | Default `recursive: false`. |
| `rm` | `(path: string, options?: { recursive?: boolean; force?: boolean }) => Promise<void>` | Default both flags `false`. |

### Optional Methods (snapshot / fork / suspend / resume)

Implement these only if the provider supports them natively. Fabric will gracefully no-op when absent.

- `snapshot()` — capture provider-native state (e.g. E2B persistent sandbox, Daytona snapshot). Return a `SandboxSnapshot` (`{ provider, providerData }`).
- `restore(snapshot)` — restore in-place.
- `fork(snapshot)` — return a *new* `RemoteSandboxApi` initialized from the snapshot. Origin unaffected.
- `suspend()` / `resume()` — pause/start the sandbox to save cost while preserving filesystem state.

---

## Path Resolution

- Default `cwd` is whatever the provider's "home" is (e.g. `/workspace`, `/home/user`). Pick a sensible default in your factory and pass it via `options.cwd`.
- All paths in `RemoteSandboxApi` calls are POSIX. Don't accept Windows-style paths.
- Relative paths in tool calls are resolved against the session `cwd`. Your `exec` MUST honor `options.cwd`.

---

## Cleanup

If the provider has a teardown (e.g. `sandbox.kill()`, `sandbox.destroy()`), accept a `cleanup` option:

```ts
export function provider(
  sandbox: ProviderSandbox,
  options: ProviderSandboxOptions & { cleanup?: boolean | (() => Promise<void>) } = {},
): SandboxEnv {
  const api = new ProviderSandboxApi(sandbox);
  return createRemoteSandboxEnv(api, {
    ...options,
    ...(options.cleanup === true
      ? { cleanup: () => sandbox.destroy() }
      : typeof options.cleanup === 'function'
      ? { cleanup: options.cleanup }
      : {}),
  });
}
```

When `cleanup` runs, the user's session is ending — release resources but don't throw.

---

## Error Contract

- Network/transient errors → throw a regular `Error` with a clear message. Fabric retries based on its own policy.
- Path-not-found, permission-denied → throw `Error` whose `message` includes the path. Don't wrap in custom error classes (they don't survive cross-process boundaries).
- `exec` non-zero exit codes are NOT errors — they're returned as `{ exitCode: 1, stdout, stderr }`. Only throw if the call itself fails.

---

## Cross-Process Refs (advanced)

If this sandbox might outlive the agent process (durable sandbox, long task, replay), pass `encodeRef` so Fabric can serialize the sandbox identity:

```ts
return createRemoteSandboxEnv(api, {
  encodeRef: () => ({
    provider: '<provider-name>',
    providerData: { sandboxId: sandbox.id, region: sandbox.region },
  }),
});
```

The matching decoder is registered separately via `registerSandboxRefDecoder` in the host process. See `apps/docs/content/docs/building/sandbox-connectors.mdx` for a full example.

---

## Worked Example (Daytona)

```ts
import { createRemoteSandboxEnv } from '@fabric-harness/sdk';
import type {
  RemoteSandboxApi,
  RemoteSandboxOptions,
  SandboxEnv,
  SandboxExecOptions,
  FileStat,
  ShellResult,
} from '@fabric-harness/sdk';
import type { Workspace } from '@daytonaio/sdk';

class DaytonaSandboxApi implements RemoteSandboxApi {
  constructor(private workspace: Workspace) {}

  async exec(command: string, options?: SandboxExecOptions): Promise<ShellResult> {
    const start = Date.now();
    const result = await this.workspace.exec(command, {
      cwd: options?.cwd,
      env: options?.env,
      timeout: options?.timeoutMs,
    });
    return {
      command,
      exitCode: result.exitCode,
      stdout: result.stdout,
      stderr: result.stderr,
      durationMs: Date.now() - start,
    };
  }

  async readFile(path: string): Promise<string> {
    return this.workspace.fs.readFile(path, 'utf8');
  }
  async readFileBuffer(path: string): Promise<Uint8Array> {
    return this.workspace.fs.readFile(path);
  }
  async writeFile(path: string, content: string | Uint8Array): Promise<void> {
    await this.workspace.fs.writeFile(path, content);
  }
  async stat(path: string): Promise<FileStat> {
    const s = await this.workspace.fs.stat(path);
    return { size: s.size, isDirectory: s.isDir, isFile: !s.isDir, mtimeMs: s.mtimeMs };
  }
  async readdir(path: string): Promise<string[]> {
    return this.workspace.fs.readdir(path);
  }
  async exists(path: string): Promise<boolean> {
    return this.workspace.fs.exists(path);
  }
  async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
    await this.workspace.fs.mkdir(path, options);
  }
  async rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
    await this.workspace.fs.rm(path, options);
  }
}

export interface DaytonaSandboxOptions extends RemoteSandboxOptions {}

export function daytona(workspace: Workspace, options: DaytonaSandboxOptions = {}): SandboxEnv {
  return createRemoteSandboxEnv(new DaytonaSandboxApi(workspace), {
    cwd: '/workspace',
    ...options,
  });
}
```

---

## Checklist Before Submitting

Before handing the file back to the user, verify:

- [ ] Single TypeScript file, no inline test code, exports the factory function.
- [ ] All 9 required `RemoteSandboxApi` methods are implemented.
- [ ] `exec` honors `cwd`, `env`, `timeoutMs`, and returns `{ command, exitCode, stdout, stderr, durationMs }`.
- [ ] Path arguments are POSIX.
- [ ] No new runtime dependencies beyond the provider's official SDK and `@fabric-harness/sdk`.
- [ ] Cleanup is wired only if requested via options.
- [ ] Imports use the package's public entry: `@fabric-harness/sdk` (NOT subpaths like `/dist/...`).
- [ ] Function and type names follow the provider's casing convention (e.g. `daytona`, `e2b`, `modal`, `vercelSandbox`).
- [ ] At the top of the file, leave a one-line comment naming the provider and the SDK version targeted.

If the provider's docs are unclear on any method, leave a `// TODO` comment with a specific question rather than guessing.
