> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orcapods.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# VirtualFS SDKs

> Use the standalone Python and TypeScript clients, the Claude Bash adapter, and the OpenAI Agents sandbox adapter to call the VirtualFS HTTP API.

The VirtualFS SDKs are hand-written clients for the standalone VirtualFS HTTP server documented in the [VirtualFS API Reference](/virtualfs/api). They are separate from the generated Conductor SDKs (`orcapods`, `@agent-orc/...`, `sdks/go`) and they target the `/vfs/...` HTTP surface directly.

Use a VirtualFS SDK when application code needs to:

* Read or write files in tenant-scoped mounts from outside the agent runtime.
* Run sealed shell commands against the VirtualFS without going through the conductor.
* Expose VFS as agent tools to OpenAI, Vercel AI SDK, or Claude SDK loops.
* Provide a sandbox-shaped facade to existing agent runtime code.

***

## Package map

The VirtualFS SDKs ship as four packages in this repository:

| Package                 | Path                                   | Language   | Role                                                                                           |
| ----------------------- | -------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------- |
| `agent-orc-vfs`         | `virtualfs/sdk/py`                     | Python     | Core `Workspace` client, mount factories, OpenAI function tools                                |
| `@agent-orc/vfs`        | `virtualfs/sdk/ts`                     | TypeScript | Core `Workspace` client, mount factories, Vercel AI / OpenAI tools, in-package `SandboxClient` |
| `@agent-orc/claude-vfs` | `virtualfs/sdk/adapters/claude-sdk`    | TypeScript | Claude Messages Bash tool adapter that routes through `Workspace.execute()`                    |
| `agent-orc-vfs-openai`  | `virtualfs/sdk/adapters/openai-agents` | Python     | Virtual sandbox protocol adapter (`SandboxClient`) backed by a `Workspace`                     |

Runnable examples live under `virtualfs/examples/python` and `virtualfs/examples/typescript`.

There is no Go VirtualFS SDK. Go callers use `virtualfs.New(dispatcher, policy)` and `Workspace.Execute(ctx, cmd, ExecOpts)` directly from the in-process Go package documented in the [API Reference Workspace Execute](/virtualfs/api#workspace-execute) section.

***

## Server prerequisites

Every SDK client speaks the structured `/vfs/...` surface plus `POST /vfs/exec`. Boot a server before running any SDK example:

```bash theme={null}
go run ./virtualfs/cmd/vfs serve
```

Defaults:

* Standalone `serve` listens on `:8080`. The compose-backed VirtualFS service published by `docker-compose.yml` listens on `:8090`.
* Without `VFS_AUTH_TOKEN`, the server uses `smokeAuth` and accepts any bearer token, including the default `dev-token` printed in the boot banner. Set `VFS_AUTH_TOKEN` for any non-local deployment.
* The smoke driver seeds `/s3/log.jsonl` and `/s3/report.parquet` unless `--seed=false` is passed.

Common environment variables consumed by the SDKs and examples:

| Variable         | SDK use                                                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `VFS_BASE_URL`   | Base URL for the SDK client. `http://localhost:8080` for `vfs serve`, `http://localhost:8090` for the compose service. |
| `VFS_TOKEN`      | Bearer token sent as `Authorization: Bearer <token>`. Defaults to `dev-token` for `smokeAuth`.                         |
| `VFS_AUTH_TOKEN` | Server-side env consumed by `vfs serve`; SDK clients should send the same value as `VFS_TOKEN`.                        |

<Note>
  When VirtualFS is fronted by the conductor (`/api/vfs/*`) or the Vercel-hosted dashboard (`/vfs/*` rewrite), client `Authorization` headers are stripped and the proxy injects `Bearer ${VFS_AUTH_TOKEN}` server-side. The SDK `token` argument is for direct use against a standalone or Railway-hosted VirtualFS — not for browser code that already routes through one of those proxies.
</Note>

***

## Python: `agent-orc-vfs`

A thin async + sync HTTP client built on `httpx` and Pydantic v2.

### Install

The package is not yet on PyPI. Install from the repo:

```bash theme={null}
pip install ./virtualfs/sdk/py
```

For editable development with `uv`:

```bash theme={null}
cd virtualfs/sdk/py && uv sync
```

Runtime requirements: Python `>=3.11`, `httpx>=0.27`, `pydantic>=2.7`.

### Workspace

`Workspace` is the entry point. It carries declared mount metadata used by `file_prompt` and by `validate()` plus a base URL and bearer token used by every HTTP call.

```python theme={null}
import os

from agent_orc_vfs import Workspace, ram

ws = Workspace(
    {
        "/": ram(),
    },
    base_url=os.getenv("VFS_BASE_URL", "http://localhost:8080"),
    token=os.environ["VFS_TOKEN"],
)
```

Constructor signature:

```python theme={null}
Workspace(
    mounts: dict[str, Mount],
    *,
    base_url: str,
    token: str,
    timeout: float = 30.0,
    file_prompt_template: Callable[[dict[str, Mount]], str] | None = None,
)
```

| Argument               | Purpose                                                                                                                                                                                                                                                                                                                                                                  |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `mounts`               | Client-side declared mount specs used to render `file_prompt` and by `validate()`. The server owns the live mount table; call `mounts()` for the live data or `validate()` to assert declared specs match server reality. The default prod/dev/disk profiles expose one live mount at `/`; declare non-root paths only for runtime user mounts that exist on the server. |
| `base_url`             | Base URL of the VirtualFS HTTP API. Trailing slash is normalized away.                                                                                                                                                                                                                                                                                                   |
| `token`                | Bearer token sent on every request.                                                                                                                                                                                                                                                                                                                                      |
| `timeout`              | Per-request HTTP timeout in seconds.                                                                                                                                                                                                                                                                                                                                     |
| `file_prompt_template` | Optional callable that receives the mount dict and returns the system-prompt fragment. Defaults to `default_template`.                                                                                                                                                                                                                                                   |

The instance lazily opens an `httpx.AsyncClient` and `httpx.Client` the first time async or sync methods are called. Both close paths are explicit:

```python theme={null}
ws.close()           # close sync client
await ws.aclose()    # close async client

with ws:             # sync context manager — calls close()
    ws.execute_sync("ls /")

async with ws:       # async context manager — calls aclose()
    await ws.execute("ls /")
```

### Mount factories

Mount specs declare the backend each path is expected to be backed by. The server validates the declaration at connect time via `validate()`.

```python theme={null}
from agent_orc_vfs import ram, r2

ram()                                                        # ephemeral in-process RAM (dev profile)
r2(bucket="agent-orc-prod")                                  # R2 bucket root
r2(bucket="agent-orc-prod", prefix="kb/")                    # R2 bucket with key prefix
r2(bucket="agent-orc-prod", endpoint="https://<acct>.r2.cloudflarestorage.com")  # explicit endpoint
```

Both return frozen dataclasses (`RamMount`, `R2Mount`). Clients cannot register additional mount kinds; the server is the source of truth for which paths exist. Call `validate()` (or `validate_sync()`) to assert that every declared path exists on the server and that its kind, bucket, and endpoint match. See [Strict mount validation](/virtualfs/sdks#strict-mount-validation-python).

### Typed file operations

Every async helper has a sync sibling. All operations target the typed `POST /vfs/...` endpoints described in the [API reference](/virtualfs/api#typed-file-operations).

| Async                                                                   | Sync                  | Returns           | Notes                                                                                                                |
| ----------------------------------------------------------------------- | --------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| `cat(path, *, range_start=0, range_end=0)`                              | `cat_sync(...)`       | `bytes`           | Binary-safe; decodes server `text` or `base64` envelope automatically.                                               |
| `cat_text(path)`                                                        | `cat_text_sync(path)` | `str`             | UTF-8 convenience wrapper.                                                                                           |
| `ls(path, *, recursive=False, limit=0, cursor="")`                      | `ls_sync(...)`        | `list[Entry]`     |                                                                                                                      |
| `stat(path)`                                                            | `stat_sync(path)`     | `Entry`           | Raises `NotFound` if the path does not exist.                                                                        |
| `find(path, *, name=None, type=None, limit=0)`                          | `find_sync(...)`      | `list[Entry]`     | `type` accepts `"file"` or `"dir"`.                                                                                  |
| `grep(pattern, path, *, recursive=False, regex=False, max_matches=100)` | `grep_sync(...)`      | `list[GrepMatch]` |                                                                                                                      |
| `write(path, content, *, mime=None)`                                    | `write_sync(...)`     | `Entry`           | `bytes` is sent base64-encoded; `str` is sent as text.                                                               |
| `delete(path, *, recursive=False)`                                      | `delete_sync(...)`    | `None`            |                                                                                                                      |
| `mounts()`                                                              | `mounts_sync()`       | `list[MountInfo]` | Returns the live mount table from `GET /vfs/mounts`.                                                                 |
| `validate()`                                                            | `validate_sync()`     | `None`            | Asserts declared mount specs match the server's live table. Raises `MountMismatch` or `MountUnavailable` on failure. |

### Strict mount validation (Python)

Call `validate()` immediately after constructing `Workspace` so a misconfigured server fails fast instead of silently routing reads to the wrong store.

```python theme={null}
ws = Workspace(
    {
        "/": r2(bucket="agent-orc-prod", endpoint="https://<acct>.r2.cloudflarestorage.com"),
    },
    base_url=os.getenv("VFS_BASE_URL", "http://localhost:8080"),
    token=os.environ["VFS_TOKEN"],
)
await ws.validate()   # raises MountMismatch or MountUnavailable if something is wrong
```

Validation raises:

* `MountMismatch` — a declared path is not registered on the server, or the backend kind / bucket / endpoint does not match what the server reports.
* `MountUnavailable` — the server has the mount but its backend failed to initialize at boot; the server-reported `reason` is included in the exception message.

```python theme={null}
await ws.write("/data/report.md", "# Report\n", mime="text/markdown")

entries = await ws.ls("/data")
entry = await ws.stat("/data/report.md")
text = await ws.cat_text("/data/report.md")
raw = await ws.cat("/data/report.md")
matches = await ws.grep("Report", "/data/report.md")
found = await ws.find("/data", name="report.md")

await ws.delete("/data/report.md")
```

### Shell execution

`execute` and `execute_sync` post to `POST /vfs/exec`. There is no host shell fallback; unsupported syntax raises a `VfsError` subclass.

```python theme={null}
out = await ws.execute("grep ALERT /s3/log.jsonl | wc -l", cwd="/")

if out.exit_code != 0:
    raise RuntimeError(out.stderr or f"vfs command failed: {out.exit_code}")

print(out.stdout)
```

### Response models

All responses are Pydantic v2 models. Use `.model_dump()` for plain dicts.

```python theme={null}
class Output(BaseModel):
    stdout: str = ""
    stderr: str = ""
    exit_code: int = 0
    duration_ms: int = 0
    truncated: bool = False

class Entry(BaseModel):
    name: str
    path: str
    is_dir: bool = False
    size: int | None = None
    mtime: str | None = None
    mime: str | None = None

class GrepMatch(BaseModel):
    path: str
    line: int
    col: int
    snippet: str

class MountInfo(BaseModel):
    name: str
    path_prefix: str
    mode: str
    ttl_seconds: int = 0
    status: str                    # "ready" | "unavailable"
    reason: str = ""               # set when status="unavailable"
    backend: BackendInfo           # kind, bucket, endpoint, prefix

class BackendInfo(BaseModel):
    kind: str                      # "ram" | "r2"
    bucket: str = ""
    endpoint: str = ""
    prefix: str = ""
```

### `file_prompt`

`ws.file_prompt` returns a system-prompt fragment describing the configured mounts. Pass it to the agent's `instructions` or prepend it to the system prompt.

```python theme={null}
print(ws.file_prompt)
```

```text theme={null}
You have access to a virtual filesystem with the following mounts:
- /data (RAM, ephemeral; lost between sandbox sessions)
- /s3 (R2 bucket "agent-orc-prod"; persistent)

Use shell commands like `cat`, `ls`, `grep`, `find`, `cp`, `head`, `tail`, `wc`
through the Bash tool. The shell supports pipes, redirects, and globs but not
variables, command substitution, or control flow.
```

Override the rendering by passing `file_prompt_template=callable` to the constructor.

### OpenAI function tools

`tools(ws)` returns a list of OpenAI chat-completions tool dicts. Each entry carries an extra `executor` callable used for local dispatch. The surface is intentionally two tools:

| Tool          | Purpose                                                                                                                                                                                                        |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vfs_execute` | Sealed shell entry point. Builtins: `ls`, `cat`, `find`, `grep`, `head`, `tail`, `mkdir`, `rm`, `mv`, `cp`, `wc`, `echo`, `stat`, `pwd`, `cd`, `touch`. Pipes, redirects, globs, and `&&`/`\|\|`/`;` chaining. |
| `vfs_grep`    | Bounded structured search; returns `list[GrepMatch]`.                                                                                                                                                          |

```python theme={null}
from agent_orc_vfs import dispatch_tool, tool_schemas, tools

tool_defs = tools(ws)
openai_tools = tool_schemas(tool_defs)

# Send openai_tools to the model:
# response = await openai.chat.completions.create(..., tools=openai_tools)

# Dispatch a tool call returned by the model:
output = await dispatch_tool(
    tool_defs,
    "vfs_execute",
    {"cmd": "grep ALERT /s3/log.jsonl | wc -l"},
)
```

Helpers exposed alongside `tools(...)`:

| Helper                                      | Purpose                                                                                                                |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `tool_schemas(tool_defs)`                   | Returns OpenAI-safe schemas with `executor` keys stripped.                                                             |
| `tool_executors(tool_defs)`                 | Returns a `name -> async callable` mapping.                                                                            |
| `dispatch_tool(tool_defs, name, arguments)` | Awaits the executor by name and returns a string result. Catches `VfsError` and serializes structured results as JSON. |

### Errors

All non-2xx responses are mapped to typed exceptions:

```python theme={null}
from agent_orc_vfs import (
    VfsError,
    PermissionDenied,
    NotFound,
    MountUnavailable,
    InvalidPath,
    UnsupportedShellFeature,
    CommandNotFound,
    DriverError,
)

try:
    await ws.cat("/secret/file")
except PermissionDenied as exc:
    print(exc.code, exc.allowed)
except NotFound as exc:
    print(exc.detail.get("path"))
except VfsError as exc:
    print(exc.code, exc.message, exc.detail)
```

Each exception carries `code` (string), `message`, and `detail` (dict). The shared error code table is in [Error codes](#error-codes) below.

***

## TypeScript: `@agent-orc/vfs`

An ESM + CJS package that ships a thin `Workspace` HTTP client, mount factories, a Vercel AI SDK / OpenAI tool bundle, and an in-package `SandboxClient` facade.

### Install

The package is not published to npm. Install it from this repository:

```bash theme={null}
bun add file:../virtualfs/sdk/ts
```

Build outputs ship to `dist/index.js` (ESM), `dist/index.cjs` (CJS), and `dist/index.d.ts`. The only runtime dependency is `zod ^3.23.8`.

### Workspace

```typescript theme={null}
import { Workspace, ram } from "@agent-orc/vfs";

const ws = new Workspace(
  {
    "/": ram(),
  },
  {
    baseUrl: process.env.VFS_BASE_URL ?? "http://localhost:8080",
    token: process.env.VFS_TOKEN ?? "dev-token",
  },
);
```

Constructor signature:

```typescript theme={null}
new Workspace(
  mounts: Record<string, Mount>,
  opts: {
    baseUrl: string;
    token: string;
    filePromptTemplate?: (mounts: Record<string, Mount>) => string;
  },
)
```

The client uses the global `fetch` and exposes one `request<T>(method, path, body?)` private helper that injects `Authorization: Bearer <token>` and parses non-2xx responses through `parseErrorResponse`.

### Mount factories

```typescript theme={null}
import { ram, r2 } from "@agent-orc/vfs";

ram();                                                                             // RamMount
r2({ bucket: "agent-orc-prod" });                                                  // R2Mount
r2({ bucket: "agent-orc-prod", prefix: "kb/" });                                   // R2Mount with prefix
r2({ bucket: "agent-orc-prod", endpoint: "https://<acct>.r2.cloudflarestorage.com" }); // explicit endpoint
```

Mount union: `Mount = R2Mount | RamMount | NotionMount`. `NotionMount` is a placeholder for a future driver. `describeMountAt(mountPath, mount)` is exported for custom prompt templates.

### Typed file operations

| Method                                                            | Returns                  | Notes                                                                                                                      |
| ----------------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `execute(cmd, opts?: { cwd? })`                                   | `Promise<Output>`        | Sealed shell.                                                                                                              |
| `cat(path, opts?: { rangeStart?, rangeEnd? })`                    | `Promise<Buffer>`        | Decodes `text` or `base64` envelope.                                                                                       |
| `catText(path, opts?)`                                            | `Promise<string>`        | UTF-8 convenience wrapper.                                                                                                 |
| `ls(path, opts?: { recursive?, limit?, cursor? })`                | `Promise<Entry[]>`       |                                                                                                                            |
| `stat(path)`                                                      | `Promise<Entry \| null>` | Returns `null` on `NOT_FOUND`. Other errors throw.                                                                         |
| `find(path, opts?: { name?, type?: "file" \| "dir", limit? })`    | `Promise<Entry[]>`       |                                                                                                                            |
| `grep(path, pattern, opts?: { recursive?, regex?, maxMatches? })` | `Promise<GrepMatch[]>`   | Note arg order: `(path, pattern)`.                                                                                         |
| `write(path, content, opts?: { mime? })`                          | `Promise<Entry>`         | Accepts `string` or `Uint8Array`. Buffers go base64.                                                                       |
| `delete(path, opts?: { recursive? })`                             | `Promise<void>`          |                                                                                                                            |
| `mounts()`                                                        | `Promise<MountInfo[]>`   | Live mount table from the server.                                                                                          |
| `validate()`                                                      | `Promise<void>`          | Asserts declared mount specs match the server's live table. Rejects with `MountMismatch` or `MountUnavailable` on failure. |

### Strict mount validation (TypeScript)

Call `validate()` immediately after constructing `Workspace` so a misconfigured server fails fast instead of silently routing reads to the wrong store.

```typescript theme={null}
import { Workspace, r2, MountMismatch, MountUnavailable } from "@agent-orc/vfs";

const ws = new Workspace(
  { "/": r2({ bucket: "agent-orc-prod", endpoint: "https://<acct>.r2.cloudflarestorage.com" }) },
  { baseUrl: process.env.VFS_BASE_URL!, token: process.env.VFS_TOKEN! }
);

await ws.validate(); // throws MountMismatch or MountUnavailable if something is wrong
```

Validation rejects with:

* `MountMismatch` — a declared path is not registered on the server, or the backend kind / bucket / endpoint does not match what the server reports.
* `MountUnavailable` — the server has the mount but its backend failed to initialize at boot; the server-reported `reason` is included.

<Note>
  The TypeScript `stat()` swallows `NOT_FOUND` and returns `null`, while the Python `stat()` raises `NotFound`. Pick one shape per call site rather than mixing.
</Note>

```typescript theme={null}
const out = await ws.execute("grep ALERT /s3/log.jsonl | wc -l", { cwd: "/" });
if (out.exitCode !== 0) {
  throw new Error(out.stderr || `vfs command failed: ${out.exitCode}`);
}

await ws.write("/data/report.md", "# Report\n", { mime: "text/markdown" });
const entries = await ws.ls("/data");
const text = await ws.catText("/data/report.md");
const matches = await ws.grep("/data/report.md", "Report");
```

### Response types

```typescript theme={null}
interface Output {
  stdout: string;
  stderr: string;
  exitCode: number;
  durationMs: number;
  truncated?: boolean;
}

interface Entry {
  path: string;
  name: string;
  isDir: boolean;
  size?: number;
  mtime?: string;
  mime?: string;
}

interface GrepMatch {
  path: string;
  line: number;
  col: number;
  snippet: string;
}

interface BackendInfo {
  kind: string;          // "ram" | "r2"
  bucket?: string;
  endpoint?: string;
  prefix?: string;
}

interface MountInfo {
  name: string;
  pathPrefix: string;
  mode: string;
  ttlSeconds: number;
  status: string;        // "ready" | "unavailable"
  reason?: string;       // set when status="unavailable"
  backend: BackendInfo;
}
```

### `file_prompt`

`ws.file_prompt` is a getter, not a method. It renders the configured mounts using `defaultFilePromptTemplate` unless `opts.filePromptTemplate` was supplied.

```typescript theme={null}
const systemPrompt = ws.file_prompt;
```

### Agent tools

`tools(ws)` returns a `VfsTools` object compatible with the Vercel AI SDK `tools:` argument and with manual OpenAI function-calling loops. Each entry has a Zod `inputSchema` and an `execute(input)` callable that routes through `Workspace`.

```typescript theme={null}
import { tools } from "@agent-orc/vfs";

const vfsTools = tools(ws);

// Vercel AI SDK:
const result = await generateText({ model, tools: vfsTools, ... });

// OpenAI function calling: invoke vfsTools.vfs_execute.execute(...) when
// the model returns a tool_call with the matching name.
const out = await vfsTools.vfs_execute.execute({
  cmd: "grep ALERT /s3/log.jsonl | wc -l",
});
```

The two-tool surface mirrors the Python adapter: `vfs_execute` returns `{ stdout, stderr, exitCode, durationMs }`, and `vfs_grep` returns `{ matches }`.

### `SandboxClient`

The TypeScript SDK ships its sandbox facade in-package. There is no separate `@agent-orc/openai-vfs` for TypeScript; the OpenAI Agents adapter is Python-only.

```typescript theme={null}
import { SandboxClient } from "@agent-orc/vfs";

const sandbox = new SandboxClient(ws);
const sessionId = await sandbox.openSandbox();          // UUID, no container provisioned
await sandbox.runCommand("echo ok > /data/out.txt");
const body = await sandbox.readFile("/data/out.txt");   // Buffer
await sandbox.writeFile("/data/out.txt", "new contents");
await sandbox.close();                                  // no-op in v1.5
```

| Method                             | Returns                                 | Routes to                                                       |
| ---------------------------------- | --------------------------------------- | --------------------------------------------------------------- |
| `openSandbox()`                    | `Promise<string>`                       | Generates a UUID stable for this client instance. No HTTP call. |
| `runCommand(cmd, opts?: { cwd? })` | `Promise<{ stdout, stderr, exitCode }>` | `Workspace.execute(cmd, opts)`                                  |
| `readFile(path)`                   | `Promise<Buffer>`                       | `Workspace.cat(path)`                                           |
| `writeFile(path, content)`         | `Promise<void>`                         | `Workspace.write(path, content)`                                |
| `close()`                          | `Promise<void>`                         | Clears the cached session id. No HTTP call.                     |

### Errors

The TypeScript SDK uses a single `VfsError` class with `code`, `message`, `httpStatus`, and optional `allowed`. There are no per-code subclasses; branch on `err.code` instead.

```typescript theme={null}
import { VfsError } from "@agent-orc/vfs";

try {
  await ws.cat("/secret/file");
} catch (err) {
  if (err instanceof VfsError) {
    if (err.code === "PERMISSION_DENIED") {
      console.warn("not allowed; permitted prefixes:", err.allowed);
    } else {
      console.error(err.code, err.message, err.httpStatus);
    }
  } else {
    throw err;
  }
}
```

***

## Claude adapter: `@agent-orc/claude-vfs`

A TypeScript-only adapter that exposes Claude's Bash tool and routes its invocations through `Workspace.execute()`. Lives at `virtualfs/sdk/adapters/claude-sdk`.

### Install

```bash theme={null}
bun add file:../virtualfs/sdk/adapters/claude-sdk
```

The adapter has `@anthropic-ai/sdk` as an optional peer dependency (`>=0.26.0`); the adapter itself does not import the SDK at module load time, so it can be used with any Anthropic SDK loop or with no SDK at all.

### Surface

`bashTool(ws)` returns `{ definition, handler }`:

```typescript theme={null}
interface BashToolDefinition {
  type: "bash_20250124";
  name: "Bash";
}

interface BashToolInput {
  command: string;
  restart?: boolean;
}

interface BashToolResult {
  output: string;     // stdout, plus stderr appended when non-empty
  exit_code: number;  // surfaced separately so callers can gate on non-zero
}

function bashTool(
  ws: Pick<Workspace, "execute">,
): { definition: BashToolDefinition; handler: (input: BashToolInput) => Promise<BashToolResult> };
```

### Usage

```typescript theme={null}
import Anthropic from "@anthropic-ai/sdk";
import { Workspace, ram } from "@agent-orc/vfs";
import { bashTool } from "@agent-orc/claude-vfs";

const ws = new Workspace(
  { "/": ram() },
  { baseUrl: "http://localhost:8080", token: "dev-token" },
);
const bash = bashTool(ws);

const client = new Anthropic();
const message = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 4096,
  tools: [bash.definition],
  messages: [{ role: "user", content: "Count ALERT lines in /data/log.jsonl" }],
});

for (const block of message.content) {
  if (block.type === "tool_use" && block.name === "Bash") {
    const result = await bash.handler(block.input as { command: string });
    // Send result.output back as a tool_result content block.
    // Inspect result.exit_code to decide whether to follow up.
  }
}
```

### Behavior notes

* `restart: true` is a no-op. The VFS shell is stateless, so the handler returns a notice with `exit_code: 0` instead of clearing any session state.
* An empty `command` string returns `{ output: "", exit_code: 0 }`.
* `stdout` and `stderr` are concatenated into `output`; `stderr` is appended after `stdout` when both are present.
* Any thrown error (`VfsError`, network error, etc.) is caught and surfaced as `{ output: error.message, exit_code: 1 }` so the agent loop can keep running.

This adapter is not a sandbox: Claude itself runs locally, only its Bash tool calls are intercepted. For a sandbox-shaped facade, use `SandboxClient` from `@agent-orc/vfs` (TypeScript) or `agent-orc-vfs-openai` (Python).

***

## OpenAI Agents adapter: `agent-orc-vfs-openai`

A Python-only adapter that wraps a `Workspace` in a sandbox-shaped facade for agent runtime code that expects `BaseSandboxClient`-style operations. Lives at `virtualfs/sdk/adapters/openai-agents`.

### Install

```bash theme={null}
pip install ./virtualfs/sdk/py
pip install ./virtualfs/sdk/adapters/openai-agents
```

The adapter intentionally does not import `openai-agents` at module load time. The OpenAI Agents SDK's sandbox surface (`BaseSandboxClient`, `SandboxSession`) is still beta; this package exposes a smaller `SandboxClientProtocol` that is direct-protocol compatible today and can be wrapped in the future when agent-orc adopts the official shape.

### Surface

```python theme={null}
from agent_orc_vfs_openai import (
    SandboxClient,
    SandboxClientProtocol,
    CommandResult,
)
```

| Symbol                     | Description                                                                                                                                 |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `SandboxClient(workspace)` | Routes each method through the supplied `agent_orc_vfs.Workspace`. Async-only API.                                                          |
| `SandboxClientProtocol`    | `typing.Protocol` (`@runtime_checkable`) that `SandboxClient` implements; lets callers type-check without importing `openai-agents`.        |
| `CommandResult`            | Dataclass returned by `run_command`. Fields: `stdout`, `stderr`, `exit_code`, `duration_ms`. Property `ok` is `True` when `exit_code == 0`. |

### Methods

| Method                                              | Behavior                                                                                                                  |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `await open_sandbox() -> str`                       | Returns a UUID4 string stable for this client instance. No container is provisioned; subsequent calls return the same id. |
| `await run_command(cmd, cwd=None) -> CommandResult` | Awaits `Workspace.execute(cmd, cwd=cwd)` and adapts the result.                                                           |
| `await read_file(path) -> bytes`                    | Awaits `Workspace.cat(path)`. Binary-safe.                                                                                |
| `await write_file(path, content: bytes) -> None`    | Awaits `Workspace.write(path, content)`. Bytes are sent base64-encoded.                                                   |
| `await close() -> None`                             | No-op. The `Workspace` HTTP client lifecycle is the caller's responsibility (call `await ws.aclose()` separately).        |

### Usage

```python theme={null}
import os

from agent_orc_vfs import Workspace, r2
from agent_orc_vfs_openai import SandboxClient


async def run() -> None:
    ws = Workspace(
        {
            "/": r2(bucket="agent-orc-prod"),
        },
        base_url=os.environ["VFS_BASE_URL"],
        token=os.environ["VFS_TOKEN"],
    )

    sandbox = SandboxClient(ws)
    session_id = await sandbox.open_sandbox()
    result = await sandbox.run_command("grep alert /s3/log.jsonl | wc -l")

    if not result.ok:
        raise RuntimeError(result.stderr or f"exit {result.exit_code}")

    print(session_id, result.stdout)
    await sandbox.close()
    await ws.aclose()
```

`VfsError` and its subclasses propagate from `run_command`, `read_file`, and `write_file` exactly as they do from the underlying `Workspace`.

***

## Error codes

Every endpoint uses the shared `{ "error": { "code", "message", ... } }` envelope. The Python SDK maps each code to a dedicated exception class; the TypeScript SDK exposes a single `VfsError` and asks callers to branch on `code`.

| Code                                         | Python class              | When                                                                                                     |
| -------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------- |
| `PERMISSION_DENIED`                          | `PermissionDenied`        | Path is outside an allowed prefix. `detail.allowed` lists permitted prefixes.                            |
| `NOT_FOUND`                                  | `NotFound`                | Path or mount entry does not exist. `detail.path` carries the requested path.                            |
| `MOUNT_UNAVAILABLE`                          | `MountUnavailable`        | The server has no driver registered for the mount, or the driver failed to initialize.                   |
| `INVALID_PATH`                               | `InvalidPath`             | Path is malformed or fails normalization.                                                                |
| `UNSUPPORTED_SHELL_FEATURE`                  | `UnsupportedShellFeature` | Parsed command uses variable expansion, command substitution, control flow, subshells, or backgrounding. |
| `COMMAND_NOT_FOUND`                          | `CommandNotFound`         | Command is not in the sealed builtin registry; no host `PATH` lookup is performed.                       |
| `DRIVER_ERROR`                               | `DriverError`             | Underlying mount driver returned an error.                                                               |
| `HTTP_ERROR` (Python) / fallback `code` (TS) | `VfsError`                | Any other failure, including non-JSON error bodies.                                                      |

The `cmd` shape that triggers `UNSUPPORTED_SHELL_FEATURE` and the full builtin list live in the [Shell Parser and Compiler](/virtualfs/api#shell-parser-and-compiler) section of the API reference.

***

## Examples

Runnable scripts ship with the SDKs:

| Path                                                   | What it shows                                                                  |
| ------------------------------------------------------ | ------------------------------------------------------------------------------ |
| `virtualfs/examples/python/01_basic_execute.py`        | `await ws.execute("grep alert /s3/log.jsonl \| wc -l")` end to end.            |
| `virtualfs/examples/python/02_read_write.py`           | Typed file helpers: `write`, `cat`, `ls`, `find`, `delete`.                    |
| `virtualfs/examples/python/03_with_openai_tools.py`    | `tools(ws)`, `tool_schemas(...)`, `dispatch_tool(...)` against the OpenAI SDK. |
| `virtualfs/examples/python/04_with_openai_agents.py`   | `SandboxClient(ws)` driven through an agent loop.                              |
| `virtualfs/examples/typescript/01-basic-execute.ts`    | `Workspace.execute()`.                                                         |
| `virtualfs/examples/typescript/02-read-write.ts`       | Typed helpers and `mounts()`.                                                  |
| `virtualfs/examples/typescript/03-agent-tools.ts`      | `tools(ws)` as a Vercel AI SDK tool bundle.                                    |
| `virtualfs/examples/typescript/04-claude-bash-tool.ts` | `bashTool(ws)` dry-run loop without an Anthropic API key.                      |
| `virtualfs/examples/typescript/05-sandbox-client.ts`   | `SandboxClient(ws)`.                                                           |

To run them, boot a server with `go run ./virtualfs/cmd/vfs serve`, export `VFS_BASE_URL` and `VFS_TOKEN`, then follow the `README.md` in the example directory.
