@aragon-agent/core 0.1.3
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/CHANGELOG.md +126 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/engine/agent-loop.d.ts +52 -0
- package/dist/engine/agent-loop.js +260 -0
- package/dist/engine/agent.d.ts +129 -0
- package/dist/engine/agent.js +283 -0
- package/dist/engine/message-manager.d.ts +26 -0
- package/dist/engine/message-manager.js +39 -0
- package/dist/engine/steering.d.ts +27 -0
- package/dist/engine/steering.js +51 -0
- package/dist/engine/watchdog.d.ts +24 -0
- package/dist/engine/watchdog.js +50 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +63 -0
- package/dist/llm/model-registry.d.ts +51 -0
- package/dist/llm/model-registry.js +277 -0
- package/dist/llm/provider.d.ts +72 -0
- package/dist/llm/provider.js +77 -0
- package/dist/llm/providers/anthropic.d.ts +23 -0
- package/dist/llm/providers/anthropic.js +420 -0
- package/dist/llm/providers/google.d.ts +22 -0
- package/dist/llm/providers/google.js +336 -0
- package/dist/llm/providers/index.d.ts +67 -0
- package/dist/llm/providers/index.js +127 -0
- package/dist/llm/providers/openai.d.ts +22 -0
- package/dist/llm/providers/openai.js +353 -0
- package/dist/llm/stream-utils.d.ts +33 -0
- package/dist/llm/stream-utils.js +132 -0
- package/dist/llm/types.d.ts +129 -0
- package/dist/llm/types.js +17 -0
- package/dist/sandbox/console-capture.d.ts +29 -0
- package/dist/sandbox/console-capture.js +56 -0
- package/dist/sandbox/http-bridge.d.ts +47 -0
- package/dist/sandbox/http-bridge.js +143 -0
- package/dist/sandbox/isolated-vm-sandbox.d.ts +50 -0
- package/dist/sandbox/isolated-vm-sandbox.js +226 -0
- package/dist/sandbox/tool-bridge.d.ts +42 -0
- package/dist/sandbox/tool-bridge.js +98 -0
- package/dist/sandbox/types.d.ts +84 -0
- package/dist/sandbox/types.js +8 -0
- package/dist/skills/constants.d.ts +122 -0
- package/dist/skills/constants.js +179 -0
- package/dist/skills/disclosure.d.ts +146 -0
- package/dist/skills/disclosure.js +788 -0
- package/dist/skills/frontmatter.d.ts +27 -0
- package/dist/skills/frontmatter.js +177 -0
- package/dist/skills/index.d.ts +21 -0
- package/dist/skills/index.js +18 -0
- package/dist/skills/skill-find-tool.d.ts +41 -0
- package/dist/skills/skill-find-tool.js +117 -0
- package/dist/skills/skill-registry.d.ts +96 -0
- package/dist/skills/skill-registry.js +208 -0
- package/dist/skills/skill-tool.d.ts +44 -0
- package/dist/skills/skill-tool.js +96 -0
- package/dist/skills/tool-policy.d.ts +91 -0
- package/dist/skills/tool-policy.js +272 -0
- package/dist/skills/types.d.ts +271 -0
- package/dist/skills/types.js +8 -0
- package/dist/skills/validate.d.ts +37 -0
- package/dist/skills/validate.js +264 -0
- package/dist/tools/executor.d.ts +54 -0
- package/dist/tools/executor.js +206 -0
- package/dist/tools/helpers.d.ts +69 -0
- package/dist/tools/helpers.js +87 -0
- package/dist/tools/registry.d.ts +58 -0
- package/dist/tools/registry.js +119 -0
- package/dist/tools/types.d.ts +140 -0
- package/dist/tools/types.js +7 -0
- package/dist/tools/validator.d.ts +46 -0
- package/dist/tools/validator.js +210 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +7 -0
- package/package.json +115 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeAct Sandbox — Console Capture
|
|
3
|
+
*
|
|
4
|
+
* Captures console.log/warn/error/info calls from inside the isolated-vm
|
|
5
|
+
* sandbox and buffers them on the host side. Uses synchronous ivm.Reference
|
|
6
|
+
* callbacks (applySync) so console calls behave naturally in sandbox code.
|
|
7
|
+
*/
|
|
8
|
+
import ivm from 'isolated-vm';
|
|
9
|
+
/** Maximum number of console entries to buffer before discarding oldest. */
|
|
10
|
+
const MAX_BUFFER_SIZE = 1000;
|
|
11
|
+
/**
|
|
12
|
+
* ConsoleCapture intercepts console method calls inside an isolated-vm
|
|
13
|
+
* Context and stores them as ConsoleEntry records on the host.
|
|
14
|
+
*/
|
|
15
|
+
export class ConsoleCapture {
|
|
16
|
+
entries = [];
|
|
17
|
+
/**
|
|
18
|
+
* Inject console overrides into the given isolated-vm context.
|
|
19
|
+
*
|
|
20
|
+
* Replaces globalThis.console with an object whose log/warn/error/info
|
|
21
|
+
* methods forward arguments (stringified) to the host via a synchronous
|
|
22
|
+
* ivm.Reference callback.
|
|
23
|
+
*/
|
|
24
|
+
async inject(context) {
|
|
25
|
+
const captureRef = new ivm.Reference((level, message) => {
|
|
26
|
+
const entry = {
|
|
27
|
+
level: level,
|
|
28
|
+
args: [message],
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
if (this.entries.length >= MAX_BUFFER_SIZE) {
|
|
32
|
+
// Discard the oldest entry to stay within the buffer limit.
|
|
33
|
+
this.entries.shift();
|
|
34
|
+
}
|
|
35
|
+
this.entries.push(entry);
|
|
36
|
+
});
|
|
37
|
+
await context.evalClosure(`
|
|
38
|
+
const _capture = $0;
|
|
39
|
+
globalThis.console = {
|
|
40
|
+
log: (...args) => _capture.applySync(undefined, ['log', args.map(String).join(' ')]),
|
|
41
|
+
warn: (...args) => _capture.applySync(undefined, ['warn', args.map(String).join(' ')]),
|
|
42
|
+
error: (...args) => _capture.applySync(undefined, ['error', args.map(String).join(' ')]),
|
|
43
|
+
info: (...args) => _capture.applySync(undefined, ['info', args.map(String).join(' ')]),
|
|
44
|
+
};
|
|
45
|
+
`, [captureRef], { arguments: { reference: true } });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Return all captured console entries and clear the internal buffer.
|
|
49
|
+
* Typically called after each execute() to collect output.
|
|
50
|
+
*/
|
|
51
|
+
drain() {
|
|
52
|
+
const result = this.entries;
|
|
53
|
+
this.entries = [];
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeAct Sandbox — HTTP Bridge
|
|
3
|
+
*
|
|
4
|
+
* Provides a domain-whitelisted `fetch` function inside the isolated-vm
|
|
5
|
+
* sandbox. All HTTP requests are proxied through the host process using
|
|
6
|
+
* Node.js native fetch, with configurable timeout and response size limits.
|
|
7
|
+
*/
|
|
8
|
+
import ivm from 'isolated-vm';
|
|
9
|
+
/** Configuration for the HTTP bridge. */
|
|
10
|
+
export interface HttpBridgeConfig {
|
|
11
|
+
/** Allowed hostnames. Requests to unlisted hosts are rejected. */
|
|
12
|
+
allowedHosts: string[];
|
|
13
|
+
/** Per-request timeout in milliseconds. Default: 10_000. */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
/** Maximum response body size in bytes. Default: 1_048_576 (1 MB). */
|
|
16
|
+
maxResponseSize?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* HttpBridge injects a `globalThis.fetch` function into the sandbox that
|
|
20
|
+
* proxies requests through the host with domain whitelisting.
|
|
21
|
+
*/
|
|
22
|
+
export declare class HttpBridge {
|
|
23
|
+
private readonly allowedHosts;
|
|
24
|
+
private readonly timeout;
|
|
25
|
+
private readonly maxResponseSize;
|
|
26
|
+
constructor(config: HttpBridgeConfig);
|
|
27
|
+
/**
|
|
28
|
+
* Inject the `fetch` global async function into the given context.
|
|
29
|
+
* Sandbox signature: `async function fetch(url, options?) => { status, statusText, headers, body }`
|
|
30
|
+
*/
|
|
31
|
+
inject(context: ivm.Context): Promise<void>;
|
|
32
|
+
/** Allowed URL protocols. Blocks file://, data://, etc. */
|
|
33
|
+
private static readonly ALLOWED_PROTOCOLS;
|
|
34
|
+
/**
|
|
35
|
+
* Extract hostname from a URL string and validate protocol.
|
|
36
|
+
* Throws if the URL is malformed or uses a disallowed protocol.
|
|
37
|
+
*/
|
|
38
|
+
private extractHostname;
|
|
39
|
+
/**
|
|
40
|
+
* Read the response body as text, enforcing the maximum size limit.
|
|
41
|
+
* If the body exceeds the limit, it is truncated and a warning appended.
|
|
42
|
+
*
|
|
43
|
+
* NOTE: We always stream-read to enforce the limit, regardless of
|
|
44
|
+
* Content-Length header (which can be spoofed by the remote server).
|
|
45
|
+
*/
|
|
46
|
+
private readBodyWithLimit;
|
|
47
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeAct Sandbox — HTTP Bridge
|
|
3
|
+
*
|
|
4
|
+
* Provides a domain-whitelisted `fetch` function inside the isolated-vm
|
|
5
|
+
* sandbox. All HTTP requests are proxied through the host process using
|
|
6
|
+
* Node.js native fetch, with configurable timeout and response size limits.
|
|
7
|
+
*/
|
|
8
|
+
import ivm from 'isolated-vm';
|
|
9
|
+
/** Default request timeout (10 seconds). */
|
|
10
|
+
const DEFAULT_TIMEOUT = 10_000;
|
|
11
|
+
/** Default maximum response body size (1 MB). */
|
|
12
|
+
const DEFAULT_MAX_RESPONSE_SIZE = 1_048_576;
|
|
13
|
+
/**
|
|
14
|
+
* HttpBridge injects a `globalThis.fetch` function into the sandbox that
|
|
15
|
+
* proxies requests through the host with domain whitelisting.
|
|
16
|
+
*/
|
|
17
|
+
export class HttpBridge {
|
|
18
|
+
allowedHosts;
|
|
19
|
+
timeout;
|
|
20
|
+
maxResponseSize;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.allowedHosts = new Set(config.allowedHosts.map((h) => h.toLowerCase()));
|
|
23
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
24
|
+
this.maxResponseSize = config.maxResponseSize ?? DEFAULT_MAX_RESPONSE_SIZE;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Inject the `fetch` global async function into the given context.
|
|
28
|
+
* Sandbox signature: `async function fetch(url, options?) => { status, statusText, headers, body }`
|
|
29
|
+
*/
|
|
30
|
+
async inject(context) {
|
|
31
|
+
const fetchRef = new ivm.Reference(async (requestJson) => {
|
|
32
|
+
const { url, options } = JSON.parse(requestJson);
|
|
33
|
+
// --- Domain whitelist check ---
|
|
34
|
+
const hostname = this.extractHostname(url);
|
|
35
|
+
if (!this.allowedHosts.has(hostname.toLowerCase())) {
|
|
36
|
+
throw new Error(`HTTP request blocked: hostname "${hostname}" is not in the allowed hosts list`);
|
|
37
|
+
}
|
|
38
|
+
// --- Execute host-side fetch with timeout ---
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
method: options?.method ?? 'GET',
|
|
44
|
+
headers: options?.headers,
|
|
45
|
+
body: options?.body,
|
|
46
|
+
signal: controller.signal,
|
|
47
|
+
// SECURITY: Do not follow redirects automatically.
|
|
48
|
+
// A whitelisted domain could redirect to an internal service (SSRF).
|
|
49
|
+
redirect: 'manual',
|
|
50
|
+
});
|
|
51
|
+
// Read body with size limit enforcement.
|
|
52
|
+
const body = await this.readBodyWithLimit(response);
|
|
53
|
+
// Collect response headers into a plain object.
|
|
54
|
+
const headers = {};
|
|
55
|
+
response.headers.forEach((value, key) => {
|
|
56
|
+
headers[key] = value;
|
|
57
|
+
});
|
|
58
|
+
const result = {
|
|
59
|
+
status: response.status,
|
|
60
|
+
statusText: response.statusText,
|
|
61
|
+
headers,
|
|
62
|
+
body,
|
|
63
|
+
};
|
|
64
|
+
return JSON.stringify(result);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
68
|
+
throw new Error(`HTTP request timed out after ${this.timeout}ms`);
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// Register fetch in the sandbox. The sandbox function serializes
|
|
77
|
+
// arguments to JSON, sends to host, and deserializes the response.
|
|
78
|
+
await context.evalClosure(`globalThis.fetch = async function(url, options) {
|
|
79
|
+
const requestJson = JSON.stringify({ url, options });
|
|
80
|
+
const responseJson = await $0.apply(undefined, [requestJson], { result: { promise: true } });
|
|
81
|
+
return JSON.parse(responseJson);
|
|
82
|
+
}`, [fetchRef], { arguments: { reference: true } });
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Private helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
/** Allowed URL protocols. Blocks file://, data://, etc. */
|
|
88
|
+
static ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);
|
|
89
|
+
/**
|
|
90
|
+
* Extract hostname from a URL string and validate protocol.
|
|
91
|
+
* Throws if the URL is malformed or uses a disallowed protocol.
|
|
92
|
+
*/
|
|
93
|
+
extractHostname(url) {
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = new URL(url);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error(`Invalid URL: "${url}"`);
|
|
100
|
+
}
|
|
101
|
+
// SECURITY: Only allow http/https to prevent file://, data://, etc.
|
|
102
|
+
if (!HttpBridge.ALLOWED_PROTOCOLS.has(parsed.protocol)) {
|
|
103
|
+
throw new Error(`Blocked URL protocol "${parsed.protocol}" — only http: and https: are allowed`);
|
|
104
|
+
}
|
|
105
|
+
return parsed.hostname;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Read the response body as text, enforcing the maximum size limit.
|
|
109
|
+
* If the body exceeds the limit, it is truncated and a warning appended.
|
|
110
|
+
*
|
|
111
|
+
* NOTE: We always stream-read to enforce the limit, regardless of
|
|
112
|
+
* Content-Length header (which can be spoofed by the remote server).
|
|
113
|
+
*/
|
|
114
|
+
async readBodyWithLimit(response) {
|
|
115
|
+
// Stream-read with size enforcement.
|
|
116
|
+
const reader = response.body?.getReader();
|
|
117
|
+
if (!reader) {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
const decoder = new TextDecoder();
|
|
121
|
+
const chunks = [];
|
|
122
|
+
let totalBytes = 0;
|
|
123
|
+
try {
|
|
124
|
+
while (true) {
|
|
125
|
+
const { done, value } = await reader.read();
|
|
126
|
+
if (done)
|
|
127
|
+
break;
|
|
128
|
+
totalBytes += value.byteLength;
|
|
129
|
+
if (totalBytes > this.maxResponseSize) {
|
|
130
|
+
// Decode what we have so far and truncate.
|
|
131
|
+
chunks.push(decoder.decode(value, { stream: false }));
|
|
132
|
+
chunks.push(`\n\n[Response truncated at ${this.maxResponseSize} bytes]`);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
reader.releaseLock();
|
|
140
|
+
}
|
|
141
|
+
return chunks.join('');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeAct Sandbox — IsolatedVmSandbox
|
|
3
|
+
*
|
|
4
|
+
* Primary implementation of the CodeActSandbox interface using the
|
|
5
|
+
* `isolated-vm` package. Provides a memory-limited, time-limited V8
|
|
6
|
+
* Isolate with bridged tool access, console capture, and HTTP fetch.
|
|
7
|
+
*
|
|
8
|
+
* Key design decisions:
|
|
9
|
+
* - Lazy initialization: the V8 Isolate is not created until first execute().
|
|
10
|
+
* - Variable persistence: the same Context is reused across execute() calls
|
|
11
|
+
* so variables defined in one call are available in subsequent calls.
|
|
12
|
+
* - reset() destroys the Context and re-creates it (clears all variables).
|
|
13
|
+
* - dispose() releases the Isolate entirely.
|
|
14
|
+
*/
|
|
15
|
+
import type { CodeActSandbox, ExecuteOptions, ExecuteResult, SandboxConfig } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* IsolatedVmSandbox executes LLM-generated JavaScript code in an isolated
|
|
18
|
+
* V8 environment with injected tool bridges, console capture, and HTTP access.
|
|
19
|
+
*/
|
|
20
|
+
export declare class IsolatedVmSandbox implements CodeActSandbox {
|
|
21
|
+
private isolate;
|
|
22
|
+
private context;
|
|
23
|
+
private disposed;
|
|
24
|
+
private readonly memoryLimitMB;
|
|
25
|
+
private readonly defaultTimeout;
|
|
26
|
+
private readonly tools;
|
|
27
|
+
private readonly allowedHosts;
|
|
28
|
+
private readonly globals;
|
|
29
|
+
private consoleCapture;
|
|
30
|
+
private toolBridge;
|
|
31
|
+
private httpBridge;
|
|
32
|
+
constructor(config?: SandboxConfig);
|
|
33
|
+
get isDisposed(): boolean;
|
|
34
|
+
execute(code: string, options?: ExecuteOptions): Promise<ExecuteResult>;
|
|
35
|
+
reset(): Promise<void>;
|
|
36
|
+
dispose(): void;
|
|
37
|
+
/** Full lazy initialization: create Isolate, Context, and inject bridges. */
|
|
38
|
+
private initialize;
|
|
39
|
+
/**
|
|
40
|
+
* Create a new Context within the existing Isolate and inject all
|
|
41
|
+
* bridges (console, tools, HTTP, globals).
|
|
42
|
+
*/
|
|
43
|
+
private createContextAndInjectBridges;
|
|
44
|
+
/** Validate that a name is a safe JavaScript identifier. */
|
|
45
|
+
private static validateIdentifier;
|
|
46
|
+
/** Inject user-provided global variables into the sandbox context. */
|
|
47
|
+
private injectGlobals;
|
|
48
|
+
/** Throw immediately if the sandbox has been disposed. */
|
|
49
|
+
private assertNotDisposed;
|
|
50
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeAct Sandbox — IsolatedVmSandbox
|
|
3
|
+
*
|
|
4
|
+
* Primary implementation of the CodeActSandbox interface using the
|
|
5
|
+
* `isolated-vm` package. Provides a memory-limited, time-limited V8
|
|
6
|
+
* Isolate with bridged tool access, console capture, and HTTP fetch.
|
|
7
|
+
*
|
|
8
|
+
* Key design decisions:
|
|
9
|
+
* - Lazy initialization: the V8 Isolate is not created until first execute().
|
|
10
|
+
* - Variable persistence: the same Context is reused across execute() calls
|
|
11
|
+
* so variables defined in one call are available in subsequent calls.
|
|
12
|
+
* - reset() destroys the Context and re-creates it (clears all variables).
|
|
13
|
+
* - dispose() releases the Isolate entirely.
|
|
14
|
+
*/
|
|
15
|
+
import ivm from 'isolated-vm';
|
|
16
|
+
import { ConsoleCapture } from './console-capture.js';
|
|
17
|
+
import { ToolBridge } from './tool-bridge.js';
|
|
18
|
+
import { HttpBridge } from './http-bridge.js';
|
|
19
|
+
/** Default V8 Isolate memory limit (megabytes). */
|
|
20
|
+
const DEFAULT_MEMORY_LIMIT_MB = 128;
|
|
21
|
+
/** Default per-execution timeout (milliseconds). */
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
23
|
+
/**
|
|
24
|
+
* IsolatedVmSandbox executes LLM-generated JavaScript code in an isolated
|
|
25
|
+
* V8 environment with injected tool bridges, console capture, and HTTP access.
|
|
26
|
+
*/
|
|
27
|
+
export class IsolatedVmSandbox {
|
|
28
|
+
isolate = null;
|
|
29
|
+
context = null;
|
|
30
|
+
disposed = false;
|
|
31
|
+
memoryLimitMB;
|
|
32
|
+
defaultTimeout;
|
|
33
|
+
tools;
|
|
34
|
+
allowedHosts;
|
|
35
|
+
globals;
|
|
36
|
+
// Bridge instances — recreated on reset().
|
|
37
|
+
consoleCapture = null;
|
|
38
|
+
toolBridge = null;
|
|
39
|
+
httpBridge = null;
|
|
40
|
+
constructor(config = {}) {
|
|
41
|
+
this.memoryLimitMB = config.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB;
|
|
42
|
+
this.defaultTimeout = config.defaultTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
43
|
+
this.tools = config.tools ?? [];
|
|
44
|
+
this.allowedHosts = config.allowedHosts ?? [];
|
|
45
|
+
this.globals = config.globals ?? {};
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// CodeActSandbox interface
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
get isDisposed() {
|
|
51
|
+
return this.disposed;
|
|
52
|
+
}
|
|
53
|
+
async execute(code, options) {
|
|
54
|
+
this.assertNotDisposed();
|
|
55
|
+
// Lazy initialization on first execute().
|
|
56
|
+
if (!this.isolate) {
|
|
57
|
+
await this.initialize();
|
|
58
|
+
}
|
|
59
|
+
// Honor AbortSignal if already aborted.
|
|
60
|
+
if (options?.signal?.aborted) {
|
|
61
|
+
return {
|
|
62
|
+
consoleOutput: [],
|
|
63
|
+
error: { name: 'AbortError', message: 'Execution aborted before start' },
|
|
64
|
+
duration: 0,
|
|
65
|
+
toolCalls: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const timeout = options?.timeout ?? this.defaultTimeout;
|
|
69
|
+
const startTime = Date.now();
|
|
70
|
+
// Clear per-execution state.
|
|
71
|
+
this.toolBridge.clearToolCalls();
|
|
72
|
+
// Wrap user code in an async IIFE for top-level await support.
|
|
73
|
+
const wrappedCode = wrapCode(code);
|
|
74
|
+
try {
|
|
75
|
+
// Use context.eval with promise:true to support async code.
|
|
76
|
+
const rawResult = await this.context.eval(wrappedCode, {
|
|
77
|
+
timeout,
|
|
78
|
+
promise: true,
|
|
79
|
+
});
|
|
80
|
+
// Serialize the return value. Handle non-serializable values gracefully.
|
|
81
|
+
let returnValue;
|
|
82
|
+
if (rawResult !== undefined && rawResult !== null) {
|
|
83
|
+
try {
|
|
84
|
+
returnValue = typeof rawResult === 'string'
|
|
85
|
+
? rawResult
|
|
86
|
+
: JSON.stringify(rawResult);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
returnValue = String(rawResult);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
returnValue,
|
|
94
|
+
consoleOutput: this.consoleCapture.drain(),
|
|
95
|
+
duration: Date.now() - startTime,
|
|
96
|
+
toolCalls: this.toolBridge.getToolCalls(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const errorInfo = extractErrorInfo(err);
|
|
101
|
+
return {
|
|
102
|
+
consoleOutput: this.consoleCapture.drain(),
|
|
103
|
+
error: errorInfo,
|
|
104
|
+
duration: Date.now() - startTime,
|
|
105
|
+
toolCalls: this.toolBridge.getToolCalls(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async reset() {
|
|
110
|
+
this.assertNotDisposed();
|
|
111
|
+
if (!this.isolate) {
|
|
112
|
+
// Not yet initialized — nothing to reset.
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Release the current context.
|
|
116
|
+
if (this.context) {
|
|
117
|
+
this.context.release();
|
|
118
|
+
this.context = null;
|
|
119
|
+
}
|
|
120
|
+
// Create a fresh context and re-inject all bridges.
|
|
121
|
+
await this.createContextAndInjectBridges();
|
|
122
|
+
}
|
|
123
|
+
dispose() {
|
|
124
|
+
if (this.disposed)
|
|
125
|
+
return;
|
|
126
|
+
this.disposed = true;
|
|
127
|
+
if (this.context) {
|
|
128
|
+
this.context.release();
|
|
129
|
+
this.context = null;
|
|
130
|
+
}
|
|
131
|
+
if (this.isolate) {
|
|
132
|
+
this.isolate.dispose();
|
|
133
|
+
this.isolate = null;
|
|
134
|
+
}
|
|
135
|
+
this.consoleCapture = null;
|
|
136
|
+
this.toolBridge = null;
|
|
137
|
+
this.httpBridge = null;
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Private — Initialization
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
/** Full lazy initialization: create Isolate, Context, and inject bridges. */
|
|
143
|
+
async initialize() {
|
|
144
|
+
this.isolate = new ivm.Isolate({ memoryLimit: this.memoryLimitMB });
|
|
145
|
+
await this.createContextAndInjectBridges();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Create a new Context within the existing Isolate and inject all
|
|
149
|
+
* bridges (console, tools, HTTP, globals).
|
|
150
|
+
*/
|
|
151
|
+
async createContextAndInjectBridges() {
|
|
152
|
+
this.context = await this.isolate.createContext();
|
|
153
|
+
// --- Console capture ---
|
|
154
|
+
this.consoleCapture = new ConsoleCapture();
|
|
155
|
+
await this.consoleCapture.inject(this.context);
|
|
156
|
+
// --- Tool bridge ---
|
|
157
|
+
this.toolBridge = new ToolBridge(this.tools);
|
|
158
|
+
await this.toolBridge.injectAll(this.context);
|
|
159
|
+
// --- HTTP bridge ---
|
|
160
|
+
this.httpBridge = new HttpBridge({ allowedHosts: this.allowedHosts });
|
|
161
|
+
await this.httpBridge.inject(this.context);
|
|
162
|
+
// --- Pre-injected globals ---
|
|
163
|
+
await this.injectGlobals();
|
|
164
|
+
}
|
|
165
|
+
/** Validate that a name is a safe JavaScript identifier. */
|
|
166
|
+
static validateIdentifier(name) {
|
|
167
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
168
|
+
throw new Error(`Invalid global variable name "${name}": must be a valid JS identifier`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Inject user-provided global variables into the sandbox context. */
|
|
172
|
+
async injectGlobals() {
|
|
173
|
+
const jail = this.context.global;
|
|
174
|
+
for (const [key, value] of Object.entries(this.globals)) {
|
|
175
|
+
// SECURITY: Validate key is a safe identifier before interpolating into eval'd code.
|
|
176
|
+
IsolatedVmSandbox.validateIdentifier(key);
|
|
177
|
+
// Only primitive values and JSON-serializable objects can be transferred.
|
|
178
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
179
|
+
await jail.set(key, value);
|
|
180
|
+
}
|
|
181
|
+
else if (value === null || value === undefined) {
|
|
182
|
+
await jail.set(key, value ?? null);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Serialize complex values as JSON and parse inside sandbox.
|
|
186
|
+
const json = JSON.stringify(value);
|
|
187
|
+
await this.context.eval(`globalThis.${key} = ${json};`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Private — Helpers
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
/** Throw immediately if the sandbox has been disposed. */
|
|
195
|
+
assertNotDisposed() {
|
|
196
|
+
if (this.disposed) {
|
|
197
|
+
throw new Error('Cannot use a disposed CodeActSandbox');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Module-level helpers
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
/**
|
|
205
|
+
* Wrap user code in an async IIFE so top-level `await` and `return` work.
|
|
206
|
+
*/
|
|
207
|
+
function wrapCode(userCode) {
|
|
208
|
+
return `(async () => {\n${userCode}\n})()`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Extract a structured error object from an unknown thrown value.
|
|
212
|
+
* Handles isolated-vm timeout errors, memory errors, and generic JS errors.
|
|
213
|
+
*/
|
|
214
|
+
function extractErrorInfo(err) {
|
|
215
|
+
if (err instanceof Error) {
|
|
216
|
+
return {
|
|
217
|
+
name: err.name,
|
|
218
|
+
message: err.message,
|
|
219
|
+
stack: err.stack,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
name: 'Error',
|
|
224
|
+
message: String(err),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeAct Sandbox — Tool Bridge
|
|
3
|
+
*
|
|
4
|
+
* Bridges AgentTool instances into the isolated-vm sandbox as globally
|
|
5
|
+
* available async functions. Only tools marked `bridgeable: true` are
|
|
6
|
+
* injected. Each call is recorded for inclusion in ExecuteResult.toolCalls.
|
|
7
|
+
*/
|
|
8
|
+
import ivm from 'isolated-vm';
|
|
9
|
+
import type { AgentTool } from '../tools/types.js';
|
|
10
|
+
import type { SandboxToolCall } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* ToolBridge manages the injection of AgentTools into an isolated-vm
|
|
13
|
+
* Context and records every tool invocation made from sandbox code.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ToolBridge {
|
|
16
|
+
private readonly tools;
|
|
17
|
+
private toolCalls;
|
|
18
|
+
constructor(tools: AgentTool[]);
|
|
19
|
+
/**
|
|
20
|
+
* Inject all bridgeable tools as global async functions in the given context.
|
|
21
|
+
*
|
|
22
|
+
* For each tool with `bridgeable: true`, a global function is registered
|
|
23
|
+
* using the tool's `bridgeName` (falling back to `name`). The sandbox
|
|
24
|
+
* function signature is:
|
|
25
|
+
*
|
|
26
|
+
* async function toolName(args: object): Promise<string>
|
|
27
|
+
*
|
|
28
|
+
* The host-side callback executes the real tool, records the call, and
|
|
29
|
+
* returns the text result (or throws on error).
|
|
30
|
+
*/
|
|
31
|
+
injectAll(context: ivm.Context): Promise<void>;
|
|
32
|
+
/** Return all tool call records accumulated since the last clear. */
|
|
33
|
+
getToolCalls(): SandboxToolCall[];
|
|
34
|
+
/** Clear accumulated tool call records. Called before each execute(). */
|
|
35
|
+
clearToolCalls(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Validate that a function name is a safe JavaScript identifier.
|
|
38
|
+
* Prevents code injection via malicious tool names in evalClosure.
|
|
39
|
+
*/
|
|
40
|
+
private static validateIdentifier;
|
|
41
|
+
private injectTool;
|
|
42
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeAct Sandbox — Tool Bridge
|
|
3
|
+
*
|
|
4
|
+
* Bridges AgentTool instances into the isolated-vm sandbox as globally
|
|
5
|
+
* available async functions. Only tools marked `bridgeable: true` are
|
|
6
|
+
* injected. Each call is recorded for inclusion in ExecuteResult.toolCalls.
|
|
7
|
+
*/
|
|
8
|
+
import ivm from 'isolated-vm';
|
|
9
|
+
/**
|
|
10
|
+
* ToolBridge manages the injection of AgentTools into an isolated-vm
|
|
11
|
+
* Context and records every tool invocation made from sandbox code.
|
|
12
|
+
*/
|
|
13
|
+
export class ToolBridge {
|
|
14
|
+
tools;
|
|
15
|
+
toolCalls = [];
|
|
16
|
+
constructor(tools) {
|
|
17
|
+
this.tools = tools;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Inject all bridgeable tools as global async functions in the given context.
|
|
21
|
+
*
|
|
22
|
+
* For each tool with `bridgeable: true`, a global function is registered
|
|
23
|
+
* using the tool's `bridgeName` (falling back to `name`). The sandbox
|
|
24
|
+
* function signature is:
|
|
25
|
+
*
|
|
26
|
+
* async function toolName(args: object): Promise<string>
|
|
27
|
+
*
|
|
28
|
+
* The host-side callback executes the real tool, records the call, and
|
|
29
|
+
* returns the text result (or throws on error).
|
|
30
|
+
*/
|
|
31
|
+
async injectAll(context) {
|
|
32
|
+
const bridgeableTools = this.tools.filter((t) => t.bridgeable);
|
|
33
|
+
for (const tool of bridgeableTools) {
|
|
34
|
+
await this.injectTool(context, tool);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Return all tool call records accumulated since the last clear. */
|
|
38
|
+
getToolCalls() {
|
|
39
|
+
return [...this.toolCalls];
|
|
40
|
+
}
|
|
41
|
+
/** Clear accumulated tool call records. Called before each execute(). */
|
|
42
|
+
clearToolCalls() {
|
|
43
|
+
this.toolCalls = [];
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Private
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Validate that a function name is a safe JavaScript identifier.
|
|
50
|
+
* Prevents code injection via malicious tool names in evalClosure.
|
|
51
|
+
*/
|
|
52
|
+
static validateIdentifier(name) {
|
|
53
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
54
|
+
throw new Error(`Invalid bridge function name "${name}": must be a valid JS identifier (letters, digits, _, $)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async injectTool(context, tool) {
|
|
58
|
+
const fnName = tool.bridgeName || tool.name;
|
|
59
|
+
// SECURITY: Validate fnName is a safe identifier before interpolating into eval'd code.
|
|
60
|
+
ToolBridge.validateIdentifier(fnName);
|
|
61
|
+
// Host-side callback invoked when sandbox code calls the bridged function.
|
|
62
|
+
const callback = new ivm.Reference(async (argsJson) => {
|
|
63
|
+
const args = JSON.parse(argsJson);
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
try {
|
|
66
|
+
const result = await tool.execute(`sandbox-${Date.now()}`, args, { signal: undefined });
|
|
67
|
+
const text = result.content
|
|
68
|
+
.filter((c) => c.type === 'text')
|
|
69
|
+
.map((c) => c.text)
|
|
70
|
+
.join('\n');
|
|
71
|
+
this.toolCalls.push({
|
|
72
|
+
toolName: tool.name,
|
|
73
|
+
args,
|
|
74
|
+
result: text,
|
|
75
|
+
duration: Date.now() - startTime,
|
|
76
|
+
});
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
this.toolCalls.push({
|
|
82
|
+
toolName: tool.name,
|
|
83
|
+
args,
|
|
84
|
+
error: errMsg,
|
|
85
|
+
duration: Date.now() - startTime,
|
|
86
|
+
});
|
|
87
|
+
throw new Error(errMsg);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Register the async function in the sandbox's global scope.
|
|
91
|
+
// $0 is the ivm.Reference to the host callback.
|
|
92
|
+
// The sandbox function JSON-stringifies args before sending to host,
|
|
93
|
+
// and awaits the promise returned by the host callback.
|
|
94
|
+
await context.evalClosure(`globalThis.${fnName} = async function(args) {
|
|
95
|
+
return await $0.apply(undefined, [JSON.stringify(args)], { result: { promise: true } });
|
|
96
|
+
}`, [callback], { arguments: { reference: true } });
|
|
97
|
+
}
|
|
98
|
+
}
|