PAIPAI

createInMemorySandbox()

A Sandbox with in-memory files, for testing tools that need the sandbox capability.

A Sandbox for unit tests. Pass it to runTool for a tool that declares the sandbox capability, then read back what the tool wrote. It has no test-runner dependency, so it works under Jest and Vitest alike.

import { createInMemorySandbox } from "@pai/sandbox/memory";
import { runTool } from "@pai/test-utils";

const sandbox = createInMemorySandbox();
await runTool(exportReport, {
  input: { month: "2026-09" },
  capabilities: { sandbox },
});
expect(await sandbox.readFile({ path: "/workspace/report.csv" })).not.toBeNull();

Import

import { createInMemorySandbox, type InMemorySandboxOptions } from "@pai/sandbox/memory";

Signature

function createInMemorySandbox(options?: {
  runCommand?: (command: string) => CommandAnswer | Promise<CommandAnswer>;
  seed?: SandboxSeed<"global">;
}): Sandbox;

type CommandAnswer = Partial<SandboxCommandResult> | void; // void: exit 0, no output

Parameters

options?: object

runCommand?: (command) => CommandAnswer | Promise<CommandAnswer>

Answers each runCommand call. There is no shell: commands never change the files, and a command this leaves unanswered exits 0 with no output. Record commands here to assert on them, or return output or an exitCode to stand in for a script or a failure.

seed?: SandboxSeed<"global">

Files the sandbox holds from its first operation, and again after each reset(), as a binding's seed gives them to every environment, ledger included. A function receives only signal: this sandbox has no thread, scope or user, and no file provider, so stored files (fileId) are skipped. Seeding executable files sends runCommand chmod +x commands.

Returns

A Sandbox.

The real facade sandboxCapability() gives tools, over in-memory files: files start empty or seeded, writes create their parent folders, useExclusive serializes, and reset() returns the files to the seeded state and sets environmentReset, as a recreated sandbox does. Paths resolve inside /workspace, as on GKE: relative paths start there, and a path outside it is rejected. Unlike the per-call facades a runtime gives tools, one sandbox lives for the whole test, so environmentReset stays set after a reset.

Examples

In a runtime test

createTestRuntime takes a capability binding, so wrap the sandbox in one. Every thread in the test shares it.

import { createCapabilityBinding } from "@pai/core";
import { sandbox } from "@pai/sandbox";
import { createInMemorySandbox } from "@pai/sandbox/memory";
import { createTestRuntime } from "@pai/test-utils";

const workspace = createInMemorySandbox();
await using runtime = createTestRuntime({
  agent,
  model,
  capabilities: {
    sandbox: createCapabilityBinding({ capability: sandbox, acquire: () => workspace }),
  },
});

Answer a script the model runs

The sandbox runs nothing, so return the output the test needs:

const workspace = createInMemorySandbox({
  runCommand: (command) =>
    command.startsWith("node ") ? { stdout: '{"rows":3}\n' } : undefined,
});

Learn more

On this page