@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.
- package/README.md +182 -0
- package/dist/esm/agents-file.d.ts +36 -0
- package/dist/esm/agents-file.js +44 -0
- package/dist/esm/agents-file.js.map +1 -0
- package/dist/esm/approvals.d.ts +38 -0
- package/dist/esm/approvals.js +36 -0
- package/dist/esm/approvals.js.map +1 -0
- package/dist/esm/bootstrap.d.ts +17 -0
- package/dist/esm/bootstrap.js +124 -0
- package/dist/esm/bootstrap.js.map +1 -0
- package/dist/esm/bridge-events.d.ts +21 -0
- package/dist/esm/bridge-events.js +76 -0
- package/dist/esm/bridge-events.js.map +1 -0
- package/dist/esm/capabilities.d.ts +26 -0
- package/dist/esm/capabilities.js +29 -0
- package/dist/esm/capabilities.js.map +1 -0
- package/dist/esm/contracts.d.ts +211 -0
- package/dist/esm/errors.d.ts +16 -0
- package/dist/esm/errors.js +25 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/git-exec.d.ts +2 -0
- package/dist/esm/git-exec.js +68 -0
- package/dist/esm/git-exec.js.map +1 -0
- package/dist/esm/harness-cwd.d.ts +2 -0
- package/dist/esm/harness-cwd.js +24 -0
- package/dist/esm/harness-cwd.js.map +1 -0
- package/dist/esm/index.d.ts +39 -0
- package/dist/esm/index.js +103 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/key.d.ts +20 -0
- package/dist/esm/key.js +41 -0
- package/dist/esm/key.js.map +1 -0
- package/dist/esm/middleware.d.ts +5 -0
- package/dist/esm/middleware.js +140 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/ngrok.d.ts +16 -0
- package/dist/esm/ngrok.js +54 -0
- package/dist/esm/ngrok.js.map +1 -0
- package/dist/esm/policy.d.ts +47 -0
- package/dist/esm/policy.js +44 -0
- package/dist/esm/policy.js.map +1 -0
- package/dist/esm/projection.d.ts +31 -0
- package/dist/esm/projection.js +9 -0
- package/dist/esm/projection.js.map +1 -0
- package/dist/esm/remote-tools.d.ts +48 -0
- package/dist/esm/remote-tools.js +76 -0
- package/dist/esm/remote-tools.js.map +1 -0
- package/dist/esm/run-log.d.ts +81 -0
- package/dist/esm/run-log.js +107 -0
- package/dist/esm/run-log.js.map +1 -0
- package/dist/esm/run.d.ts +58 -0
- package/dist/esm/run.js +89 -0
- package/dist/esm/run.js.map +1 -0
- package/dist/esm/runner.d.ts +21 -0
- package/dist/esm/runner.js +54 -0
- package/dist/esm/runner.js.map +1 -0
- package/dist/esm/sandbox.d.ts +79 -0
- package/dist/esm/sandbox.js +125 -0
- package/dist/esm/sandbox.js.map +1 -0
- package/dist/esm/secrets.d.ts +37 -0
- package/dist/esm/secrets.js +59 -0
- package/dist/esm/secrets.js.map +1 -0
- package/dist/esm/setup-plan.d.ts +13 -0
- package/dist/esm/setup-plan.js +16 -0
- package/dist/esm/setup-plan.js.map +1 -0
- package/dist/esm/shell.d.ts +45 -0
- package/dist/esm/shell.js +164 -0
- package/dist/esm/shell.js.map +1 -0
- package/dist/esm/store.d.ts +53 -0
- package/dist/esm/store.js +34 -0
- package/dist/esm/store.js.map +1 -0
- package/dist/esm/tool-bridge.d.ts +130 -0
- package/dist/esm/tool-bridge.js +197 -0
- package/dist/esm/tool-bridge.js.map +1 -0
- package/dist/esm/watch.d.ts +36 -0
- package/dist/esm/watch.js +144 -0
- package/dist/esm/watch.js.map +1 -0
- package/dist/esm/workspace.d.ts +128 -0
- package/dist/esm/workspace.js +42 -0
- package/dist/esm/workspace.js.map +1 -0
- package/package.json +72 -0
- package/skills/ai-sandbox/SKILL.md +366 -0
- package/src/agents-file.ts +101 -0
- package/src/approvals.ts +96 -0
- package/src/bootstrap.ts +196 -0
- package/src/bridge-events.ts +112 -0
- package/src/capabilities.ts +47 -0
- package/src/contracts.ts +236 -0
- package/src/errors.ts +31 -0
- package/src/git-exec.ts +114 -0
- package/src/harness-cwd.ts +38 -0
- package/src/index.ts +222 -0
- package/src/key.ts +70 -0
- package/src/middleware.ts +233 -0
- package/src/ngrok.ts +85 -0
- package/src/policy.ts +111 -0
- package/src/projection.ts +46 -0
- package/src/remote-tools.ts +180 -0
- package/src/run-log.ts +224 -0
- package/src/run.ts +167 -0
- package/src/runner.ts +99 -0
- package/src/sandbox.ts +259 -0
- package/src/secrets.ts +101 -0
- package/src/setup-plan.ts +25 -0
- package/src/shell.ts +288 -0
- package/src/store.ts +83 -0
- package/src/tool-bridge.ts +399 -0
- package/src/watch.ts +256 -0
- 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 & 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.
|