{"_id":"@ai-craft/sandbox","name":"@ai-craft/sandbox","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@ai-craft/sandbox","version":"1.0.0","description":"AI-agent sandboxing SDK — sandcastle-shaped, multi-provider","type":"module","main":"./index.js","module":"./index.js","types":"./index.d.ts","bin":{"ai-sandbox":"cli/host-cli.js"},"exports":{".":{"types":"./index.d.ts","import":"./index.js"},"./providers/docker":{"types":"./providers/docker/index.d.ts","import":"./providers/docker/index.js"},"./providers/podman":{"types":"./providers/podman/index.d.ts","import":"./providers/podman/index.js"},"./providers/kubernetes":{"types":"./providers/kubernetes/index.d.ts","import":"./providers/kubernetes/index.js"},"./providers/e2b":{"types":"./providers/e2b/index.d.ts","import":"./providers/e2b/index.js"},"./providers/daytona":{"types":"./providers/daytona/index.d.ts","import":"./providers/daytona/index.js"},"./providers/no-sandbox":{"types":"./providers/no-sandbox/index.d.ts","import":"./providers/no-sandbox/index.js"},"./providers/fake":{"types":"./providers/fake/index.d.ts","import":"./providers/fake/index.js"},"./providers/srt":{"types":"./providers/srt/index.d.ts","import":"./providers/srt/index.js"},"./agents/claude-code":{"types":"./agents/claude-code.d.ts","import":"./agents/claude-code.js"},"./agents/codex":{"types":"./agents/codex.d.ts","import":"./agents/codex.js"},"./agents/opencode":{"types":"./agents/opencode.d.ts","import":"./agents/opencode.js"},"./agents/aider":{"types":"./agents/aider.d.ts","import":"./agents/aider.js"},"./agents/native":{"types":"./agents/native.d.ts","import":"./agents/native.js"},"./testing":{"types":"./testing/contract.d.ts","import":"./testing/contract.js"}},"peerDependencies":{"@kubernetes/client-node":">=1.0.0","@e2b/code-interpreter":">=1.0.0","@daytonaio/sdk":">=0.1.0","@anthropic-ai/sandbox-runtime":">=0.0.51"},"peerDependenciesMeta":{"@kubernetes/client-node":{"optional":true},"@e2b/code-interpreter":{"optional":true},"@daytonaio/sdk":{"optional":true},"@anthropic-ai/sandbox-runtime":{"optional":true}},"engines":{"node":">=22.0.0"},"publishConfig":{"access":"public"},"_id":"@ai-craft/sandbox@1.0.0","_integrity":"sha512-tbEFw/UT8RDcyF7Qn1wxRJHkRkhjtKyYsDhbRJTMaeTa/wjtVKHRVsrb2QcBQtdj4DWT53pV9zt/XOJeiR5vTw==","_resolved":"/private/var/folders/_j/tzygz83s12v4rnxgchnxz55m0000gp/T/c0476508347a3bfe9f9a4183cbe7417f/ai-craft-sandbox-1.0.0.tgz","_from":"file:ai-craft-sandbox-1.0.0.tgz","_nodeVersion":"22.14.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-tbEFw/UT8RDcyF7Qn1wxRJHkRkhjtKyYsDhbRJTMaeTa/wjtVKHRVsrb2QcBQtdj4DWT53pV9zt/XOJeiR5vTw==","shasum":"844ddd91f48ad2d8d529cd82836adaded983d2a0","tarball":"https://registry.npmjs.org/@ai-craft/sandbox/-/sandbox-1.0.0.tgz","fileCount":88,"unpackedSize":442544,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBbDEXBOQdZ/TnjfsDT6k8ZTzyNnRKpP3Z4Zvg53BPdhAiEAlbsYxPWzTh1ypNqb4M4fTBlU3irE3+jnggQKmVJF66I="}]},"_npmUser":{"name":"volkz","email":"delacruzd93@gmail.com"},"directories":{},"maintainers":[{"name":"volkz","email":"delacruzd93@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sandbox_1.0.0_1784450392526_0.8349933356434669"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-19T08:39:52.369Z","1.0.0":"2026-07-19T08:39:52.664Z","modified":"2026-07-19T08:39:52.876Z"},"maintainers":[{"name":"volkz","email":"delacruzd93@gmail.com"}],"description":"AI-agent sandboxing SDK — sandcastle-shaped, multi-provider","readme":"# @ai-craft/sandbox\n\nAI-agent sandboxing SDK — provider abstraction, multi-provider lifecycle management.\n\n## Install\n\n```bash\nnpm install @ai-craft/sandbox\n```\n\n## Quick Start\n\n```ts\nimport { createSandbox } from '@ai-craft/sandbox';\nimport { FakeSandboxProvider } from '@ai-craft/sandbox/providers/fake';\n\nconst provider = new FakeSandboxProvider();\n\n// Create a sandbox (automatically selects image based on provider)\nconst sb = await createSandbox({ provider, image: 'ubuntu:22.04' });\n\ntry {\n  const result = await sb.exec(['echo', 'hello world']);\n  console.log(result.stdout); // \"hello world\\n\"\n} finally {\n  await sb.stop();\n}\n\n// Or use the AsyncDisposable pattern:\n{\n  await using sb = await createSandbox({ provider });\n  await sb.exec(['apt-get', 'install', '-y', 'git']);\n}\n```\n\n## Default agent — `ai-agent`\n\nBy 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.\n\n### Why\n\nNative 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.\n\n### How (default `run()`)\n\n```ts\nimport { run } from '@ai-craft/sandbox';\nimport { DockerProvider } from '@ai-craft/sandbox/providers/docker';\n\n// No `agent` argument — defaults to nativeAgent() (the `ai-agent` CLI inside the pod).\nawait run({\n  sandbox: { provider: new DockerProvider(), image: 'ai-craft-sandbox:latest' },\n});\n```\n\n### Override\n\nPass any of the four alternate factories — `claudeCode`, `codex`, `aider`, `opencode` — to swap the in-pod agent:\n\n```ts\nimport { run } from '@ai-craft/sandbox';\nimport { claudeCode } from '@ai-craft/sandbox/agents/claude-code';\n// also available: codex, aider, opencode (each from their own subpath)\n\nawait run({\n  agent: claudeCode(),\n  sandbox: { provider: new DockerProvider(), image: 'ai-craft-sandbox:latest' },\n});\n```\n\n### Build the image\n\n```bash\npnpm exec nx run sandbox:image\n```\n\nThis 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.\n\n### Permission model\n\nInside 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.\n\nSet `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.\n\nSee `packages/agent-cli/README.md#permission-model` for the full deny-list and override path.\n\n## Policy Model\n\n`@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.\n\n### Three rule types\n\n| Surface | Semantics | Notes |\n|---------|-----------|-------|\n| **Writes** (`filesystem.write`) | Allow-only | Empty `allowWrite` means deny all writes. |\n| **Reads** (`filesystem.read`) | Deny-then-allow | `denyRead` closes first; `allowRead` carves back within denied regions. |\n| **Network** (`network`) | Allow-only | `allowedDomains` is enforced. `deniedDomains` takes precedence over `allowedDomains`. |\n\n### Example\n\n```ts\nimport { createSandbox, type SandboxPolicy } from '@ai-craft/sandbox';\nimport { DockerProvider } from '@ai-craft/sandbox/providers/docker';\n\nconst policy: SandboxPolicy = {\n  filesystem: {\n    write: { allowWrite: ['/tmp/workspace'] },\n    read: { denyRead: ['~/.ssh', '~/.aws/credentials'], allowRead: ['/etc/hosts'] },\n  },\n  network: { allowedDomains: ['api.anthropic.com'] },\n};\n\nconst sb = await createSandbox({\n  provider: new DockerProvider(),\n  image: 'ubuntu:22.04',\n  policy,\n  failIfUnavailable: false, // warn-only mode; set true to fail on unenforced rules\n});\n```\n\n### Provider Capability Matrix\n\nEnforcement levels per provider for each policy surface:\n\n| Provider | reads | writes | network | exec |\n|----------|-------|--------|---------|------|\n| `DockerProvider` | `translated` | `translated` | `unenforced`* | `unenforced` |\n| `PodmanProvider` | `translated` | `translated` | `unenforced`* | `unenforced` |\n| `KubernetesProvider` | `translated` | `translated` | `unenforced`* | `unenforced` |\n| `NoSandboxProvider` | `unenforced` | `unenforced` | `unenforced` | `unenforced` |\n| `FakeSandboxProvider` | `unenforced` | `unenforced` | `unenforced` | `unenforced` |\n\n**Enforcement levels:**\n- `native` — enforced by the OS or runtime directly (no translation needed)\n- `translated` — policy is translated into provider-native constraints (volume mounts, etc.)\n- `unenforced` — provider cannot enforce this surface; a `SandboxViolationEvent` with `severity: 'unenforced'` is emitted\n\n*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.\n\n## Providers\n\n| Provider | Description | Subpath |\n|---|---|---|\n| `FakeSandboxProvider` | In-memory stub for testing | `@ai-craft/sandbox/providers/fake` |\n| `KubernetesProvider` | Real K8s pod execution | `@ai-craft/sandbox/providers/kubernetes` |\n| `DockerProvider` | Local Docker containers | `@ai-craft/sandbox/providers/docker` |\n| `PodmanProvider` | Local Podman containers | `@ai-craft/sandbox/providers/podman` |\n| `E2BProvider` | E2B cloud sandboxes | `@ai-craft/sandbox/providers/e2b` |\n| `DaytonaProvider` | Daytona cloud sandboxes | `@ai-craft/sandbox/providers/daytona` |\n| `NoSandboxProvider` | Run directly on host (no isolation) | `@ai-craft/sandbox/providers/no-sandbox` |\n\n## Subpath Exports\n\n| Import path | Exports |\n|---|---|\n| `@ai-craft/sandbox` | `createSandbox`, `createWorktree`, `run`, types |\n| `@ai-craft/sandbox/providers/fake` | `FakeSandboxProvider`, `FakeProviderHandle` |\n| `@ai-craft/sandbox/providers/kubernetes` | `KubernetesProvider` |\n| `@ai-craft/sandbox/providers/docker` | `DockerProvider` |\n| `@ai-craft/sandbox/testing` | `runProviderContract` |\n| `@ai-craft/sandbox/agents/claude-code` | `ClaudeCodeAgent` |\n| `@ai-craft/sandbox/agents/codex` | `CodexAgent` |\n\n## Key Types\n\n| Type | Description |\n|---|---|\n| `Sandbox` | AsyncDisposable sandbox handle returned by `createSandbox()` |\n| `SandboxProvider` | Provider interface (implement to add a new backend) |\n| `ExecResult` | `{ exitCode, stdout, stderr, durationMs, timedOut, truncated }` |\n| `ExecOptions` | `{ cwd, env, stdin, signal, timeoutMs, idleTimeoutMs, onLine }` |\n| `CreateSandboxOptions` | `{ provider, image?, env?, mounts?, network?, resources?, hooks? }` |\n\n## Local machine push (SSH)\n\nWhen 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.\n\n### One-time setup (on your laptop)\n\n```bash\n# Create a bare repo to receive the push (once per project)\ngit init --bare ~/sandbox-repos/myproject.git\n\n# Make your laptop reachable from wherever the orchestrator runs\n# (Tailscale, LAN IP, or a public SSH endpoint)\n```\n\n### Orchestrator configuration\n\nSet two environment variables before starting the orchestrator:\n\n```bash\nexport SANDBOX_LOCAL_SSH_HOST=user@laptop.example.com\nexport SANDBOX_LOCAL_REPO_PATH=/Users/me/sandbox-repos/myproject.git\n```\n\nWhen 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.\n\n**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.\n\n**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.\n\n### Retrieving commits on your laptop\n\n```bash\ncd ~/myproject\ngit fetch ~/sandbox-repos/myproject.git <branch-name>\ngit checkout <branch-name>\n```\n\n### Programmatic API\n\nYou can also pass `pushDestination` directly to `createWorktree()`:\n\n```ts\nimport { createWorktree } from '@ai-craft/sandbox';\n\nconst worktree = await createWorktree({\n  repoPath: '/path/to/repo',\n  strategy: {\n    type: 'branch',\n    name: 'sandbox/my-session',\n    push: true,\n    pushDestination: {\n      sshHost: 'user@laptop.example.com',\n      repoPath: '/Users/me/sandbox-repos/myproject.git',\n      // remoteName: 'sandbox-local',  // optional; defaults to 'sandbox-local'\n    },\n  },\n});\n\nconst result = await worktree.finalize();\nconsole.log(result.pushed); // true\n```\n\n> **Prerequisite:** 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.\n\n## Requirements\n\n- Node ≥ 22.0.0\n\n## Build\n\n```bash\nnx build sandbox\n```\n\n## Test\n\n```bash\nnx test sandbox\n```\n","readmeFilename":"README.md","_rev":"1-bb9c7301126015ea06bb7f67fb83e072"}