Agent SkillsTanStack/ai › ai-sandbox

ai-sandbox

GitHub

用于在隔离沙箱中运行 Claude Code 等适配器。支持声明式工作区配置、类型安全密钥管理、技能插件注入及生命周期快照,确保执行环境的安全与可复现性。

packages/ai-sandbox/skills/ai-sandbox/SKILL.md TanStack/ai

触发场景

需要为 AI 适配器提供隔离的执行环境 构建或配置沙箱提供者 涉及敏感密钥的安全传递与使用 需要自动化克隆仓库并安装依赖

安装

npx skills add TanStack/ai --skill ai-sandbox -g -y
更多选项

非标准路径

npx skills add https://github.com/TanStack/ai/tree/main/packages/ai-sandbox/skills/ai-sandbox -g -y

不安装直接使用

npx skills use TanStack/ai@ai-sandbox

指定 Agent (Claude Code)

npx skills add TanStack/ai --skill ai-sandbox -a claude-code -g -y

安装 repo 全部 skill

npx skills add TanStack/ai --all -g -y

预览 repo 内 skill

npx skills add TanStack/ai --list

SKILL.md

Frontmatter
{
    "name": "ai-sandbox",
    "type": "sub-skill",
    "library": "tanstack-ai",
    "sources": [
        "TanStack\/ai:docs\/sandbox\/overview.md"
    ],
    "description": "Run harness adapters (Claude Code, Codex, OpenCode) INSIDE isolated sandboxes via defineSandbox + withSandbox + a provider (localProcessSandbox \/ dockerSandbox). Covers declarative provisioning: createSecrets + secret\/bearer, skills (agentSkill\/gitSkill\/mcpSkill\/ fileSkill), plugins, instructions → canonical AGENTS.md + symlinks projected per harness; shallow-clone default with depth opt-out; serial\/parallel setup callback over a persistent shell; snapshot-after-setup default with snapshotMaxAge TTL; defineWorkspace (git\/setup\/scripts\/skills\/secrets\/ instructions\/plugins), defineSandboxPolicy (allow\/ask\/deny), lifecycle\/resume, the SandboxHandle (fs\/git\/process\/ports), capability tokens, defineSandbox hooks (onFile\/onFileCreate\/onFileChange\/onFileDelete\/onReady\/onError\/ onDestroy) + fileEvents flag, chat middleware sandbox group (defineChatMiddleware sandbox hooks), the sandbox debug category, watchWorkspace as a low-level building block, and the file.changed \/ sandbox.file \/ claude-code.session-id events. Use whenever a harness adapter needs a sandbox or when building sandbox providers.\n",
    "library_version": "0.1.0"
}

Sandboxes

Harness adapters declare requires: [SandboxCapability]. chat() errors unless some middleware provides it — withSandbox(...) does. The adapter then runs the agent CLI inside the sandbox and streams its events back.

Setup — Claude Code in a Docker sandbox

import { chat } from '@tanstack/ai'
import { claudeCodeText } from '@tanstack/ai-claude-code'
import {
  defineSandbox,
  defineWorkspace,
  withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'

const sandbox = defineSandbox({
  id: 'repo-agent',
  provider: dockerSandbox({ image: 'node:22' }),
  workspace: defineWorkspace({
    source: { type: 'git', url: 'https://github.com/owner/repo', ref: 'main' },
    packageManager: 'pnpm',
    setup: ['corepack enable', 'pnpm install'],
    scripts: { test: 'pnpm test' },
    secrets: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '' },
  }),
  lifecycle: { reuse: 'thread', snapshot: 'after-setup', keepAlive: '30m' },
})

const stream = chat({
  threadId,
  adapter: claudeCodeText('sonnet'),
  messages,
  middleware: [withSandbox(sandbox)],
})

Type-safe secrets

import { createSecrets, bearer } from '@tanstack/ai-sandbox'

const secrets = createSecrets({
  GH: process.env.GH_TOKEN ?? '',
  SENTRY: process.env.SENTRY_TOKEN ?? '',
})
// secrets.GH is a SecretRef — the underlying string is stored in a
// non-enumerable symbol-keyed registry and never logged, snapshotted,
// or written to the sandbox store.

Pass secrets to defineWorkspace({ secrets }) so skill and MCP projectors can resolve them. Use secret: secrets.GH in gitSkill for private-repo auth and secrets.GH / bearer(secrets.GH) in MCP header values:

  • secrets.GH — resolves to the raw token value.
  • bearer(secrets.GH) — resolves to "Bearer <value>".

Declarative provisioning (skills, plugins, MCP, instructions)

import {
  agentSkill,
  gitSkill,
  mcpSkill,
  fileSkill,
  bearer,
  createSecrets,
  defineWorkspace,
} from '@tanstack/ai-sandbox'

const secrets = createSecrets({ GH: process.env.GH_TOKEN ?? '' })

defineWorkspace({
  source: { type: 'git', url: 'https://github.com/owner/repo' },
  secrets,
  skills: [
    agentSkill('tanstack'), // named skill (no-op with warning on CLIs that lack the concept)
    gitSkill({
      repo: 'owner/private-skills',
      secret: secrets.GH, // resolved at bootstrap time, never stored
      // into: '/abs/path/inside/sandbox'  // optional; defaults to .tanstack-skills/<repo>
    }),
    mcpSkill('my-mcp', {
      url: 'https://mcp.example.com',
      headers: { Authorization: bearer(secrets.GH) },
    }),
    fileSkill({ path: '.hints.md', content: 'Prefer pnpm.' }),
  ],
  plugins: ['@anthropic/plugin-foo'], // no-op with warning on CLIs without a plugin concept
  instructions: 'Always run `pnpm test` before proposing a change.',
})

Each skill type is projected per harness (Claude Code → .mcp.json; Codex → .codex/config.toml; OpenCode → opencode.json). instructions is written as AGENTS.md at the workspace root; CLAUDE.md and GEMINI.md are created as symlinks (falling back to copies on symlink failure). Skills/plugins that a CLI lacks emit a console.warn and are skipped.

gitSkill into field: an absolute path inside the sandbox where the repo is cloned. Defaults to <root>/.tanstack-skills/<repo-basename>.

Fast init

Shallow clone (depth)

githubRepo / gitSource default to --depth 1 --single-branch. Override:

import { githubRepo, defineWorkspace } from '@tanstack/ai-sandbox'

defineWorkspace({ source: githubRepo({ repo: 'owner/app' }) }) // depth 1 (default)
defineWorkspace({ source: githubRepo({ repo: 'owner/app', depth: 10 }) }) // 10 commits
defineWorkspace({ source: githubRepo({ repo: 'owner/app', depth: 'full' }) }) // full history

Serial / parallel setup callback

setup accepts a plain Array<string> (all serial) or a callback that records serial and parallel groups over a persistent shell whose cwd/env carry over between serial steps:

defineWorkspace({
  source: githubRepo({ repo: 'owner/app' }),
  setup: ({ serial, parallel }) => {
    serial('corepack enable')
    serial('pnpm install')
    parallel(['pnpm build', 'pnpm typecheck']) // concurrent; inherit cwd+env from shell
    serial('echo done')
  },
})

Snapshot-after-setup and snapshotMaxAge

When the provider supports snapshots, bootstrap takes one automatically after setup completes. Subsequent runs resume from the snapshot (skipping setup). Override or add a TTL:

lifecycle: {
  snapshot: 'after-setup', // default when provider.capabilities().snapshots
  snapshotMaxAge: '24h',   // re-create when the snapshot is older than this
}

Providers without snapshot support skip the step silently.

Providers

  • localProcessSandbox() — runs on the host (no isolation; dev loop only).
  • dockerSandbox({ image }) — isolated container; snapshots, fork, resume-by-id.

Both implement the same SandboxHandle: fs (read/write/list/mkdir/remove/ rename/exists), git (clone/status/add/commit/push/pull/branch), process (exec + duplex spawn), ports.connect(port), env.set, optional snapshot()/fork(), destroy(). Providers advertise support via capabilities(); calling an unsupported optional method throws UnsupportedCapabilityError.

Policy

import { defineSandboxPolicy } from '@tanstack/ai-sandbox'

const policy = defineSandboxPolicy({
  commands: {
    allow: ['pnpm test'],
    ask: ['curl *'],
    deny: ['sudo *', 'rm -rf *'],
  },
  capabilities: { fileWrite: 'allow', network: 'ask' },
  default: 'ask', // deny > ask > allow
})
// pass to defineSandbox({ policy }); harness adapters map it to native permissions

Lifecycle & resume

reuse: 'thread' resumes one sandbox per threadId; the compound key folds in provider + workspace hash + tenant so changing the repo/setup/image starts fresh. Ensure order: resume running → restore snapshot → create + bootstrap.

File-event hooks

Watch the workspace for create/change/delete events. Provider-agnostic: native fs.watch on local-process, a portable find poll on Docker/exec-only providers (no extra deps or image changes).

Declare hooks on defineSandbox({ hooks }) (sandbox-scoped) or on any chat middleware via the sandbox group (run-scoped):

import {
  defineSandbox,
  defineChatMiddleware,
  withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'

// Sandbox-scoped hooks (all optional):
const sandbox = defineSandbox({
  id: 'repo-agent',
  provider: dockerSandbox({ image: 'node:22' }),
  hooks: {
    onFile: (e) => console.log(e.type, e.path), // catch-all
    onFileCreate: (e) => console.log('created', e.path),
    onFileChange: (e) => console.log('changed', e.path),
    onFileDelete: (e) => console.log('deleted', e.path),
    onReady: (handle) => console.log('ready', handle.id),
    onError: (err) => console.error(err),
    onDestroy: () => console.log('destroyed'),
  },
  fileEvents: true, // default; set false to disable watching entirely
})

// Run-scoped hooks via chat middleware (ctx is ChatMiddlewareContext):
const auditMiddleware = defineChatMiddleware({
  name: 'audit',
  sandbox: {
    onFile: (ctx, e) => console.log(ctx.runId, e.type, e.path),
    onFileCreate: (ctx, e) => db.log({ run: ctx.runId, event: e }),
    onFileChange: (ctx, e) => metrics.increment('file.change'),
    onFileDelete: (ctx, e) => console.warn('deleted', e.path),
  },
})

// No extra middleware needed — sandbox.file CUSTOM events are emitted
// automatically. Read them from the stream:
for await (const chunk of stream) {
  if (chunk.type === 'CUSTOM' && chunk.name === 'sandbox.file') {
    const value = chunk.value
    if (
      value !== null &&
      typeof value === 'object' &&
      'type' in value &&
      'path' in value
    ) {
      console.log('file event', value) // { type, path, timestamp }
    }
  }
}

watchWorkspace() is available as a low-level building block for watching outside a chat() run:

import { watchWorkspace } from '@tanstack/ai-sandbox'

const watcher = await watchWorkspace(handle, {
  onEvent: (e) => console.log(e.type, e.path),
  ignore: ['.git', 'node_modules'], // default
})
await watcher.stop()

Enable the sandbox debug category to log watcher start/stop, event dispatch, and lifecycle transitions:

chat({ threadId, adapter, messages, debug: { sandbox: true } })
// or debug: true to enable all categories

Edge / serverless execution

A request-scoped Worker can't hold a multi-minute agent run open. The serverless/edge model splits this: a trigger starts the run and returns immediately, a durable orchestrator drives it, and clients tail from a resumable cursor.

Core primitives (@tanstack/ai-sandbox, transport- and runtime-agnostic):

  • RunEventLog / InMemoryRunEventLog — append-only, seq-indexed log of a run's StreamChunks with replay-then-tail reads. A dropped connection / new tab / hibernated orchestrator reconnect by passing their last-seen seq (read({ fromSeq })). TerminalRunStatus = done | error | aborted.
  • pipeToRunLog / RunController — the run driver. pipeToRunLog pumps a chat() stream into a log and never rejects: a thrown stream error becomes a terminal RUN_ERROR event, so detached clients always observe failures. RunController.start is fire-and-track; attach(runId, { fromSeq }) tails; drain() awaits in-flight runs (e.g. in a waitUntil).
  • Transport-agnostic tool-bridgecreateToolBridgeCore + handleBridgeJsonRpc are the portable core; startHostToolBridge is the node:http host transport. The ToolBridgeProvisioner capability injects the transport, so an edge orchestrator serves the same core from its own fetch handler (no raw TCP listener). Default = host transport.
  • Co-located host-tool seamtoolDescriptors / remoteToolStubs / httpRemoteToolExecutor (container side) + executeHostTool (orchestrator side): only chat()-tool EXECUTION crosses the container→orchestrator boundary, not the whole MCP protocol.
  • SandboxCapabilities.writableStdinfalse for providers (e.g. Cloudflare) with no writable host→process stdin; stdin-fed harnesses then deliver the prompt via a file + in-shell redirection (claude -p … < file).

Cloudflare runtime (@tanstack/ai-sandbox-cloudflare):

  • createCloudflareSandboxAgent(config){ Coordinator, Sandbox, worker } — an app's worker.ts is one configured call plus the wrangler-required DO re-exports. Two models via mode: do-drives (the DO runs chat()) and colocated (harness + bridge run in-container; the DO is a thin coordinator, pair with runInContainerHarness from /runner).
  • DurableObjectRunEventLog mirrors InMemoryRunEventLog over DO storage; timingSafeBearerEqualWeb is the Web-Crypto constant-time bearer check.

Events

  • claude-code.session-id (CUSTOM) — resumable session id → pass back via modelOptions.sessionId.
  • file.changed (CUSTOM) — { path, diff } working-tree diff after the run.
  • sandbox.file (CUSTOM) — { type, path, timestamp } per file create/change/ delete, emitted automatically when a sandbox is active.

Critical rules

  • Harness adapters require a sandbox. Always include withSandbox(...) in middleware — without it chat() throws a missing-capability error.
  • Secrets (workspace.secrets) are injected into the sandbox env and never persisted (no snapshots, no sandbox store, no event log). Always create them with createSecrets(...) so the values stay hidden behind SecretRef tokens. The agent binary (claude) must exist in the sandbox image (install it in setup or bake it into the image).
  • Secret-bearing projected files (e.g. MCP config with resolved header values) are re-written on every projection call so rotated secrets re-apply; they are never included in a snapshot.
  • chat()-provided tools are bridged into the in-sandbox agent over a host-side MCP tool-proxy: the agent calls them as mcp__tanstack__<tool> and each call is proxied back to the host where the tool's execute() runs (with its closures / DB / secrets). The agent also has its own native tools (Bash/Edit/Read/…). The host bridge binds on the host; the sandbox reaches it (localhost, or host.docker.internal for Docker), gated by a per-run bearer token.
  • Use localProcessSandbox() only in trusted/dev contexts (no isolation).
  • Skills/plugins that a CLI lacks (e.g. agentSkill on Codex, plugins on Codex) warn and skip — they do not throw.

版本历史

  • 5deda27 当前 2026-07-05 10:52

同 Skill 集合

.claude/skills/gap-analysis/SKILL.md
packages/ai-code-mode/skills/ai-code-mode/SKILL.md
packages/ai-mcp/skills/ai-mcp/SKILL.md
packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md
packages/ai/skills/ai-core/chat-experience/SKILL.md
packages/ai/skills/ai-core/custom-backend-integration/SKILL.md
packages/ai/skills/ai-core/debug-logging/SKILL.md
packages/ai/skills/ai-core/middleware/SKILL.md
packages/ai/skills/ai-core/SKILL.md
packages/ai/skills/ai-core/tool-calling/SKILL.md
.claude/skills/triage-github/SKILL.md
packages/ai/skills/ai-core/adapter-configuration/SKILL.md
packages/ai/skills/ai-core/media-generation/SKILL.md
packages/ai/skills/ai-core/structured-outputs/SKILL.md

元信息

文件数
0
版本
5fcaf90
Hash
dc4dc484
收录时间
2026-07-05 10:52

首页 - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-13 20:39
浙ICP备14020137号-1 $访客地图$