@tanstack/ai-sandbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +182 -0
  2. package/dist/esm/agents-file.d.ts +36 -0
  3. package/dist/esm/agents-file.js +44 -0
  4. package/dist/esm/agents-file.js.map +1 -0
  5. package/dist/esm/approvals.d.ts +38 -0
  6. package/dist/esm/approvals.js +36 -0
  7. package/dist/esm/approvals.js.map +1 -0
  8. package/dist/esm/bootstrap.d.ts +17 -0
  9. package/dist/esm/bootstrap.js +124 -0
  10. package/dist/esm/bootstrap.js.map +1 -0
  11. package/dist/esm/bridge-events.d.ts +21 -0
  12. package/dist/esm/bridge-events.js +76 -0
  13. package/dist/esm/bridge-events.js.map +1 -0
  14. package/dist/esm/capabilities.d.ts +26 -0
  15. package/dist/esm/capabilities.js +29 -0
  16. package/dist/esm/capabilities.js.map +1 -0
  17. package/dist/esm/contracts.d.ts +211 -0
  18. package/dist/esm/errors.d.ts +16 -0
  19. package/dist/esm/errors.js +25 -0
  20. package/dist/esm/errors.js.map +1 -0
  21. package/dist/esm/git-exec.d.ts +2 -0
  22. package/dist/esm/git-exec.js +68 -0
  23. package/dist/esm/git-exec.js.map +1 -0
  24. package/dist/esm/harness-cwd.d.ts +2 -0
  25. package/dist/esm/harness-cwd.js +24 -0
  26. package/dist/esm/harness-cwd.js.map +1 -0
  27. package/dist/esm/index.d.ts +39 -0
  28. package/dist/esm/index.js +103 -0
  29. package/dist/esm/index.js.map +1 -0
  30. package/dist/esm/key.d.ts +20 -0
  31. package/dist/esm/key.js +41 -0
  32. package/dist/esm/key.js.map +1 -0
  33. package/dist/esm/middleware.d.ts +5 -0
  34. package/dist/esm/middleware.js +140 -0
  35. package/dist/esm/middleware.js.map +1 -0
  36. package/dist/esm/ngrok.d.ts +16 -0
  37. package/dist/esm/ngrok.js +54 -0
  38. package/dist/esm/ngrok.js.map +1 -0
  39. package/dist/esm/policy.d.ts +47 -0
  40. package/dist/esm/policy.js +44 -0
  41. package/dist/esm/policy.js.map +1 -0
  42. package/dist/esm/projection.d.ts +31 -0
  43. package/dist/esm/projection.js +9 -0
  44. package/dist/esm/projection.js.map +1 -0
  45. package/dist/esm/remote-tools.d.ts +48 -0
  46. package/dist/esm/remote-tools.js +76 -0
  47. package/dist/esm/remote-tools.js.map +1 -0
  48. package/dist/esm/run-log.d.ts +81 -0
  49. package/dist/esm/run-log.js +107 -0
  50. package/dist/esm/run-log.js.map +1 -0
  51. package/dist/esm/run.d.ts +58 -0
  52. package/dist/esm/run.js +89 -0
  53. package/dist/esm/run.js.map +1 -0
  54. package/dist/esm/runner.d.ts +21 -0
  55. package/dist/esm/runner.js +54 -0
  56. package/dist/esm/runner.js.map +1 -0
  57. package/dist/esm/sandbox.d.ts +79 -0
  58. package/dist/esm/sandbox.js +125 -0
  59. package/dist/esm/sandbox.js.map +1 -0
  60. package/dist/esm/secrets.d.ts +37 -0
  61. package/dist/esm/secrets.js +59 -0
  62. package/dist/esm/secrets.js.map +1 -0
  63. package/dist/esm/setup-plan.d.ts +13 -0
  64. package/dist/esm/setup-plan.js +16 -0
  65. package/dist/esm/setup-plan.js.map +1 -0
  66. package/dist/esm/shell.d.ts +45 -0
  67. package/dist/esm/shell.js +164 -0
  68. package/dist/esm/shell.js.map +1 -0
  69. package/dist/esm/store.d.ts +53 -0
  70. package/dist/esm/store.js +34 -0
  71. package/dist/esm/store.js.map +1 -0
  72. package/dist/esm/tool-bridge.d.ts +130 -0
  73. package/dist/esm/tool-bridge.js +197 -0
  74. package/dist/esm/tool-bridge.js.map +1 -0
  75. package/dist/esm/watch.d.ts +36 -0
  76. package/dist/esm/watch.js +144 -0
  77. package/dist/esm/watch.js.map +1 -0
  78. package/dist/esm/workspace.d.ts +128 -0
  79. package/dist/esm/workspace.js +42 -0
  80. package/dist/esm/workspace.js.map +1 -0
  81. package/package.json +72 -0
  82. package/skills/ai-sandbox/SKILL.md +366 -0
  83. package/src/agents-file.ts +101 -0
  84. package/src/approvals.ts +96 -0
  85. package/src/bootstrap.ts +196 -0
  86. package/src/bridge-events.ts +112 -0
  87. package/src/capabilities.ts +47 -0
  88. package/src/contracts.ts +236 -0
  89. package/src/errors.ts +31 -0
  90. package/src/git-exec.ts +114 -0
  91. package/src/harness-cwd.ts +38 -0
  92. package/src/index.ts +222 -0
  93. package/src/key.ts +70 -0
  94. package/src/middleware.ts +233 -0
  95. package/src/ngrok.ts +85 -0
  96. package/src/policy.ts +111 -0
  97. package/src/projection.ts +46 -0
  98. package/src/remote-tools.ts +180 -0
  99. package/src/run-log.ts +224 -0
  100. package/src/run.ts +167 -0
  101. package/src/runner.ts +99 -0
  102. package/src/sandbox.ts +259 -0
  103. package/src/secrets.ts +101 -0
  104. package/src/setup-plan.ts +25 -0
  105. package/src/shell.ts +288 -0
  106. package/src/store.ts +83 -0
  107. package/src/tool-bridge.ts +399 -0
  108. package/src/watch.ts +256 -0
  109. package/src/workspace.ts +151 -0
@@ -0,0 +1,128 @@
1
+ import { SetupInput } from './setup-plan.js';
2
+ import { BearerRef, SecretRef, Secrets } from './secrets.js';
3
+ /**
4
+ * Workspace definition — the portable description of what the agent sees
5
+ * inside the sandbox. Each harness adapter PROJECTS this into its own native
6
+ * format via `projectWorkspace()` (e.g. Claude Code → CLAUDE.md + .claude/skills
7
+ * + --mcp-config). The definition itself is provider- and harness-agnostic.
8
+ */
9
+ /** Where the working tree comes from. */
10
+ export type WorkspaceSource = {
11
+ type: 'git';
12
+ url: string;
13
+ ref?: string;
14
+ auth?: {
15
+ username?: string;
16
+ token: string;
17
+ };
18
+ /**
19
+ * Clone depth. Defaults to `1` (shallow). Pass a number for a specific
20
+ * depth, or `'full'` to fetch the entire history.
21
+ */
22
+ depth?: number | 'full';
23
+ } | {
24
+ type: 'local';
25
+ path: string;
26
+ } | {
27
+ type: 'none';
28
+ };
29
+ /** Clone a git repo into the workspace. `githubRepo` is a convenience wrapper. */
30
+ export declare function gitSource(input: {
31
+ url: string;
32
+ ref?: string;
33
+ auth?: {
34
+ username?: string;
35
+ token: string;
36
+ };
37
+ depth?: number | 'full';
38
+ }): WorkspaceSource;
39
+ export declare function githubRepo(input: {
40
+ repo: string;
41
+ ref?: string;
42
+ auth?: {
43
+ username?: string;
44
+ token: string;
45
+ };
46
+ depth?: number | 'full';
47
+ }): WorkspaceSource;
48
+ export declare function localSource(path: string): WorkspaceSource;
49
+ /**
50
+ * An MCP server config where header names/values may be plain strings or
51
+ * unresolved SecretRef values. Secrets are resolved by each harness projector
52
+ * at projection time — never at definition time.
53
+ */
54
+ export type McpConfig = {
55
+ headers?: Record<string, string | SecretRef | BearerRef>;
56
+ [key: string]: unknown;
57
+ };
58
+ /** A unit of agent guidance/config projected into the harness's native format. */
59
+ export type WorkspaceSkill = {
60
+ kind: 'file';
61
+ path: string;
62
+ content: string;
63
+ } | {
64
+ kind: 'agent-skill';
65
+ name: string;
66
+ } | {
67
+ kind: 'mcp';
68
+ name: string;
69
+ config: McpConfig;
70
+ } | {
71
+ kind: 'git';
72
+ /** Short `owner/repo` or a full HTTPS URL. */
73
+ repo: string;
74
+ /** Optional SecretRef for private-repo authentication. */
75
+ secret?: SecretRef;
76
+ /** Absolute path inside the sandbox to clone into. Defaults to a `.tanstack-skills/<repo>` dir under the workspace root. */
77
+ into?: string;
78
+ };
79
+ /** Write a file (e.g. CLAUDE.md) into the workspace / harness config. */
80
+ export declare function fileSkill(input: {
81
+ path: string;
82
+ content: string;
83
+ }): WorkspaceSkill;
84
+ /** Reference a named agent skill the harness should load. */
85
+ export declare function agentSkill(name: string): WorkspaceSkill;
86
+ /** Project an MCP server into the harness. Header values may be SecretRefs. */
87
+ export declare function mcpSkill(name: string, config: McpConfig): WorkspaceSkill;
88
+ /**
89
+ * Clone a git repository as a workspace skill (e.g. a private skill repo).
90
+ * The clone is performed during bootstrap; `secret` is resolved from the
91
+ * workspace `secrets` registry at that time.
92
+ */
93
+ export declare function gitSkill(input: {
94
+ repo: string;
95
+ secret?: SecretRef;
96
+ into?: string;
97
+ }): WorkspaceSkill;
98
+ export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'auto';
99
+ export interface WorkspaceDefinition {
100
+ source: WorkspaceSource;
101
+ /** Defaults to `'auto'` — detect from the lockfile after the source lands. */
102
+ packageManager?: PackageManager;
103
+ /** Commands run once during bootstrap. Accepts a string array (serial) or a builder function for serial/parallel groups. */
104
+ setup?: SetupInput;
105
+ /** Named commands the agent/user can invoke (e.g. { test: 'pnpm test' }). */
106
+ scripts?: Record<string, string>;
107
+ /** Guidance/config projected into the harness. */
108
+ skills?: Array<WorkspaceSkill>;
109
+ /**
110
+ * Natural-language instructions written to AGENTS.md (and symlinked as
111
+ * CLAUDE.md, GEMINI.md, etc.) inside the sandbox during bootstrap.
112
+ */
113
+ instructions?: string;
114
+ /**
115
+ * Harness plugin identifiers installed idempotently by each harness
116
+ * projector (e.g. `['@anthropic/plugin-foo']` for Claude Code).
117
+ */
118
+ plugins?: Array<string>;
119
+ /**
120
+ * Typed secret references. The underlying values are injected into the
121
+ * sandbox env at create/resume — NEVER written to snapshots, the
122
+ * SandboxStore, or the event log.
123
+ */
124
+ secrets?: Secrets;
125
+ /** Workspace root inside the sandbox. Defaults to `/workspace`. */
126
+ root?: string;
127
+ }
128
+ export declare function defineWorkspace(definition: WorkspaceDefinition): WorkspaceDefinition;
@@ -0,0 +1,42 @@
1
+ function gitSource(input) {
2
+ return { type: "git", ...input };
3
+ }
4
+ function githubRepo(input) {
5
+ const url = input.repo.startsWith("http") ? input.repo : `https://github.com/${input.repo}.git`;
6
+ return {
7
+ type: "git",
8
+ url,
9
+ ref: input.ref,
10
+ auth: input.auth,
11
+ depth: input.depth
12
+ };
13
+ }
14
+ function localSource(path) {
15
+ return { type: "local", path };
16
+ }
17
+ function fileSkill(input) {
18
+ return { kind: "file", ...input };
19
+ }
20
+ function agentSkill(name) {
21
+ return { kind: "agent-skill", name };
22
+ }
23
+ function mcpSkill(name, config) {
24
+ return { kind: "mcp", name, config };
25
+ }
26
+ function gitSkill(input) {
27
+ return { kind: "git", ...input };
28
+ }
29
+ function defineWorkspace(definition) {
30
+ return definition;
31
+ }
32
+ export {
33
+ agentSkill,
34
+ defineWorkspace,
35
+ fileSkill,
36
+ gitSkill,
37
+ gitSource,
38
+ githubRepo,
39
+ localSource,
40
+ mcpSkill
41
+ };
42
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sources":["../../src/workspace.ts"],"sourcesContent":["import type { SetupInput } from './setup-plan'\nimport type { BearerRef, SecretRef, Secrets } from './secrets'\n\n/**\n * Workspace definition — the portable description of what the agent sees\n * inside the sandbox. Each harness adapter PROJECTS this into its own native\n * format via `projectWorkspace()` (e.g. Claude Code → CLAUDE.md + .claude/skills\n * + --mcp-config). The definition itself is provider- and harness-agnostic.\n */\n\n/** Where the working tree comes from. */\nexport type WorkspaceSource =\n | {\n type: 'git'\n url: string\n ref?: string\n auth?: { username?: string; token: string }\n /**\n * Clone depth. Defaults to `1` (shallow). Pass a number for a specific\n * depth, or `'full'` to fetch the entire history.\n */\n depth?: number | 'full'\n }\n | { type: 'local'; path: string }\n | { type: 'none' }\n\n/** Clone a git repo into the workspace. `githubRepo` is a convenience wrapper. */\nexport function gitSource(input: {\n url: string\n ref?: string\n auth?: { username?: string; token: string }\n depth?: number | 'full'\n}): WorkspaceSource {\n return { type: 'git', ...input }\n}\n\nexport function githubRepo(input: {\n repo: string\n ref?: string\n auth?: { username?: string; token: string }\n depth?: number | 'full'\n}): WorkspaceSource {\n const url = input.repo.startsWith('http')\n ? input.repo\n : `https://github.com/${input.repo}.git`\n return {\n type: 'git',\n url,\n ref: input.ref,\n auth: input.auth,\n depth: input.depth,\n }\n}\n\nexport function localSource(path: string): WorkspaceSource {\n return { type: 'local', path }\n}\n\n/**\n * An MCP server config where header names/values may be plain strings or\n * unresolved SecretRef values. Secrets are resolved by each harness projector\n * at projection time — never at definition time.\n */\nexport type McpConfig = {\n headers?: Record<string, string | SecretRef | BearerRef>\n [key: string]: unknown\n}\n\n/** A unit of agent guidance/config projected into the harness's native format. */\nexport type WorkspaceSkill =\n | { kind: 'file'; path: string; content: string }\n | { kind: 'agent-skill'; name: string }\n | { kind: 'mcp'; name: string; config: McpConfig }\n | {\n kind: 'git'\n /** Short `owner/repo` or a full HTTPS URL. */\n repo: string\n /** Optional SecretRef for private-repo authentication. */\n secret?: SecretRef\n /** Absolute path inside the sandbox to clone into. Defaults to a `.tanstack-skills/<repo>` dir under the workspace root. */\n into?: string\n }\n\n/** Write a file (e.g. CLAUDE.md) into the workspace / harness config. */\nexport function fileSkill(input: {\n path: string\n content: string\n}): WorkspaceSkill {\n return { kind: 'file', ...input }\n}\n\n/** Reference a named agent skill the harness should load. */\nexport function agentSkill(name: string): WorkspaceSkill {\n return { kind: 'agent-skill', name }\n}\n\n/** Project an MCP server into the harness. Header values may be SecretRefs. */\nexport function mcpSkill(name: string, config: McpConfig): WorkspaceSkill {\n return { kind: 'mcp', name, config }\n}\n\n/**\n * Clone a git repository as a workspace skill (e.g. a private skill repo).\n * The clone is performed during bootstrap; `secret` is resolved from the\n * workspace `secrets` registry at that time.\n */\nexport function gitSkill(input: {\n repo: string\n secret?: SecretRef\n into?: string\n}): WorkspaceSkill {\n return { kind: 'git', ...input }\n}\n\nexport type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'auto'\n\nexport interface WorkspaceDefinition {\n source: WorkspaceSource\n /** Defaults to `'auto'` — detect from the lockfile after the source lands. */\n packageManager?: PackageManager\n /** Commands run once during bootstrap. Accepts a string array (serial) or a builder function for serial/parallel groups. */\n setup?: SetupInput\n /** Named commands the agent/user can invoke (e.g. { test: 'pnpm test' }). */\n scripts?: Record<string, string>\n /** Guidance/config projected into the harness. */\n skills?: Array<WorkspaceSkill>\n /**\n * Natural-language instructions written to AGENTS.md (and symlinked as\n * CLAUDE.md, GEMINI.md, etc.) inside the sandbox during bootstrap.\n */\n instructions?: string\n /**\n * Harness plugin identifiers installed idempotently by each harness\n * projector (e.g. `['@anthropic/plugin-foo']` for Claude Code).\n */\n plugins?: Array<string>\n /**\n * Typed secret references. The underlying values are injected into the\n * sandbox env at create/resume — NEVER written to snapshots, the\n * SandboxStore, or the event log.\n */\n secrets?: Secrets\n /** Workspace root inside the sandbox. Defaults to `/workspace`. */\n root?: string\n}\n\nexport function defineWorkspace(\n definition: WorkspaceDefinition,\n): WorkspaceDefinition {\n return definition\n}\n"],"names":[],"mappings":"AA2BO,SAAS,UAAU,OAKN;AAClB,SAAO,EAAE,MAAM,OAAO,GAAG,MAAA;AAC3B;AAEO,SAAS,WAAW,OAKP;AAClB,QAAM,MAAM,MAAM,KAAK,WAAW,MAAM,IACpC,MAAM,OACN,sBAAsB,MAAM,IAAI;AACpC,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,KAAK,MAAM;AAAA,IACX,MAAM,MAAM;AAAA,IACZ,OAAO,MAAM;AAAA,EAAA;AAEjB;AAEO,SAAS,YAAY,MAA+B;AACzD,SAAO,EAAE,MAAM,SAAS,KAAA;AAC1B;AA4BO,SAAS,UAAU,OAGP;AACjB,SAAO,EAAE,MAAM,QAAQ,GAAG,MAAA;AAC5B;AAGO,SAAS,WAAW,MAA8B;AACvD,SAAO,EAAE,MAAM,eAAe,KAAA;AAChC;AAGO,SAAS,SAAS,MAAc,QAAmC;AACxE,SAAO,EAAE,MAAM,OAAO,MAAM,OAAA;AAC9B;AAOO,SAAS,SAAS,OAIN;AACjB,SAAO,EAAE,MAAM,OAAO,GAAG,MAAA;AAC3B;AAkCO,SAAS,gBACd,YACqB;AACrB,SAAO;AACT;"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@tanstack/ai-sandbox",
3
+ "version": "0.1.0",
4
+ "description": "Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/ai.git",
10
+ "directory": "packages/ai-sandbox"
11
+ },
12
+ "keywords": [
13
+ "ai",
14
+ "ai-sdk",
15
+ "typescript",
16
+ "tanstack",
17
+ "sandbox",
18
+ "harness",
19
+ "agent",
20
+ "coding-agent",
21
+ "isolation",
22
+ "workspace"
23
+ ],
24
+ "type": "module",
25
+ "module": "./dist/esm/index.js",
26
+ "types": "./dist/esm/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/esm/index.d.ts",
30
+ "import": "./dist/esm/index.js"
31
+ },
32
+ "./ngrok": {
33
+ "types": "./dist/esm/ngrok.d.ts",
34
+ "import": "./dist/esm/ngrok.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "skills"
41
+ ],
42
+ "scripts": {
43
+ "build": "vite build",
44
+ "clean": "premove ./build ./dist",
45
+ "lint:fix": "eslint ./src --fix",
46
+ "test:build": "publint --strict",
47
+ "test:eslint": "eslint ./src",
48
+ "test:lib": "vitest",
49
+ "test:lib:dev": "pnpm test:lib --watch",
50
+ "test:types": "tsc"
51
+ },
52
+ "dependencies": {
53
+ "@modelcontextprotocol/sdk": "^1.29.0"
54
+ },
55
+ "peerDependencies": {
56
+ "@ngrok/ngrok": "^1.0.0",
57
+ "@tanstack/ai": "workspace:^"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@ngrok/ngrok": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "devDependencies": {
65
+ "@ngrok/ngrok": "^1.7.0",
66
+ "@tanstack/ai": "workspace:*",
67
+ "@vitest/coverage-v8": "4.0.14"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ }
72
+ }
@@ -0,0 +1,366 @@
1
+ ---
2
+ name: ai-sandbox
3
+ description: >
4
+ Run harness adapters (Claude Code, Codex, OpenCode) INSIDE
5
+ isolated sandboxes via defineSandbox + withSandbox + a provider
6
+ (localProcessSandbox / dockerSandbox). Covers declarative provisioning:
7
+ createSecrets + secret/bearer, skills (agentSkill/gitSkill/mcpSkill/
8
+ fileSkill), plugins, instructions → canonical AGENTS.md + symlinks projected
9
+ per harness; shallow-clone default with depth opt-out; serial/parallel setup
10
+ callback over a persistent shell; snapshot-after-setup default with
11
+ snapshotMaxAge TTL; defineWorkspace (git/setup/scripts/skills/secrets/
12
+ instructions/plugins), defineSandboxPolicy (allow/ask/deny), lifecycle/resume,
13
+ the SandboxHandle (fs/git/process/ports), capability tokens, defineSandbox
14
+ hooks (onFile/onFileCreate/onFileChange/onFileDelete/onReady/onError/
15
+ onDestroy) + fileEvents flag, chat middleware sandbox group
16
+ (defineChatMiddleware sandbox hooks), the sandbox debug category,
17
+ watchWorkspace as a low-level building block, and the file.changed /
18
+ sandbox.file / claude-code.session-id events. Use whenever a harness adapter
19
+ needs a sandbox or when building sandbox providers.
20
+ type: sub-skill
21
+ library: tanstack-ai
22
+ library_version: '0.1.0'
23
+ sources:
24
+ - 'TanStack/ai:docs/sandbox/overview.md'
25
+ ---
26
+
27
+ # Sandboxes
28
+
29
+ Harness adapters declare `requires: [SandboxCapability]`. `chat()` errors unless
30
+ some middleware provides it — `withSandbox(...)` does. The adapter then runs the
31
+ agent CLI **inside** the sandbox and streams its events back.
32
+
33
+ ## Setup — Claude Code in a Docker sandbox
34
+
35
+ ```typescript
36
+ import { chat } from '@tanstack/ai'
37
+ import { claudeCodeText } from '@tanstack/ai-claude-code'
38
+ import {
39
+ defineSandbox,
40
+ defineWorkspace,
41
+ withSandbox,
42
+ } from '@tanstack/ai-sandbox'
43
+ import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
44
+
45
+ const sandbox = defineSandbox({
46
+ id: 'repo-agent',
47
+ provider: dockerSandbox({ image: 'node:22' }),
48
+ workspace: defineWorkspace({
49
+ source: { type: 'git', url: 'https://github.com/owner/repo', ref: 'main' },
50
+ packageManager: 'pnpm',
51
+ setup: ['corepack enable', 'pnpm install'],
52
+ scripts: { test: 'pnpm test' },
53
+ secrets: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '' },
54
+ }),
55
+ lifecycle: { reuse: 'thread', snapshot: 'after-setup', keepAlive: '30m' },
56
+ })
57
+
58
+ const stream = chat({
59
+ threadId,
60
+ adapter: claudeCodeText('sonnet'),
61
+ messages,
62
+ middleware: [withSandbox(sandbox)],
63
+ })
64
+ ```
65
+
66
+ ## Type-safe secrets
67
+
68
+ ```typescript
69
+ import { createSecrets, bearer } from '@tanstack/ai-sandbox'
70
+
71
+ const secrets = createSecrets({
72
+ GH: process.env.GH_TOKEN ?? '',
73
+ SENTRY: process.env.SENTRY_TOKEN ?? '',
74
+ })
75
+ // secrets.GH is a SecretRef — the underlying string is stored in a
76
+ // non-enumerable symbol-keyed registry and never logged, snapshotted,
77
+ // or written to the sandbox store.
78
+ ```
79
+
80
+ Pass `secrets` to `defineWorkspace({ secrets })` so skill and MCP projectors
81
+ can resolve them. Use `secret: secrets.GH` in `gitSkill` for private-repo auth
82
+ and `secrets.GH` / `bearer(secrets.GH)` in MCP header values:
83
+
84
+ - `secrets.GH` — resolves to the raw token value.
85
+ - `bearer(secrets.GH)` — resolves to `"Bearer <value>"`.
86
+
87
+ ## Declarative provisioning (skills, plugins, MCP, instructions)
88
+
89
+ ```typescript
90
+ import {
91
+ agentSkill,
92
+ gitSkill,
93
+ mcpSkill,
94
+ fileSkill,
95
+ bearer,
96
+ createSecrets,
97
+ defineWorkspace,
98
+ } from '@tanstack/ai-sandbox'
99
+
100
+ const secrets = createSecrets({ GH: process.env.GH_TOKEN ?? '' })
101
+
102
+ defineWorkspace({
103
+ source: { type: 'git', url: 'https://github.com/owner/repo' },
104
+ secrets,
105
+ skills: [
106
+ agentSkill('tanstack'), // named skill (no-op with warning on CLIs that lack the concept)
107
+ gitSkill({
108
+ repo: 'owner/private-skills',
109
+ secret: secrets.GH, // resolved at bootstrap time, never stored
110
+ // into: '/abs/path/inside/sandbox' // optional; defaults to .tanstack-skills/<repo>
111
+ }),
112
+ mcpSkill('my-mcp', {
113
+ url: 'https://mcp.example.com',
114
+ headers: { Authorization: bearer(secrets.GH) },
115
+ }),
116
+ fileSkill({ path: '.hints.md', content: 'Prefer pnpm.' }),
117
+ ],
118
+ plugins: ['@anthropic/plugin-foo'], // no-op with warning on CLIs without a plugin concept
119
+ instructions: 'Always run `pnpm test` before proposing a change.',
120
+ })
121
+ ```
122
+
123
+ Each skill type is projected per harness (Claude Code → `.mcp.json`; Codex →
124
+ `.codex/config.toml`; OpenCode → `opencode.json`).
125
+ `instructions` is written as `AGENTS.md` at the workspace root; `CLAUDE.md` and
126
+ `GEMINI.md` are created as symlinks (falling back to copies on symlink failure).
127
+ Skills/plugins that a CLI lacks emit a `console.warn` and are skipped.
128
+
129
+ **`gitSkill` `into` field:** an **absolute path inside the sandbox** where the
130
+ repo is cloned. Defaults to `<root>/.tanstack-skills/<repo-basename>`.
131
+
132
+ ## Fast init
133
+
134
+ ### Shallow clone (`depth`)
135
+
136
+ `githubRepo` / `gitSource` default to `--depth 1 --single-branch`. Override:
137
+
138
+ ```typescript
139
+ import { githubRepo, defineWorkspace } from '@tanstack/ai-sandbox'
140
+
141
+ defineWorkspace({ source: githubRepo({ repo: 'owner/app' }) }) // depth 1 (default)
142
+ defineWorkspace({ source: githubRepo({ repo: 'owner/app', depth: 10 }) }) // 10 commits
143
+ defineWorkspace({ source: githubRepo({ repo: 'owner/app', depth: 'full' }) }) // full history
144
+ ```
145
+
146
+ ### Serial / parallel `setup` callback
147
+
148
+ `setup` accepts a plain `Array<string>` (all serial) or a callback that records
149
+ serial and parallel groups over a **persistent shell** whose cwd/env carry over
150
+ between serial steps:
151
+
152
+ ```typescript
153
+ defineWorkspace({
154
+ source: githubRepo({ repo: 'owner/app' }),
155
+ setup: ({ serial, parallel }) => {
156
+ serial('corepack enable')
157
+ serial('pnpm install')
158
+ parallel(['pnpm build', 'pnpm typecheck']) // concurrent; inherit cwd+env from shell
159
+ serial('echo done')
160
+ },
161
+ })
162
+ ```
163
+
164
+ ### Snapshot-after-setup and `snapshotMaxAge`
165
+
166
+ When the provider supports snapshots, bootstrap takes one automatically after
167
+ `setup` completes. Subsequent runs resume from the snapshot (skipping setup).
168
+ Override or add a TTL:
169
+
170
+ ```typescript
171
+ lifecycle: {
172
+ snapshot: 'after-setup', // default when provider.capabilities().snapshots
173
+ snapshotMaxAge: '24h', // re-create when the snapshot is older than this
174
+ }
175
+ ```
176
+
177
+ Providers without snapshot support skip the step silently.
178
+
179
+ ## Providers
180
+
181
+ - `localProcessSandbox()` — runs on the host (no isolation; dev loop only).
182
+ - `dockerSandbox({ image })` — isolated container; snapshots, fork, resume-by-id.
183
+
184
+ Both implement the same `SandboxHandle`: `fs` (read/write/list/mkdir/remove/
185
+ rename/exists), `git` (clone/status/add/commit/push/pull/branch), `process`
186
+ (`exec` + duplex `spawn`), `ports.connect(port)`, `env.set`, optional
187
+ `snapshot()`/`fork()`, `destroy()`. Providers advertise support via
188
+ `capabilities()`; calling an unsupported optional method throws
189
+ `UnsupportedCapabilityError`.
190
+
191
+ ## Policy
192
+
193
+ ```typescript
194
+ import { defineSandboxPolicy } from '@tanstack/ai-sandbox'
195
+
196
+ const policy = defineSandboxPolicy({
197
+ commands: {
198
+ allow: ['pnpm test'],
199
+ ask: ['curl *'],
200
+ deny: ['sudo *', 'rm -rf *'],
201
+ },
202
+ capabilities: { fileWrite: 'allow', network: 'ask' },
203
+ default: 'ask', // deny > ask > allow
204
+ })
205
+ // pass to defineSandbox({ policy }); harness adapters map it to native permissions
206
+ ```
207
+
208
+ ## Lifecycle &amp; resume
209
+
210
+ `reuse: 'thread'` resumes one sandbox per `threadId`; the compound key folds in
211
+ provider + workspace hash + tenant so changing the repo/setup/image starts
212
+ fresh. Ensure order: resume running → restore snapshot → create + bootstrap.
213
+
214
+ ## File-event hooks
215
+
216
+ Watch the workspace for create/change/delete events. Provider-agnostic: native
217
+ `fs.watch` on local-process, a portable `find` poll on Docker/exec-only
218
+ providers (no extra deps or image changes).
219
+
220
+ Declare hooks on `defineSandbox({ hooks })` (sandbox-scoped) or on any chat
221
+ middleware via the `sandbox` group (run-scoped):
222
+
223
+ ```typescript
224
+ import {
225
+ defineSandbox,
226
+ defineChatMiddleware,
227
+ withSandbox,
228
+ } from '@tanstack/ai-sandbox'
229
+ import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
230
+
231
+ // Sandbox-scoped hooks (all optional):
232
+ const sandbox = defineSandbox({
233
+ id: 'repo-agent',
234
+ provider: dockerSandbox({ image: 'node:22' }),
235
+ hooks: {
236
+ onFile: (e) => console.log(e.type, e.path), // catch-all
237
+ onFileCreate: (e) => console.log('created', e.path),
238
+ onFileChange: (e) => console.log('changed', e.path),
239
+ onFileDelete: (e) => console.log('deleted', e.path),
240
+ onReady: (handle) => console.log('ready', handle.id),
241
+ onError: (err) => console.error(err),
242
+ onDestroy: () => console.log('destroyed'),
243
+ },
244
+ fileEvents: true, // default; set false to disable watching entirely
245
+ })
246
+
247
+ // Run-scoped hooks via chat middleware (ctx is ChatMiddlewareContext):
248
+ const auditMiddleware = defineChatMiddleware({
249
+ name: 'audit',
250
+ sandbox: {
251
+ onFile: (ctx, e) => console.log(ctx.runId, e.type, e.path),
252
+ onFileCreate: (ctx, e) => db.log({ run: ctx.runId, event: e }),
253
+ onFileChange: (ctx, e) => metrics.increment('file.change'),
254
+ onFileDelete: (ctx, e) => console.warn('deleted', e.path),
255
+ },
256
+ })
257
+
258
+ // No extra middleware needed — sandbox.file CUSTOM events are emitted
259
+ // automatically. Read them from the stream:
260
+ for await (const chunk of stream) {
261
+ if (chunk.type === 'CUSTOM' && chunk.name === 'sandbox.file') {
262
+ const value = chunk.value
263
+ if (
264
+ value !== null &&
265
+ typeof value === 'object' &&
266
+ 'type' in value &&
267
+ 'path' in value
268
+ ) {
269
+ console.log('file event', value) // { type, path, timestamp }
270
+ }
271
+ }
272
+ }
273
+ ```
274
+
275
+ `watchWorkspace()` is available as a low-level building block for watching
276
+ outside a `chat()` run:
277
+
278
+ ```typescript
279
+ import { watchWorkspace } from '@tanstack/ai-sandbox'
280
+
281
+ const watcher = await watchWorkspace(handle, {
282
+ onEvent: (e) => console.log(e.type, e.path),
283
+ ignore: ['.git', 'node_modules'], // default
284
+ })
285
+ await watcher.stop()
286
+ ```
287
+
288
+ Enable the `sandbox` debug category to log watcher start/stop, event dispatch,
289
+ and lifecycle transitions:
290
+
291
+ ```typescript
292
+ chat({ threadId, adapter, messages, debug: { sandbox: true } })
293
+ // or debug: true to enable all categories
294
+ ```
295
+
296
+ ## Edge / serverless execution
297
+
298
+ A request-scoped Worker can't hold a multi-minute agent run open. The
299
+ serverless/edge model splits this: a **trigger** starts the run and returns
300
+ immediately, a **durable orchestrator** drives it, and clients **tail from a
301
+ resumable cursor**.
302
+
303
+ Core primitives (`@tanstack/ai-sandbox`, transport- and runtime-agnostic):
304
+
305
+ - **`RunEventLog` / `InMemoryRunEventLog`** — append-only, `seq`-indexed log of a
306
+ run's `StreamChunk`s with replay-then-tail reads. A dropped connection / new
307
+ tab / hibernated orchestrator reconnect by passing their last-seen `seq`
308
+ (`read({ fromSeq })`). `TerminalRunStatus` = `done | error | aborted`.
309
+ - **`pipeToRunLog` / `RunController`** — the run driver. `pipeToRunLog` pumps a
310
+ `chat()` stream into a log and **never rejects**: a thrown stream error becomes
311
+ a terminal `RUN_ERROR` event, so detached clients always observe failures.
312
+ `RunController.start` is fire-and-track; `attach(runId, { fromSeq })` tails;
313
+ `drain()` awaits in-flight runs (e.g. in a `waitUntil`).
314
+ - **Transport-agnostic tool-bridge** — `createToolBridgeCore` +
315
+ `handleBridgeJsonRpc` are the portable core; `startHostToolBridge` is the
316
+ `node:http` host transport. The `ToolBridgeProvisioner` capability injects the
317
+ transport, so an edge orchestrator serves the same core from its own `fetch`
318
+ handler (no raw TCP listener). Default = host transport.
319
+ - **Co-located host-tool seam** — `toolDescriptors` / `remoteToolStubs` /
320
+ `httpRemoteToolExecutor` (container side) + `executeHostTool` (orchestrator
321
+ side): only chat()-tool EXECUTION crosses the container→orchestrator boundary,
322
+ not the whole MCP protocol.
323
+ - **`SandboxCapabilities.writableStdin`** — `false` for providers (e.g.
324
+ Cloudflare) with no writable host→process stdin; stdin-fed harnesses then
325
+ deliver the prompt via a file + in-shell redirection (`claude -p … < file`).
326
+
327
+ Cloudflare runtime (`@tanstack/ai-sandbox-cloudflare`):
328
+
329
+ - `createCloudflareSandboxAgent(config)` → `{ Coordinator, Sandbox, worker }` —
330
+ an app's `worker.ts` is one configured call plus the wrangler-required DO
331
+ re-exports. Two models via `mode`: `do-drives` (the DO runs `chat()`) and
332
+ `colocated` (harness + bridge run in-container; the DO is a thin coordinator,
333
+ pair with `runInContainerHarness` from `/runner`).
334
+ - `DurableObjectRunEventLog` mirrors `InMemoryRunEventLog` over DO storage;
335
+ `timingSafeBearerEqualWeb` is the Web-Crypto constant-time bearer check.
336
+
337
+ ## Events
338
+
339
+ - `claude-code.session-id` (CUSTOM) — resumable session id → pass back via
340
+ `modelOptions.sessionId`.
341
+ - `file.changed` (CUSTOM) — `{ path, diff }` working-tree diff after the run.
342
+ - `sandbox.file` (CUSTOM) — `{ type, path, timestamp }` per file create/change/
343
+ delete, emitted automatically when a sandbox is active.
344
+
345
+ ## Critical rules
346
+
347
+ - **Harness adapters require a sandbox.** Always include `withSandbox(...)` in
348
+ `middleware` — without it `chat()` throws a missing-capability error.
349
+ - **Secrets** (`workspace.secrets`) are injected into the sandbox env and never
350
+ persisted (no snapshots, no sandbox store, no event log). Always create them
351
+ with `createSecrets(...)` so the values stay hidden behind `SecretRef` tokens.
352
+ The agent binary (`claude`) must exist in the sandbox image (install it in
353
+ `setup` or bake it into the image).
354
+ - **Secret-bearing projected files** (e.g. MCP config with resolved header
355
+ values) are re-written on every projection call so rotated secrets re-apply;
356
+ they are never included in a snapshot.
357
+ - **chat()-provided `tools` are bridged** into the in-sandbox agent over a
358
+ host-side MCP tool-proxy: the agent calls them as `mcp__tanstack__<tool>` and
359
+ each call is proxied back to the host where the tool's `execute()` runs (with
360
+ its closures / DB / secrets). The agent also has its own native tools
361
+ (Bash/Edit/Read/…). The host bridge binds on the host; the sandbox reaches it
362
+ (localhost, or `host.docker.internal` for Docker), gated by a per-run bearer
363
+ token.
364
+ - Use `localProcessSandbox()` only in trusted/dev contexts (no isolation).
365
+ - Skills/plugins that a CLI lacks (e.g. `agentSkill` on Codex, `plugins` on
366
+ Codex) warn and skip — they do not throw.