pi-crew 0.5.5 → 0.5.7
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 +153 -0
- package/README.md +17 -1
- package/docs/architecture.md +2 -0
- package/docs/migration-v0.4-v0.5.md +19 -2
- package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
- package/package.json +7 -5
- package/src/benchmark/benchmark-runner.ts +45 -0
- package/src/benchmark/feedback-loop.ts +5 -0
- package/src/config/config.ts +38 -4
- package/src/config/defaults.ts +5 -0
- package/src/config/suggestions.ts +8 -0
- package/src/extension/async-notifier.ts +10 -1
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/notification-router.ts +18 -0
- package/src/extension/register.ts +13 -17
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/team-tool/anchor.ts +201 -0
- package/src/extension/team-tool/api.ts +2 -1
- package/src/extension/team-tool/auto-summarize.ts +154 -0
- package/src/extension/team-tool/run.ts +37 -2
- package/src/extension/team-tool.ts +44 -2
- package/src/hooks/registry.ts +1 -3
- package/src/observability/event-bus.ts +13 -4
- package/src/observability/event-to-metric.ts +0 -2
- package/src/runtime/anchor-manager.ts +473 -0
- package/src/runtime/async-runner.ts +8 -4
- package/src/runtime/auto-summarize.ts +350 -0
- package/src/runtime/background-runner.ts +2 -1
- package/src/runtime/budget-tracker.ts +354 -0
- package/src/runtime/chain-runner.ts +507 -0
- package/src/runtime/child-pi.ts +24 -6
- package/src/runtime/crash-recovery.ts +5 -4
- package/src/runtime/crew-agent-records.ts +32 -1
- package/src/runtime/custom-tools/irc-tool.ts +13 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
- package/src/runtime/delivery-coordinator.ts +10 -3
- package/src/runtime/dynamic-script-runner.ts +482 -0
- package/src/runtime/handoff-manager.ts +589 -0
- package/src/runtime/hidden-handoff.ts +424 -0
- package/src/runtime/live-agent-manager.ts +20 -4
- package/src/runtime/live-session-runtime.ts +39 -4
- package/src/runtime/manifest-cache.ts +2 -1
- package/src/runtime/model-resolver.ts +16 -4
- package/src/runtime/phase-tracker.ts +373 -0
- package/src/runtime/pipeline-runner.ts +514 -0
- package/src/runtime/retry-runner.ts +354 -0
- package/src/runtime/sandbox.ts +252 -0
- package/src/runtime/scheduler.ts +7 -2
- package/src/runtime/subagent-manager.ts +1 -1
- package/src/runtime/task-graph.ts +11 -1
- package/src/runtime/task-runner.ts +15 -1
- package/src/runtime/team-runner.ts +4 -3
- package/src/schema/team-tool-schema.ts +31 -0
- package/src/skills/discover-skills.ts +5 -0
- package/src/state/active-run-registry.ts +19 -3
- package/src/state/contracts.ts +9 -0
- package/src/state/crew-init.ts +3 -3
- package/src/state/decision-ledger.ts +26 -32
- package/src/state/event-log-rotation.ts +2 -2
- package/src/state/event-log.ts +17 -4
- package/src/state/mailbox.ts +35 -1
- package/src/state/run-cache.ts +18 -8
- package/src/tools/safe-bash-extension.ts +1 -0
- package/src/tools/safe-bash.ts +153 -20
- package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
- package/src/ui/powerbar-publisher.ts +1 -0
- package/src/ui/transcript-cache.ts +13 -0
- package/src/utils/bm25-search.ts +16 -8
- package/src/utils/env-filter.ts +8 -5
- package/src/utils/redaction.ts +169 -15
- package/src/utils/sse-parser.ts +10 -1
- package/src/worktree/cleanup.ts +6 -1
- package/workflows/chain.workflow.md +252 -0
- package/workflows/pipeline.workflow.md +27 -0
|
@@ -28,11 +28,10 @@ export class DeliveryCoordinator {
|
|
|
28
28
|
private flushing = false;
|
|
29
29
|
private readonly deps: DeliveryCoordinatorDeps;
|
|
30
30
|
private ttlTimer: ReturnType<typeof setInterval> | undefined;
|
|
31
|
+
private timerStarted = false;
|
|
31
32
|
|
|
32
33
|
constructor(deps: DeliveryCoordinatorDeps) {
|
|
33
34
|
this.deps = deps;
|
|
34
|
-
this.ttlTimer = setInterval(() => this.evictExpired(), 60_000);
|
|
35
|
-
this.ttlTimer.unref();
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
activate(sessionId: string): void {
|
|
@@ -102,9 +101,11 @@ export class DeliveryCoordinator {
|
|
|
102
101
|
|
|
103
102
|
flushQueuedResults(): void {
|
|
104
103
|
if (!this.active || this.pending.length === 0) return;
|
|
105
|
-
//
|
|
104
|
+
// HIGH-16/ MEDIUM-16: Set flushing BEFORE splice to prevent re-entrancy
|
|
106
105
|
if (this.flushing) return;
|
|
107
106
|
this.flushing = true;
|
|
107
|
+
// Note: this.flushing is now set, so concurrent calls will exit early due to the check above
|
|
108
|
+
// This serves as a simple lock to prevent race conditions
|
|
108
109
|
const batch = this.pending.splice(0);
|
|
109
110
|
try {
|
|
110
111
|
const retryLater: PendingDelivery[] = [];
|
|
@@ -162,6 +163,12 @@ export class DeliveryCoordinator {
|
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
private enqueue(delivery: PendingDelivery): void {
|
|
166
|
+
// Lazily start the TTL timer on first enqueue (only if never started)
|
|
167
|
+
if (!this.timerStarted) {
|
|
168
|
+
this.timerStarted = true;
|
|
169
|
+
this.ttlTimer = setInterval(() => this.evictExpired(), 60_000);
|
|
170
|
+
this.ttlTimer.unref();
|
|
171
|
+
}
|
|
165
172
|
this.pending.push({ ...delivery, generation: this.generation });
|
|
166
173
|
}
|
|
167
174
|
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import * as vm from "node:vm";
|
|
2
|
+
import * as acorn from "acorn";
|
|
3
|
+
import { WorkflowSandbox, type SandboxOptions } from "./sandbox.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Forbidden globals that could compromise sandbox security or cause side effects.
|
|
7
|
+
* These are checked during AST validation before execution.
|
|
8
|
+
*/
|
|
9
|
+
export const FORBIDDEN_GLOBALS = [
|
|
10
|
+
"Date",
|
|
11
|
+
"Math.random",
|
|
12
|
+
"require",
|
|
13
|
+
"import",
|
|
14
|
+
"module",
|
|
15
|
+
"exports",
|
|
16
|
+
"__dirname",
|
|
17
|
+
"__filename",
|
|
18
|
+
"process.exit",
|
|
19
|
+
"process.kill",
|
|
20
|
+
"process.hrtime",
|
|
21
|
+
"process.memoryUsage",
|
|
22
|
+
"process.cpuUsage",
|
|
23
|
+
"process.binding",
|
|
24
|
+
"process.dlopen",
|
|
25
|
+
"process._tickCallback",
|
|
26
|
+
"eval",
|
|
27
|
+
"Function",
|
|
28
|
+
"AsyncFunction",
|
|
29
|
+
"GeneratorFunction",
|
|
30
|
+
"Proxy",
|
|
31
|
+
"Reflect",
|
|
32
|
+
"WebAssembly",
|
|
33
|
+
"global",
|
|
34
|
+
"globalThis",
|
|
35
|
+
"window",
|
|
36
|
+
"document",
|
|
37
|
+
"XMLHttpRequest",
|
|
38
|
+
"fetch",
|
|
39
|
+
"WebSocket",
|
|
40
|
+
"Worker",
|
|
41
|
+
"SharedArrayBuffer",
|
|
42
|
+
"Atomics",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
// Freeze the array at runtime to ensure it's truly immutable
|
|
46
|
+
Object.freeze(FORBIDDEN_GLOBALS);
|
|
47
|
+
|
|
48
|
+
export type ForbiddenGlobal = (typeof FORBIDDEN_GLOBALS)[number];
|
|
49
|
+
|
|
50
|
+
export interface ScriptValidationResult {
|
|
51
|
+
valid: boolean;
|
|
52
|
+
errors: ScriptValidationError[];
|
|
53
|
+
warnings: ScriptValidationWarning[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ScriptValidationError {
|
|
57
|
+
type: "forbidden_global" | "forbidden_syntax" | "parse_error";
|
|
58
|
+
message: string;
|
|
59
|
+
location?: { line: number; column: number };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ScriptValidationWarning {
|
|
63
|
+
type: "deprecated_api" | "potentially_unsafe";
|
|
64
|
+
message: string;
|
|
65
|
+
location?: { line: number; column: number };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DynamicScriptOptions {
|
|
69
|
+
timeout?: number;
|
|
70
|
+
maxTokens?: number;
|
|
71
|
+
allowAwait?: boolean;
|
|
72
|
+
allowAsync?: boolean;
|
|
73
|
+
strictMode?: boolean;
|
|
74
|
+
/** Enable strict AST whitelist mode (C2) - reject dynamic property access, call expressions */
|
|
75
|
+
strictAstWhitelist?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ScriptExecutionResult {
|
|
79
|
+
success: boolean;
|
|
80
|
+
value?: unknown;
|
|
81
|
+
error?: string;
|
|
82
|
+
executionTime: number;
|
|
83
|
+
validation: ScriptValidationResult;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* DynamicScriptRunner executes JavaScript in a VM sandbox with AST validation
|
|
88
|
+
* and forbidden pattern detection.
|
|
89
|
+
*
|
|
90
|
+
* Note: AST parsing is simplified without acorn. For full AST validation,
|
|
91
|
+
* add acorn as a dependency.
|
|
92
|
+
*/
|
|
93
|
+
export class DynamicScriptRunner {
|
|
94
|
+
private sandbox: WorkflowSandbox;
|
|
95
|
+
private defaultTimeout: number;
|
|
96
|
+
private options: DynamicScriptOptions;
|
|
97
|
+
|
|
98
|
+
constructor(options: DynamicScriptOptions = {}) {
|
|
99
|
+
this.defaultTimeout = options.timeout ?? 30000;
|
|
100
|
+
this.options = options;
|
|
101
|
+
this.sandbox = new WorkflowSandbox({
|
|
102
|
+
timeout: this.defaultTimeout,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate script before execution using full AST parsing (C1).
|
|
108
|
+
* Uses acorn for comprehensive syntax tree analysis.
|
|
109
|
+
*/
|
|
110
|
+
validate(code: string): ScriptValidationResult {
|
|
111
|
+
const errors: ScriptValidationError[] = [];
|
|
112
|
+
const warnings: ScriptValidationWarning[] = [];
|
|
113
|
+
|
|
114
|
+
// C1: Full AST parsing with acorn for complete validation
|
|
115
|
+
let ast: acorn.Node | null = null;
|
|
116
|
+
try {
|
|
117
|
+
ast = acorn.parse(code, {
|
|
118
|
+
ecmaVersion: "latest",
|
|
119
|
+
sourceType: "script",
|
|
120
|
+
allowReturnOutsideFunction: true,
|
|
121
|
+
allowAwaitOutsideFunction: this.options.allowAwait ?? false,
|
|
122
|
+
});
|
|
123
|
+
} catch (parseError) {
|
|
124
|
+
errors.push({
|
|
125
|
+
type: "parse_error",
|
|
126
|
+
message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
127
|
+
});
|
|
128
|
+
return { valid: false, errors, warnings };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// C2: Strict AST whitelist mode - reject dynamic property access and call expressions
|
|
132
|
+
if (this.options.strictAstWhitelist) {
|
|
133
|
+
this.validateAstWhitelist(ast, errors);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// C4: Bytecode compilation verification - verify script compiles correctly
|
|
137
|
+
this.verifyCompilation(code, errors);
|
|
138
|
+
|
|
139
|
+
// Check for forbidden globals using regex patterns (fallback)
|
|
140
|
+
this.checkForForbiddenGlobals(code, errors);
|
|
141
|
+
|
|
142
|
+
// Check for forbidden syntax patterns
|
|
143
|
+
this.checkForForbiddenSyntax(code, errors, warnings);
|
|
144
|
+
|
|
145
|
+
// Check for potentially unsafe patterns
|
|
146
|
+
this.checkForPotentiallyUnsafePatterns(code, warnings);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
valid: errors.length === 0,
|
|
150
|
+
errors,
|
|
151
|
+
warnings,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* C2: Validate AST against strict whitelist - reject dynamic property access,
|
|
157
|
+
* call expressions, and other potentially dangerous patterns.
|
|
158
|
+
*/
|
|
159
|
+
private validateAstWhitelist(
|
|
160
|
+
ast: acorn.Node,
|
|
161
|
+
errors: ScriptValidationError[],
|
|
162
|
+
): void {
|
|
163
|
+
const walkNode = (node: acorn.Node): void => {
|
|
164
|
+
const n = node as acorn.Node & { type: string };
|
|
165
|
+
|
|
166
|
+
// Reject MemberExpression (e.g., obj.prop, obj[key])
|
|
167
|
+
if (n.type === "MemberExpression") {
|
|
168
|
+
errors.push({
|
|
169
|
+
type: "forbidden_syntax",
|
|
170
|
+
message: "Dynamic property access is not allowed in strict whitelist mode",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Reject CallExpression (e.g., fn(), obj.method())
|
|
175
|
+
if (n.type === "CallExpression") {
|
|
176
|
+
errors.push({
|
|
177
|
+
type: "forbidden_syntax",
|
|
178
|
+
message: "Call expressions are not allowed in strict whitelist mode",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Reject NewExpression (e.g., new Foo())
|
|
183
|
+
if (n.type === "NewExpression") {
|
|
184
|
+
errors.push({
|
|
185
|
+
type: "forbidden_syntax",
|
|
186
|
+
message: "Constructor calls are not allowed in strict whitelist mode",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Reject UpdateExpression (++a, a--, etc.)
|
|
191
|
+
if (n.type === "UpdateExpression") {
|
|
192
|
+
errors.push({
|
|
193
|
+
type: "forbidden_syntax",
|
|
194
|
+
message: "Update expressions are not allowed in strict whitelist mode",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Reject AssignmentExpression (a = b, a += b, etc.)
|
|
199
|
+
if (n.type === "AssignmentExpression") {
|
|
200
|
+
errors.push({
|
|
201
|
+
type: "forbidden_syntax",
|
|
202
|
+
message: "Assignment expressions are not allowed in strict whitelist mode",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Reject ForInStatement and ForOfStatement
|
|
207
|
+
if (n.type === "ForInStatement" || n.type === "ForOfStatement") {
|
|
208
|
+
errors.push({
|
|
209
|
+
type: "forbidden_syntax",
|
|
210
|
+
message: "For-in/for-of loops are not allowed in strict whitelist mode",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Recurse into children
|
|
215
|
+
for (const key of Object.keys(n as object)) {
|
|
216
|
+
const value = (n as unknown as Record<string, unknown>)[key];
|
|
217
|
+
if (value && typeof value === "object") {
|
|
218
|
+
if (Array.isArray(value)) {
|
|
219
|
+
for (const child of value) {
|
|
220
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
221
|
+
walkNode(child as acorn.Node);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} else if ("type" in value) {
|
|
225
|
+
walkNode(value as acorn.Node);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Cast to access body property (Program node)
|
|
232
|
+
const programNode = ast as unknown as { body: acorn.Node[] };
|
|
233
|
+
for (const node of programNode.body) {
|
|
234
|
+
walkNode(node);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* C4: Verify script compiles to bytecode correctly. If compilation fails,
|
|
240
|
+
* the script cannot be executed safely.
|
|
241
|
+
*/
|
|
242
|
+
private verifyCompilation(code: string, errors: ScriptValidationError[]): void {
|
|
243
|
+
try {
|
|
244
|
+
const wrappedCode = `(function(){ ${code} })()`;
|
|
245
|
+
new vm.Script(wrappedCode, {
|
|
246
|
+
filename: "compile-check.js",
|
|
247
|
+
});
|
|
248
|
+
} catch (compileError) {
|
|
249
|
+
errors.push({
|
|
250
|
+
type: "parse_error",
|
|
251
|
+
message: `Compilation verification failed: ${compileError instanceof Error ? compileError.message : String(compileError)}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private checkForForbiddenGlobals(code: string, errors: ScriptValidationError[]): void {
|
|
257
|
+
// Check each forbidden global pattern
|
|
258
|
+
for (const forbidden of FORBIDDEN_GLOBALS) {
|
|
259
|
+
if (forbidden.includes(".")) {
|
|
260
|
+
// Check for member expressions like Math.random, process.exit
|
|
261
|
+
const [obj, prop] = forbidden.split(".");
|
|
262
|
+
const pattern = new RegExp(`\\b${obj}\\s*\\.\\s*${prop}\\b`);
|
|
263
|
+
if (pattern.test(code)) {
|
|
264
|
+
errors.push({
|
|
265
|
+
type: "forbidden_global",
|
|
266
|
+
message: `Forbidden global access: '${forbidden}'`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
// Check for simple identifiers
|
|
271
|
+
// But avoid false positives like "myDate" matching "Date"
|
|
272
|
+
const pattern = new RegExp(`\\b${forbidden}\\b`);
|
|
273
|
+
if (pattern.test(code)) {
|
|
274
|
+
errors.push({
|
|
275
|
+
type: "forbidden_global",
|
|
276
|
+
message: `Forbidden global: '${forbidden}'`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private checkForForbiddenSyntax(
|
|
284
|
+
code: string,
|
|
285
|
+
errors: ScriptValidationError[],
|
|
286
|
+
warnings: ScriptValidationWarning[],
|
|
287
|
+
): void {
|
|
288
|
+
// Check for eval()
|
|
289
|
+
if (/\beval\s*\(/.test(code)) {
|
|
290
|
+
errors.push({
|
|
291
|
+
type: "forbidden_syntax",
|
|
292
|
+
message: "eval() is not allowed",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check for Function constructor
|
|
297
|
+
if (/\bnew\s+Function\s*\(/.test(code) || /\bFunction\s*\(\s*['"`]/.test(code)) {
|
|
298
|
+
errors.push({
|
|
299
|
+
type: "forbidden_syntax",
|
|
300
|
+
message: "Function constructor is not allowed",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check for AsyncFunction constructor
|
|
305
|
+
if (/\bnew\s+AsyncFunction\s*\(/.test(code) || /\bAsyncFunction\s*\(\s*['"`]/.test(code)) {
|
|
306
|
+
errors.push({
|
|
307
|
+
type: "forbidden_syntax",
|
|
308
|
+
message: "AsyncFunction constructor is not allowed",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check for GeneratorFunction constructor
|
|
313
|
+
if (/\bnew\s+GeneratorFunction\s*\(/.test(code)) {
|
|
314
|
+
errors.push({
|
|
315
|
+
type: "forbidden_syntax",
|
|
316
|
+
message: "GeneratorFunction constructor is not allowed",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check for Promise constructor - warn but don't block
|
|
321
|
+
if (/\bnew\s+Promise\s*\(/.test(code)) {
|
|
322
|
+
warnings.push({
|
|
323
|
+
type: "potentially_unsafe",
|
|
324
|
+
message: "Direct Promise constructor usage - consider using async/await instead",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private checkForPotentiallyUnsafePatterns(code: string, warnings: ScriptValidationWarning[]): void {
|
|
330
|
+
// Check for try-catch with broad catch - warn
|
|
331
|
+
if (/\bcatch\s*\(\s*\)\s*\{/.test(code)) {
|
|
332
|
+
warnings.push({
|
|
333
|
+
type: "potentially_unsafe",
|
|
334
|
+
message: "Broad catch clause - consider catching specific error types",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check for nested function declarations - warn about potential complexity
|
|
339
|
+
if (/function\s+\w+\s*\([^)]*\)\s*\{[^}]*function\s+/.test(code)) {
|
|
340
|
+
warnings.push({
|
|
341
|
+
type: "potentially_unsafe",
|
|
342
|
+
message: "Nested function declaration - consider extracting to module level",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check for with statement - deprecated and potentially unsafe
|
|
347
|
+
if (/\bwith\s*\(/.test(code)) {
|
|
348
|
+
warnings.push({
|
|
349
|
+
type: "potentially_unsafe",
|
|
350
|
+
message: "with statement is deprecated and potentially unsafe",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Execute a script with validation.
|
|
357
|
+
* @param code - The JavaScript code to execute
|
|
358
|
+
* @param options - Execution options
|
|
359
|
+
* @returns The execution result
|
|
360
|
+
*/
|
|
361
|
+
execute(code: string, options?: DynamicScriptOptions): ScriptExecutionResult {
|
|
362
|
+
const startTime = Date.now();
|
|
363
|
+
const timeout = options?.timeout ?? this.defaultTimeout;
|
|
364
|
+
|
|
365
|
+
// Validate first
|
|
366
|
+
const validation = this.validate(code);
|
|
367
|
+
if (!validation.valid) {
|
|
368
|
+
return {
|
|
369
|
+
success: false,
|
|
370
|
+
error: validation.errors.map((e) => e.message).join("; "),
|
|
371
|
+
executionTime: Date.now() - startTime,
|
|
372
|
+
validation,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const value = this.sandbox.execute(code, timeout);
|
|
378
|
+
return {
|
|
379
|
+
success: true,
|
|
380
|
+
value,
|
|
381
|
+
executionTime: Date.now() - startTime,
|
|
382
|
+
validation,
|
|
383
|
+
};
|
|
384
|
+
} catch (error) {
|
|
385
|
+
return {
|
|
386
|
+
success: false,
|
|
387
|
+
error: error instanceof Error ? error.message : String(error),
|
|
388
|
+
executionTime: Date.now() - startTime,
|
|
389
|
+
validation,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Execute an async script with validation.
|
|
396
|
+
* @param code - The JavaScript code to execute (must be async or return Promise)
|
|
397
|
+
* @param options - Execution options
|
|
398
|
+
* @returns Promise resolving to the execution result
|
|
399
|
+
*/
|
|
400
|
+
async executeAsync(code: string, options?: DynamicScriptOptions): Promise<ScriptExecutionResult> {
|
|
401
|
+
const startTime = Date.now();
|
|
402
|
+
const timeout = options?.timeout ?? this.defaultTimeout;
|
|
403
|
+
|
|
404
|
+
// Wrap in async IIFE for async/await support
|
|
405
|
+
const asyncCode = `(async () => { ${code} })()`;
|
|
406
|
+
|
|
407
|
+
// Validate the wrapped code (not the original code)
|
|
408
|
+
const validation = this.validate(asyncCode);
|
|
409
|
+
if (!validation.valid) {
|
|
410
|
+
return {
|
|
411
|
+
success: false,
|
|
412
|
+
error: validation.errors.map((e) => e.message).join("; "),
|
|
413
|
+
executionTime: Date.now() - startTime,
|
|
414
|
+
validation,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
// Execute using vm directly for async support
|
|
420
|
+
const script = new vm.Script(asyncCode, {
|
|
421
|
+
filename: "workflow.js",
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const result = await script.runInContext(this.sandbox.getContext(), {
|
|
425
|
+
timeout,
|
|
426
|
+
displayErrors: true,
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
success: true,
|
|
430
|
+
value: result,
|
|
431
|
+
executionTime: Date.now() - startTime,
|
|
432
|
+
validation,
|
|
433
|
+
};
|
|
434
|
+
} catch (error) {
|
|
435
|
+
return {
|
|
436
|
+
success: false,
|
|
437
|
+
error: error instanceof Error ? error.message : String(error),
|
|
438
|
+
executionTime: Date.now() - startTime,
|
|
439
|
+
validation,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Execute a script without validation (assumes pre-validated).
|
|
446
|
+
* Use with caution - prefer execute() for untrusted scripts.
|
|
447
|
+
*/
|
|
448
|
+
executeUnchecked(code: string, timeout?: number): ScriptExecutionResult {
|
|
449
|
+
const startTime = Date.now();
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const value = this.sandbox.execute(code, timeout ?? this.defaultTimeout);
|
|
453
|
+
return {
|
|
454
|
+
success: true,
|
|
455
|
+
value,
|
|
456
|
+
executionTime: Date.now() - startTime,
|
|
457
|
+
validation: { valid: true, errors: [], warnings: [] },
|
|
458
|
+
};
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
success: false,
|
|
462
|
+
error: error instanceof Error ? error.message : String(error),
|
|
463
|
+
executionTime: Date.now() - startTime,
|
|
464
|
+
validation: { valid: true, errors: [], warnings: [] },
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get the list of forbidden globals for documentation.
|
|
471
|
+
*/
|
|
472
|
+
getForbiddenGlobals(): readonly string[] {
|
|
473
|
+
return FORBIDDEN_GLOBALS;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Create a pre-configured script runner for workflow execution.
|
|
479
|
+
*/
|
|
480
|
+
export function createScriptRunner(options?: DynamicScriptOptions): DynamicScriptRunner {
|
|
481
|
+
return new DynamicScriptRunner(options);
|
|
482
|
+
}
|