@mastra/isolated-vm 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150
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 +48 -0
- package/LICENSE.md +30 -0
- package/dist/index.cjs +325 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +301 -0
- package/dist/index.js.map +1 -0
- package/dist/transport.d.ts +57 -0
- package/dist/transport.d.ts.map +1 -0
- package/package.json +66 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @mastra/isolated-vm
|
|
2
|
+
|
|
3
|
+
## 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`c5e56ff`](https://github.com/mastra-ai/mastra/commit/c5e56ff3bcabdf062708f2d48744fec304df6792), [`7f4e26d`](https://github.com/mastra-ai/mastra/commit/7f4e26dd57bd9b23c278ea21235ab823a3810a6c), [`322daa6`](https://github.com/mastra-ai/mastra/commit/322daa6d90552909204044790d850958f6745fed), [`cadaa13`](https://github.com/mastra-ai/mastra/commit/cadaa1372e1077c8e85eb64c5499ba8803caa323), [`b582f7f`](https://github.com/mastra-ai/mastra/commit/b582f7fa2f9c1f87d19efc63d344fbe5dda2608c), [`06000d7`](https://github.com/mastra-ai/mastra/commit/06000d73712911572e913b8a83339270296d0a22), [`cf24430`](https://github.com/mastra-ai/mastra/commit/cf24430226a6ecb104bc1d54d6e401defd7cb53f), [`4e35a56`](https://github.com/mastra-ai/mastra/commit/4e35a56cdf8d74a5ff6d5eda01f2c1deaf6cc7be), [`3de0188`](https://github.com/mastra-ai/mastra/commit/3de0188bfaf9a9c09c95fe322b53838cf52c70b6), [`b582f7f`](https://github.com/mastra-ai/mastra/commit/b582f7fa2f9c1f87d19efc63d344fbe5dda2608c)]:
|
|
8
|
+
- @mastra/core@0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150
|
|
9
|
+
|
|
10
|
+
## 0.1.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- Added `@mastra/isolated-vm`, a new package with `IsolatedVmCodeModeTransport` — a Code Mode transport that runs model-authored programs in an in-process V8 isolate (backed by `isolated-vm`). The isolate is the execution boundary, so no workspace sandbox is required: the program has no filesystem, network, or process access, and its only capabilities are the `external_*` tool functions bridged back to the host. ([#20359](https://github.com/mastra-ai/mastra/pull/20359))
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { createCodeMode } from '@mastra/core/tools';
|
|
18
|
+
import { IsolatedVmCodeModeTransport } from '@mastra/isolated-vm';
|
|
19
|
+
|
|
20
|
+
const { tool, instructions } = createCodeMode({ tools }, new IsolatedVmCodeModeTransport({ memoryLimitMb: 128 }));
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Note: `isolated-vm` is a native addon, and on Node.js 20+ the host process must be started with `--no-node-snapshot` (for example `NODE_OPTIONS=--no-node-snapshot`). See the docs for setup details. Resolves https://github.com/mastra-ai/mastra/issues/20329.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [[`3f472b4`](https://github.com/mastra-ai/mastra/commit/3f472b468892a1ff14ccb43cc0343b86f7d8fd7d), [`ba369f2`](https://github.com/mastra-ai/mastra/commit/ba369f2a0aaf998da0d6aa033d26f64f96bef8ac), [`35b929b`](https://github.com/mastra-ai/mastra/commit/35b929b7abc3d20d85c7985880960ac2d04a6c86), [`55c9e24`](https://github.com/mastra-ai/mastra/commit/55c9e248c27c1d72b5bb7e94ea6b8a3999eee49f), [`dcfed93`](https://github.com/mastra-ai/mastra/commit/dcfed93e1e256c6abfa792cbb7ca836f5d0e8638), [`2876e15`](https://github.com/mastra-ai/mastra/commit/2876e15b4d2f616a3bc1ed3af57d546c268384ce), [`9b3626a`](https://github.com/mastra-ai/mastra/commit/9b3626aeb1d16fcd34b0a8e94c114ddb80a3b240), [`4696963`](https://github.com/mastra-ai/mastra/commit/469696312ac4c618bc8475b0c5ed7949b8a3455e), [`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73), [`07f5b4b`](https://github.com/mastra-ai/mastra/commit/07f5b4ba9d608d88865030732e580298296adf99), [`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73), [`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73), [`598080f`](https://github.com/mastra-ai/mastra/commit/598080f224edb3f0f5b801035b067fac50a56a03)]:
|
|
28
|
+
- @mastra/core@1.55.0
|
|
29
|
+
|
|
30
|
+
## 0.1.0-alpha.0
|
|
31
|
+
|
|
32
|
+
### Minor Changes
|
|
33
|
+
|
|
34
|
+
- Added `@mastra/isolated-vm`, a new package with `IsolatedVmCodeModeTransport` — a Code Mode transport that runs model-authored programs in an in-process V8 isolate (backed by `isolated-vm`). The isolate is the execution boundary, so no workspace sandbox is required: the program has no filesystem, network, or process access, and its only capabilities are the `external_*` tool functions bridged back to the host. ([#20359](https://github.com/mastra-ai/mastra/pull/20359))
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { createCodeMode } from '@mastra/core/tools';
|
|
38
|
+
import { IsolatedVmCodeModeTransport } from '@mastra/isolated-vm';
|
|
39
|
+
|
|
40
|
+
const { tool, instructions } = createCodeMode({ tools }, new IsolatedVmCodeModeTransport({ memoryLimitMb: 128 }));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Note: `isolated-vm` is a native addon, and on Node.js 20+ the host process must be started with `--no-node-snapshot` (for example `NODE_OPTIONS=--no-node-snapshot`). See the docs for setup details. Resolves https://github.com/mastra-ai/mastra/issues/20329.
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [[`ba369f2`](https://github.com/mastra-ai/mastra/commit/ba369f2a0aaf998da0d6aa033d26f64f96bef8ac), [`dcfed93`](https://github.com/mastra-ai/mastra/commit/dcfed93e1e256c6abfa792cbb7ca836f5d0e8638), [`2876e15`](https://github.com/mastra-ai/mastra/commit/2876e15b4d2f616a3bc1ed3af57d546c268384ce), [`598080f`](https://github.com/mastra-ai/mastra/commit/598080f224edb3f0f5b801035b067fac50a56a03)]:
|
|
48
|
+
- @mastra/core@1.55.0-alpha.1
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Portions of this software are licensed as follows:
|
|
2
|
+
|
|
3
|
+
- All content that resides under any directory named "ee/" within this
|
|
4
|
+
repository, including but not limited to:
|
|
5
|
+
- `packages/core/src/auth/ee/`
|
|
6
|
+
- `packages/server/src/server/auth/ee/`
|
|
7
|
+
is licensed under the license defined in `ee/LICENSE`.
|
|
8
|
+
|
|
9
|
+
- All third-party components incorporated into the Mastra Software are
|
|
10
|
+
licensed under the original license provided by the owner of the
|
|
11
|
+
applicable component.
|
|
12
|
+
|
|
13
|
+
- Content outside of the above-mentioned directories or restrictions is
|
|
14
|
+
available under the "Apache License 2.0" as defined below.
|
|
15
|
+
|
|
16
|
+
# Apache License 2.0
|
|
17
|
+
|
|
18
|
+
Copyright (c) 2025 Kepler Software, Inc.
|
|
19
|
+
|
|
20
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
21
|
+
you may not use this file except in compliance with the License.
|
|
22
|
+
You may obtain a copy of the License at
|
|
23
|
+
|
|
24
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
25
|
+
|
|
26
|
+
Unless required by applicable law or agreed to in writing, software
|
|
27
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
28
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
29
|
+
See the License for the specific language governing permissions and
|
|
30
|
+
limitations under the License.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let _mastra_core_tools = require("@mastra/core/tools");
|
|
25
|
+
let esbuild = require("esbuild");
|
|
26
|
+
let isolated_vm = require("isolated-vm");
|
|
27
|
+
isolated_vm = __toESM(isolated_vm, 1);
|
|
28
|
+
//#region src/transport.ts
|
|
29
|
+
/**
|
|
30
|
+
* Code Mode — isolated-vm transport
|
|
31
|
+
*
|
|
32
|
+
* Runs the model's TypeScript program inside an in-process V8 isolate
|
|
33
|
+
* (via `isolated-vm`) instead of a WorkspaceSandbox. The isolate is a real
|
|
34
|
+
* security boundary: the guest has no filesystem, network, process, or module
|
|
35
|
+
* access — the only capabilities are the injected `external_*` functions,
|
|
36
|
+
* which call back into the host dispatcher (allow-list enforced host-side).
|
|
37
|
+
*
|
|
38
|
+
* Unlike the stdio/E2B transports there is no process boundary, so the frame
|
|
39
|
+
* protocol (`FRAME_PREFIX`, stdout parsing, stdin writes) is not used. Host
|
|
40
|
+
* calls cross the isolate boundary as JSON strings in both directions, so no
|
|
41
|
+
* host object references are ever exposed to guest code.
|
|
42
|
+
*
|
|
43
|
+
* TypeScript is stripped on the host with esbuild before evaluation (isolates
|
|
44
|
+
* run plain JavaScript).
|
|
45
|
+
*
|
|
46
|
+
* Requirements:
|
|
47
|
+
* - `isolated-vm` is a native addon (prebuilt binaries for common platforms,
|
|
48
|
+
* compiled from source elsewhere).
|
|
49
|
+
* - On Node 20+ the host process must be started with `--no-node-snapshot`
|
|
50
|
+
* (e.g. `node --no-node-snapshot server.js` or
|
|
51
|
+
* `NODE_OPTIONS=--no-node-snapshot`). The constructor fails fast with an
|
|
52
|
+
* actionable error when the flag is missing.
|
|
53
|
+
*/
|
|
54
|
+
/** Default isolate heap limit, in MiB. */
|
|
55
|
+
const DEFAULT_MEMORY_LIMIT_MB = 128;
|
|
56
|
+
/**
|
|
57
|
+
* On Node 20+, isolated-vm cannot create isolates unless the host process was
|
|
58
|
+
* started with `--no-node-snapshot` — without it, isolate creation crashes the
|
|
59
|
+
* whole process. Detect the documented mechanisms and fail fast instead.
|
|
60
|
+
*/
|
|
61
|
+
function assertNoNodeSnapshot() {
|
|
62
|
+
if (Number(process.versions.node.split(".")[0]) < 20) return;
|
|
63
|
+
if (process.execArgv.includes("--no-node-snapshot")) return;
|
|
64
|
+
if ((process.env.NODE_OPTIONS ?? "").includes("--no-node-snapshot")) return;
|
|
65
|
+
throw new Error("IsolatedVmCodeModeTransport requires the --no-node-snapshot flag on Node 20+. Start your process with `node --no-node-snapshot ...` or set NODE_OPTIONS=\"--no-node-snapshot\". Without it, creating a V8 isolate crashes the Node process.");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Code Mode transport that executes programs in an in-process V8 isolate.
|
|
69
|
+
*
|
|
70
|
+
* No sandbox is required — the isolate is the execution boundary
|
|
71
|
+
* (`requiresSandbox: false`).
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* import { createCodeMode } from '@mastra/core/tools';
|
|
76
|
+
* import { IsolatedVmCodeModeTransport } from '@mastra/isolated-vm';
|
|
77
|
+
*
|
|
78
|
+
* const { tool, instructions } = createCodeMode(
|
|
79
|
+
* { tools: { getWeather, getForecast } },
|
|
80
|
+
* new IsolatedVmCodeModeTransport({ memoryLimitMb: 128 }),
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
var IsolatedVmCodeModeTransport = class {
|
|
85
|
+
requiresSandbox = false;
|
|
86
|
+
#memoryLimitMb;
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
assertNoNodeSnapshot();
|
|
89
|
+
this.#memoryLimitMb = options.memoryLimitMb ?? DEFAULT_MEMORY_LIMIT_MB;
|
|
90
|
+
}
|
|
91
|
+
async run(opts) {
|
|
92
|
+
const { program, toolIds, dispatch, timeout, abortSignal, onExternalCall, onExternalResult } = opts;
|
|
93
|
+
const externals = toolIds.map((toolId) => ({
|
|
94
|
+
toolId,
|
|
95
|
+
externalName: (0, _mastra_core_tools.sanitizeToolId)(toolId)
|
|
96
|
+
}));
|
|
97
|
+
const allowList = new Set(toolIds);
|
|
98
|
+
const logs = [];
|
|
99
|
+
const notifyCall = (tool, args) => {
|
|
100
|
+
try {
|
|
101
|
+
onExternalCall?.(tool, args);
|
|
102
|
+
} catch {}
|
|
103
|
+
};
|
|
104
|
+
const notifyResult = (tool, durationMs, error) => {
|
|
105
|
+
try {
|
|
106
|
+
onExternalResult?.(tool, durationMs, error);
|
|
107
|
+
} catch {}
|
|
108
|
+
};
|
|
109
|
+
let strippedProgram;
|
|
110
|
+
try {
|
|
111
|
+
strippedProgram = (0, esbuild.transformSync)(`(async () => {\n${program}\n})`, {
|
|
112
|
+
loader: "ts",
|
|
113
|
+
target: "es2022"
|
|
114
|
+
}).code;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
logs,
|
|
119
|
+
error: {
|
|
120
|
+
message: error?.message ?? String(error),
|
|
121
|
+
name: "SyntaxError"
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const rpcHost = async (tool, argsJson) => {
|
|
126
|
+
const started = Date.now();
|
|
127
|
+
let args;
|
|
128
|
+
try {
|
|
129
|
+
args = JSON.parse(argsJson);
|
|
130
|
+
} catch {
|
|
131
|
+
args = void 0;
|
|
132
|
+
}
|
|
133
|
+
notifyCall(tool, args);
|
|
134
|
+
if (!allowList.has(tool)) {
|
|
135
|
+
notifyResult(tool, Date.now() - started, /* @__PURE__ */ new Error("not allowed"));
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
ok: false,
|
|
138
|
+
error: {
|
|
139
|
+
message: `Tool "${tool}" is not available in Code Mode`,
|
|
140
|
+
name: "NotAllowedError"
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const result = await dispatch(tool, args);
|
|
146
|
+
const json = JSON.stringify({
|
|
147
|
+
ok: true,
|
|
148
|
+
result
|
|
149
|
+
});
|
|
150
|
+
notifyResult(tool, Date.now() - started);
|
|
151
|
+
return json;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const err = error;
|
|
154
|
+
notifyResult(tool, Date.now() - started, error instanceof Error ? error : new Error(String(error)));
|
|
155
|
+
return JSON.stringify({
|
|
156
|
+
ok: false,
|
|
157
|
+
error: {
|
|
158
|
+
message: err?.message ?? String(error),
|
|
159
|
+
name: err?.name
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const logHost = (message) => {
|
|
165
|
+
logs.push(message);
|
|
166
|
+
};
|
|
167
|
+
const isolate = new isolated_vm.default.Isolate({ memoryLimit: this.#memoryLimitMb });
|
|
168
|
+
let disposed = false;
|
|
169
|
+
const disposeIsolate = () => {
|
|
170
|
+
if (disposed) return;
|
|
171
|
+
disposed = true;
|
|
172
|
+
try {
|
|
173
|
+
isolate.dispose();
|
|
174
|
+
} catch {}
|
|
175
|
+
};
|
|
176
|
+
let timedOut = false;
|
|
177
|
+
let timer;
|
|
178
|
+
const onAbort = () => disposeIsolate();
|
|
179
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
180
|
+
try {
|
|
181
|
+
if (abortSignal?.aborted) return {
|
|
182
|
+
success: false,
|
|
183
|
+
logs,
|
|
184
|
+
error: {
|
|
185
|
+
message: "Code Mode execution aborted",
|
|
186
|
+
name: "AbortError"
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const context = await isolate.createContext();
|
|
190
|
+
await context.evalClosure(`
|
|
191
|
+
const __rpc = (tool, args) => {
|
|
192
|
+
let argsJson;
|
|
193
|
+
try { argsJson = JSON.stringify(args === undefined ? null : args); } catch (e) {
|
|
194
|
+
return Promise.reject(new Error('Arguments passed to an external_* function must be JSON-serializable'));
|
|
195
|
+
}
|
|
196
|
+
return $0
|
|
197
|
+
.apply(undefined, [String(tool), argsJson], { result: { promise: true, copy: true } })
|
|
198
|
+
.then((json) => {
|
|
199
|
+
const res = JSON.parse(json);
|
|
200
|
+
if (res.ok) return res.result;
|
|
201
|
+
const err = new Error((res.error && res.error.message) || 'external tool failed');
|
|
202
|
+
if (res.error && res.error.name) err.name = res.error.name;
|
|
203
|
+
throw err;
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
const __safeStringify = (value) => {
|
|
207
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
208
|
+
};
|
|
209
|
+
const __log = (level, args) => {
|
|
210
|
+
const message = args.map((a) => (typeof a === 'string' ? a : __safeStringify(a))).join(' ');
|
|
211
|
+
$1.applyIgnored(undefined, [message]);
|
|
212
|
+
};
|
|
213
|
+
globalThis.console = Object.freeze({
|
|
214
|
+
log: (...args) => __log('log', args),
|
|
215
|
+
info: (...args) => __log('info', args),
|
|
216
|
+
warn: (...args) => __log('warn', args),
|
|
217
|
+
error: (...args) => __log('error', args),
|
|
218
|
+
});
|
|
219
|
+
for (const { externalName, toolId } of ${JSON.stringify(externals)}) {
|
|
220
|
+
globalThis['external_' + externalName] = (input) => __rpc(toolId, input);
|
|
221
|
+
}
|
|
222
|
+
`, [rpcHost, logHost], { arguments: { reference: true } });
|
|
223
|
+
const evalSource = `
|
|
224
|
+
(async () => {
|
|
225
|
+
const __user = ${strippedProgram}
|
|
226
|
+
try {
|
|
227
|
+
const __result = await __user();
|
|
228
|
+
try {
|
|
229
|
+
const json = JSON.stringify({ ok: true, value: __result });
|
|
230
|
+
return json === undefined ? '{"ok":true}' : json;
|
|
231
|
+
} catch {
|
|
232
|
+
return JSON.stringify({
|
|
233
|
+
ok: false,
|
|
234
|
+
error: { message: 'Code Mode result is not JSON-serializable', name: 'TypeError' },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
return JSON.stringify({
|
|
239
|
+
ok: false,
|
|
240
|
+
error: { message: (e && e.message) || String(e), name: e && e.name },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
})()
|
|
244
|
+
`;
|
|
245
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
246
|
+
timer = setTimeout(() => {
|
|
247
|
+
timedOut = true;
|
|
248
|
+
resolve("timeout");
|
|
249
|
+
disposeIsolate();
|
|
250
|
+
}, timeout);
|
|
251
|
+
});
|
|
252
|
+
const evalPromise = context.eval(evalSource, {
|
|
253
|
+
promise: true,
|
|
254
|
+
copy: true,
|
|
255
|
+
timeout
|
|
256
|
+
});
|
|
257
|
+
const outcome = await Promise.race([evalPromise.then((json) => ({
|
|
258
|
+
kind: "done",
|
|
259
|
+
json,
|
|
260
|
+
error: void 0
|
|
261
|
+
}), (error) => ({
|
|
262
|
+
kind: "error",
|
|
263
|
+
json: void 0,
|
|
264
|
+
error
|
|
265
|
+
})), timeoutPromise.then(() => ({
|
|
266
|
+
kind: "timeout",
|
|
267
|
+
json: void 0,
|
|
268
|
+
error: void 0
|
|
269
|
+
}))]);
|
|
270
|
+
if (outcome.kind === "timeout" || timedOut) return {
|
|
271
|
+
success: false,
|
|
272
|
+
logs,
|
|
273
|
+
error: {
|
|
274
|
+
message: `Code Mode execution timed out after ${timeout}ms`,
|
|
275
|
+
name: "TimeoutError"
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
if (outcome.kind === "error") {
|
|
279
|
+
const err = outcome.error;
|
|
280
|
+
if (abortSignal?.aborted) return {
|
|
281
|
+
success: false,
|
|
282
|
+
logs,
|
|
283
|
+
error: {
|
|
284
|
+
message: "Code Mode execution aborted",
|
|
285
|
+
name: "AbortError"
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
if (/timed out/i.test(err?.message ?? "")) return {
|
|
289
|
+
success: false,
|
|
290
|
+
logs,
|
|
291
|
+
error: {
|
|
292
|
+
message: `Code Mode execution timed out after ${timeout}ms`,
|
|
293
|
+
name: "TimeoutError"
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
logs,
|
|
299
|
+
error: {
|
|
300
|
+
message: err?.message ?? String(outcome.error),
|
|
301
|
+
name: err?.name
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const envelope = JSON.parse(outcome.json);
|
|
306
|
+
return envelope.ok ? {
|
|
307
|
+
success: true,
|
|
308
|
+
result: envelope.value,
|
|
309
|
+
logs
|
|
310
|
+
} : {
|
|
311
|
+
success: false,
|
|
312
|
+
error: envelope.error,
|
|
313
|
+
logs
|
|
314
|
+
};
|
|
315
|
+
} finally {
|
|
316
|
+
if (timer) clearTimeout(timer);
|
|
317
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
318
|
+
disposeIsolate();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
//#endregion
|
|
323
|
+
exports.IsolatedVmCodeModeTransport = IsolatedVmCodeModeTransport;
|
|
324
|
+
|
|
325
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#memoryLimitMb","err","ivm"],"sources":["../src/transport.ts"],"sourcesContent":["/**\n * Code Mode — isolated-vm transport\n *\n * Runs the model's TypeScript program inside an in-process V8 isolate\n * (via `isolated-vm`) instead of a WorkspaceSandbox. The isolate is a real\n * security boundary: the guest has no filesystem, network, process, or module\n * access — the only capabilities are the injected `external_*` functions,\n * which call back into the host dispatcher (allow-list enforced host-side).\n *\n * Unlike the stdio/E2B transports there is no process boundary, so the frame\n * protocol (`FRAME_PREFIX`, stdout parsing, stdin writes) is not used. Host\n * calls cross the isolate boundary as JSON strings in both directions, so no\n * host object references are ever exposed to guest code.\n *\n * TypeScript is stripped on the host with esbuild before evaluation (isolates\n * run plain JavaScript).\n *\n * Requirements:\n * - `isolated-vm` is a native addon (prebuilt binaries for common platforms,\n * compiled from source elsewhere).\n * - On Node 20+ the host process must be started with `--no-node-snapshot`\n * (e.g. `node --no-node-snapshot server.js` or\n * `NODE_OPTIONS=--no-node-snapshot`). The constructor fails fast with an\n * actionable error when the flag is missing.\n */\n\nimport { sanitizeToolId } from '@mastra/core/tools';\nimport type { CodeModeToolResult, CodeModeTransport } from '@mastra/core/tools';\nimport { transformSync } from 'esbuild';\nimport ivm from 'isolated-vm';\n\n/** Default isolate heap limit, in MiB. */\nconst DEFAULT_MEMORY_LIMIT_MB = 128;\n\nexport interface IsolatedVmCodeModeTransportOptions {\n /**\n * V8 isolate heap limit in MiB. Exceeding it terminates the program.\n * Default: 128.\n */\n memoryLimitMb?: number;\n}\n\n/**\n * On Node 20+, isolated-vm cannot create isolates unless the host process was\n * started with `--no-node-snapshot` — without it, isolate creation crashes the\n * whole process. Detect the documented mechanisms and fail fast instead.\n */\nfunction assertNoNodeSnapshot(): void {\n const major = Number(process.versions.node.split('.')[0]);\n if (major < 20) return;\n if (process.execArgv.includes('--no-node-snapshot')) return;\n if ((process.env.NODE_OPTIONS ?? '').includes('--no-node-snapshot')) return;\n throw new Error(\n 'IsolatedVmCodeModeTransport requires the --no-node-snapshot flag on Node 20+. ' +\n 'Start your process with `node --no-node-snapshot ...` or set NODE_OPTIONS=\"--no-node-snapshot\". ' +\n 'Without it, creating a V8 isolate crashes the Node process.',\n );\n}\n\n/** Envelope returned by the guest program eval (as a JSON string). */\ninterface GuestResultEnvelope {\n ok: boolean;\n value?: unknown;\n error?: { message: string; name?: string };\n}\n\n/** Envelope returned by the host RPC handler (as a JSON string). */\ninterface HostRpcEnvelope {\n ok: boolean;\n result?: unknown;\n error?: { message: string; name?: string };\n}\n\n/**\n * Code Mode transport that executes programs in an in-process V8 isolate.\n *\n * No sandbox is required — the isolate is the execution boundary\n * (`requiresSandbox: false`).\n *\n * @example\n * ```typescript\n * import { createCodeMode } from '@mastra/core/tools';\n * import { IsolatedVmCodeModeTransport } from '@mastra/isolated-vm';\n *\n * const { tool, instructions } = createCodeMode(\n * { tools: { getWeather, getForecast } },\n * new IsolatedVmCodeModeTransport({ memoryLimitMb: 128 }),\n * );\n * ```\n */\nexport class IsolatedVmCodeModeTransport implements CodeModeTransport {\n readonly requiresSandbox = false;\n\n readonly #memoryLimitMb: number;\n\n constructor(options: IsolatedVmCodeModeTransportOptions = {}) {\n assertNoNodeSnapshot();\n this.#memoryLimitMb = options.memoryLimitMb ?? DEFAULT_MEMORY_LIMIT_MB;\n }\n\n async run(opts: Parameters<CodeModeTransport['run']>[0]): Promise<CodeModeToolResult> {\n const { program, toolIds, dispatch, timeout, abortSignal, onExternalCall, onExternalResult } = opts;\n\n const externals = toolIds.map(toolId => ({ toolId, externalName: sanitizeToolId(toolId) }));\n const allowList = new Set(toolIds);\n const logs: string[] = [];\n\n // Observer hooks are caller-supplied and best-effort: a throwing hook must\n // never prevent the RPC from responding, or the matching in-isolate promise\n // would hang until the timeout.\n const notifyCall = (tool: string, args: unknown): void => {\n try {\n onExternalCall?.(tool, args);\n } catch {\n /* observer errors are non-fatal */\n }\n };\n const notifyResult = (tool: string, durationMs: number, error?: Error): void => {\n try {\n onExternalResult?.(tool, durationMs, error);\n } catch {\n /* observer errors are non-fatal */\n }\n };\n\n // Strip TypeScript on the host; the isolate runs plain JavaScript. Wrapping\n // in an async arrow makes top-level `return`/`await`/`const` legal, same as\n // the core runner's program module.\n let strippedProgram: string;\n try {\n strippedProgram = transformSync(`(async () => {\\n${program}\\n})`, { loader: 'ts', target: 'es2022' }).code;\n } catch (error) {\n const err = error as { message?: string };\n return {\n success: false,\n logs,\n error: { message: err?.message ?? String(error), name: 'SyntaxError' },\n };\n }\n\n // Host-side RPC handler. Crosses the boundary as JSON strings in both\n // directions so no host references or non-JSON values ever leak into the\n // guest. Errors are returned in the envelope (never thrown) so the guest\n // can rebuild them with the original error name.\n const rpcHost = async (tool: string, argsJson: string): Promise<string> => {\n const started = Date.now();\n let args: unknown;\n try {\n args = JSON.parse(argsJson);\n } catch {\n args = undefined;\n }\n notifyCall(tool, args);\n // Allow-list enforcement: never invoke a tool that wasn't exposed.\n if (!allowList.has(tool)) {\n notifyResult(tool, Date.now() - started, new Error('not allowed'));\n return JSON.stringify({\n ok: false,\n error: { message: `Tool \"${tool}\" is not available in Code Mode`, name: 'NotAllowedError' },\n } satisfies HostRpcEnvelope);\n }\n try {\n const result = await dispatch(tool, args);\n // Serialize before reporting success so a non-serializable result\n // (circular reference, BigInt) falls through to the error path without\n // emitting a success event first.\n const json = JSON.stringify({ ok: true, result } satisfies HostRpcEnvelope);\n notifyResult(tool, Date.now() - started);\n return json;\n } catch (error) {\n const err = error as { message?: string; name?: string };\n notifyResult(tool, Date.now() - started, error instanceof Error ? error : new Error(String(error)));\n return JSON.stringify({\n ok: false,\n error: { message: err?.message ?? String(error), name: err?.name },\n } satisfies HostRpcEnvelope);\n }\n };\n\n const logHost = (message: string): void => {\n logs.push(message);\n };\n\n const isolate = new ivm.Isolate({ memoryLimit: this.#memoryLimitMb });\n let disposed = false;\n const disposeIsolate = (): void => {\n if (disposed) return;\n disposed = true;\n try {\n isolate.dispose();\n } catch {\n /* already disposed */\n }\n };\n\n let timedOut = false;\n let timer: NodeJS.Timeout | undefined;\n const onAbort = (): void => disposeIsolate();\n abortSignal?.addEventListener('abort', onAbort, { once: true });\n\n try {\n if (abortSignal?.aborted) {\n return { success: false, logs, error: { message: 'Code Mode execution aborted', name: 'AbortError' } };\n }\n\n const context = await isolate.createContext();\n\n // Bootstrap: console capture, the RPC bridge, and the `external_*`\n // globals. `$0`/`$1` are References to the host functions; they stay in\n // this closure's scope and are never attached to guest-reachable\n // objects — guest code only sees plain functions that close over them.\n await context.evalClosure(\n `\n const __rpc = (tool, args) => {\n let argsJson;\n try { argsJson = JSON.stringify(args === undefined ? null : args); } catch (e) {\n return Promise.reject(new Error('Arguments passed to an external_* function must be JSON-serializable'));\n }\n return $0\n .apply(undefined, [String(tool), argsJson], { result: { promise: true, copy: true } })\n .then((json) => {\n const res = JSON.parse(json);\n if (res.ok) return res.result;\n const err = new Error((res.error && res.error.message) || 'external tool failed');\n if (res.error && res.error.name) err.name = res.error.name;\n throw err;\n });\n };\n const __safeStringify = (value) => {\n try { return JSON.stringify(value); } catch { return String(value); }\n };\n const __log = (level, args) => {\n const message = args.map((a) => (typeof a === 'string' ? a : __safeStringify(a))).join(' ');\n $1.applyIgnored(undefined, [message]);\n };\n globalThis.console = Object.freeze({\n log: (...args) => __log('log', args),\n info: (...args) => __log('info', args),\n warn: (...args) => __log('warn', args),\n error: (...args) => __log('error', args),\n });\n for (const { externalName, toolId } of ${JSON.stringify(externals)}) {\n globalThis['external_' + externalName] = (input) => __rpc(toolId, input);\n }\n `,\n [rpcHost, logHost],\n { arguments: { reference: true } },\n );\n\n // The guest returns a JSON envelope string so results and errors cross\n // the boundary as data, mirroring the frame protocol's JSON semantics.\n const evalSource = `\n (async () => {\n const __user = ${strippedProgram}\n try {\n const __result = await __user();\n try {\n const json = JSON.stringify({ ok: true, value: __result });\n return json === undefined ? '{\"ok\":true}' : json;\n } catch {\n return JSON.stringify({\n ok: false,\n error: { message: 'Code Mode result is not JSON-serializable', name: 'TypeError' },\n });\n }\n } catch (e) {\n return JSON.stringify({\n ok: false,\n error: { message: (e && e.message) || String(e), name: e && e.name },\n });\n }\n })()\n `;\n\n // Two timeout layers: the eval `timeout` interrupts runaway synchronous\n // code; the host timer covers time spent awaiting (RPC round trips,\n // guest promises) by disposing the isolate, which rejects the eval.\n const timeoutPromise = new Promise<'timeout'>(resolve => {\n timer = setTimeout(() => {\n timedOut = true;\n resolve('timeout');\n disposeIsolate();\n }, timeout);\n });\n\n // isolated-vm types this as a nested promise; `await` flattens it and\n // `copy: true` guarantees the resolved value is the guest's JSON string.\n const evalPromise = context.eval(evalSource, {\n promise: true,\n copy: true,\n timeout,\n }) as unknown as Promise<string>;\n\n const outcome = await Promise.race([\n evalPromise.then(\n json => ({ kind: 'done' as const, json, error: undefined }),\n error => ({ kind: 'error' as const, json: undefined, error }),\n ),\n timeoutPromise.then(() => ({ kind: 'timeout' as const, json: undefined, error: undefined })),\n ]);\n\n if (outcome.kind === 'timeout' || timedOut) {\n return {\n success: false,\n logs,\n error: { message: `Code Mode execution timed out after ${timeout}ms`, name: 'TimeoutError' },\n };\n }\n\n if (outcome.kind === 'error') {\n const err = outcome.error as { message?: string; name?: string };\n if (abortSignal?.aborted) {\n return { success: false, logs, error: { message: 'Code Mode execution aborted', name: 'AbortError' } };\n }\n // The eval `timeout` option rejects with \"Script execution timed out\".\n if (/timed out/i.test(err?.message ?? '')) {\n return {\n success: false,\n logs,\n error: { message: `Code Mode execution timed out after ${timeout}ms`, name: 'TimeoutError' },\n };\n }\n return {\n success: false,\n logs,\n error: { message: err?.message ?? String(outcome.error), name: err?.name },\n };\n }\n\n const envelope = JSON.parse(outcome.json) as GuestResultEnvelope;\n return envelope.ok\n ? { success: true, result: envelope.value, logs }\n : { success: false, error: envelope.error, logs };\n } finally {\n if (timer) clearTimeout(timer);\n abortSignal?.removeEventListener('abort', onAbort);\n disposeIsolate();\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,KAAK,kCAAkC,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { sanitizeToolId } from "@mastra/core/tools";
|
|
2
|
+
import { transformSync } from "esbuild";
|
|
3
|
+
import ivm from "isolated-vm";
|
|
4
|
+
//#region src/transport.ts
|
|
5
|
+
/**
|
|
6
|
+
* Code Mode — isolated-vm transport
|
|
7
|
+
*
|
|
8
|
+
* Runs the model's TypeScript program inside an in-process V8 isolate
|
|
9
|
+
* (via `isolated-vm`) instead of a WorkspaceSandbox. The isolate is a real
|
|
10
|
+
* security boundary: the guest has no filesystem, network, process, or module
|
|
11
|
+
* access — the only capabilities are the injected `external_*` functions,
|
|
12
|
+
* which call back into the host dispatcher (allow-list enforced host-side).
|
|
13
|
+
*
|
|
14
|
+
* Unlike the stdio/E2B transports there is no process boundary, so the frame
|
|
15
|
+
* protocol (`FRAME_PREFIX`, stdout parsing, stdin writes) is not used. Host
|
|
16
|
+
* calls cross the isolate boundary as JSON strings in both directions, so no
|
|
17
|
+
* host object references are ever exposed to guest code.
|
|
18
|
+
*
|
|
19
|
+
* TypeScript is stripped on the host with esbuild before evaluation (isolates
|
|
20
|
+
* run plain JavaScript).
|
|
21
|
+
*
|
|
22
|
+
* Requirements:
|
|
23
|
+
* - `isolated-vm` is a native addon (prebuilt binaries for common platforms,
|
|
24
|
+
* compiled from source elsewhere).
|
|
25
|
+
* - On Node 20+ the host process must be started with `--no-node-snapshot`
|
|
26
|
+
* (e.g. `node --no-node-snapshot server.js` or
|
|
27
|
+
* `NODE_OPTIONS=--no-node-snapshot`). The constructor fails fast with an
|
|
28
|
+
* actionable error when the flag is missing.
|
|
29
|
+
*/
|
|
30
|
+
/** Default isolate heap limit, in MiB. */
|
|
31
|
+
const DEFAULT_MEMORY_LIMIT_MB = 128;
|
|
32
|
+
/**
|
|
33
|
+
* On Node 20+, isolated-vm cannot create isolates unless the host process was
|
|
34
|
+
* started with `--no-node-snapshot` — without it, isolate creation crashes the
|
|
35
|
+
* whole process. Detect the documented mechanisms and fail fast instead.
|
|
36
|
+
*/
|
|
37
|
+
function assertNoNodeSnapshot() {
|
|
38
|
+
if (Number(process.versions.node.split(".")[0]) < 20) return;
|
|
39
|
+
if (process.execArgv.includes("--no-node-snapshot")) return;
|
|
40
|
+
if ((process.env.NODE_OPTIONS ?? "").includes("--no-node-snapshot")) return;
|
|
41
|
+
throw new Error("IsolatedVmCodeModeTransport requires the --no-node-snapshot flag on Node 20+. Start your process with `node --no-node-snapshot ...` or set NODE_OPTIONS=\"--no-node-snapshot\". Without it, creating a V8 isolate crashes the Node process.");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Code Mode transport that executes programs in an in-process V8 isolate.
|
|
45
|
+
*
|
|
46
|
+
* No sandbox is required — the isolate is the execution boundary
|
|
47
|
+
* (`requiresSandbox: false`).
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* import { createCodeMode } from '@mastra/core/tools';
|
|
52
|
+
* import { IsolatedVmCodeModeTransport } from '@mastra/isolated-vm';
|
|
53
|
+
*
|
|
54
|
+
* const { tool, instructions } = createCodeMode(
|
|
55
|
+
* { tools: { getWeather, getForecast } },
|
|
56
|
+
* new IsolatedVmCodeModeTransport({ memoryLimitMb: 128 }),
|
|
57
|
+
* );
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
var IsolatedVmCodeModeTransport = class {
|
|
61
|
+
requiresSandbox = false;
|
|
62
|
+
#memoryLimitMb;
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
assertNoNodeSnapshot();
|
|
65
|
+
this.#memoryLimitMb = options.memoryLimitMb ?? DEFAULT_MEMORY_LIMIT_MB;
|
|
66
|
+
}
|
|
67
|
+
async run(opts) {
|
|
68
|
+
const { program, toolIds, dispatch, timeout, abortSignal, onExternalCall, onExternalResult } = opts;
|
|
69
|
+
const externals = toolIds.map((toolId) => ({
|
|
70
|
+
toolId,
|
|
71
|
+
externalName: sanitizeToolId(toolId)
|
|
72
|
+
}));
|
|
73
|
+
const allowList = new Set(toolIds);
|
|
74
|
+
const logs = [];
|
|
75
|
+
const notifyCall = (tool, args) => {
|
|
76
|
+
try {
|
|
77
|
+
onExternalCall?.(tool, args);
|
|
78
|
+
} catch {}
|
|
79
|
+
};
|
|
80
|
+
const notifyResult = (tool, durationMs, error) => {
|
|
81
|
+
try {
|
|
82
|
+
onExternalResult?.(tool, durationMs, error);
|
|
83
|
+
} catch {}
|
|
84
|
+
};
|
|
85
|
+
let strippedProgram;
|
|
86
|
+
try {
|
|
87
|
+
strippedProgram = transformSync(`(async () => {\n${program}\n})`, {
|
|
88
|
+
loader: "ts",
|
|
89
|
+
target: "es2022"
|
|
90
|
+
}).code;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
logs,
|
|
95
|
+
error: {
|
|
96
|
+
message: error?.message ?? String(error),
|
|
97
|
+
name: "SyntaxError"
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const rpcHost = async (tool, argsJson) => {
|
|
102
|
+
const started = Date.now();
|
|
103
|
+
let args;
|
|
104
|
+
try {
|
|
105
|
+
args = JSON.parse(argsJson);
|
|
106
|
+
} catch {
|
|
107
|
+
args = void 0;
|
|
108
|
+
}
|
|
109
|
+
notifyCall(tool, args);
|
|
110
|
+
if (!allowList.has(tool)) {
|
|
111
|
+
notifyResult(tool, Date.now() - started, /* @__PURE__ */ new Error("not allowed"));
|
|
112
|
+
return JSON.stringify({
|
|
113
|
+
ok: false,
|
|
114
|
+
error: {
|
|
115
|
+
message: `Tool "${tool}" is not available in Code Mode`,
|
|
116
|
+
name: "NotAllowedError"
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const result = await dispatch(tool, args);
|
|
122
|
+
const json = JSON.stringify({
|
|
123
|
+
ok: true,
|
|
124
|
+
result
|
|
125
|
+
});
|
|
126
|
+
notifyResult(tool, Date.now() - started);
|
|
127
|
+
return json;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const err = error;
|
|
130
|
+
notifyResult(tool, Date.now() - started, error instanceof Error ? error : new Error(String(error)));
|
|
131
|
+
return JSON.stringify({
|
|
132
|
+
ok: false,
|
|
133
|
+
error: {
|
|
134
|
+
message: err?.message ?? String(error),
|
|
135
|
+
name: err?.name
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const logHost = (message) => {
|
|
141
|
+
logs.push(message);
|
|
142
|
+
};
|
|
143
|
+
const isolate = new ivm.Isolate({ memoryLimit: this.#memoryLimitMb });
|
|
144
|
+
let disposed = false;
|
|
145
|
+
const disposeIsolate = () => {
|
|
146
|
+
if (disposed) return;
|
|
147
|
+
disposed = true;
|
|
148
|
+
try {
|
|
149
|
+
isolate.dispose();
|
|
150
|
+
} catch {}
|
|
151
|
+
};
|
|
152
|
+
let timedOut = false;
|
|
153
|
+
let timer;
|
|
154
|
+
const onAbort = () => disposeIsolate();
|
|
155
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
156
|
+
try {
|
|
157
|
+
if (abortSignal?.aborted) return {
|
|
158
|
+
success: false,
|
|
159
|
+
logs,
|
|
160
|
+
error: {
|
|
161
|
+
message: "Code Mode execution aborted",
|
|
162
|
+
name: "AbortError"
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const context = await isolate.createContext();
|
|
166
|
+
await context.evalClosure(`
|
|
167
|
+
const __rpc = (tool, args) => {
|
|
168
|
+
let argsJson;
|
|
169
|
+
try { argsJson = JSON.stringify(args === undefined ? null : args); } catch (e) {
|
|
170
|
+
return Promise.reject(new Error('Arguments passed to an external_* function must be JSON-serializable'));
|
|
171
|
+
}
|
|
172
|
+
return $0
|
|
173
|
+
.apply(undefined, [String(tool), argsJson], { result: { promise: true, copy: true } })
|
|
174
|
+
.then((json) => {
|
|
175
|
+
const res = JSON.parse(json);
|
|
176
|
+
if (res.ok) return res.result;
|
|
177
|
+
const err = new Error((res.error && res.error.message) || 'external tool failed');
|
|
178
|
+
if (res.error && res.error.name) err.name = res.error.name;
|
|
179
|
+
throw err;
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
const __safeStringify = (value) => {
|
|
183
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
184
|
+
};
|
|
185
|
+
const __log = (level, args) => {
|
|
186
|
+
const message = args.map((a) => (typeof a === 'string' ? a : __safeStringify(a))).join(' ');
|
|
187
|
+
$1.applyIgnored(undefined, [message]);
|
|
188
|
+
};
|
|
189
|
+
globalThis.console = Object.freeze({
|
|
190
|
+
log: (...args) => __log('log', args),
|
|
191
|
+
info: (...args) => __log('info', args),
|
|
192
|
+
warn: (...args) => __log('warn', args),
|
|
193
|
+
error: (...args) => __log('error', args),
|
|
194
|
+
});
|
|
195
|
+
for (const { externalName, toolId } of ${JSON.stringify(externals)}) {
|
|
196
|
+
globalThis['external_' + externalName] = (input) => __rpc(toolId, input);
|
|
197
|
+
}
|
|
198
|
+
`, [rpcHost, logHost], { arguments: { reference: true } });
|
|
199
|
+
const evalSource = `
|
|
200
|
+
(async () => {
|
|
201
|
+
const __user = ${strippedProgram}
|
|
202
|
+
try {
|
|
203
|
+
const __result = await __user();
|
|
204
|
+
try {
|
|
205
|
+
const json = JSON.stringify({ ok: true, value: __result });
|
|
206
|
+
return json === undefined ? '{"ok":true}' : json;
|
|
207
|
+
} catch {
|
|
208
|
+
return JSON.stringify({
|
|
209
|
+
ok: false,
|
|
210
|
+
error: { message: 'Code Mode result is not JSON-serializable', name: 'TypeError' },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
ok: false,
|
|
216
|
+
error: { message: (e && e.message) || String(e), name: e && e.name },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
})()
|
|
220
|
+
`;
|
|
221
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
222
|
+
timer = setTimeout(() => {
|
|
223
|
+
timedOut = true;
|
|
224
|
+
resolve("timeout");
|
|
225
|
+
disposeIsolate();
|
|
226
|
+
}, timeout);
|
|
227
|
+
});
|
|
228
|
+
const evalPromise = context.eval(evalSource, {
|
|
229
|
+
promise: true,
|
|
230
|
+
copy: true,
|
|
231
|
+
timeout
|
|
232
|
+
});
|
|
233
|
+
const outcome = await Promise.race([evalPromise.then((json) => ({
|
|
234
|
+
kind: "done",
|
|
235
|
+
json,
|
|
236
|
+
error: void 0
|
|
237
|
+
}), (error) => ({
|
|
238
|
+
kind: "error",
|
|
239
|
+
json: void 0,
|
|
240
|
+
error
|
|
241
|
+
})), timeoutPromise.then(() => ({
|
|
242
|
+
kind: "timeout",
|
|
243
|
+
json: void 0,
|
|
244
|
+
error: void 0
|
|
245
|
+
}))]);
|
|
246
|
+
if (outcome.kind === "timeout" || timedOut) return {
|
|
247
|
+
success: false,
|
|
248
|
+
logs,
|
|
249
|
+
error: {
|
|
250
|
+
message: `Code Mode execution timed out after ${timeout}ms`,
|
|
251
|
+
name: "TimeoutError"
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
if (outcome.kind === "error") {
|
|
255
|
+
const err = outcome.error;
|
|
256
|
+
if (abortSignal?.aborted) return {
|
|
257
|
+
success: false,
|
|
258
|
+
logs,
|
|
259
|
+
error: {
|
|
260
|
+
message: "Code Mode execution aborted",
|
|
261
|
+
name: "AbortError"
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
if (/timed out/i.test(err?.message ?? "")) return {
|
|
265
|
+
success: false,
|
|
266
|
+
logs,
|
|
267
|
+
error: {
|
|
268
|
+
message: `Code Mode execution timed out after ${timeout}ms`,
|
|
269
|
+
name: "TimeoutError"
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
logs,
|
|
275
|
+
error: {
|
|
276
|
+
message: err?.message ?? String(outcome.error),
|
|
277
|
+
name: err?.name
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const envelope = JSON.parse(outcome.json);
|
|
282
|
+
return envelope.ok ? {
|
|
283
|
+
success: true,
|
|
284
|
+
result: envelope.value,
|
|
285
|
+
logs
|
|
286
|
+
} : {
|
|
287
|
+
success: false,
|
|
288
|
+
error: envelope.error,
|
|
289
|
+
logs
|
|
290
|
+
};
|
|
291
|
+
} finally {
|
|
292
|
+
if (timer) clearTimeout(timer);
|
|
293
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
294
|
+
disposeIsolate();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
//#endregion
|
|
299
|
+
export { IsolatedVmCodeModeTransport };
|
|
300
|
+
|
|
301
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#memoryLimitMb","err"],"sources":["../src/transport.ts"],"sourcesContent":["/**\n * Code Mode — isolated-vm transport\n *\n * Runs the model's TypeScript program inside an in-process V8 isolate\n * (via `isolated-vm`) instead of a WorkspaceSandbox. The isolate is a real\n * security boundary: the guest has no filesystem, network, process, or module\n * access — the only capabilities are the injected `external_*` functions,\n * which call back into the host dispatcher (allow-list enforced host-side).\n *\n * Unlike the stdio/E2B transports there is no process boundary, so the frame\n * protocol (`FRAME_PREFIX`, stdout parsing, stdin writes) is not used. Host\n * calls cross the isolate boundary as JSON strings in both directions, so no\n * host object references are ever exposed to guest code.\n *\n * TypeScript is stripped on the host with esbuild before evaluation (isolates\n * run plain JavaScript).\n *\n * Requirements:\n * - `isolated-vm` is a native addon (prebuilt binaries for common platforms,\n * compiled from source elsewhere).\n * - On Node 20+ the host process must be started with `--no-node-snapshot`\n * (e.g. `node --no-node-snapshot server.js` or\n * `NODE_OPTIONS=--no-node-snapshot`). The constructor fails fast with an\n * actionable error when the flag is missing.\n */\n\nimport { sanitizeToolId } from '@mastra/core/tools';\nimport type { CodeModeToolResult, CodeModeTransport } from '@mastra/core/tools';\nimport { transformSync } from 'esbuild';\nimport ivm from 'isolated-vm';\n\n/** Default isolate heap limit, in MiB. */\nconst DEFAULT_MEMORY_LIMIT_MB = 128;\n\nexport interface IsolatedVmCodeModeTransportOptions {\n /**\n * V8 isolate heap limit in MiB. Exceeding it terminates the program.\n * Default: 128.\n */\n memoryLimitMb?: number;\n}\n\n/**\n * On Node 20+, isolated-vm cannot create isolates unless the host process was\n * started with `--no-node-snapshot` — without it, isolate creation crashes the\n * whole process. Detect the documented mechanisms and fail fast instead.\n */\nfunction assertNoNodeSnapshot(): void {\n const major = Number(process.versions.node.split('.')[0]);\n if (major < 20) return;\n if (process.execArgv.includes('--no-node-snapshot')) return;\n if ((process.env.NODE_OPTIONS ?? '').includes('--no-node-snapshot')) return;\n throw new Error(\n 'IsolatedVmCodeModeTransport requires the --no-node-snapshot flag on Node 20+. ' +\n 'Start your process with `node --no-node-snapshot ...` or set NODE_OPTIONS=\"--no-node-snapshot\". ' +\n 'Without it, creating a V8 isolate crashes the Node process.',\n );\n}\n\n/** Envelope returned by the guest program eval (as a JSON string). */\ninterface GuestResultEnvelope {\n ok: boolean;\n value?: unknown;\n error?: { message: string; name?: string };\n}\n\n/** Envelope returned by the host RPC handler (as a JSON string). */\ninterface HostRpcEnvelope {\n ok: boolean;\n result?: unknown;\n error?: { message: string; name?: string };\n}\n\n/**\n * Code Mode transport that executes programs in an in-process V8 isolate.\n *\n * No sandbox is required — the isolate is the execution boundary\n * (`requiresSandbox: false`).\n *\n * @example\n * ```typescript\n * import { createCodeMode } from '@mastra/core/tools';\n * import { IsolatedVmCodeModeTransport } from '@mastra/isolated-vm';\n *\n * const { tool, instructions } = createCodeMode(\n * { tools: { getWeather, getForecast } },\n * new IsolatedVmCodeModeTransport({ memoryLimitMb: 128 }),\n * );\n * ```\n */\nexport class IsolatedVmCodeModeTransport implements CodeModeTransport {\n readonly requiresSandbox = false;\n\n readonly #memoryLimitMb: number;\n\n constructor(options: IsolatedVmCodeModeTransportOptions = {}) {\n assertNoNodeSnapshot();\n this.#memoryLimitMb = options.memoryLimitMb ?? DEFAULT_MEMORY_LIMIT_MB;\n }\n\n async run(opts: Parameters<CodeModeTransport['run']>[0]): Promise<CodeModeToolResult> {\n const { program, toolIds, dispatch, timeout, abortSignal, onExternalCall, onExternalResult } = opts;\n\n const externals = toolIds.map(toolId => ({ toolId, externalName: sanitizeToolId(toolId) }));\n const allowList = new Set(toolIds);\n const logs: string[] = [];\n\n // Observer hooks are caller-supplied and best-effort: a throwing hook must\n // never prevent the RPC from responding, or the matching in-isolate promise\n // would hang until the timeout.\n const notifyCall = (tool: string, args: unknown): void => {\n try {\n onExternalCall?.(tool, args);\n } catch {\n /* observer errors are non-fatal */\n }\n };\n const notifyResult = (tool: string, durationMs: number, error?: Error): void => {\n try {\n onExternalResult?.(tool, durationMs, error);\n } catch {\n /* observer errors are non-fatal */\n }\n };\n\n // Strip TypeScript on the host; the isolate runs plain JavaScript. Wrapping\n // in an async arrow makes top-level `return`/`await`/`const` legal, same as\n // the core runner's program module.\n let strippedProgram: string;\n try {\n strippedProgram = transformSync(`(async () => {\\n${program}\\n})`, { loader: 'ts', target: 'es2022' }).code;\n } catch (error) {\n const err = error as { message?: string };\n return {\n success: false,\n logs,\n error: { message: err?.message ?? String(error), name: 'SyntaxError' },\n };\n }\n\n // Host-side RPC handler. Crosses the boundary as JSON strings in both\n // directions so no host references or non-JSON values ever leak into the\n // guest. Errors are returned in the envelope (never thrown) so the guest\n // can rebuild them with the original error name.\n const rpcHost = async (tool: string, argsJson: string): Promise<string> => {\n const started = Date.now();\n let args: unknown;\n try {\n args = JSON.parse(argsJson);\n } catch {\n args = undefined;\n }\n notifyCall(tool, args);\n // Allow-list enforcement: never invoke a tool that wasn't exposed.\n if (!allowList.has(tool)) {\n notifyResult(tool, Date.now() - started, new Error('not allowed'));\n return JSON.stringify({\n ok: false,\n error: { message: `Tool \"${tool}\" is not available in Code Mode`, name: 'NotAllowedError' },\n } satisfies HostRpcEnvelope);\n }\n try {\n const result = await dispatch(tool, args);\n // Serialize before reporting success so a non-serializable result\n // (circular reference, BigInt) falls through to the error path without\n // emitting a success event first.\n const json = JSON.stringify({ ok: true, result } satisfies HostRpcEnvelope);\n notifyResult(tool, Date.now() - started);\n return json;\n } catch (error) {\n const err = error as { message?: string; name?: string };\n notifyResult(tool, Date.now() - started, error instanceof Error ? error : new Error(String(error)));\n return JSON.stringify({\n ok: false,\n error: { message: err?.message ?? String(error), name: err?.name },\n } satisfies HostRpcEnvelope);\n }\n };\n\n const logHost = (message: string): void => {\n logs.push(message);\n };\n\n const isolate = new ivm.Isolate({ memoryLimit: this.#memoryLimitMb });\n let disposed = false;\n const disposeIsolate = (): void => {\n if (disposed) return;\n disposed = true;\n try {\n isolate.dispose();\n } catch {\n /* already disposed */\n }\n };\n\n let timedOut = false;\n let timer: NodeJS.Timeout | undefined;\n const onAbort = (): void => disposeIsolate();\n abortSignal?.addEventListener('abort', onAbort, { once: true });\n\n try {\n if (abortSignal?.aborted) {\n return { success: false, logs, error: { message: 'Code Mode execution aborted', name: 'AbortError' } };\n }\n\n const context = await isolate.createContext();\n\n // Bootstrap: console capture, the RPC bridge, and the `external_*`\n // globals. `$0`/`$1` are References to the host functions; they stay in\n // this closure's scope and are never attached to guest-reachable\n // objects — guest code only sees plain functions that close over them.\n await context.evalClosure(\n `\n const __rpc = (tool, args) => {\n let argsJson;\n try { argsJson = JSON.stringify(args === undefined ? null : args); } catch (e) {\n return Promise.reject(new Error('Arguments passed to an external_* function must be JSON-serializable'));\n }\n return $0\n .apply(undefined, [String(tool), argsJson], { result: { promise: true, copy: true } })\n .then((json) => {\n const res = JSON.parse(json);\n if (res.ok) return res.result;\n const err = new Error((res.error && res.error.message) || 'external tool failed');\n if (res.error && res.error.name) err.name = res.error.name;\n throw err;\n });\n };\n const __safeStringify = (value) => {\n try { return JSON.stringify(value); } catch { return String(value); }\n };\n const __log = (level, args) => {\n const message = args.map((a) => (typeof a === 'string' ? a : __safeStringify(a))).join(' ');\n $1.applyIgnored(undefined, [message]);\n };\n globalThis.console = Object.freeze({\n log: (...args) => __log('log', args),\n info: (...args) => __log('info', args),\n warn: (...args) => __log('warn', args),\n error: (...args) => __log('error', args),\n });\n for (const { externalName, toolId } of ${JSON.stringify(externals)}) {\n globalThis['external_' + externalName] = (input) => __rpc(toolId, input);\n }\n `,\n [rpcHost, logHost],\n { arguments: { reference: true } },\n );\n\n // The guest returns a JSON envelope string so results and errors cross\n // the boundary as data, mirroring the frame protocol's JSON semantics.\n const evalSource = `\n (async () => {\n const __user = ${strippedProgram}\n try {\n const __result = await __user();\n try {\n const json = JSON.stringify({ ok: true, value: __result });\n return json === undefined ? '{\"ok\":true}' : json;\n } catch {\n return JSON.stringify({\n ok: false,\n error: { message: 'Code Mode result is not JSON-serializable', name: 'TypeError' },\n });\n }\n } catch (e) {\n return JSON.stringify({\n ok: false,\n error: { message: (e && e.message) || String(e), name: e && e.name },\n });\n }\n })()\n `;\n\n // Two timeout layers: the eval `timeout` interrupts runaway synchronous\n // code; the host timer covers time spent awaiting (RPC round trips,\n // guest promises) by disposing the isolate, which rejects the eval.\n const timeoutPromise = new Promise<'timeout'>(resolve => {\n timer = setTimeout(() => {\n timedOut = true;\n resolve('timeout');\n disposeIsolate();\n }, timeout);\n });\n\n // isolated-vm types this as a nested promise; `await` flattens it and\n // `copy: true` guarantees the resolved value is the guest's JSON string.\n const evalPromise = context.eval(evalSource, {\n promise: true,\n copy: true,\n timeout,\n }) as unknown as Promise<string>;\n\n const outcome = await Promise.race([\n evalPromise.then(\n json => ({ kind: 'done' as const, json, error: undefined }),\n error => ({ kind: 'error' as const, json: undefined, error }),\n ),\n timeoutPromise.then(() => ({ kind: 'timeout' as const, json: undefined, error: undefined })),\n ]);\n\n if (outcome.kind === 'timeout' || timedOut) {\n return {\n success: false,\n logs,\n error: { message: `Code Mode execution timed out after ${timeout}ms`, name: 'TimeoutError' },\n };\n }\n\n if (outcome.kind === 'error') {\n const err = outcome.error as { message?: string; name?: string };\n if (abortSignal?.aborted) {\n return { success: false, logs, error: { message: 'Code Mode execution aborted', name: 'AbortError' } };\n }\n // The eval `timeout` option rejects with \"Script execution timed out\".\n if (/timed out/i.test(err?.message ?? '')) {\n return {\n success: false,\n logs,\n error: { message: `Code Mode execution timed out after ${timeout}ms`, name: 'TimeoutError' },\n };\n }\n return {\n success: false,\n logs,\n error: { message: err?.message ?? String(outcome.error), name: err?.name },\n };\n }\n\n const envelope = JSON.parse(outcome.json) as GuestResultEnvelope;\n return envelope.ok\n ? { success: true, result: envelope.value, logs }\n : { success: false, error: envelope.error, logs };\n } finally {\n if (timer) clearTimeout(timer);\n abortSignal?.removeEventListener('abort', onAbort);\n disposeIsolate();\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Mode — isolated-vm transport
|
|
3
|
+
*
|
|
4
|
+
* Runs the model's TypeScript program inside an in-process V8 isolate
|
|
5
|
+
* (via `isolated-vm`) instead of a WorkspaceSandbox. The isolate is a real
|
|
6
|
+
* security boundary: the guest has no filesystem, network, process, or module
|
|
7
|
+
* access — the only capabilities are the injected `external_*` functions,
|
|
8
|
+
* which call back into the host dispatcher (allow-list enforced host-side).
|
|
9
|
+
*
|
|
10
|
+
* Unlike the stdio/E2B transports there is no process boundary, so the frame
|
|
11
|
+
* protocol (`FRAME_PREFIX`, stdout parsing, stdin writes) is not used. Host
|
|
12
|
+
* calls cross the isolate boundary as JSON strings in both directions, so no
|
|
13
|
+
* host object references are ever exposed to guest code.
|
|
14
|
+
*
|
|
15
|
+
* TypeScript is stripped on the host with esbuild before evaluation (isolates
|
|
16
|
+
* run plain JavaScript).
|
|
17
|
+
*
|
|
18
|
+
* Requirements:
|
|
19
|
+
* - `isolated-vm` is a native addon (prebuilt binaries for common platforms,
|
|
20
|
+
* compiled from source elsewhere).
|
|
21
|
+
* - On Node 20+ the host process must be started with `--no-node-snapshot`
|
|
22
|
+
* (e.g. `node --no-node-snapshot server.js` or
|
|
23
|
+
* `NODE_OPTIONS=--no-node-snapshot`). The constructor fails fast with an
|
|
24
|
+
* actionable error when the flag is missing.
|
|
25
|
+
*/
|
|
26
|
+
import type { CodeModeToolResult, CodeModeTransport } from '@mastra/core/tools';
|
|
27
|
+
export interface IsolatedVmCodeModeTransportOptions {
|
|
28
|
+
/**
|
|
29
|
+
* V8 isolate heap limit in MiB. Exceeding it terminates the program.
|
|
30
|
+
* Default: 128.
|
|
31
|
+
*/
|
|
32
|
+
memoryLimitMb?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Code Mode transport that executes programs in an in-process V8 isolate.
|
|
36
|
+
*
|
|
37
|
+
* No sandbox is required — the isolate is the execution boundary
|
|
38
|
+
* (`requiresSandbox: false`).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { createCodeMode } from '@mastra/core/tools';
|
|
43
|
+
* import { IsolatedVmCodeModeTransport } from '@mastra/isolated-vm';
|
|
44
|
+
*
|
|
45
|
+
* const { tool, instructions } = createCodeMode(
|
|
46
|
+
* { tools: { getWeather, getForecast } },
|
|
47
|
+
* new IsolatedVmCodeModeTransport({ memoryLimitMb: 128 }),
|
|
48
|
+
* );
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare class IsolatedVmCodeModeTransport implements CodeModeTransport {
|
|
52
|
+
#private;
|
|
53
|
+
readonly requiresSandbox = false;
|
|
54
|
+
constructor(options?: IsolatedVmCodeModeTransportOptions);
|
|
55
|
+
run(opts: Parameters<CodeModeTransport['run']>[0]): Promise<CodeModeToolResult>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAOhF,MAAM,WAAW,kCAAkC;IACjD;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAiCD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,2BAA4B,YAAW,iBAAiB;;IACnE,QAAQ,CAAC,eAAe,SAAS;gBAIrB,OAAO,GAAE,kCAAuC;IAKtD,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA+OtF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mastra/isolated-vm",
|
|
3
|
+
"version": "0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150",
|
|
4
|
+
"description": "In-process V8 isolate Code Mode transport for Mastra, backed by isolated-vm",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"esbuild": "^0.28.0",
|
|
24
|
+
"isolated-vm": "^6.1.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "22.20.1",
|
|
28
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
29
|
+
"eslint": "^10.7.0",
|
|
30
|
+
"tsdown": "0.22.9",
|
|
31
|
+
"typescript": "^6.0.3",
|
|
32
|
+
"vitest": "4.1.10",
|
|
33
|
+
"@internal/lint": "0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150",
|
|
34
|
+
"@mastra/core": "0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150",
|
|
35
|
+
"@internal/types-builder": "0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@mastra/core": "0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"CHANGELOG.md"
|
|
43
|
+
],
|
|
44
|
+
"homepage": "https://mastra.ai",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/mastra-ai/mastra.git",
|
|
48
|
+
"directory": "packages/isolated-vm"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/mastra-ai/mastra/issues"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=22.13.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsdown --silent --config tsdown.config.ts",
|
|
58
|
+
"build:lib": "pnpm build",
|
|
59
|
+
"build:watch": "pnpm build --watch",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:watch": "vitest watch",
|
|
63
|
+
"lint": "oxlint . && eslint .",
|
|
64
|
+
"lint:fix": "oxlint --fix . && eslint --fix ."
|
|
65
|
+
}
|
|
66
|
+
}
|