# Sandbox API Reference

This document is the complete API reference for the `@coda/sandbox` package.

---

## SandboxEngineConfig

Configuration passed to the `SandboxEngine` constructor. All fields are required.

```typescript
interface SandboxEngineConfig {
  maxConcurrentIsolates: number;
  warmPoolSize: number;
  wasmModules: WasmModuleConfig[];
  defaultIsolateLimits: IsolateLimits;
  defaultBridgeLimits: BridgeLimits;
}
```

| Field                   | Type                 | Description                                                                                                                                             |
| ----------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `maxConcurrentIsolates` | `number`             | Hard ceiling on the number of live V8 isolates at any moment. If exceeded, `submit()` will throw a capacity error. Prevents DDOS via isolate creation.  |
| `warmPoolSize`          | `number`             | Number of pre-created isolates to keep ready. Warm isolates use `defaultIsolateLimits`. Set to `0` to disable pooling (all isolates created on demand). |
| `wasmModules`           | `WasmModuleConfig[]` | WASM modules to load at engine startup and inject into every isolate context. See `WasmModuleConfig` below.                                             |
| `defaultIsolateLimits`  | `IsolateLimits`      | Baseline V8 limits applied to every execution. Individual requests may override these via `ExecuteMessage.isolateLimits`.                               |
| `defaultBridgeLimits`   | `BridgeLimits`       | Baseline bridge limits applied to every execution. Individual requests may override these via `ExecuteMessage.bridgeLimits`.                            |

---

## SandboxEngine

The main entry point for the sandbox package.

### `constructor(config: SandboxEngineConfig)`

Creates a new engine. Does not start any background work. The warm pool starts empty — `IsolateManager.warm()` can be called to pre-populate it, but this method is not exposed on `SandboxEngine`'s public API in the current implementation. The pool is filled opportunistically as executions create isolates.

### `submit(req: ExecuteMessage): SandboxSession`

Starts execution of `req.code` in a V8 isolate. **Non-blocking**: execution begins in the background; `submit()` returns a `SandboxSession` immediately before the isolate has been acquired or any code has run.

Per-request limits are merged with the engine defaults:

```typescript
const isolateLimits = {
  ...this.config.defaultIsolateLimits,
  ...req.isolateLimits,
};
const bridgeLimits = {
  ...this.config.defaultBridgeLimits,
  ...req.bridgeLimits,
};
```

Fields specified in `req.isolateLimits` / `req.bridgeLimits` override the defaults; omitted fields fall back to the defaults.

### `shutdown(): Promise<void>`

Drains all active and pooled isolates. Disposes each one and clears the pool and active sets. Call this during process shutdown (`SIGTERM` / `SIGINT`) to release V8 resources cleanly.

---

## SandboxSession

The handle returned by `submit()`. Provides the outbound event stream and methods to interact with a running execution.

```typescript
interface SandboxSession {
  readonly events: AsyncIterable<OutboundMessage>;
  send(msg: DataResponseMessage): void;
  abort(): void;
}
```

### `events: AsyncIterable<OutboundMessage>`

An async iterable of all outbound messages produced by the execution. Consuming it with `for await...of` will yield events in order as they are produced and will complete when the execution finishes (either `result` or `error`).

Events arrive in this typical sequence:

1. Zero or more `status` events (from `reportStatus()` or `console.*`)
2. Zero or more `data_request` / `data_response` round-trips (interleaved with status)
3. Exactly one terminal event: either `result` (success) or `error` (failure)

The iterator returns `done: true` after the terminal event. Do not hold the iterator open indefinitely — execution always ends in a terminal event.

### `send(msg: DataResponseMessage): void`

Delivers a data response to a pending `data_request`. Must be called while iterating `events` when a `data_request` event is observed.

The `msg.dataRequestId` must match the `dataRequestId` from the `data_request` event. If no pending request matches, the call is a no-op. After `abort()` is called, `send()` is also a no-op.

To respond with an error (e.g., the tool call failed):

```typescript
session.send({
  type: "data_response",
  requestId: req.requestId,
  dataRequestId: event.dataRequestId,
  error: { code: "TOOL_ERROR", message: "Account not found" },
});
```

### `abort(): void`

Terminates the execution immediately. All pending `data_request` promises inside the isolate are rejected with `SandboxAbortedError`. An `error` event with `code: ABORTED` is pushed to `events`, and the iterator closes. Subsequent calls to `abort()` are no-ops.

---

## SandboxMessage Union

All messages exchanged between caller and engine are variants of the `SandboxMessage` discriminated union.

```typescript
type SandboxMessage = InboundMessage | OutboundMessage;
type InboundMessage = ExecuteMessage | DataResponseMessage;
type OutboundMessage =
  | StatusMessage
  | DataRequestMessage
  | ResultMessage
  | ErrorMessage;
```

### 1. `execute` — `ExecuteMessage` (Caller → Engine)

Initiates a sandbox execution. Passed to `engine.submit()`.

| Field           | Type                     | Required | Description                                                                                                                                                                       |
| --------------- | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`          | `'execute'`              | yes      | Discriminant                                                                                                                                                                      |
| `requestId`     | `string`                 | yes      | Unique identifier for this execution. Echoed on all outbound messages for correlation.                                                                                            |
| `code`          | `string`                 | yes      | JavaScript source code to execute inside the isolate. Must be valid JS; syntax errors produce a `VALIDATION` error.                                                               |
| `isolateLimits` | `Partial<IsolateLimits>` | no       | Per-request V8 overrides. Merged with `defaultIsolateLimits`.                                                                                                                     |
| `bridgeLimits`  | `Partial<BridgeLimits>`  | no       | Per-request bridge overrides. Merged with `defaultBridgeLimits`.                                                                                                                  |
| `toolPolicy`    | `ToolPolicy`             | yes      | Declares which tools the code is allowed to call and whether the execution is read-only. Currently informational; enforcement is implemented by the caller's data response logic. |

### 2. `data_response` success — `DataResponseMessage` (Caller → Engine)

Delivers a successful tool result back to waiting isolate code.

| Field           | Type               | Description                                                                |
| --------------- | ------------------ | -------------------------------------------------------------------------- |
| `type`          | `'data_response'`  | Discriminant                                                               |
| `requestId`     | `string`           | Must match the `requestId` of the active execution                         |
| `dataRequestId` | `string`           | Must match the `dataRequestId` from the corresponding `data_request` event |
| `data`          | `JsonSerializable` | The result data to return to `dataProxy.request()` inside the isolate      |

### 3. `data_response` error — `DataResponseMessage` (Caller → Engine)

Signals that a tool call failed. The error is thrown inside the isolate from `dataProxy.request()`.

| Field           | Type                                | Description                                                                |
| --------------- | ----------------------------------- | -------------------------------------------------------------------------- |
| `type`          | `'data_response'`                   | Discriminant                                                               |
| `requestId`     | `string`                            | Must match the `requestId` of the active execution                         |
| `dataRequestId` | `string`                            | Must match the `dataRequestId` from the corresponding `data_request` event |
| `error`         | `{ code: string; message: string }` | Error details; `message` is thrown as an `Error` inside the isolate        |

### 4. `status` — `StatusMessage` (Engine → Caller)

Emitted when user code calls `reportStatus()` or `console.log/warn/error()`.

| Field       | Type       | Description                                                                                  |
| ----------- | ---------- | -------------------------------------------------------------------------------------------- |
| `type`      | `'status'` | Discriminant                                                                                 |
| `requestId` | `string`   | Identifies the execution                                                                     |
| `stage`     | `string`   | Category label (e.g., `'processing'`, `'console'`)                                           |
| `message`   | `string`   | Human-readable message. Console calls are prefixed: `[log] ...`, `[warn] ...`, `[error] ...` |

### 5. `data_request` — `DataRequestMessage` (Engine → Caller)

Emitted when user code calls `await dataProxy.request(method, params)`. The execution is suspended until the caller responds with a matching `data_response`.

| Field           | Type               | Description                                                                          |
| --------------- | ------------------ | ------------------------------------------------------------------------------------ |
| `type`          | `'data_request'`   | Discriminant                                                                         |
| `requestId`     | `string`           | Identifies the execution                                                             |
| `dataRequestId` | `string`           | Unique identifier for this specific request. Use this when calling `session.send()`. |
| `method`        | `string`           | The tool/method name passed by user code                                             |
| `params`        | `JsonSerializable` | The params object passed by user code, JSON-deserialized                             |

### 6. `result` — `ResultMessage` (Engine → Caller)

Terminal success event. Emitted when user code returns a value (or execution ends without error).

| Field       | Type               | Description                                                                                            |
| ----------- | ------------------ | ------------------------------------------------------------------------------------------------------ |
| `type`      | `'result'`         | Discriminant                                                                                           |
| `requestId` | `string`           | Identifies the execution                                                                               |
| `payload`   | `JsonSerializable` | The return value of user code, JSON-deserialized. `null` if user code returned `undefined` or nothing. |
| `stats`     | `ExecutionStats`   | Execution metrics (see `ExecutionStats` below)                                                         |

### 7. `error` — `ErrorMessage` (Engine → Caller)

Terminal failure event. Emitted on any unhandled error, timeout, OOM, validation failure, or abort.

| Field       | Type        | Description                                                 |
| ----------- | ----------- | ----------------------------------------------------------- |
| `type`      | `'error'`   | Discriminant                                                |
| `requestId` | `string`    | Identifies the execution                                    |
| `code`      | `ErrorCode` | One of `TIMEOUT`, `OOM`, `RUNTIME`, `VALIDATION`, `ABORTED` |
| `message`   | `string`    | Human-readable error description                            |

---

## IsolateLimits and BridgeLimits

### IsolateLimits

Controls enforced by the V8 engine itself.

| Field          | Type     | Default   | Description                                                                                                                                                                                                          |
| -------------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `memoryMb`     | `number` | `256`     | V8 heap size cap in megabytes. Exceeding it kills the isolate with `SandboxOOMError`.                                                                                                                                |
| `timeoutMs`    | `number` | `30_000`  | Maximum wall-clock time for a single execution in milliseconds. Enforced by both a V8 built-in timeout (sync loops) and an external `setTimeout` (async loops). See the security docs for the dual timeout strategy. |
| `maxCodeBytes` | `number` | `524_288` | Maximum allowed size of the source code in bytes (UTF-8 encoded). Checked before compilation. Exceeding it throws `SandboxValidationError` immediately.                                                              |

### BridgeLimits

Controls enforced in the `SandboxEngine` bridge logic.

| Field             | Type     | Default              | Description                                                                                                   |
| ----------------- | -------- | -------------------- | ------------------------------------------------------------------------------------------------------------- |
| `maxDataRequests` | `number` | `20`                 | Maximum number of `dataProxy.request()` calls allowed per execution. Prevents request amplification.          |
| `maxDataBytes`    | `number` | `52_428_800` (50 MB) | Maximum cumulative bytes received across all data responses in a single execution. Prevents memory flooding.  |
| `maxResultBytes`  | `number` | `5_242_880` (5 MB)   | Maximum size of the serialized return value. Checked after execution; emits a `VALIDATION` error if exceeded. |
| `maxStatusEvents` | `number` | `1000`               | Maximum number of `status` events emitted per execution. Events beyond the cap are silently dropped.          |

---

## Error Hierarchy

```
SandboxError (base)
├── SandboxTimeoutError     code: TIMEOUT
├── SandboxOOMError         code: OOM
├── SandboxRuntimeError     code: RUNTIME
├── SandboxValidationError  code: VALIDATION
└── SandboxAbortedError     code: ABORTED
```

All errors extend `SandboxError`, which extends `Error` and carries a `code: ErrorCode` field.

| Class                    | Code         | When thrown                                                                                                                                                      |
| ------------------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SandboxTimeoutError`    | `TIMEOUT`    | V8 built-in timeout fires on a sync CPU loop, or the external `setTimeout` fires on an async hang. Message: `"Execution time limit exceeded"`.                   |
| `SandboxOOMError`        | `OOM`        | V8 heap allocation fails because `memoryMb` was exceeded. ivm surfaces this as an allocation failure or out-of-memory error. Message: `"Memory limit exceeded"`. |
| `SandboxRuntimeError`    | `RUNTIME`    | Any unhandled JavaScript error thrown by user code (e.g., `throw new Error('boom')`, `TypeError: x is not a function`). Message: the original error message.     |
| `SandboxValidationError` | `VALIDATION` | Code exceeds `maxCodeBytes`; result exceeds `maxResultBytes`; or the code has a syntax error caught at compile time.                                             |
| `SandboxAbortedError`    | `ABORTED`    | `session.abort()` was called by the external caller. Message: `"Execution aborted by caller"`.                                                                   |

Errors are caught in `SandboxEngine.execute()` and converted to `error` outbound messages. They are not re-thrown to the caller.

---

## ExecutionStats

Included in every `result` message.

| Field               | Type     | Description                                                                                                              |
| ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `wallMs`            | `number` | Wall-clock elapsed time from start of execution to completion, in milliseconds. Minimum value: 1.                        |
| `cpuMs`             | `number` | V8 CPU time consumed by the isolate, in milliseconds. Derived from `isolate.cpuTime` (nanoseconds), converted to ms.     |
| `heapMb`            | `number` | V8 used heap size at completion, in megabytes. Derived from `isolate.getHeapStatistics().used_heap_size`.                |
| `dataRequestCount`  | `number` | Total number of `dataProxy.request()` calls made during this execution.                                                  |
| `dataBytesReceived` | `number` | Total bytes received from the host across all data responses (measured as `JSON.stringify(result).length` per response). |

---

## ToolPolicy

Declared in every `ExecuteMessage`. Currently informational — the sandbox does not enforce which tools are called; that is the caller's responsibility in its data response logic.

```typescript
type ToolPolicy = {
  allowedTools: string[];
  readOnly: boolean;
};
```

| Field          | Type       | Description                                                                          |
| -------------- | ---------- | ------------------------------------------------------------------------------------ |
| `allowedTools` | `string[]` | List of tool/method names the code is permitted to call via `dataProxy.request()`.   |
| `readOnly`     | `boolean`  | Whether the execution should be treated as read-only (no side-effecting tool calls). |

---

## WasmModuleConfig

Configuration for a WASM module to load and inject into isolate contexts.

```typescript
interface WasmModuleConfig {
  name: string;
  wasmPath: string;
  jsBindingPath: string;
}
```

| Field           | Type     | Description                                                                                                                                                                                               |
| --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | `string` | Unique identifier for this module. Used as the key in the `WasmLoader` cache.                                                                                                                             |
| `wasmPath`      | `string` | Absolute path to the `.wasm` binary file. Read from disk at engine startup and passed into the isolate as a read-only `ArrayBuffer`.                                                                      |
| `jsBindingPath` | `string` | Absolute path to a JavaScript binding file. Compiled and run inside the isolate context after the bootstrap script. Provides the user-facing API for the WASM module (e.g., sets up `globalThis.duckdb`). |

The corresponding `LoadedModule` interface (returned by `WasmLoader`) is:

```typescript
interface LoadedModule {
  name: string;
  binary: ArrayBuffer; // raw .wasm bytes
  bindings: string; // JS binding source, run in isolate context
}
```

---

## In-Isolate APIs

These globals are available to user code running inside the sandbox.

### `dataProxy.request(method, params): Promise<any>`

Makes a data request to the host and waits for a response.

```javascript
const result = await dataProxy.request("get_account", { id: 123 });
```

- `method` — string name of the tool or data source to call
- `params` — any JSON-serializable value; serialized before crossing the isolate boundary
- Returns the deserialized JSON response from the host

Throws if:

- The host responds with an error (`data_response.error`)
- `maxDataRequests` is exceeded
- `maxDataBytes` is exceeded (cumulative)
- The execution is aborted

### `reportStatus(stage, message): void`

Emits a `status` event to the caller synchronously.

```javascript
reportStatus("fetching", "Loading account data...");
```

- `stage` — a short category label (e.g., `'fetching'`, `'computing'`, `'done'`)
- `message` — a human-readable message
- Silently no-ops once `maxStatusEvents` is reached

### `console.log/warn/error(...args)`

Captured and forwarded to the caller as `status` events with `stage: 'console'` and message prefixed with `[log]`, `[warn]`, or `[error]`.

```javascript
console.log("rows:", rows.length); // → status { stage: 'console', message: '[log] rows: 42' }
console.warn("missing field"); // → status { stage: 'console', message: '[warn] missing field' }
console.error("unexpected null"); // → status { stage: 'console', message: '[error] unexpected null' }
```

All arguments are converted to strings via `String(arg)`. Capped at `CONSOLE_CAP = 1000` calls (independent of `maxStatusEvents`).

### WASM Modules

When `wasmModules` are configured, their binding scripts run in the isolate context before user code. The bindings expose whatever globals the binding defines. For example, a DuckDB-WASM binding might expose `globalThis.duckdb`; a GLPK binding might expose `globalThis.glpk`.

Refer to each binding file to see what APIs it exposes.

### What Is NOT Available

The following are explicitly absent from the isolate context:

- `fetch` — no Web API networking
- `process` — no Node.js process object
- `require` — no CommonJS module loading
- Dynamic `import()` — disabled by isolated-vm
- `fs`, `os`, `path`, `crypto`, `child_process` — no Node.js built-ins
- `XMLHttpRequest`, `WebSocket` — no Web APIs
- Any global not explicitly injected by `ContextBuilder` or a WASM binding

---

## Usage Examples

### 1. Basic Execution (No Data Requests)

```typescript
import {
  SandboxEngine,
  DEFAULT_ISOLATE_LIMITS,
  DEFAULT_BRIDGE_LIMITS,
} from "@coda/sandbox";

const engine = new SandboxEngine({
  maxConcurrentIsolates: 10,
  warmPoolSize: 2,
  wasmModules: [],
  defaultIsolateLimits: DEFAULT_ISOLATE_LIMITS,
  defaultBridgeLimits: DEFAULT_BRIDGE_LIMITS,
});

const session = engine.submit({
  type: "execute",
  requestId: "req-1",
  code: `
    const result = [1, 2, 3].map(x => x * 2);
    return result;
  `,
  toolPolicy: { allowedTools: [], readOnly: true },
});

for await (const event of session.events) {
  if (event.type === "result") {
    console.log("Result:", event.payload); // [2, 4, 6]
    console.log("Stats:", event.stats);
  } else if (event.type === "error") {
    console.error("Error:", event.code, event.message);
  }
}

await engine.shutdown();
```

### 2. Execution with Data Request Handling

```typescript
const session = engine.submit({
  type: "execute",
  requestId: "req-2",
  code: `
    reportStatus('fetching', 'Loading account...');
    const account = await dataProxy.request('get_account', { id: 42 });
    reportStatus('done', 'Got account: ' + account.name);
    return account;
  `,
  toolPolicy: { allowedTools: ["get_account"], readOnly: true },
});

for await (const event of session.events) {
  if (event.type === "data_request") {
    // Fetch real data and respond
    const data = await myDatabase.getAccount(event.params.id);
    session.send({
      type: "data_response",
      requestId: event.requestId,
      dataRequestId: event.dataRequestId,
      data,
    });
  } else if (event.type === "status") {
    console.log(`[${event.stage}] ${event.message}`);
  } else if (event.type === "result") {
    console.log("Account:", event.payload);
  } else if (event.type === "error") {
    console.error(event.code, event.message);
  }
}
```

### 3. Abort Mid-Execution

```typescript
const session = engine.submit({
  type: "execute",
  requestId: "req-3",
  code: `
    const a = await dataProxy.request('step_one', {});
    const b = await dataProxy.request('step_two', { input: a });
    return b;
  `,
  toolPolicy: { allowedTools: ["step_one", "step_two"], readOnly: true },
});

for await (const event of session.events) {
  if (event.type === "data_request") {
    if (shouldCancel) {
      session.abort(); // Stops execution; next event will be error { code: 'ABORTED' }
    } else {
      session.send({ ...responseFor(event) });
    }
  } else if (event.type === "error" && event.code === "ABORTED") {
    console.log("Execution was cancelled");
  }
}
```

### 4. Using Custom Limits

```typescript
const session = engine.submit({
  type: "execute",
  requestId: "req-4",
  code: heavyAnalysisCode,
  isolateLimits: {
    memoryMb: 512, // Allow more memory for heavy workload
    timeoutMs: 60_000, // 60 second timeout
  },
  bridgeLimits: {
    maxDataRequests: 50, // Allow more tool calls
    maxResultBytes: 10 * 1024 * 1024, // Allow up to 10 MB result
  },
  toolPolicy: { allowedTools: ["query_db", "get_rates"], readOnly: true },
});
```
