bashkit 0.2.4 → 0.3.1
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/AGENTS.md +13 -8
- package/README.md +29 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +144 -37
- package/dist/sandbox/e2b.d.ts +6 -1
- package/dist/sandbox/ensure-tools.d.ts +22 -0
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/interface.d.ts +5 -0
- package/dist/sandbox/lazy-singleton.d.ts +25 -0
- package/dist/sandbox/ripgrep.d.ts +11 -0
- package/dist/sandbox/vercel.d.ts +6 -1
- package/dist/workflow.d.ts +52 -0
- package/dist/workflow.js +458 -0
- package/package.json +1 -1
- package/dist/tools/web-constants.d.ts +0 -5
package/AGENTS.md
CHANGED
|
@@ -34,9 +34,12 @@ Runs in isolated Firecracker microVMs on Vercel's infrastructure.
|
|
|
34
34
|
```typescript
|
|
35
35
|
import { createAgentTools, createVercelSandbox } from "bashkit";
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// Async - automatically installs ripgrep for Grep tool
|
|
38
|
+
const sandbox = await createVercelSandbox({
|
|
38
39
|
runtime: "node22",
|
|
39
40
|
resources: { vcpus: 2 },
|
|
41
|
+
// ensureTools: true (default) - auto-setup ripgrep
|
|
42
|
+
// ensureTools: false - skip for faster startup if you don't need Grep
|
|
40
43
|
});
|
|
41
44
|
const { tools } = createAgentTools(sandbox);
|
|
42
45
|
|
|
@@ -51,8 +54,11 @@ Runs in E2B's cloud sandboxes. Requires `@e2b/code-interpreter` peer dependency.
|
|
|
51
54
|
```typescript
|
|
52
55
|
import { createAgentTools, createE2BSandbox } from "bashkit";
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
// Async - automatically installs ripgrep for Grep tool
|
|
58
|
+
const sandbox = await createE2BSandbox({
|
|
55
59
|
apiKey: process.env.E2B_API_KEY,
|
|
60
|
+
// ensureTools: true (default) - auto-setup ripgrep
|
|
61
|
+
// ensureTools: false - skip for faster startup if you don't need Grep
|
|
56
62
|
});
|
|
57
63
|
const { tools } = createAgentTools(sandbox);
|
|
58
64
|
|
|
@@ -65,18 +71,17 @@ Cloud sandboxes (E2B, Vercel) support reconnection via the `id` property and `sa
|
|
|
65
71
|
|
|
66
72
|
```typescript
|
|
67
73
|
// Create a new sandbox
|
|
68
|
-
const sandbox = createE2BSandbox({ apiKey: process.env.E2B_API_KEY });
|
|
74
|
+
const sandbox = await createE2BSandbox({ apiKey: process.env.E2B_API_KEY });
|
|
69
75
|
|
|
70
|
-
//
|
|
71
|
-
await sandbox.exec("echo hello");
|
|
76
|
+
// Sandbox ID is available immediately after creation
|
|
72
77
|
const sandboxId = sandbox.id; // "sbx_abc123..."
|
|
73
78
|
|
|
74
79
|
// Store sandboxId in your database (e.g., chat metadata)
|
|
75
80
|
await db.chat.update({ where: { id: chatId }, data: { sandboxId } });
|
|
76
81
|
|
|
77
|
-
// Later: reconnect to the same sandbox
|
|
82
|
+
// Later: reconnect to the same sandbox (fast - ripgrep already installed)
|
|
78
83
|
const savedId = chat.sandboxId;
|
|
79
|
-
const reconnected = createE2BSandbox({
|
|
84
|
+
const reconnected = await createE2BSandbox({
|
|
80
85
|
apiKey: process.env.E2B_API_KEY,
|
|
81
86
|
sandboxId: savedId, // Reconnects instead of creating new
|
|
82
87
|
});
|
|
@@ -442,7 +447,7 @@ const config = {
|
|
|
442
447
|
},
|
|
443
448
|
};
|
|
444
449
|
|
|
445
|
-
const sandbox = createVercelSandbox({});
|
|
450
|
+
const sandbox = await createVercelSandbox({});
|
|
446
451
|
const { skills } = await setupAgentEnvironment(sandbox, config);
|
|
447
452
|
|
|
448
453
|
// Use same config in prompt - stays in sync!
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# bashkit
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/bashkit)
|
|
4
|
+
|
|
3
5
|
Agentic coding tools for Vercel AI SDK. Give AI agents the ability to execute code, read/write files, and perform coding tasks in a sandboxed environment.
|
|
4
6
|
|
|
5
7
|
## Overview
|
|
@@ -69,7 +71,8 @@ import { anthropic } from '@ai-sdk/anthropic';
|
|
|
69
71
|
import { streamText, stepCountIs } from 'ai';
|
|
70
72
|
|
|
71
73
|
// Create a Vercel sandbox (isolated Firecracker microVM)
|
|
72
|
-
|
|
74
|
+
// Note: async - automatically sets up ripgrep for Grep tool
|
|
75
|
+
const sandbox = await createVercelSandbox({
|
|
73
76
|
runtime: 'node22',
|
|
74
77
|
resources: { vcpus: 2 },
|
|
75
78
|
});
|
|
@@ -144,17 +147,19 @@ Runs in isolated Firecracker microVMs on Vercel's infrastructure. **Use when you
|
|
|
144
147
|
```typescript
|
|
145
148
|
import { createVercelSandbox } from 'bashkit';
|
|
146
149
|
|
|
147
|
-
|
|
150
|
+
// Async - automatically installs ripgrep for Grep tool
|
|
151
|
+
const sandbox = await createVercelSandbox({
|
|
148
152
|
runtime: 'node22',
|
|
149
153
|
resources: { vcpus: 2 },
|
|
154
|
+
// ensureTools: true (default) - auto-setup ripgrep
|
|
155
|
+
// ensureTools: false - skip for faster startup if you don't need Grep
|
|
150
156
|
});
|
|
151
157
|
|
|
152
|
-
//
|
|
153
|
-
await sandbox.exec('echo hello');
|
|
158
|
+
// Sandbox ID available immediately after creation
|
|
154
159
|
console.log(sandbox.id); // Sandbox ID for reconnection
|
|
155
160
|
|
|
156
|
-
// Later: reconnect to the same sandbox
|
|
157
|
-
const reconnected = createVercelSandbox({
|
|
161
|
+
// Later: reconnect to the same sandbox (fast - ripgrep already installed)
|
|
162
|
+
const reconnected = await createVercelSandbox({
|
|
158
163
|
sandboxId: 'existing-sandbox-id',
|
|
159
164
|
});
|
|
160
165
|
```
|
|
@@ -166,16 +171,18 @@ Runs in E2B's cloud sandboxes. Requires `@e2b/code-interpreter` peer dependency.
|
|
|
166
171
|
```typescript
|
|
167
172
|
import { createE2BSandbox } from 'bashkit';
|
|
168
173
|
|
|
169
|
-
|
|
174
|
+
// Async - automatically installs ripgrep for Grep tool
|
|
175
|
+
const sandbox = await createE2BSandbox({
|
|
170
176
|
apiKey: process.env.E2B_API_KEY,
|
|
177
|
+
// ensureTools: true (default) - auto-setup ripgrep
|
|
178
|
+
// ensureTools: false - skip for faster startup if you don't need Grep
|
|
171
179
|
});
|
|
172
180
|
|
|
173
|
-
//
|
|
174
|
-
await sandbox.exec('echo hello');
|
|
181
|
+
// Sandbox ID available immediately after creation
|
|
175
182
|
console.log(sandbox.id); // "sbx_abc123..."
|
|
176
183
|
|
|
177
|
-
// Later: reconnect to the same sandbox
|
|
178
|
-
const reconnected = createE2BSandbox({
|
|
184
|
+
// Later: reconnect to the same sandbox (fast - ripgrep already installed)
|
|
185
|
+
const reconnected = await createE2BSandbox({
|
|
179
186
|
apiKey: process.env.E2B_API_KEY,
|
|
180
187
|
sandboxId: 'sbx_abc123...', // Reconnect to existing sandbox
|
|
181
188
|
});
|
|
@@ -782,14 +789,20 @@ interface Sandbox {
|
|
|
782
789
|
writeFile(path: string, content: string): Promise<void>;
|
|
783
790
|
readDir(path: string): Promise<string[]>;
|
|
784
791
|
fileExists(path: string): Promise<boolean>;
|
|
792
|
+
isDirectory(path: string): Promise<boolean>;
|
|
785
793
|
destroy(): Promise<void>;
|
|
786
794
|
|
|
787
795
|
// Optional: Sandbox ID for reconnection (cloud providers only)
|
|
788
796
|
readonly id?: string;
|
|
797
|
+
|
|
798
|
+
// Path to ripgrep binary (set by ensureSandboxTools)
|
|
799
|
+
rgPath?: string;
|
|
789
800
|
}
|
|
790
801
|
```
|
|
791
802
|
|
|
792
|
-
The `id` property is available on cloud sandboxes (E2B, Vercel) after
|
|
803
|
+
The `id` property is available on cloud sandboxes (E2B, Vercel) after creation. Use it to persist the sandbox ID and reconnect later.
|
|
804
|
+
|
|
805
|
+
The `rgPath` property is set by `ensureSandboxTools()` (called automatically during sandbox creation). It points to the ripgrep binary for the Grep tool. Supports x86_64 and ARM64 architectures.
|
|
793
806
|
|
|
794
807
|
### Custom Sandbox Example
|
|
795
808
|
|
|
@@ -866,9 +879,10 @@ Creates a set of agent tools bound to a sandbox instance.
|
|
|
866
879
|
|
|
867
880
|
### Sandbox Factories
|
|
868
881
|
|
|
869
|
-
- `createLocalSandbox(config?)` - Local execution sandbox
|
|
870
|
-
- `createVercelSandbox(config?)` - Vercel Firecracker sandbox
|
|
871
|
-
- `createE2BSandbox(config?)` - E2B cloud sandbox
|
|
882
|
+
- `createLocalSandbox(config?)` - Local execution sandbox (sync)
|
|
883
|
+
- `createVercelSandbox(config?)` - Vercel Firecracker sandbox (async, auto-installs ripgrep)
|
|
884
|
+
- `createE2BSandbox(config?)` - E2B cloud sandbox (async, auto-installs ripgrep)
|
|
885
|
+
- `ensureSandboxTools(sandbox)` - Manually setup tools (called automatically by default)
|
|
872
886
|
|
|
873
887
|
### Workflow Tools
|
|
874
888
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { UIMessageStreamWriter, StreamTextResult, Tool, ToolSet, LanguageModel, LanguageModelMiddleware, Output, } from "ai";
|
|
2
2
|
export { anthropicPromptCacheMiddleware, anthropicPromptCacheMiddlewareV2, } from "./middleware";
|
|
3
3
|
export type { E2BSandboxConfig, LocalSandboxConfig, VercelSandboxConfig, } from "./sandbox";
|
|
4
|
-
export { createE2BSandbox, createLocalSandbox, createVercelSandbox, } from "./sandbox";
|
|
4
|
+
export { createE2BSandbox, createLocalSandbox, createVercelSandbox, ensureSandboxTools, } from "./sandbox";
|
|
5
5
|
export type { ExecOptions, ExecResult, Sandbox } from "./sandbox/interface";
|
|
6
6
|
export type { AgentToolsResult, AskUserError, AskUserOutput, AskUserResponseHandler, BashError, BashOutput, EditError, EditOutput, EnterPlanModeError, EnterPlanModeOutput, ExitPlanModeError, ExitPlanModeOutput, PlanModeState, GlobError, GlobOutput, GrepContentOutput, GrepCountOutput, GrepError, GrepFilesOutput, GrepMatch, GrepOutput, ReadDirectoryOutput, ReadError, ReadOutput, ReadTextOutput, SkillError, SkillOutput, SkillToolConfig, SubagentEventData, SubagentStepEvent, SubagentTypeConfig, TaskError, TaskOutput, TaskToolConfig, TodoItem, TodoState, TodoWriteError, TodoWriteOutput, WebFetchError, WebFetchOutput, WebSearchError, WebSearchOutput, WebSearchResult, WriteError, WriteOutput, } from "./tools";
|
|
7
7
|
export { createAgentTools, createAskUserTool, createBashTool, createEditTool, createEnterPlanModeTool, createExitPlanModeTool, createGlobTool, createGrepTool, createReadTool, createSkillTool, createTaskTool, createTodoWriteTool, createWebFetchTool, createWebSearchTool, createWriteTool, } from "./tools";
|
package/dist/index.js
CHANGED
|
@@ -49,15 +49,91 @@ var anthropicPromptCacheMiddleware = {
|
|
|
49
49
|
specificationVersion: "v3",
|
|
50
50
|
transformParams: async ({ params }) => applyCacheMarkers(params)
|
|
51
51
|
};
|
|
52
|
+
// src/sandbox/lazy-singleton.ts
|
|
53
|
+
function createLazySingleton(factory) {
|
|
54
|
+
let promise = null;
|
|
55
|
+
return {
|
|
56
|
+
get: () => {
|
|
57
|
+
if (!promise) {
|
|
58
|
+
promise = factory();
|
|
59
|
+
}
|
|
60
|
+
return promise;
|
|
61
|
+
},
|
|
62
|
+
reset: () => {
|
|
63
|
+
promise = null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/sandbox/ripgrep.ts
|
|
69
|
+
async function getBundledRgPath() {
|
|
70
|
+
try {
|
|
71
|
+
const { rgPath } = await import("@vscode/ripgrep");
|
|
72
|
+
return rgPath;
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function getBundledRgPathSync() {
|
|
78
|
+
try {
|
|
79
|
+
const { rgPath } = __require("@vscode/ripgrep");
|
|
80
|
+
return rgPath;
|
|
81
|
+
} catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/sandbox/ensure-tools.ts
|
|
87
|
+
var RIPGREP_VERSION = "14.1.0";
|
|
88
|
+
var ARCH_MAP = {
|
|
89
|
+
x86_64: "x86_64-unknown-linux-musl",
|
|
90
|
+
aarch64: "aarch64-unknown-linux-gnu",
|
|
91
|
+
arm64: "aarch64-unknown-linux-gnu"
|
|
92
|
+
};
|
|
93
|
+
async function ensureSandboxTools(sandbox) {
|
|
94
|
+
const bundledRgPath = await getBundledRgPath();
|
|
95
|
+
if (bundledRgPath) {
|
|
96
|
+
const bundledCheck = await sandbox.exec(`test -x "${bundledRgPath}" && echo found`);
|
|
97
|
+
if (bundledCheck.stdout.includes("found")) {
|
|
98
|
+
sandbox.rgPath = bundledRgPath;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const tmpCheck = await sandbox.exec("test -x /tmp/rg && echo found");
|
|
103
|
+
if (tmpCheck.stdout.includes("found")) {
|
|
104
|
+
sandbox.rgPath = "/tmp/rg";
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const systemCheck = await sandbox.exec("which rg 2>/dev/null");
|
|
108
|
+
if (systemCheck.exitCode === 0 && systemCheck.stdout.trim()) {
|
|
109
|
+
sandbox.rgPath = systemCheck.stdout.trim();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const archResult = await sandbox.exec("uname -m");
|
|
113
|
+
const arch = archResult.stdout.trim();
|
|
114
|
+
const ripgrepArch = ARCH_MAP[arch];
|
|
115
|
+
if (!ripgrepArch) {
|
|
116
|
+
throw new Error(`Unsupported architecture: ${arch}. Supported: ${Object.keys(ARCH_MAP).join(", ")}`);
|
|
117
|
+
}
|
|
118
|
+
const ripgrepUrl = `https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-${ripgrepArch}.tar.gz`;
|
|
119
|
+
const tarPath = `ripgrep-${RIPGREP_VERSION}-${ripgrepArch}/rg`;
|
|
120
|
+
const installResult = await sandbox.exec(`
|
|
121
|
+
curl -sL "${ripgrepUrl}" |
|
|
122
|
+
tar xzf - -C /tmp --strip-components=1 ${tarPath} &&
|
|
123
|
+
chmod +x /tmp/rg
|
|
124
|
+
`);
|
|
125
|
+
if (installResult.exitCode !== 0) {
|
|
126
|
+
throw new Error(`Failed to install ripgrep: ${installResult.stderr}`);
|
|
127
|
+
}
|
|
128
|
+
sandbox.rgPath = "/tmp/rg";
|
|
129
|
+
}
|
|
130
|
+
|
|
52
131
|
// src/sandbox/e2b.ts
|
|
53
|
-
function createE2BSandbox(config = {}) {
|
|
54
|
-
let sandbox = null;
|
|
132
|
+
async function createE2BSandbox(config = {}) {
|
|
55
133
|
let sandboxId = config.sandboxId;
|
|
56
134
|
const workingDirectory = config.cwd || "/home/user";
|
|
57
135
|
const timeout = config.timeout ?? 300000;
|
|
58
|
-
const
|
|
59
|
-
if (sandbox)
|
|
60
|
-
return sandbox;
|
|
136
|
+
const sandbox = createLazySingleton(async () => {
|
|
61
137
|
let E2BSandboxSDK;
|
|
62
138
|
try {
|
|
63
139
|
const module = await import("@e2b/code-interpreter");
|
|
@@ -65,20 +141,21 @@ function createE2BSandbox(config = {}) {
|
|
|
65
141
|
} catch {
|
|
66
142
|
throw new Error("E2BSandbox requires @e2b/code-interpreter. Install with: npm install @e2b/code-interpreter");
|
|
67
143
|
}
|
|
144
|
+
let sbx;
|
|
68
145
|
if (config.sandboxId) {
|
|
69
|
-
|
|
146
|
+
sbx = await E2BSandboxSDK.connect(config.sandboxId);
|
|
70
147
|
} else {
|
|
71
|
-
|
|
148
|
+
sbx = await E2BSandboxSDK.create({
|
|
72
149
|
apiKey: config.apiKey,
|
|
73
150
|
timeoutMs: timeout,
|
|
74
151
|
metadata: config.metadata
|
|
75
152
|
});
|
|
76
|
-
sandboxId =
|
|
153
|
+
sandboxId = sbx.sandboxId;
|
|
77
154
|
}
|
|
78
|
-
return
|
|
79
|
-
};
|
|
155
|
+
return sbx;
|
|
156
|
+
});
|
|
80
157
|
const exec = async (command, options) => {
|
|
81
|
-
const sbx = await
|
|
158
|
+
const sbx = await sandbox.get();
|
|
82
159
|
const startTime = performance.now();
|
|
83
160
|
try {
|
|
84
161
|
const result = await sbx.commands.run(command, {
|
|
@@ -118,11 +195,18 @@ function createE2BSandbox(config = {}) {
|
|
|
118
195
|
throw error;
|
|
119
196
|
}
|
|
120
197
|
};
|
|
121
|
-
|
|
198
|
+
let rgPath;
|
|
199
|
+
const sandboxObj = {
|
|
122
200
|
exec,
|
|
123
201
|
get id() {
|
|
124
202
|
return sandboxId;
|
|
125
203
|
},
|
|
204
|
+
get rgPath() {
|
|
205
|
+
return rgPath;
|
|
206
|
+
},
|
|
207
|
+
set rgPath(path) {
|
|
208
|
+
rgPath = path;
|
|
209
|
+
},
|
|
126
210
|
async readFile(path) {
|
|
127
211
|
const result = await exec(`cat "${path}"`);
|
|
128
212
|
if (result.exitCode !== 0) {
|
|
@@ -131,7 +215,7 @@ function createE2BSandbox(config = {}) {
|
|
|
131
215
|
return result.stdout;
|
|
132
216
|
},
|
|
133
217
|
async writeFile(path, content) {
|
|
134
|
-
const sbx = await
|
|
218
|
+
const sbx = await sandbox.get();
|
|
135
219
|
await sbx.files.write(path, content);
|
|
136
220
|
},
|
|
137
221
|
async readDir(path) {
|
|
@@ -151,17 +235,23 @@ function createE2BSandbox(config = {}) {
|
|
|
151
235
|
return result.exitCode === 0;
|
|
152
236
|
},
|
|
153
237
|
async destroy() {
|
|
154
|
-
|
|
155
|
-
await sandbox.
|
|
156
|
-
|
|
157
|
-
}
|
|
238
|
+
try {
|
|
239
|
+
const sbx = await sandbox.get();
|
|
240
|
+
await sbx.kill();
|
|
241
|
+
} catch {}
|
|
242
|
+
sandbox.reset();
|
|
158
243
|
}
|
|
159
244
|
};
|
|
245
|
+
if (config.ensureTools !== false) {
|
|
246
|
+
await ensureSandboxTools(sandboxObj);
|
|
247
|
+
}
|
|
248
|
+
return sandboxObj;
|
|
160
249
|
}
|
|
161
250
|
// src/sandbox/local.ts
|
|
162
251
|
import { existsSync, mkdirSync } from "node:fs";
|
|
163
252
|
function createLocalSandbox(config = {}) {
|
|
164
253
|
const workingDirectory = config.cwd || "/tmp";
|
|
254
|
+
const rgPath = getBundledRgPathSync();
|
|
165
255
|
if (!existsSync(workingDirectory)) {
|
|
166
256
|
mkdirSync(workingDirectory, { recursive: true });
|
|
167
257
|
}
|
|
@@ -201,6 +291,7 @@ function createLocalSandbox(config = {}) {
|
|
|
201
291
|
};
|
|
202
292
|
return {
|
|
203
293
|
exec,
|
|
294
|
+
rgPath,
|
|
204
295
|
async readFile(path) {
|
|
205
296
|
const fullPath = path.startsWith("/") ? path : `${workingDirectory}/${path}`;
|
|
206
297
|
const file = Bun.file(fullPath);
|
|
@@ -239,8 +330,7 @@ function createLocalSandbox(config = {}) {
|
|
|
239
330
|
};
|
|
240
331
|
}
|
|
241
332
|
// src/sandbox/vercel.ts
|
|
242
|
-
function createVercelSandbox(config = {}) {
|
|
243
|
-
let sandbox = null;
|
|
333
|
+
async function createVercelSandbox(config = {}) {
|
|
244
334
|
let sandboxId = config.sandboxId;
|
|
245
335
|
const workingDirectory = config.cwd || "/vercel/sandbox";
|
|
246
336
|
const resolvedConfig = {
|
|
@@ -248,9 +338,7 @@ function createVercelSandbox(config = {}) {
|
|
|
248
338
|
resources: config.resources ?? { vcpus: 2 },
|
|
249
339
|
timeout: config.timeout ?? 300000
|
|
250
340
|
};
|
|
251
|
-
const
|
|
252
|
-
if (sandbox)
|
|
253
|
-
return sandbox;
|
|
341
|
+
const sandbox = createLazySingleton(async () => {
|
|
254
342
|
let VercelSandboxSDK;
|
|
255
343
|
try {
|
|
256
344
|
const module = await import("@vercel/sandbox");
|
|
@@ -269,16 +357,17 @@ function createVercelSandbox(config = {}) {
|
|
|
269
357
|
token: config.token
|
|
270
358
|
});
|
|
271
359
|
}
|
|
360
|
+
let sbx;
|
|
272
361
|
if (config.sandboxId) {
|
|
273
|
-
|
|
362
|
+
sbx = await VercelSandboxSDK.get({ sandboxId: config.sandboxId });
|
|
274
363
|
} else {
|
|
275
|
-
|
|
364
|
+
sbx = await VercelSandboxSDK.create(createOptions);
|
|
276
365
|
}
|
|
277
|
-
sandboxId =
|
|
278
|
-
return
|
|
279
|
-
};
|
|
366
|
+
sandboxId = sbx.sandboxId;
|
|
367
|
+
return sbx;
|
|
368
|
+
});
|
|
280
369
|
const exec = async (command, options) => {
|
|
281
|
-
const sbx = await
|
|
370
|
+
const sbx = await sandbox.get();
|
|
282
371
|
const startTime = performance.now();
|
|
283
372
|
let interrupted = false;
|
|
284
373
|
const abortController = new AbortController;
|
|
@@ -326,13 +415,20 @@ function createVercelSandbox(config = {}) {
|
|
|
326
415
|
throw error;
|
|
327
416
|
}
|
|
328
417
|
};
|
|
329
|
-
|
|
418
|
+
let rgPath;
|
|
419
|
+
const sandboxObj = {
|
|
330
420
|
exec,
|
|
331
421
|
get id() {
|
|
332
422
|
return sandboxId;
|
|
333
423
|
},
|
|
424
|
+
get rgPath() {
|
|
425
|
+
return rgPath;
|
|
426
|
+
},
|
|
427
|
+
set rgPath(path) {
|
|
428
|
+
rgPath = path;
|
|
429
|
+
},
|
|
334
430
|
async readFile(path) {
|
|
335
|
-
const sbx = await
|
|
431
|
+
const sbx = await sandbox.get();
|
|
336
432
|
const stream = await sbx.readFile({ path });
|
|
337
433
|
if (!stream) {
|
|
338
434
|
throw new Error(`File not found: ${path}`);
|
|
@@ -344,7 +440,7 @@ function createVercelSandbox(config = {}) {
|
|
|
344
440
|
return Buffer.concat(chunks).toString("utf-8");
|
|
345
441
|
},
|
|
346
442
|
async writeFile(path, content) {
|
|
347
|
-
const sbx = await
|
|
443
|
+
const sbx = await sandbox.get();
|
|
348
444
|
await sbx.writeFiles([
|
|
349
445
|
{
|
|
350
446
|
path,
|
|
@@ -369,12 +465,17 @@ function createVercelSandbox(config = {}) {
|
|
|
369
465
|
return result.exitCode === 0;
|
|
370
466
|
},
|
|
371
467
|
async destroy() {
|
|
372
|
-
|
|
373
|
-
await sandbox.
|
|
374
|
-
|
|
375
|
-
}
|
|
468
|
+
try {
|
|
469
|
+
const sbx = await sandbox.get();
|
|
470
|
+
await sbx.stop();
|
|
471
|
+
} catch {}
|
|
472
|
+
sandbox.reset();
|
|
376
473
|
}
|
|
377
474
|
};
|
|
475
|
+
if (config.ensureTools !== false) {
|
|
476
|
+
await ensureSandboxTools(sandboxObj);
|
|
477
|
+
}
|
|
478
|
+
return sandboxObj;
|
|
378
479
|
}
|
|
379
480
|
// src/cache/lru.ts
|
|
380
481
|
class LRUCacheStore {
|
|
@@ -991,7 +1092,6 @@ function createGlobTool(sandbox, config) {
|
|
|
991
1092
|
// src/tools/grep.ts
|
|
992
1093
|
import { tool as tool7, zodSchema as zodSchema7 } from "ai";
|
|
993
1094
|
import { z as z7 } from "zod";
|
|
994
|
-
import { rgPath } from "@vscode/ripgrep";
|
|
995
1095
|
var grepInputSchema = z7.object({
|
|
996
1096
|
pattern: z7.string().describe("The regular expression pattern to search for in file contents"),
|
|
997
1097
|
path: z7.string().optional().describe("File or directory to search in (defaults to cwd)"),
|
|
@@ -1057,7 +1157,13 @@ function createGrepTool(sandbox, config) {
|
|
|
1057
1157
|
}
|
|
1058
1158
|
}
|
|
1059
1159
|
try {
|
|
1160
|
+
if (!sandbox.rgPath) {
|
|
1161
|
+
return {
|
|
1162
|
+
error: "Ripgrep not available. Call ensureSandboxTools(sandbox) before using Grep with remote sandboxes."
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1060
1165
|
const cmd = buildRipgrepCommand({
|
|
1166
|
+
rgPath: sandbox.rgPath,
|
|
1061
1167
|
pattern,
|
|
1062
1168
|
searchPath,
|
|
1063
1169
|
output_mode,
|
|
@@ -1106,7 +1212,7 @@ function buildRipgrepCommand(opts) {
|
|
|
1106
1212
|
if (opts.type)
|
|
1107
1213
|
flags.push(`-t ${opts.type}`);
|
|
1108
1214
|
const flagStr = flags.join(" ");
|
|
1109
|
-
return `${rgPath} ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null`;
|
|
1215
|
+
return `${opts.rgPath} ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null`;
|
|
1110
1216
|
}
|
|
1111
1217
|
function parseFilesOutput(stdout) {
|
|
1112
1218
|
const files = new Set;
|
|
@@ -2778,6 +2884,7 @@ export {
|
|
|
2778
2884
|
estimateTokens,
|
|
2779
2885
|
estimateMessagesTokens,
|
|
2780
2886
|
estimateMessageTokens,
|
|
2887
|
+
ensureSandboxTools,
|
|
2781
2888
|
discoverSkills,
|
|
2782
2889
|
createWriteTool,
|
|
2783
2890
|
createWebSearchTool,
|
package/dist/sandbox/e2b.d.ts
CHANGED
|
@@ -7,5 +7,10 @@ export interface E2BSandboxConfig {
|
|
|
7
7
|
timeout?: number;
|
|
8
8
|
cwd?: string;
|
|
9
9
|
metadata?: Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Ensure tools like ripgrep are available in the sandbox.
|
|
12
|
+
* Defaults to true. Set to false for faster startup if you don't need Grep.
|
|
13
|
+
*/
|
|
14
|
+
ensureTools?: boolean;
|
|
10
15
|
}
|
|
11
|
-
export declare function createE2BSandbox(config?: E2BSandboxConfig): Sandbox
|
|
16
|
+
export declare function createE2BSandbox(config?: E2BSandboxConfig): Promise<Sandbox>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Sandbox } from "./interface";
|
|
2
|
+
/**
|
|
3
|
+
* Ensures required tools (ripgrep) are available in the sandbox.
|
|
4
|
+
* Call this once during sandbox setup, before using tools like Grep.
|
|
5
|
+
*
|
|
6
|
+
* - For local sandboxes: verifies bundled binary exists
|
|
7
|
+
* - For remote sandboxes: installs ripgrep to /tmp/rg if not present
|
|
8
|
+
*
|
|
9
|
+
* Supports x86_64 and ARM64 architectures.
|
|
10
|
+
*
|
|
11
|
+
* After calling, `sandbox.rgPath` will be set to the correct path.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const sandbox = await createVercelSandbox();
|
|
16
|
+
* await ensureSandboxTools(sandbox);
|
|
17
|
+
*
|
|
18
|
+
* const { tools } = createAgentTools(sandbox);
|
|
19
|
+
* // Grep now works
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function ensureSandboxTools(sandbox: Sandbox): Promise<void>;
|
package/dist/sandbox/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createE2BSandbox, type E2BSandboxConfig } from "./e2b";
|
|
2
|
+
export { ensureSandboxTools } from "./ensure-tools";
|
|
2
3
|
export type { ExecOptions, ExecResult, Sandbox } from "./interface";
|
|
3
4
|
export { createLocalSandbox, type LocalSandboxConfig } from "./local";
|
|
4
5
|
export { createVercelSandbox, type VercelSandboxConfig } from "./vercel";
|
|
@@ -25,4 +25,9 @@ export interface Sandbox {
|
|
|
25
25
|
* - For local sandboxes: always undefined
|
|
26
26
|
*/
|
|
27
27
|
readonly id?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Path to ripgrep binary for this sandbox.
|
|
30
|
+
* Set by ensureSandboxTools() or defaults to bundled binary for local sandboxes.
|
|
31
|
+
*/
|
|
32
|
+
rgPath?: string;
|
|
28
33
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a lazy singleton that initializes on first access.
|
|
3
|
+
* Safe for concurrent calls - all callers await the same promise.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* const sandbox = createLazySingleton(async () => {
|
|
8
|
+
* const sdk = await import("@vercel/sandbox");
|
|
9
|
+
* return sdk.Sandbox.create({ ... });
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* // Safe for parallel calls:
|
|
13
|
+
* const [a, b] = await Promise.all([sandbox.get(), sandbox.get()]);
|
|
14
|
+
* // a === b (same instance)
|
|
15
|
+
*
|
|
16
|
+
* // Reset for cleanup:
|
|
17
|
+
* sandbox.reset();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function createLazySingleton<T>(factory: () => Promise<T>): {
|
|
21
|
+
/** Get the singleton instance, creating it if needed */
|
|
22
|
+
get: () => Promise<T>;
|
|
23
|
+
/** Reset the singleton, allowing a new instance to be created */
|
|
24
|
+
reset: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamically imports @vscode/ripgrep to get the bundled binary path.
|
|
3
|
+
* Returns undefined if the package is not installed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getBundledRgPath(): Promise<string | undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* Synchronously gets the bundled ripgrep path using require.
|
|
8
|
+
* For use in synchronous contexts (e.g., LocalSandbox).
|
|
9
|
+
* Returns undefined if the package is not installed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getBundledRgPathSync(): string | undefined;
|
package/dist/sandbox/vercel.d.ts
CHANGED
|
@@ -11,5 +11,10 @@ export interface VercelSandboxConfig {
|
|
|
11
11
|
teamId?: string;
|
|
12
12
|
projectId?: string;
|
|
13
13
|
token?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Ensure tools like ripgrep are available in the sandbox.
|
|
16
|
+
* Defaults to true. Set to false for faster startup if you don't need Grep.
|
|
17
|
+
*/
|
|
18
|
+
ensureTools?: boolean;
|
|
14
19
|
}
|
|
15
|
-
export declare function createVercelSandbox(config?: VercelSandboxConfig): Sandbox
|
|
20
|
+
export declare function createVercelSandbox(config?: VercelSandboxConfig): Promise<Sandbox>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bashkit Workflow Integration
|
|
3
|
+
*
|
|
4
|
+
* Durable agent tools for Vercel's Workflow DevKit.
|
|
5
|
+
* Each tool execution is wrapped with "use step" for automatic
|
|
6
|
+
* durability, retries, and checkpointing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { createDurableAgentTools } from 'bashkit/workflow';
|
|
11
|
+
* import { DurableAgent } from '@workflow/ai/agent';
|
|
12
|
+
*
|
|
13
|
+
* export async function generateReport(sandboxId: string) {
|
|
14
|
+
* "use workflow";
|
|
15
|
+
*
|
|
16
|
+
* const { tools } = createDurableAgentTools(sandboxId);
|
|
17
|
+
*
|
|
18
|
+
* const agent = new DurableAgent({
|
|
19
|
+
* model: "anthropic/claude-sonnet-4-20250514",
|
|
20
|
+
* tools,
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* await agent.run({ prompt: "Generate the report" });
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { type ToolSet } from "ai";
|
|
28
|
+
import type { AgentConfig } from "./types";
|
|
29
|
+
export interface DurableAgentConfig extends Omit<AgentConfig, "cache"> {
|
|
30
|
+
/**
|
|
31
|
+
* E2B API key (optional, uses ANTHROPIC_API_KEY env var by default)
|
|
32
|
+
*/
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface DurableAgentToolsResult {
|
|
36
|
+
tools: ToolSet;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Creates durable agent tools for Workflow DevKit.
|
|
40
|
+
*
|
|
41
|
+
* Each tool execution:
|
|
42
|
+
* 1. Is wrapped with "use step" for durability
|
|
43
|
+
* 2. Reconnects to the E2B sandbox via sandboxId
|
|
44
|
+
* 3. Can retry independently on failure
|
|
45
|
+
* 4. Works with parallel tool calls (each gets own correlationId)
|
|
46
|
+
*
|
|
47
|
+
* @param sandboxId - E2B sandbox ID to reconnect to
|
|
48
|
+
* @param config - Optional tool configuration
|
|
49
|
+
*/
|
|
50
|
+
export declare function createDurableAgentTools(sandboxId: string, config?: DurableAgentConfig): DurableAgentToolsResult;
|
|
51
|
+
export type { Sandbox } from "./sandbox/interface";
|
|
52
|
+
export type { ToolConfig, AgentConfig } from "./types";
|
package/dist/workflow.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
// src/workflow.ts
|
|
21
|
+
import { tool, zodSchema } from "ai";
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
|
|
24
|
+
// src/sandbox/lazy-singleton.ts
|
|
25
|
+
function createLazySingleton(factory) {
|
|
26
|
+
let promise = null;
|
|
27
|
+
return {
|
|
28
|
+
get: () => {
|
|
29
|
+
if (!promise) {
|
|
30
|
+
promise = factory();
|
|
31
|
+
}
|
|
32
|
+
return promise;
|
|
33
|
+
},
|
|
34
|
+
reset: () => {
|
|
35
|
+
promise = null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/sandbox/ensure-tools.ts
|
|
41
|
+
import { rgPath as bundledRgPath } from "@vscode/ripgrep";
|
|
42
|
+
var RIPGREP_VERSION = "14.1.0";
|
|
43
|
+
var ARCH_MAP = {
|
|
44
|
+
x86_64: "x86_64-unknown-linux-musl",
|
|
45
|
+
aarch64: "aarch64-unknown-linux-gnu",
|
|
46
|
+
arm64: "aarch64-unknown-linux-gnu"
|
|
47
|
+
};
|
|
48
|
+
async function ensureSandboxTools(sandbox) {
|
|
49
|
+
const bundledCheck = await sandbox.exec(`test -x "${bundledRgPath}" && echo found`);
|
|
50
|
+
if (bundledCheck.stdout.includes("found")) {
|
|
51
|
+
sandbox.rgPath = bundledRgPath;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const tmpCheck = await sandbox.exec("test -x /tmp/rg && echo found");
|
|
55
|
+
if (tmpCheck.stdout.includes("found")) {
|
|
56
|
+
sandbox.rgPath = "/tmp/rg";
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const systemCheck = await sandbox.exec("which rg 2>/dev/null");
|
|
60
|
+
if (systemCheck.exitCode === 0 && systemCheck.stdout.trim()) {
|
|
61
|
+
sandbox.rgPath = systemCheck.stdout.trim();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const archResult = await sandbox.exec("uname -m");
|
|
65
|
+
const arch = archResult.stdout.trim();
|
|
66
|
+
const ripgrepArch = ARCH_MAP[arch];
|
|
67
|
+
if (!ripgrepArch) {
|
|
68
|
+
throw new Error(`Unsupported architecture: ${arch}. Supported: ${Object.keys(ARCH_MAP).join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
const ripgrepUrl = `https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-${ripgrepArch}.tar.gz`;
|
|
71
|
+
const tarPath = `ripgrep-${RIPGREP_VERSION}-${ripgrepArch}/rg`;
|
|
72
|
+
const installResult = await sandbox.exec(`
|
|
73
|
+
curl -sL "${ripgrepUrl}" |
|
|
74
|
+
tar xzf - -C /tmp --strip-components=1 ${tarPath} &&
|
|
75
|
+
chmod +x /tmp/rg
|
|
76
|
+
`);
|
|
77
|
+
if (installResult.exitCode !== 0) {
|
|
78
|
+
throw new Error(`Failed to install ripgrep: ${installResult.stderr}`);
|
|
79
|
+
}
|
|
80
|
+
sandbox.rgPath = "/tmp/rg";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/sandbox/e2b.ts
|
|
84
|
+
async function createE2BSandbox(config = {}) {
|
|
85
|
+
let sandboxId = config.sandboxId;
|
|
86
|
+
const workingDirectory = config.cwd || "/home/user";
|
|
87
|
+
const timeout = config.timeout ?? 300000;
|
|
88
|
+
const sandbox = createLazySingleton(async () => {
|
|
89
|
+
let E2BSandboxSDK;
|
|
90
|
+
try {
|
|
91
|
+
const module = await import("@e2b/code-interpreter");
|
|
92
|
+
E2BSandboxSDK = module.Sandbox;
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error("E2BSandbox requires @e2b/code-interpreter. Install with: npm install @e2b/code-interpreter");
|
|
95
|
+
}
|
|
96
|
+
let sbx;
|
|
97
|
+
if (config.sandboxId) {
|
|
98
|
+
sbx = await E2BSandboxSDK.connect(config.sandboxId);
|
|
99
|
+
} else {
|
|
100
|
+
sbx = await E2BSandboxSDK.create({
|
|
101
|
+
apiKey: config.apiKey,
|
|
102
|
+
timeoutMs: timeout,
|
|
103
|
+
metadata: config.metadata
|
|
104
|
+
});
|
|
105
|
+
sandboxId = sbx.sandboxId;
|
|
106
|
+
}
|
|
107
|
+
return sbx;
|
|
108
|
+
});
|
|
109
|
+
const exec = async (command, options) => {
|
|
110
|
+
const sbx = await sandbox.get();
|
|
111
|
+
const startTime = performance.now();
|
|
112
|
+
try {
|
|
113
|
+
const result = await sbx.commands.run(command, {
|
|
114
|
+
cwd: options?.cwd || workingDirectory,
|
|
115
|
+
timeoutMs: options?.timeout
|
|
116
|
+
});
|
|
117
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
118
|
+
return {
|
|
119
|
+
stdout: result.stdout,
|
|
120
|
+
stderr: result.stderr,
|
|
121
|
+
exitCode: result.exitCode,
|
|
122
|
+
durationMs,
|
|
123
|
+
interrupted: false
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
127
|
+
if (error instanceof Error && error.message.toLowerCase().includes("timeout")) {
|
|
128
|
+
return {
|
|
129
|
+
stdout: "",
|
|
130
|
+
stderr: "Command timed out",
|
|
131
|
+
exitCode: 124,
|
|
132
|
+
durationMs,
|
|
133
|
+
interrupted: true
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (error instanceof Error) {
|
|
137
|
+
const exitMatch = error.message.match(/exit status (\d+)/i);
|
|
138
|
+
const exitCode = exitMatch ? parseInt(exitMatch[1], 10) : 1;
|
|
139
|
+
return {
|
|
140
|
+
stdout: "",
|
|
141
|
+
stderr: error.message,
|
|
142
|
+
exitCode,
|
|
143
|
+
durationMs,
|
|
144
|
+
interrupted: false
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
let rgPath;
|
|
151
|
+
const sandboxObj = {
|
|
152
|
+
exec,
|
|
153
|
+
get id() {
|
|
154
|
+
return sandboxId;
|
|
155
|
+
},
|
|
156
|
+
get rgPath() {
|
|
157
|
+
return rgPath;
|
|
158
|
+
},
|
|
159
|
+
set rgPath(path) {
|
|
160
|
+
rgPath = path;
|
|
161
|
+
},
|
|
162
|
+
async readFile(path) {
|
|
163
|
+
const result = await exec(`cat "${path}"`);
|
|
164
|
+
if (result.exitCode !== 0) {
|
|
165
|
+
throw new Error(`Failed to read file: ${result.stderr}`);
|
|
166
|
+
}
|
|
167
|
+
return result.stdout;
|
|
168
|
+
},
|
|
169
|
+
async writeFile(path, content) {
|
|
170
|
+
const sbx = await sandbox.get();
|
|
171
|
+
await sbx.files.write(path, content);
|
|
172
|
+
},
|
|
173
|
+
async readDir(path) {
|
|
174
|
+
const result = await exec(`ls -1 "${path}"`);
|
|
175
|
+
if (result.exitCode !== 0) {
|
|
176
|
+
throw new Error(`Failed to read directory: ${result.stderr}`);
|
|
177
|
+
}
|
|
178
|
+
return result.stdout.split(`
|
|
179
|
+
`).filter(Boolean);
|
|
180
|
+
},
|
|
181
|
+
async fileExists(path) {
|
|
182
|
+
const result = await exec(`test -e "${path}"`);
|
|
183
|
+
return result.exitCode === 0;
|
|
184
|
+
},
|
|
185
|
+
async isDirectory(path) {
|
|
186
|
+
const result = await exec(`test -d "${path}"`);
|
|
187
|
+
return result.exitCode === 0;
|
|
188
|
+
},
|
|
189
|
+
async destroy() {
|
|
190
|
+
try {
|
|
191
|
+
const sbx = await sandbox.get();
|
|
192
|
+
await sbx.kill();
|
|
193
|
+
} catch {}
|
|
194
|
+
sandbox.reset();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
if (config.ensureTools !== false) {
|
|
198
|
+
await ensureSandboxTools(sandboxObj);
|
|
199
|
+
}
|
|
200
|
+
return sandboxObj;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/workflow.ts
|
|
204
|
+
async function reconnectSandbox(sandboxId, apiKey) {
|
|
205
|
+
return createE2BSandbox({
|
|
206
|
+
sandboxId,
|
|
207
|
+
apiKey,
|
|
208
|
+
ensureTools: false
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
var readInputSchema = z.object({
|
|
212
|
+
file_path: z.string().describe("Absolute path to file or directory"),
|
|
213
|
+
offset: z.number().optional().describe("Line number to start reading from (1-indexed)"),
|
|
214
|
+
limit: z.number().optional().describe("Maximum number of lines to read")
|
|
215
|
+
});
|
|
216
|
+
var READ_DESCRIPTION = `Reads a file from the sandbox filesystem.
|
|
217
|
+
|
|
218
|
+
Usage:
|
|
219
|
+
- The file_path parameter must be an absolute path
|
|
220
|
+
- By default, reads up to 500 lines from the beginning
|
|
221
|
+
- Use offset and limit for large files
|
|
222
|
+
- Returns line numbers starting at 1`;
|
|
223
|
+
var writeInputSchema = z.object({
|
|
224
|
+
file_path: z.string().describe("Absolute path to the file to write"),
|
|
225
|
+
content: z.string().describe("Content to write to the file")
|
|
226
|
+
});
|
|
227
|
+
var WRITE_DESCRIPTION = `Writes content to a file in the sandbox filesystem.
|
|
228
|
+
Creates parent directories if they don't exist.`;
|
|
229
|
+
var editInputSchema = z.object({
|
|
230
|
+
file_path: z.string().describe("Absolute path to the file to edit"),
|
|
231
|
+
old_string: z.string().describe("The exact string to find and replace"),
|
|
232
|
+
new_string: z.string().describe("The string to replace it with")
|
|
233
|
+
});
|
|
234
|
+
var EDIT_DESCRIPTION = `Makes a targeted edit to a file by replacing old_string with new_string.
|
|
235
|
+
The old_string must match exactly (including whitespace and indentation).`;
|
|
236
|
+
var bashInputSchema = z.object({
|
|
237
|
+
command: z.string().describe("The bash command to execute"),
|
|
238
|
+
description: z.string().optional().describe("Brief description of what this command does"),
|
|
239
|
+
timeout: z.number().optional().describe("Timeout in milliseconds (default: 120000)")
|
|
240
|
+
});
|
|
241
|
+
var BASH_DESCRIPTION = `Executes a bash command in the sandbox.
|
|
242
|
+
Use for git, npm, system commands, etc.`;
|
|
243
|
+
var globInputSchema = z.object({
|
|
244
|
+
pattern: z.string().describe('Glob pattern (e.g., "**/*.ts", "src/**/*.json")'),
|
|
245
|
+
path: z.string().optional().describe("Directory to search in")
|
|
246
|
+
});
|
|
247
|
+
var GLOB_DESCRIPTION = `Finds files matching a glob pattern.
|
|
248
|
+
Returns list of matching file paths.`;
|
|
249
|
+
var grepInputSchema = z.object({
|
|
250
|
+
pattern: z.string().describe("Regex pattern to search for"),
|
|
251
|
+
path: z.string().optional().describe("File or directory to search in"),
|
|
252
|
+
glob: z.string().optional().describe('Glob pattern to filter files (e.g., "*.ts")'),
|
|
253
|
+
output_mode: z.enum(["content", "files_with_matches", "count"]).optional().describe("Output mode (default: files_with_matches)")
|
|
254
|
+
});
|
|
255
|
+
var GREP_DESCRIPTION = `Searches for a regex pattern in files.
|
|
256
|
+
Returns matching lines with file paths and line numbers.`;
|
|
257
|
+
async function executeRead(sandbox, params, config) {
|
|
258
|
+
const { file_path, offset, limit } = params;
|
|
259
|
+
if (config?.allowedPaths) {
|
|
260
|
+
const isAllowed = config.allowedPaths.some((allowed) => file_path.startsWith(allowed));
|
|
261
|
+
if (!isAllowed) {
|
|
262
|
+
return { error: `Path not allowed: ${file_path}` };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const exists = await sandbox.fileExists(file_path);
|
|
267
|
+
if (!exists) {
|
|
268
|
+
return { error: `Path not found: ${file_path}` };
|
|
269
|
+
}
|
|
270
|
+
const isDir = await sandbox.isDirectory(file_path);
|
|
271
|
+
if (isDir) {
|
|
272
|
+
const entries = await sandbox.readDir(file_path);
|
|
273
|
+
return { type: "directory", entries, count: entries.length };
|
|
274
|
+
}
|
|
275
|
+
const content = await sandbox.readFile(file_path);
|
|
276
|
+
const allLines = content.split(`
|
|
277
|
+
`);
|
|
278
|
+
const totalLines = allLines.length;
|
|
279
|
+
const maxLinesWithoutLimit = config?.maxFileSize || 500;
|
|
280
|
+
if (!limit && totalLines > maxLinesWithoutLimit) {
|
|
281
|
+
return {
|
|
282
|
+
error: `File is large (${totalLines} lines). Use 'offset' and 'limit' to read in chunks.`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const startLine = offset ? offset - 1 : 0;
|
|
286
|
+
const endLine = limit ? startLine + limit : allLines.length;
|
|
287
|
+
const selectedLines = allLines.slice(startLine, endLine);
|
|
288
|
+
const lines = selectedLines.map((line, i) => ({
|
|
289
|
+
line_number: startLine + i + 1,
|
|
290
|
+
content: line
|
|
291
|
+
}));
|
|
292
|
+
return {
|
|
293
|
+
type: "text",
|
|
294
|
+
content: selectedLines.join(`
|
|
295
|
+
`),
|
|
296
|
+
lines,
|
|
297
|
+
total_lines: totalLines
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function executeWrite(sandbox, params) {
|
|
304
|
+
const { file_path, content } = params;
|
|
305
|
+
try {
|
|
306
|
+
const dir = file_path.split("/").slice(0, -1).join("/");
|
|
307
|
+
if (dir) {
|
|
308
|
+
await sandbox.exec(`mkdir -p "${dir}"`);
|
|
309
|
+
}
|
|
310
|
+
await sandbox.writeFile(file_path, content);
|
|
311
|
+
return { success: true, path: file_path, bytes_written: content.length };
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function executeEdit(sandbox, params) {
|
|
317
|
+
const { file_path, old_string, new_string } = params;
|
|
318
|
+
try {
|
|
319
|
+
const content = await sandbox.readFile(file_path);
|
|
320
|
+
if (!content.includes(old_string)) {
|
|
321
|
+
return { error: `old_string not found in file: ${file_path}` };
|
|
322
|
+
}
|
|
323
|
+
const newContent = content.replace(old_string, new_string);
|
|
324
|
+
await sandbox.writeFile(file_path, newContent);
|
|
325
|
+
return { success: true, path: file_path };
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function executeBash(sandbox, params, config) {
|
|
331
|
+
const { command, timeout } = params;
|
|
332
|
+
if (config?.blockedCommands) {
|
|
333
|
+
const isBlocked = config.blockedCommands.some((blocked) => command.includes(blocked));
|
|
334
|
+
if (isBlocked) {
|
|
335
|
+
return { error: `Command blocked by security policy` };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const result = await sandbox.exec(command, {
|
|
340
|
+
timeout: timeout || config?.timeout || 120000
|
|
341
|
+
});
|
|
342
|
+
return {
|
|
343
|
+
stdout: result.stdout,
|
|
344
|
+
stderr: result.stderr,
|
|
345
|
+
exit_code: result.exitCode,
|
|
346
|
+
duration_ms: result.durationMs
|
|
347
|
+
};
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function executeGlob(sandbox, params) {
|
|
353
|
+
const { pattern, path } = params;
|
|
354
|
+
const searchPath = path || ".";
|
|
355
|
+
try {
|
|
356
|
+
const result = await sandbox.exec(`find ${searchPath} -type f -name "${pattern.replace(/\*\*/g, "*")}" 2>/dev/null | head -200`);
|
|
357
|
+
const files = result.stdout.split(`
|
|
358
|
+
`).filter(Boolean);
|
|
359
|
+
return { files, count: files.length };
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function executeGrep(sandbox, params) {
|
|
365
|
+
const { pattern, path, glob, output_mode } = params;
|
|
366
|
+
const searchPath = path || ".";
|
|
367
|
+
const mode = output_mode || "files_with_matches";
|
|
368
|
+
try {
|
|
369
|
+
let cmd;
|
|
370
|
+
const rgPath = sandbox.rgPath || "rg";
|
|
371
|
+
if (mode === "files_with_matches") {
|
|
372
|
+
cmd = `${rgPath} -l "${pattern}" ${searchPath}`;
|
|
373
|
+
} else if (mode === "count") {
|
|
374
|
+
cmd = `${rgPath} -c "${pattern}" ${searchPath}`;
|
|
375
|
+
} else {
|
|
376
|
+
cmd = `${rgPath} -n "${pattern}" ${searchPath}`;
|
|
377
|
+
}
|
|
378
|
+
if (glob) {
|
|
379
|
+
cmd += ` --glob "${glob}"`;
|
|
380
|
+
}
|
|
381
|
+
cmd += " 2>/dev/null | head -100";
|
|
382
|
+
const result = await sandbox.exec(cmd);
|
|
383
|
+
const lines = result.stdout.split(`
|
|
384
|
+
`).filter(Boolean);
|
|
385
|
+
if (mode === "files_with_matches") {
|
|
386
|
+
return { files: lines, count: lines.length };
|
|
387
|
+
} else if (mode === "count") {
|
|
388
|
+
return { matches: lines };
|
|
389
|
+
} else {
|
|
390
|
+
return { content: result.stdout, match_count: lines.length };
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function createDurableAgentTools(sandboxId, config) {
|
|
397
|
+
const toolsConfig = config?.tools || {};
|
|
398
|
+
const tools = {
|
|
399
|
+
Read: tool({
|
|
400
|
+
description: READ_DESCRIPTION,
|
|
401
|
+
inputSchema: zodSchema(readInputSchema),
|
|
402
|
+
execute: async (params) => {
|
|
403
|
+
"use step";
|
|
404
|
+
const sandbox = await reconnectSandbox(sandboxId, config?.apiKey);
|
|
405
|
+
return executeRead(sandbox, params, toolsConfig.Read);
|
|
406
|
+
}
|
|
407
|
+
}),
|
|
408
|
+
Write: tool({
|
|
409
|
+
description: WRITE_DESCRIPTION,
|
|
410
|
+
inputSchema: zodSchema(writeInputSchema),
|
|
411
|
+
execute: async (params) => {
|
|
412
|
+
"use step";
|
|
413
|
+
const sandbox = await reconnectSandbox(sandboxId, config?.apiKey);
|
|
414
|
+
return executeWrite(sandbox, params);
|
|
415
|
+
}
|
|
416
|
+
}),
|
|
417
|
+
Edit: tool({
|
|
418
|
+
description: EDIT_DESCRIPTION,
|
|
419
|
+
inputSchema: zodSchema(editInputSchema),
|
|
420
|
+
execute: async (params) => {
|
|
421
|
+
"use step";
|
|
422
|
+
const sandbox = await reconnectSandbox(sandboxId, config?.apiKey);
|
|
423
|
+
return executeEdit(sandbox, params);
|
|
424
|
+
}
|
|
425
|
+
}),
|
|
426
|
+
Bash: tool({
|
|
427
|
+
description: BASH_DESCRIPTION,
|
|
428
|
+
inputSchema: zodSchema(bashInputSchema),
|
|
429
|
+
execute: async (params) => {
|
|
430
|
+
"use step";
|
|
431
|
+
const sandbox = await reconnectSandbox(sandboxId, config?.apiKey);
|
|
432
|
+
return executeBash(sandbox, params, toolsConfig.Bash);
|
|
433
|
+
}
|
|
434
|
+
}),
|
|
435
|
+
Glob: tool({
|
|
436
|
+
description: GLOB_DESCRIPTION,
|
|
437
|
+
inputSchema: zodSchema(globInputSchema),
|
|
438
|
+
execute: async (params) => {
|
|
439
|
+
"use step";
|
|
440
|
+
const sandbox = await reconnectSandbox(sandboxId, config?.apiKey);
|
|
441
|
+
return executeGlob(sandbox, params);
|
|
442
|
+
}
|
|
443
|
+
}),
|
|
444
|
+
Grep: tool({
|
|
445
|
+
description: GREP_DESCRIPTION,
|
|
446
|
+
inputSchema: zodSchema(grepInputSchema),
|
|
447
|
+
execute: async (params) => {
|
|
448
|
+
"use step";
|
|
449
|
+
const sandbox = await reconnectSandbox(sandboxId, config?.apiKey);
|
|
450
|
+
return executeGrep(sandbox, params);
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
};
|
|
454
|
+
return { tools };
|
|
455
|
+
}
|
|
456
|
+
export {
|
|
457
|
+
createDurableAgentTools
|
|
458
|
+
};
|
package/package.json
CHANGED