@inkeep/agents-run-api 0.14.16 → 0.16.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.
@@ -0,0 +1,319 @@
1
+ import { __publicField } from './chunk-PKBMQBKP.js';
2
+ import { getLogger } from '@inkeep/agents-core';
3
+ import { spawn } from 'child_process';
4
+ import { createHash } from 'crypto';
5
+ import { mkdirSync, existsSync, rmSync, writeFileSync } from 'fs';
6
+ import { tmpdir } from 'os';
7
+ import { join } from 'path';
8
+
9
+ var logger = getLogger("local-sandbox-executor");
10
+ var _LocalSandboxExecutor = class _LocalSandboxExecutor {
11
+ constructor() {
12
+ __publicField(this, "tempDir");
13
+ __publicField(this, "sandboxPool", {});
14
+ __publicField(this, "POOL_TTL", 5 * 60 * 1e3);
15
+ // 5 minutes
16
+ __publicField(this, "MAX_USE_COUNT", 50);
17
+ this.tempDir = join(tmpdir(), "inkeep-sandboxes");
18
+ this.ensureTempDir();
19
+ this.startPoolCleanup();
20
+ }
21
+ static getInstance() {
22
+ if (!_LocalSandboxExecutor.instance) {
23
+ _LocalSandboxExecutor.instance = new _LocalSandboxExecutor();
24
+ }
25
+ return _LocalSandboxExecutor.instance;
26
+ }
27
+ ensureTempDir() {
28
+ try {
29
+ mkdirSync(this.tempDir, { recursive: true });
30
+ } catch {
31
+ }
32
+ }
33
+ generateDependencyHash(dependencies) {
34
+ const sortedDeps = Object.keys(dependencies).sort().map((key) => `${key}@${dependencies[key]}`).join(",");
35
+ return createHash("sha256").update(sortedDeps).digest("hex").substring(0, 16);
36
+ }
37
+ getCachedSandbox(dependencyHash) {
38
+ const poolKey = dependencyHash;
39
+ const sandbox = this.sandboxPool[poolKey];
40
+ if (sandbox && existsSync(sandbox.sandboxDir)) {
41
+ const now = Date.now();
42
+ if (now - sandbox.lastUsed < this.POOL_TTL && sandbox.useCount < this.MAX_USE_COUNT) {
43
+ sandbox.lastUsed = now;
44
+ sandbox.useCount++;
45
+ logger.debug(
46
+ {
47
+ poolKey,
48
+ useCount: sandbox.useCount,
49
+ sandboxDir: sandbox.sandboxDir,
50
+ lastUsed: new Date(sandbox.lastUsed).toISOString()
51
+ },
52
+ "Reusing cached sandbox"
53
+ );
54
+ return sandbox.sandboxDir;
55
+ } else {
56
+ this.cleanupSandbox(sandbox.sandboxDir);
57
+ delete this.sandboxPool[poolKey];
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ addToPool(dependencyHash, sandboxDir, dependencies) {
63
+ const poolKey = dependencyHash;
64
+ if (this.sandboxPool[poolKey]) {
65
+ this.cleanupSandbox(this.sandboxPool[poolKey].sandboxDir);
66
+ }
67
+ this.sandboxPool[poolKey] = {
68
+ sandboxDir,
69
+ lastUsed: Date.now(),
70
+ useCount: 1,
71
+ dependencies
72
+ };
73
+ logger.debug({ poolKey, sandboxDir }, "Added sandbox to pool");
74
+ }
75
+ cleanupSandbox(sandboxDir) {
76
+ try {
77
+ rmSync(sandboxDir, { recursive: true, force: true });
78
+ logger.debug({ sandboxDir }, "Cleaned up sandbox");
79
+ } catch (error) {
80
+ logger.warn({ sandboxDir, error }, "Failed to clean up sandbox");
81
+ }
82
+ }
83
+ startPoolCleanup() {
84
+ setInterval(() => {
85
+ const now = Date.now();
86
+ const keysToDelete = [];
87
+ for (const [key, sandbox] of Object.entries(this.sandboxPool)) {
88
+ if (now - sandbox.lastUsed > this.POOL_TTL || sandbox.useCount >= this.MAX_USE_COUNT) {
89
+ this.cleanupSandbox(sandbox.sandboxDir);
90
+ keysToDelete.push(key);
91
+ }
92
+ }
93
+ keysToDelete.forEach((key) => {
94
+ delete this.sandboxPool[key];
95
+ });
96
+ if (keysToDelete.length > 0) {
97
+ logger.debug({ cleanedCount: keysToDelete.length }, "Cleaned up expired sandboxes");
98
+ }
99
+ }, 6e4);
100
+ }
101
+ detectModuleType(executeCode) {
102
+ const esmPatterns = [
103
+ /import\s+.*\s+from\s+['"]/g,
104
+ // import ... from '...'
105
+ /import\s*\(/g,
106
+ // import(...)
107
+ /export\s+(default|const|let|var|function|class)/g,
108
+ // export statements
109
+ /export\s*\{/g
110
+ // export { ... }
111
+ ];
112
+ const cjsPatterns = [
113
+ /require\s*\(/g,
114
+ // require(...)
115
+ /module\.exports/g,
116
+ // module.exports
117
+ /exports\./g
118
+ // exports.something
119
+ ];
120
+ const hasEsmSyntax = esmPatterns.some((pattern) => pattern.test(executeCode));
121
+ const hasCjsSyntax = cjsPatterns.some((pattern) => pattern.test(executeCode));
122
+ if (hasEsmSyntax && hasCjsSyntax) {
123
+ logger.warn(
124
+ { executeCode: `${executeCode.substring(0, 100)}...` },
125
+ "Both ESM and CommonJS syntax detected, defaulting to ESM"
126
+ );
127
+ return "esm";
128
+ }
129
+ if (hasEsmSyntax) {
130
+ return "esm";
131
+ }
132
+ if (hasCjsSyntax) {
133
+ return "cjs";
134
+ }
135
+ return "cjs";
136
+ }
137
+ async executeFunctionTool(toolId, args, config) {
138
+ const dependencies = config.dependencies || {};
139
+ const dependencyHash = this.generateDependencyHash(dependencies);
140
+ logger.debug(
141
+ {
142
+ toolId,
143
+ dependencies,
144
+ dependencyHash,
145
+ poolSize: Object.keys(this.sandboxPool).length
146
+ },
147
+ "Executing function tool"
148
+ );
149
+ let sandboxDir = this.getCachedSandbox(dependencyHash);
150
+ let isNewSandbox = false;
151
+ if (!sandboxDir) {
152
+ sandboxDir = join(this.tempDir, `sandbox-${dependencyHash}-${Date.now()}`);
153
+ mkdirSync(sandboxDir, { recursive: true });
154
+ isNewSandbox = true;
155
+ logger.debug(
156
+ {
157
+ toolId,
158
+ dependencyHash,
159
+ sandboxDir,
160
+ dependencies
161
+ },
162
+ "Creating new sandbox"
163
+ );
164
+ const moduleType = this.detectModuleType(config.executeCode);
165
+ const packageJson = {
166
+ name: `function-tool-${toolId}`,
167
+ version: "1.0.0",
168
+ ...moduleType === "esm" && { type: "module" },
169
+ dependencies,
170
+ scripts: {
171
+ start: moduleType === "esm" ? "node index.mjs" : "node index.js"
172
+ }
173
+ };
174
+ writeFileSync(join(sandboxDir, "package.json"), JSON.stringify(packageJson, null, 2), "utf8");
175
+ if (Object.keys(dependencies).length > 0) {
176
+ await this.installDependencies(sandboxDir);
177
+ }
178
+ this.addToPool(dependencyHash, sandboxDir, dependencies);
179
+ }
180
+ try {
181
+ const moduleType = this.detectModuleType(config.executeCode);
182
+ const executionCode = this.wrapFunctionCode(config.executeCode, args);
183
+ const fileExtension = moduleType === "esm" ? "mjs" : "js";
184
+ writeFileSync(join(sandboxDir, `index.${fileExtension}`), executionCode, "utf8");
185
+ const result = await this.executeInSandbox(
186
+ sandboxDir,
187
+ config.sandboxConfig?.timeout || 3e4,
188
+ moduleType
189
+ );
190
+ return result;
191
+ } catch (error) {
192
+ if (isNewSandbox) {
193
+ this.cleanupSandbox(sandboxDir);
194
+ const poolKey = dependencyHash;
195
+ delete this.sandboxPool[poolKey];
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+ async installDependencies(sandboxDir) {
201
+ return new Promise((resolve, reject) => {
202
+ const npm = spawn("npm", ["install"], {
203
+ cwd: sandboxDir,
204
+ stdio: "pipe"
205
+ });
206
+ let stderr = "";
207
+ npm.stdout?.on("data", () => {
208
+ });
209
+ npm.stderr?.on("data", (data) => {
210
+ stderr += data.toString();
211
+ });
212
+ npm.on("close", (code) => {
213
+ if (code === 0) {
214
+ logger.debug({ sandboxDir }, "Dependencies installed successfully");
215
+ resolve();
216
+ } else {
217
+ logger.error({ sandboxDir, code, stderr }, "Failed to install dependencies");
218
+ reject(new Error(`npm install failed with code ${code}: ${stderr}`));
219
+ }
220
+ });
221
+ npm.on("error", (err) => {
222
+ logger.error({ sandboxDir, error: err }, "Failed to spawn npm install");
223
+ reject(err);
224
+ });
225
+ });
226
+ }
227
+ async executeInSandbox(sandboxDir, timeout, moduleType) {
228
+ return new Promise((resolve, reject) => {
229
+ const fileExtension = moduleType === "esm" ? "mjs" : "js";
230
+ const spawnOptions = {
231
+ cwd: sandboxDir,
232
+ stdio: "pipe",
233
+ // Security: drop privileges and limit resources
234
+ uid: process.getuid ? process.getuid() : void 0,
235
+ gid: process.getgid ? process.getgid() : void 0
236
+ };
237
+ const node = spawn("node", [`index.${fileExtension}`], spawnOptions);
238
+ let stdout = "";
239
+ let stderr = "";
240
+ let outputSize = 0;
241
+ const MAX_OUTPUT_SIZE = 1024 * 1024;
242
+ node.stdout?.on("data", (data) => {
243
+ const dataStr = data.toString();
244
+ outputSize += dataStr.length;
245
+ if (outputSize > MAX_OUTPUT_SIZE) {
246
+ node.kill("SIGTERM");
247
+ reject(new Error(`Output size exceeded limit of ${MAX_OUTPUT_SIZE} bytes`));
248
+ return;
249
+ }
250
+ stdout += dataStr;
251
+ });
252
+ node.stderr?.on("data", (data) => {
253
+ const dataStr = data.toString();
254
+ outputSize += dataStr.length;
255
+ if (outputSize > MAX_OUTPUT_SIZE) {
256
+ node.kill("SIGTERM");
257
+ reject(new Error(`Output size exceeded limit of ${MAX_OUTPUT_SIZE} bytes`));
258
+ return;
259
+ }
260
+ stderr += dataStr;
261
+ });
262
+ const timeoutId = setTimeout(() => {
263
+ logger.warn({ sandboxDir, timeout }, "Function execution timed out, killing process");
264
+ node.kill("SIGTERM");
265
+ setTimeout(() => {
266
+ try {
267
+ node.kill("SIGKILL");
268
+ } catch {
269
+ }
270
+ }, 5e3);
271
+ reject(new Error(`Function execution timed out after ${timeout}ms`));
272
+ }, timeout);
273
+ node.on("close", (code, signal) => {
274
+ clearTimeout(timeoutId);
275
+ if (code === 0) {
276
+ try {
277
+ const result = JSON.parse(stdout);
278
+ if (result.success) {
279
+ resolve(result.result);
280
+ } else {
281
+ reject(new Error(result.error || "Function execution failed"));
282
+ }
283
+ } catch (parseError) {
284
+ logger.error({ stdout, stderr, parseError }, "Failed to parse function result");
285
+ reject(new Error(`Invalid function result: ${stdout}`));
286
+ }
287
+ } else {
288
+ const errorMsg = signal ? `Function execution killed by signal ${signal}: ${stderr}` : `Function execution failed with code ${code}: ${stderr}`;
289
+ logger.error({ code, signal, stderr }, "Function execution failed");
290
+ reject(new Error(errorMsg));
291
+ }
292
+ });
293
+ node.on("error", (error) => {
294
+ clearTimeout(timeoutId);
295
+ logger.error({ sandboxDir, error }, "Failed to spawn node process");
296
+ reject(error);
297
+ });
298
+ });
299
+ }
300
+ wrapFunctionCode(executeCode, args) {
301
+ return `
302
+ // Wrapped function execution (ESM)
303
+ const execute = ${executeCode};
304
+ const args = ${JSON.stringify(args)};
305
+
306
+ execute(args)
307
+ .then(result => {
308
+ console.log(JSON.stringify({ success: true, result }));
309
+ })
310
+ .catch(error => {
311
+ console.log(JSON.stringify({ success: false, error: error.message }));
312
+ });
313
+ `;
314
+ }
315
+ };
316
+ __publicField(_LocalSandboxExecutor, "instance", null);
317
+ var LocalSandboxExecutor = _LocalSandboxExecutor;
318
+
319
+ export { LocalSandboxExecutor };