@pai/sandbox-inmemory
Scriptable in-process SandboxProvider implementation for tests and demos.
Package: @pai/sandbox-inmemory
Fakes a sandbox filesystem and a scriptable command runtime in memory. Nothing is isolated and state is lost when the provider instance is discarded — use it for agent tests, demos, and examples.
It is faithful to the contract a real adapter honors: expired and killed sandboxes throw SandboxNotFoundError from every session operation, connect returns null for gone sandboxes and resumes paused ones, create({ bindingKey }) returns the live sandbox already bound to the key, and command timeouts and aborts reject the same way a real runtime does. Expiry-recreate flows through the session manager and tool bundle are fully testable without a network.
Quick start
Swap it in as the capability binding — the production agent stays verbatim:
import { createTestRuntime } from "@pai/test-utils";
import { sandboxCapability } from "@pai/sandbox";
import { createInMemorySandboxProvider } from "@pai/sandbox-inmemory";
await using runtime = createTestRuntime({
agent,
model,
capabilities: {
sandbox: sandboxCapability({ provider: createInMemorySandboxProvider() }),
},
});Options
type InMemorySandboxProviderOptions = {
/** Scripted commands, checked in order before the built-ins. First match wins. */
commands?: InMemoryCommandSpec[];
/** Injectable clock for TTL bookkeeping. Defaults to `Date.now`. */
now?: () => number;
/** Default idle lifetime for created sandboxes. Defaults to 5 minutes. */
defaultTtlMs?: number;
/** Override how sandbox ids are minted. */
generateId?: () => string;
};Commands
Built-ins cover what agents commonly run: echo, pwd, printenv, sleep, cat, and ls. Anything else exits 127 with command not found on stderr. Script the commands your test needs with commands:
const provider = createInMemorySandboxProvider({
commands: [
{ match: "git status", handler: () => ({ stdout: "clean\n" }) },
{
match: /^npm test/,
handler: ({ emitStdout, files }) => {
emitStdout("1 passed\n");
files.write("report.json", '{"passed":1}');
},
},
],
});type InMemoryCommandSpec = {
/** A string matches the full command exactly; RegExp and predicate as expected. */
match: string | RegExp | ((command: string) => boolean);
handler: InMemoryCommandHandler;
};
type InMemoryCommandHandler = (
invocation: InMemoryCommandInvocation,
) => InMemoryCommandResult | void | Promise<InMemoryCommandResult | void>;
type InMemoryCommandInvocation = {
command: string;
/** Tokenized with single/double-quote support. */
argv: string[];
cwd: string;
env: Record<string, string>;
/** Stream output live to the caller's onStdout/onStderr. */
emitStdout(chunk: string): void;
emitStderr(chunk: string): void;
/** Fires on timeout or caller abort. */
signal: AbortSignal;
/** Sandbox filesystem access. Relative paths resolve against cwd. */
files: InMemoryCommandFiles;
};
type InMemoryCommandResult = {
stdout?: string;
stderr?: string;
/** Defaults to 0. */
exitCode?: number;
};Handlers stream through emitStdout/emitStderr or return output in the result, and read and write the sandbox filesystem via files. Throwing rejects runCommand; return a non-zero exit code to simulate a failing command.
Session file operations and the default command cwd resolve relative paths against IN_MEMORY_SANDBOX_HOME (/home/user).
Simulating expiry
TTL bookkeeping uses the injectable now clock, so expiry-recreate flows are testable without real timers:
let now = 0;
const provider = createInMemorySandboxProvider({ now: () => now });
const session = await provider.create({ ttlMs: 1_000 });
now += 1_000;
// The sandbox is now gone: session operations throw SandboxNotFoundError
// and provider.connect() returns null, so the session manager recreates.Command timeouts and the sleep built-in always use real timers.
Conformance
createInMemorySandboxProvider passes the full @pai/sandbox/test conformance suite. Two of the suite's shell recipes are overridden with scripted equivalents because the fake is not a POSIX shell:
import { createSandboxProviderConformanceSuite } from "@pai/sandbox/test";
import { createInMemorySandboxProvider } from "@pai/sandbox-inmemory";
createSandboxProviderConformanceSuite({
name: "createInMemorySandboxProvider",
createProvider: () =>
createInMemorySandboxProvider({
commands: [
{
match: "fail-with-oops",
handler: () => ({ stderr: "oops", exitCode: 3 }),
},
],
}),
commands: {
stderrExit3: "fail-with-oops",
printEnvVar: "printenv PAI_CONFORMANCE",
},
});