Run AI SDK sandbox sessions in Apple Container on your Mac.
@lgrammel/apple-container-sandbox is an AI SDK Harness V1 sandbox provider
that creates a long-lived Apple Container session for each sandbox. It gives
AI agents and local tools a Docker-compatible image, isolated file access,
command execution, and optional localhost port publishing without leaving your
Apple silicon development machine.
- Use the AI SDK sandbox API with Apple Container instead of a remote sandbox service.
- Run commands in Docker-compatible images through the Apple Container CLI.
- Read and write files inside the sandbox with the AI SDK
Experimental_SandboxSessionmethods. - Publish selected sandbox ports on
127.0.0.1for local bridges, servers, or harness integrations. - Clean up containers automatically when a session stops.
- Apple silicon Mac
- macOS 26 or newer
- Apple Container CLI available as
container, or a customcontainerBinaryoption - Node.js 22 or newer
- ESM project configuration
Install Apple Container:
brew install container
container system start
container --versionIf you do not use Homebrew, download a signed installer from the Apple Container releases page.
Install the sandbox provider:
pnpm add @lgrammel/apple-container-sandboxWrite a file, run it in a sandbox, read the result, and stop the session:
import { createAppleContainerSandbox } from "@lgrammel/apple-container-sandbox";
const appleContainerSandbox = createAppleContainerSandbox({
image: "node:22",
cwd: "/workspace",
});
const sandboxSession = await appleContainerSandbox.createSession();
try {
await sandboxSession.writeTextFile({
path: "/workspace/example.js",
content: [
"const message = 'Hello from Apple Container Sandbox';",
"await import('node:fs/promises').then((fs) =>",
" fs.writeFile('/workspace/result.txt', message),",
");",
"console.log(message);",
].join("\n"),
});
const runResult = await sandboxSession.run({
command: "node /workspace/example.js",
});
console.log(runResult.stdout.trim());
console.log(await sandboxSession.readTextFile({ path: "/workspace/result.txt" }));
} finally {
await sandboxSession.stop();
}In this repository, run the workspace example:
pnpm exampleThe apps/tool-example package shows how to pass an Apple Container sandbox to
AI SDK generateText with experimental_sandbox. Its shell tool uses the
sandbox from tool execution options and runs commands with
sandbox.run(...).
import { generateText, isStepCount, tool } from "ai";
import { z } from "zod/v4";
import { createAppleContainerSandbox } from "@lgrammel/apple-container-sandbox";
const appleContainerSandbox = createAppleContainerSandbox({
image: "node:22",
cwd: "/workspace",
});
const shellTool = tool({
description: "Run a shell command inside the Apple Container sandbox.",
inputSchema: z.object({
command: z.string(),
workingDirectory: z.string().optional(),
}),
execute: async ({ command, workingDirectory }, { experimental_sandbox, abortSignal }) => {
if (!experimental_sandbox) {
throw new Error("The shell tool requires an AI SDK experimental_sandbox.");
}
return experimental_sandbox.run({
command,
workingDirectory,
abortSignal,
});
},
});
const sandboxSession = await appleContainerSandbox.createSession();
try {
const result = await generateText({
model: "openai/gpt-4.1-mini",
tools: {
shell: shellTool,
},
experimental_sandbox: sandboxSession,
stopWhen: isStepCount(3),
prompt: "Use the shell tool to create and run a small Node.js file.",
});
console.log(result.text);
} finally {
await sandboxSession.stop();
}In this repository, run the workspace example:
cp apps/tool-example/.env.example apps/tool-example/.env
pnpm example:toolSet AI_GATEWAY_API_KEY and TOOL_MODEL in apps/tool-example/.env.
The apps/codex-example package runs
@ai-sdk/harness-codex
inside an Apple Container sandbox. The Codex harness starts a WebSocket bridge
inside the sandbox, so the sandbox publishes a local bridge port.
import "dotenv/config";
import { createCodex } from "@ai-sdk/harness-codex";
import type { HarnessV1Bootstrap, HarnessV1StreamPart } from "@ai-sdk/harness";
import {
AppleContainerSandboxError,
createAppleContainerSandbox,
type AppleContainerSandboxSession,
} from "@lgrammel/apple-container-sandbox";
const codexPort = Number.parseInt(process.env.CODEX_BRIDGE_PORT ?? "4100", 10);
const sessionId = process.env.CODEX_SESSION_ID ?? "apple-container-codex-example";
const sessionWorkDir = "/workspace/codex-demo";
const prompt =
process.argv.slice(2).join(" ") ||
[
"Create a hello.js file in the current directory.",
"It should print a concise greeting from Codex running inside Apple Container.",
"Then run it with node and report what happened.",
].join(" ");
if (!Number.isInteger(codexPort) || codexPort < 1 || codexPort > 65535) {
throw new Error("CODEX_BRIDGE_PORT must be an integer from 1 to 65535.");
}
if (!process.env.OPENAI_API_KEY && !process.env.CODEX_API_KEY && !process.env.AI_GATEWAY_API_KEY) {
throw new Error("Set OPENAI_API_KEY, CODEX_API_KEY, or AI_GATEWAY_API_KEY before running Codex.");
}
const codexHarness = createCodex({
model: process.env.CODEX_MODEL,
port: codexPort,
});
const appleContainerSandbox = createAppleContainerSandbox({
image: "node:22",
cwd: "/workspace",
ports: [codexPort],
});
let sandboxSession: AppleContainerSandboxSession | undefined;
let codexSession: Awaited<ReturnType<typeof codexHarness.doStart>> | undefined;
type CreateSessionOptions = NonNullable<Parameters<typeof appleContainerSandbox.createSession>[0]>;
type OnFirstCreate = NonNullable<CreateSessionOptions["onFirstCreate"]>;
type BootstrapSession = Parameters<OnFirstCreate>[0];
type RunOptions = Parameters<BootstrapSession["run"]>[0];
try {
const bootstrap = await codexHarness.getBootstrap?.();
sandboxSession = await appleContainerSandbox.createSession({
sessionId,
onFirstCreate: async (session, { abortSignal }) => {
await runRequired(session, {
command: "corepack enable pnpm && corepack prepare pnpm@11.9.0 --activate",
abortSignal,
});
if (bootstrap) {
await applyBootstrap(session, bootstrap, abortSignal);
}
},
});
codexSession = await codexHarness.doStart({
sessionId,
sandboxSession,
sessionWorkDir,
permissionMode: "allow-all",
});
const control = await codexSession.doPromptTurn({
prompt,
emit: printCodexEvent,
});
await control.done;
} catch (error) {
if (error instanceof AppleContainerSandboxError) {
console.error(error.message);
process.exitCode = 1;
} else {
throw error;
}
} finally {
if (codexSession) {
await Promise.resolve(codexSession.doDestroy()).catch(() => {});
}
await sandboxSession?.stop().catch(() => {});
}
async function applyBootstrap(
session: BootstrapSession,
bootstrap: HarnessV1Bootstrap,
abortSignal?: AbortSignal,
) {
for (const file of bootstrap.files) {
await session.writeTextFile({
path: file.path,
content: file.content,
abortSignal,
});
}
for (const command of bootstrap.commands) {
await runRequired(session, {
command: command.command,
workingDirectory: command.workingDirectory,
abortSignal,
});
}
}
async function runRequired(session: BootstrapSession, options: RunOptions) {
const result = await session.run(options);
if (result.exitCode !== 0) {
throw new Error(`Codex bootstrap failed: ${result.stderr || result.stdout}`);
}
}
function printCodexEvent(event: HarnessV1StreamPart) {
switch (event.type) {
case "text-delta":
case "reasoning-delta":
process.stdout.write(event.delta);
break;
case "tool-call":
console.log(`\n[tool] ${event.toolName}`);
break;
case "file-change":
console.log(`\n[file] ${event.event}: ${event.path}`);
break;
case "finish":
console.log(`\n[finish] ${event.finishReason.unified}`);
break;
case "error":
console.error(event.error);
break;
}
}cp apps/codex-example/.env.example apps/codex-example/.env
pnpm example:codexThe example loads apps/codex-example/.env with dotenv. Set one of
OPENAI_API_KEY, CODEX_API_KEY, or AI_GATEWAY_API_KEY.
Optional environment variables:
CODEX_MODEL: model passed to the Codex harness.CODEX_BRIDGE_PORT: local bridge port. Defaults to4100.CODEX_SESSION_ID: deterministic Apple Container session id.
const appleContainerSandbox = createAppleContainerSandbox({
image: "node:22",
cwd: "/workspace",
env: {
NODE_ENV: "development",
},
memory: "2G",
mounts: [
{
hostPath: process.cwd(),
containerPath: "/workspace",
readOnly: true,
},
],
ports: [4100],
containerBinary: "/opt/homebrew/bin/container",
keepContainer: false,
});image: Docker-compatible image for the sandbox. Defaults toalpine:latest.cwd: default working directory inside the sandbox. Defaults to/workspace.env: default environment variables for sandbox commands.memory: amount of memory allocated to the container. Passed tocontainer create --memory.mounts: host directories to bind mount into the sandbox. RelativehostPathvalues are resolved when the sandbox provider is created.ports: TCP ports published on127.0.0.1with the same host and container port.containerBinary: Apple Container CLI binary. Defaults tocontainer.containerArgs: extra arguments passed tocontainer createbefore the image name.name: explicit container name. A random name is generated when omitted.keepContainer: keep the container afterstop(). Defaults tofalse.
If container is installed but not on PATH, either add its directory to
PATH or set containerBinary.
MIT