Skip to content

Start typing to search the documentation.

Sandbox Adapter API

AI-generated, awaiting review View as Markdown

A sandbox adapter adapts a third-party sandbox provider’s SDK into Flue’s SandboxFactory interface so that agents can run shell commands and read or write files inside that sandbox.

If you are a coding agent building a sandbox adapter for a user, follow this document literally and produce one TypeScript file that exports a factory function such as daytona(...) returning a SandboxFactory.

High-level shape

A sandbox adapter is one TypeScript file. It exports a factory function that takes an already-initialized provider sandbox plus options and returns a SandboxFactory. Flue calls factory.createSessionEnv({ id }) once per initialized harness and uses the returned SessionEnv for all shell and file operations.

// <source-dir>/sandboxes/<provider>.ts
import { createSandboxSessionEnv } from '@flue/runtime';
import type { SandboxApi, SandboxFactory, SessionEnv, FileStat } from '@flue/runtime';
import type { Sandbox as ProviderSandbox } from '<provider-sdk>';

class ProviderSandboxApi implements SandboxApi {
  constructor(private sandbox: ProviderSandbox) {}
  // Implement every method on SandboxApi.
}

export function provider(sandbox: ProviderSandbox): SandboxFactory {
  return {
    async createSessionEnv(): Promise<SessionEnv> {
      const sandboxCwd = '/workspace';
      const api = new ProviderSandboxApi(sandbox);
      return createSandboxSessionEnv(api, sandboxCwd);
    },
  };
}

Sandbox adapters are pure adapters. They map a provider sandbox to a SessionEnv rooted at the provider-owned base cwd and stop there. They must not apply an agent definition’s cwd: Flue resolves that value once against the adapter’s base cwd while initializing a root harness. A factory may be called again for later requests or workflow runs, and closing a harness does not destroy provider infrastructure. The application owns provider resource creation, reuse, and deletion.

Imports

Import these from @flue/runtime:

  • createSandboxSessionEnv(api, cwd) wraps your SandboxApi into a SessionEnv that Flue can drive. Pass the provider-owned base cwd, not an agent definition’s cwd.
  • SandboxApi is the interface you implement.
  • SandboxFactory is what your factory returns.
  • SessionToolFactory is the optional model-facing tool factory type for a custom sandbox.
  • SessionEnv is what createSandboxSessionEnv returns. Do not construct one yourself.
  • FileStat is the return type for stat().
  • SandboxOperationUnsupportedError rejects filesystem options that a provider cannot implement exactly.

Do not import internal runtime paths. @flue/runtime is the public surface for adapter authors.

TypeScript contracts

Always typecheck against the real types from @flue/runtime. If this page drifts from the runtime package, the runtime package wins.

SandboxApi

export interface SandboxApi {
  readFile(path: string): Promise<string>;
  readFileBuffer(path: string): Promise<Uint8Array>;
  writeFile(path: string, content: string | Uint8Array): Promise<void>;
  stat(path: string): Promise<FileStat>;
  readdir(path: string): Promise<string[]>;
  exists(path: string): Promise<boolean>;
  mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
  rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
  exec(
    command: string,
    options?: {
      cwd?: string;
      env?: Record<string, string>;
      timeoutMs?: number;
      signal?: AbortSignal;
    },
  ): Promise<{ stdout: string; stderr: string; exitCode: number }>;
}

timeoutMs is the primary cancellation contract. Every adapter should honor it by forwarding to the provider SDK’s native timeout option. signal is optional: adapters whose provider SDK supports mid-flight cancellation should forward it; others may ignore it.

SandboxFactory

export interface SandboxFactory {
  createSessionEnv(options: { id: string }): Promise<SessionEnv>;
  tools?: SessionToolFactory;
}

createSessionEnv is called once for each root harness initialization, and every session and nested task or Action scope of that harness shares the returned SessionEnv. The id option is the agent instance id for direct agent work or the workflow run id for a workflow. The same agent instance may initialize a new root harness for later submissions, so an adapter that keys provider resources on id must tolerate repeated calls and deliberately reuse or replace its provider resource. Flue closes in-memory harness state after the operation or run; it does not destroy provider infrastructure.

tools replaces the framework’s default model-facing tool list for this sandbox. Omit it for the standard filesystem and shell tools.

SessionToolFactory

export type SessionToolFactory = (
  env: SessionEnv,
  options: { subagents: Record<string, AgentProfile> },
) => AgentTool<any>[];

Use this optional factory when the sandbox exposes provider-specific model-facing tools. Flue appends the task tool separately.

FileStat

export interface FileStat {
  isFile: boolean;
  isDirectory: boolean;
  isSymbolicLink?: boolean;
  size?: number;
  mtime?: Date;
}

SessionEnv

Return a SessionEnv from createSessionEnv, but get it from createSandboxSessionEnv(api, cwd). Do not write SessionEnv methods by hand in an adapter.

Required SandboxApi methods

Implement every method below. If your provider SDK does not have a direct analogue for an operation, use a shell command only when shell execution is the adapter’s normal filesystem mechanism. Do not add option-specific shell workarounds around an otherwise direct filesystem API. Reject options that the direct API cannot honor exactly before mutation.

readFile(path)

UTF-8 decode the file at path and return its contents.

readFileBuffer(path)

Return raw bytes as a Uint8Array. If the SDK gives you a Node Buffer, wrap it with new Uint8Array(buffer).

writeFile(path, content)

Write content to path. Accept both string and Uint8Array. Convert strings to UTF-8 bytes before sending them to providers that only accept buffers.

Sandbox adapters need not create parent directories; the runtime guarantees it. When a write fails, createSandboxSessionEnv calls your mkdir(parent, { recursive: true }) and retries the write once, so FlueFs.writeFile behaves identically across every sandbox mode. Let missing-parent errors from the provider propagate — do not add your own parent creation.

stat(path)

Return a FileStat. isFile and isDirectory are required. If the provider SDK does not expose modification time, size, or symlink information, omit those fields — never fabricate placeholder values such as new Date(), 0, or false, since callers cannot distinguish them from real metadata.

readdir(path)

Return names, not full paths, for entries in the directory.

exists(path)

Return true when the path exists. Most providers throw for missing paths, so catch that error and return false.

mkdir(path, options?)

Create a directory. If options.recursive is set, create parents as needed. If the provider SDK only supports a single-level operation, fall back to exec('mkdir -p ...') for the recursive case.

rm(path, options?)

Delete a file or directory. Implement options.recursive and options.force exactly or reject unsupported requested options with SandboxOperationUnsupportedError before any mutation. Never ignore an option or leave its behavior provider-defined. A direct filesystem adapter must not shell out only to emulate unsupported removal flags; shell-native adapters may continue to implement removal through their normal shell filesystem path.

exec(command, options?)

Run a shell command. Honor options.cwd, options.env, and options.timeoutMs. The timeoutMs hint is measured in milliseconds. Forward it to the provider SDK’s native timeout option, converting units when the provider uses something other than milliseconds. Implementations MAY round timeoutMs UP to their coarsest supported granularity, never down: a provider that only accepts whole seconds should use Math.ceil(options.timeoutMs / 1000) so the enforced deadline is never shorter than the requested one. If the provider SDK does not expose a native timeout option, translate the hint into AbortSignal.timeout(options.timeoutMs) and pass that signal to an SDK that accepts one, or as a last resort race the call against a timer and reject. Make a best-effort attempt to honor timeoutMs: it is how the model-facing bash tool stops a command and retries. Returning an exit-code-124 result with timeout details in stderr matches the convention used by other adapters and timeout(1).

If the provider SDK also supports an AbortSignal, forward options.signal for true mid-flight cancellation. If it cannot observe a signal, ignore that option. The createSandboxSessionEnv wrapper performs pre- and post-operation signal.aborted checks. Do not fake mid-flight signal cancellation with Promise.race: the underlying remote process would keep running.

The Daytonan adapter demonstrates the rounding rule: Daytona’s executeCommand accepts whole seconds, so it forwards Math.ceil(options.timeoutMs / 1000).

If the provider does not separately expose stderr, return ''. Default exitCode to 0 only when the call clearly succeeded.

Sandbox lifetime

Flue does not manage sandbox lifetime. The user creates the sandbox and decides when or whether to delete it. Sandbox adapters must not call sandbox.delete(), sandbox.terminate(), sandbox.kill(), or any equivalent on the user’s behalf.

Sandbox adapter factories therefore take no cleanup option, and createSandboxSessionEnv takes no cleanup callback. If the adapter opens a real socket such as SSH, it may manage that socket internally, but it must not assume Flue will trigger teardown.

Reference implementation

See the deployed Daytona blueprint for a complete implementation. It demonstrates explicit rejection of unsupported force removal, exists() error handling, and buffer or string conversion in writeFile().

Sandbox adapter file location

The user’s project root does not change. The selected source directory inside it may vary. Flue selects the first existing directory in this order:

  1. <root>/.flue/
  2. <root>/src/
  3. <root>/

Write the adapter to <source-dir>/sandboxes/<name>.ts. If the selected source directory is unclear, ask the user before writing.

Verify a generated sandbox adapter

Before finishing:

  1. Typecheck the file with npx tsc --noEmit or the project’s existing typecheck command.
  2. Confirm that the adapter imports from @flue/runtime.
  3. If the project does not depend on the provider SDK, tell the user to install it.
  4. Tell the user which environment variables they need to set.
  5. Show a minimal snippet wiring the adapter into an agent.