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 outputParameters
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,
});