@ai-craft/sandbox
v1.0.0
Published
AI-agent sandboxing SDK — sandcastle-shaped, multi-provider
Readme
@ai-craft/sandbox
AI-agent sandboxing SDK — provider abstraction, multi-provider lifecycle management.
Install
npm install @ai-craft/sandboxQuick Start
import { createSandbox } from '@ai-craft/sandbox';
import { FakeSandboxProvider } from '@ai-craft/sandbox/providers/fake';
const provider = new FakeSandboxProvider();
// Create a sandbox (automatically selects image based on provider)
const sb = await createSandbox({ provider, image: 'ubuntu:22.04' });
try {
const result = await sb.exec(['echo', 'hello world']);
console.log(result.stdout); // "hello world\n"
} finally {
await sb.stop();
}
// Or use the AsyncDisposable pattern:
{
await using sb = await createSandbox({ provider });
await sb.exec(['apt-get', 'install', '-y', 'git']);
}Default agent — ai-agent
By default, @ai-craft/sandbox runs ai-agent (the multi-provider AI agent CLI from @ai-craft/agent-cli) inside any sandbox built from the standard image.
Why
Native to the SDK — bundled into the default sandbox image at /usr/local/bin/ai-agent so pods can drive a multi-provider plan-execute harness with prompt caching and cost-optimized model routing out-of-the-box. No host round-trips per tool call: the loop runs in-pod against local FS/shell. Other coding CLIs (claude, codex, aider, opencode) coexist as opt-in overrides.
How (default run())
import { run } from '@ai-craft/sandbox';
import { DockerProvider } from '@ai-craft/sandbox/providers/docker';
// No `agent` argument — defaults to nativeAgent() (the `ai-agent` CLI inside the pod).
await run({
sandbox: { provider: new DockerProvider(), image: 'ai-craft-sandbox:latest' },
});Override
Pass any of the four alternate factories — claudeCode, codex, aider, opencode — to swap the in-pod agent:
import { run } from '@ai-craft/sandbox';
import { claudeCode } from '@ai-craft/sandbox/agents/claude-code';
// also available: codex, aider, opencode (each from their own subpath)
await run({
agent: claudeCode(),
sandbox: { provider: new DockerProvider(), image: 'ai-craft-sandbox:latest' },
});Build the image
pnpm exec nx run sandbox:imageThis depends on agent-cli:build automatically — the resulting dist/packages/agent-cli/main.js is baked into /usr/local/bin/ai-agent during docker build. The image is tagged ai-craft-sandbox:latest for local use.
Permission model
Inside the pod, ai-agent's bash tool runs commands permission-less by design. As defense-in-depth a small deny-list blocks destructive patterns (force-push, git reset --hard, rm -rf /, DROP TABLE, sudo, pipe-to-shell, etc.). When a command matches, the tool returns a structured error string identifying the matched pattern.
Set AGENT_DANGEROUS_OK=1 (per-pod env var) to bypass the deny-list when you know what you are doing. The deny-list is defense-in-depth, not a security guarantee — adversarial bypass via shell quoting is possible.
See packages/agent-cli/README.md#permission-model for the full deny-list and override path.
Policy Model
@ai-craft/sandbox supports a declarative SandboxPolicy that callers can attach to any createSandbox() call. Policy is always optional — absent policy leaves all existing behavior unchanged at every call site.
Three rule types
| Surface | Semantics | Notes |
|---------|-----------|-------|
| Writes (filesystem.write) | Allow-only | Empty allowWrite means deny all writes. |
| Reads (filesystem.read) | Deny-then-allow | denyRead closes first; allowRead carves back within denied regions. |
| Network (network) | Allow-only | allowedDomains is enforced. deniedDomains takes precedence over allowedDomains. |
Example
import { createSandbox, type SandboxPolicy } from '@ai-craft/sandbox';
import { DockerProvider } from '@ai-craft/sandbox/providers/docker';
const policy: SandboxPolicy = {
filesystem: {
write: { allowWrite: ['/tmp/workspace'] },
read: { denyRead: ['~/.ssh', '~/.aws/credentials'], allowRead: ['/etc/hosts'] },
},
network: { allowedDomains: ['api.anthropic.com'] },
};
const sb = await createSandbox({
provider: new DockerProvider(),
image: 'ubuntu:22.04',
policy,
failIfUnavailable: false, // warn-only mode; set true to fail on unenforced rules
});Provider Capability Matrix
Enforcement levels per provider for each policy surface:
| Provider | reads | writes | network | exec |
|----------|-------|--------|---------|------|
| DockerProvider | translated | translated | unenforced* | unenforced |
| PodmanProvider | translated | translated | unenforced* | unenforced |
| KubernetesProvider | translated | translated | unenforced* | unenforced |
| NoSandboxProvider | unenforced | unenforced | unenforced | unenforced |
| FakeSandboxProvider | unenforced | unenforced | unenforced | unenforced |
Enforcement levels:
native— enforced by the OS or runtime directly (no translation needed)translated— policy is translated into provider-native constraints (volume mounts, etc.)unenforced— provider cannot enforce this surface; aSandboxViolationEventwithseverity: 'unenforced'is emitted
*Network domain filtering for Docker/Podman/Kubernetes currently sets --network none + HTTP_PROXY env as a placeholder. Full per-domain enforcement requires the Stage 4 proxy bridge. With failIfUnavailable: true, these providers will throw SandboxUnenforcedPolicyError when network.allowedDomains is set.
Providers
| Provider | Description | Subpath |
|---|---|---|
| FakeSandboxProvider | In-memory stub for testing | @ai-craft/sandbox/providers/fake |
| KubernetesProvider | Real K8s pod execution | @ai-craft/sandbox/providers/kubernetes |
| DockerProvider | Local Docker containers | @ai-craft/sandbox/providers/docker |
| PodmanProvider | Local Podman containers | @ai-craft/sandbox/providers/podman |
| E2BProvider | E2B cloud sandboxes | @ai-craft/sandbox/providers/e2b |
| DaytonaProvider | Daytona cloud sandboxes | @ai-craft/sandbox/providers/daytona |
| NoSandboxProvider | Run directly on host (no isolation) | @ai-craft/sandbox/providers/no-sandbox |
Subpath Exports
| Import path | Exports |
|---|---|
| @ai-craft/sandbox | createSandbox, createWorktree, run, types |
| @ai-craft/sandbox/providers/fake | FakeSandboxProvider, FakeProviderHandle |
| @ai-craft/sandbox/providers/kubernetes | KubernetesProvider |
| @ai-craft/sandbox/providers/docker | DockerProvider |
| @ai-craft/sandbox/testing | runProviderContract |
| @ai-craft/sandbox/agents/claude-code | ClaudeCodeAgent |
| @ai-craft/sandbox/agents/codex | CodexAgent |
Key Types
| Type | Description |
|---|---|
| Sandbox | AsyncDisposable sandbox handle returned by createSandbox() |
| SandboxProvider | Provider interface (implement to add a new backend) |
| ExecResult | { exitCode, stdout, stderr, durationMs, timedOut, truncated } |
| ExecOptions | { cwd, env, stdin, signal, timeoutMs, idleTimeoutMs, onLine } |
| CreateSandboxOptions | { provider, image?, env?, mounts?, network?, resources?, hooks? } |
Local machine push (SSH)
When an agent session ends, you can have the worktree push its branch directly to your local machine over SSH — no GitHub round-trip required. This is useful when the orchestrator runs on a remote host (cloud VM, dev container, K8s pod) and you want commits available locally without pushing to a hosted origin.
One-time setup (on your laptop)
# Create a bare repo to receive the push (once per project)
git init --bare ~/sandbox-repos/myproject.git
# Make your laptop reachable from wherever the orchestrator runs
# (Tailscale, LAN IP, or a public SSH endpoint)Orchestrator configuration
Set two environment variables before starting the orchestrator:
export [email protected]
export SANDBOX_LOCAL_REPO_PATH=/Users/me/sandbox-repos/myproject.gitWhen both variables are set, tools/sandbox-run.mjs automatically wires the worktree to push to your laptop over SSH instead of origin. Full git history is preserved.
Auto-bootstrap: if the bare repo does not yet exist on the laptop, the SDK will SSH in, run git init --bare, and retry the push automatically.
SSH auth: the push runs on the orchestrator host using ~/.ssh/ keys. No extra mount or key generation is needed if your SSH agent is already configured.
Retrieving commits on your laptop
cd ~/myproject
git fetch ~/sandbox-repos/myproject.git <branch-name>
git checkout <branch-name>Programmatic API
You can also pass pushDestination directly to createWorktree():
import { createWorktree } from '@ai-craft/sandbox';
const worktree = await createWorktree({
repoPath: '/path/to/repo',
strategy: {
type: 'branch',
name: 'sandbox/my-session',
push: true,
pushDestination: {
sshHost: '[email protected]',
repoPath: '/Users/me/sandbox-repos/myproject.git',
// remoteName: 'sandbox-local', // optional; defaults to 'sandbox-local'
},
},
});
const result = await worktree.finalize();
console.log(result.pushed); // truePrerequisite: the orchestrator host must be able to reach the laptop over SSH (Tailscale, VPN, or LAN). This is a deployment concern — the SDK manages the git protocol, not the network path.
Requirements
- Node ≥ 22.0.0
Build
nx build sandboxTest
nx test sandbox