web-agent-bridge 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Runtime - Event Bus
5
+ *
6
+ * Async event system with typed events, middleware, replay buffer,
7
+ * and dead-letter queue. This is the nervous system of the Agent OS.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+
12
+ class EventBus {
13
+ constructor(options = {}) {
14
+ this._listeners = new Map(); // event → Set<{ id, handler, filter, once }>
15
+ this._middleware = []; // global middleware
16
+ this._history = []; // event replay buffer
17
+ this._deadLetter = []; // failed events
18
+ this._maxHistory = options.maxHistory || 10000;
19
+ this._maxDeadLetter = options.maxDeadLetter || 1000;
20
+ this._stats = { emitted: 0, delivered: 0, failed: 0, dropped: 0 };
21
+ }
22
+
23
+ /**
24
+ * Subscribe to an event
25
+ * @returns {string} subscription ID for unsubscribe
26
+ */
27
+ on(event, handler, options = {}) {
28
+ if (!this._listeners.has(event)) this._listeners.set(event, new Set());
29
+ const sub = {
30
+ id: `sub_${crypto.randomBytes(8).toString('hex')}`,
31
+ handler,
32
+ filter: options.filter || null,
33
+ once: options.once || false,
34
+ priority: options.priority || 0,
35
+ };
36
+ this._listeners.get(event).add(sub);
37
+ return sub.id;
38
+ }
39
+
40
+ /**
41
+ * Subscribe once
42
+ */
43
+ once(event, handler, options = {}) {
44
+ return this.on(event, handler, { ...options, once: true });
45
+ }
46
+
47
+ /**
48
+ * Unsubscribe by subscription ID
49
+ */
50
+ off(subId) {
51
+ for (const [, subs] of this._listeners) {
52
+ for (const sub of subs) {
53
+ if (sub.id === subId) { subs.delete(sub); return true; }
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+
59
+ /**
60
+ * Remove all listeners for an event
61
+ */
62
+ removeAll(event) {
63
+ if (event) this._listeners.delete(event);
64
+ else this._listeners.clear();
65
+ }
66
+
67
+ /**
68
+ * Emit an event
69
+ */
70
+ async emit(event, data, metadata = {}) {
71
+ const envelope = {
72
+ id: `evt_${crypto.randomBytes(12).toString('hex')}`,
73
+ event,
74
+ data,
75
+ metadata: {
76
+ ...metadata,
77
+ timestamp: Date.now(),
78
+ source: metadata.source || 'system',
79
+ },
80
+ };
81
+
82
+ // Run global middleware
83
+ for (const mw of this._middleware) {
84
+ try {
85
+ const result = await mw(envelope);
86
+ if (result === false) {
87
+ this._stats.dropped++;
88
+ return envelope;
89
+ }
90
+ } catch (_) { /* middleware errors don't block */ }
91
+ }
92
+
93
+ // Store in history
94
+ this._history.push(envelope);
95
+ if (this._history.length > this._maxHistory) {
96
+ this._history = this._history.slice(-Math.floor(this._maxHistory * 0.8));
97
+ }
98
+
99
+ this._stats.emitted++;
100
+
101
+ // Get listeners (event + wildcard)
102
+ const listeners = [];
103
+ const exact = this._listeners.get(event);
104
+ if (exact) for (const sub of exact) listeners.push(sub);
105
+ const wild = this._listeners.get('*');
106
+ if (wild) for (const sub of wild) listeners.push(sub);
107
+
108
+ // Also match namespace wildcards: 'task.*' matches 'task.completed'
109
+ for (const [pattern, subs] of this._listeners) {
110
+ if (pattern === event || pattern === '*') continue;
111
+ if (pattern.endsWith('.*') && event.startsWith(pattern.slice(0, -1))) {
112
+ for (const sub of subs) listeners.push(sub);
113
+ }
114
+ }
115
+
116
+ // Sort by priority (higher first)
117
+ listeners.sort((a, b) => b.priority - a.priority);
118
+
119
+ // Dispatch
120
+ const toRemove = [];
121
+ for (const sub of listeners) {
122
+ try {
123
+ if (sub.filter && !sub.filter(envelope.data, envelope.metadata)) continue;
124
+ await sub.handler(envelope.data, envelope.metadata, envelope);
125
+ this._stats.delivered++;
126
+ if (sub.once) toRemove.push(sub);
127
+ } catch (err) {
128
+ this._stats.failed++;
129
+ this._deadLetter.push({ envelope, error: err.message, subscriberId: sub.id, timestamp: Date.now() });
130
+ if (this._deadLetter.length > this._maxDeadLetter) {
131
+ this._deadLetter = this._deadLetter.slice(-Math.floor(this._maxDeadLetter * 0.8));
132
+ }
133
+ }
134
+ }
135
+
136
+ // Cleanup one-time subs
137
+ for (const sub of toRemove) {
138
+ for (const [, subs] of this._listeners) subs.delete(sub);
139
+ }
140
+
141
+ return envelope;
142
+ }
143
+
144
+ /**
145
+ * Add global middleware
146
+ */
147
+ use(middleware) {
148
+ this._middleware.push(middleware);
149
+ }
150
+
151
+ /**
152
+ * Replay events matching filter since a timestamp
153
+ */
154
+ async replay(since, filter, handler) {
155
+ const events = this._history.filter(
156
+ e => e.metadata.timestamp >= since && (!filter || filter(e))
157
+ );
158
+ for (const e of events) {
159
+ await handler(e.data, e.metadata, e);
160
+ }
161
+ return events.length;
162
+ }
163
+
164
+ /**
165
+ * Wait for a specific event (returns a promise)
166
+ */
167
+ waitFor(event, timeout = 30000, filter = null) {
168
+ return new Promise((resolve, reject) => {
169
+ const timer = setTimeout(() => {
170
+ this.off(subId);
171
+ reject(new Error(`Timeout waiting for event: ${event}`));
172
+ }, timeout);
173
+
174
+ const subId = this.once(event, (data, meta) => {
175
+ clearTimeout(timer);
176
+ resolve({ data, meta });
177
+ }, { filter });
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Get dead letter queue
183
+ */
184
+ getDeadLetters(limit = 50) {
185
+ return this._deadLetter.slice(-limit);
186
+ }
187
+
188
+ /**
189
+ * Get stats
190
+ */
191
+ getStats() {
192
+ return {
193
+ ...this._stats,
194
+ listeners: this._countListeners(),
195
+ historySize: this._history.length,
196
+ deadLetterSize: this._deadLetter.length,
197
+ };
198
+ }
199
+
200
+ _countListeners() {
201
+ let count = 0;
202
+ for (const [, subs] of this._listeners) count += subs.size;
203
+ return count;
204
+ }
205
+ }
206
+
207
+ // Singleton event bus for the runtime
208
+ const bus = new EventBus();
209
+
210
+ module.exports = { EventBus, bus };
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Runtime - Main Entry Point
5
+ *
6
+ * The Runtime is the core of the Agent OS. It provides:
7
+ * - Task scheduling with retries and timeouts
8
+ * - Agent state management with checkpoints
9
+ * - Execution sandboxing
10
+ * - Event-driven architecture
11
+ *
12
+ * This is NOT a browser wrapper - it's a real execution runtime
13
+ * comparable to Ray or Temporal.
14
+ */
15
+
16
+ const { EventBus, bus } = require('./event-bus');
17
+ const { Scheduler, TaskState } = require('./scheduler');
18
+ const { StateManager } = require('./state-manager');
19
+ const { ExecutionSandbox } = require('./sandbox');
20
+
21
+ class WABRuntime {
22
+ constructor(options = {}) {
23
+ this.scheduler = new Scheduler({
24
+ maxConcurrent: options.maxConcurrentTasks || 20,
25
+ maxQueueSize: options.maxQueueSize || 1000,
26
+ defaultRetries: options.defaultRetries || 3,
27
+ defaultTimeout: options.defaultTimeout || 30000,
28
+ });
29
+
30
+ this.state = new StateManager({
31
+ maxCheckpoints: options.maxCheckpoints || 50,
32
+ ttl: options.stateTTL || 24 * 3600_000,
33
+ });
34
+
35
+ this.sandbox = new ExecutionSandbox({
36
+ maxConcurrent: options.maxSandboxes || 100,
37
+ defaultTimeout: options.sandboxTimeout || 30000,
38
+ });
39
+
40
+ this.events = bus;
41
+ this._started = false;
42
+ this._cleanupTimer = null;
43
+
44
+ // Register built-in task handlers
45
+ this._registerBuiltinHandlers();
46
+
47
+ // Wire events
48
+ this._wireEventHandlers();
49
+ }
50
+
51
+ /**
52
+ * Start the runtime
53
+ */
54
+ start() {
55
+ if (this._started) return;
56
+ this._started = true;
57
+
58
+ // Periodic cleanup
59
+ this._cleanupTimer = setInterval(() => {
60
+ this.state.cleanup();
61
+ this.sandbox.cleanup();
62
+ }, 600_000); // 10 min
63
+ if (this._cleanupTimer.unref) this._cleanupTimer.unref();
64
+
65
+ this.events.emit('runtime.started', {
66
+ timestamp: Date.now(),
67
+ capabilities: this.getCapabilities(),
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Stop the runtime
73
+ */
74
+ stop() {
75
+ if (!this._started) return;
76
+ this._started = false;
77
+ if (this._cleanupTimer) clearInterval(this._cleanupTimer);
78
+ this.events.emit('runtime.stopped', { timestamp: Date.now() });
79
+ }
80
+
81
+ /**
82
+ * Submit a task
83
+ */
84
+ submitTask(task) {
85
+ if (!this._started) throw new Error('Runtime not started');
86
+ return this.scheduler.submit(task);
87
+ }
88
+
89
+ /**
90
+ * Execute a task in a sandbox with full lifecycle
91
+ */
92
+ async executeInSandbox(taskId, handler, options = {}) {
93
+ // Create sandbox
94
+ const sbx = this.sandbox.create(taskId, options);
95
+
96
+ // Save initial state
97
+ this.state.save(taskId, { status: 'sandbox_created', sandboxId: sbx.id });
98
+ this.state.checkpoint(taskId, 'pre-execution');
99
+
100
+ try {
101
+ const result = await this.sandbox.execute(sbx.id, handler);
102
+
103
+ if (result.success) {
104
+ this.state.save(taskId, { status: 'completed', result: result.result });
105
+ } else {
106
+ this.state.save(taskId, { status: 'failed', error: result.error });
107
+ }
108
+
109
+ return result;
110
+ } finally {
111
+ // Cleanup sandbox after a delay
112
+ setTimeout(() => this.sandbox.destroy(sbx.id), 60000);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Register a task type handler
118
+ */
119
+ registerTaskHandler(taskType, handler) {
120
+ this.scheduler.registerHandler(taskType, handler);
121
+ }
122
+
123
+ /**
124
+ * Get runtime capabilities for protocol exposure
125
+ */
126
+ getCapabilities() {
127
+ return {
128
+ scheduler: { maxConcurrent: 20, retries: true, deadlines: true, dependencies: true },
129
+ state: { checkpoints: true, rollback: true, ttl: true },
130
+ sandbox: { isolation: true, resourceLimits: true, auditTrail: true },
131
+ events: { async: true, replay: true, wildcards: true, deadLetter: true },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Get runtime health and stats
137
+ */
138
+ getHealth() {
139
+ return {
140
+ status: this._started ? 'running' : 'stopped',
141
+ uptime: this._started ? Date.now() : 0,
142
+ scheduler: this.scheduler.getStats(),
143
+ state: this.state.getStats(),
144
+ sandbox: this.sandbox.getStats(),
145
+ events: this.events.getStats(),
146
+ };
147
+ }
148
+
149
+ // ─── Built-in Handlers ──────────────────────────────────────────────────
150
+
151
+ _registerBuiltinHandlers() {
152
+ // Browser task handler (delegated to data plane)
153
+ this.scheduler.registerHandler('browser', async (params, ctx) => {
154
+ ctx.reportProgress(10, 0);
155
+ // Browser tasks are handled by data-plane executor
156
+ return { delegated: 'data-plane', params };
157
+ });
158
+
159
+ // API task handler
160
+ this.scheduler.registerHandler('api', async (params, ctx) => {
161
+ ctx.reportProgress(10, 0);
162
+ return { delegated: 'data-plane', params };
163
+ });
164
+
165
+ // Extraction task handler
166
+ this.scheduler.registerHandler('extraction', async (params, ctx) => {
167
+ ctx.reportProgress(10, 0);
168
+ return { delegated: 'data-plane', params };
169
+ });
170
+
171
+ // Workflow (composite) handler
172
+ this.scheduler.registerHandler('workflow', async (params, ctx) => {
173
+ const results = [];
174
+ for (let i = 0; i < ctx.steps.length; i++) {
175
+ ctx.reportProgress(Math.floor((i / ctx.steps.length) * 100), i);
176
+ ctx.checkpoint({ step: i, results });
177
+
178
+ const step = ctx.steps[i];
179
+ const handler = this.scheduler._handlers.get(step.type || 'general');
180
+ if (handler) {
181
+ const result = await handler(step.params || {}, ctx);
182
+ results.push({ step: i, result });
183
+ }
184
+ }
185
+ ctx.reportProgress(100, ctx.steps.length);
186
+ return { steps: results };
187
+ });
188
+
189
+ // Composite task (parallel execution)
190
+ this.scheduler.registerHandler('composite', async (params, ctx) => {
191
+ if (!params.tasks || !Array.isArray(params.tasks)) {
192
+ throw new Error('Composite task requires tasks array');
193
+ }
194
+ const promises = params.tasks.map((t, i) => {
195
+ const handler = this.scheduler._handlers.get(t.type || 'general');
196
+ if (!handler) return { step: i, error: `No handler for: ${t.type}` };
197
+ return handler(t.params || {}, ctx).then(r => ({ step: i, result: r }));
198
+ });
199
+ return { results: await Promise.allSettled(promises) };
200
+ });
201
+
202
+ // General purpose handler (pass-through)
203
+ this.scheduler.registerHandler('general', async (params) => {
204
+ return { received: params };
205
+ });
206
+ }
207
+
208
+ // ─── Event Wiring ────────────────────────────────────────────────────────
209
+
210
+ _wireEventHandlers() {
211
+ // Track task states in state manager
212
+ this.events.on('task.started', (data) => {
213
+ this.state.save(data.taskId, { status: 'running', startedAt: Date.now(), attempt: data.attempt });
214
+ });
215
+
216
+ this.events.on('task.completed', (data) => {
217
+ this.state.merge(data.taskId, { status: 'completed', duration: data.duration });
218
+ });
219
+
220
+ this.events.on('task.failed', (data) => {
221
+ this.state.merge(data.taskId, { status: 'failed', error: data.error, attempts: data.attempts });
222
+ });
223
+
224
+ this.events.on('task.progress', (data) => {
225
+ this.state.merge(data.taskId, { progress: data.progress, currentStep: data.step });
226
+ });
227
+ }
228
+ }
229
+
230
+ // Singleton runtime
231
+ const runtime = new WABRuntime();
232
+
233
+ module.exports = { WABRuntime, runtime, EventBus, bus, Scheduler, TaskState, StateManager, ExecutionSandbox };
@@ -0,0 +1,266 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Runtime - Execution Sandbox
5
+ *
6
+ * Isolates task execution. Each task runs in its own sandbox with:
7
+ * - Resource limits (memory, CPU time, network)
8
+ * - Permission boundaries
9
+ * - Isolated state
10
+ * - Audit trail
11
+ */
12
+
13
+ const crypto = require('crypto');
14
+
15
+ class ExecutionSandbox {
16
+ constructor(options = {}) {
17
+ this._sandboxes = new Map();
18
+ this._maxConcurrent = options.maxConcurrent || 100;
19
+ this._defaultTimeout = options.defaultTimeout || 30000;
20
+ this._stats = { created: 0, completed: 0, failed: 0, timedOut: 0 };
21
+ }
22
+
23
+ /**
24
+ * Create a new sandbox for a task
25
+ */
26
+ create(taskId, options = {}) {
27
+ if (this._sandboxes.size >= this._maxConcurrent) {
28
+ throw new Error('Maximum concurrent sandbox limit reached');
29
+ }
30
+
31
+ const sandbox = {
32
+ id: `sbx_${crypto.randomBytes(12).toString('hex')}`,
33
+ taskId,
34
+ agentId: options.agentId || null,
35
+ siteId: options.siteId || null,
36
+
37
+ // Resource limits
38
+ limits: {
39
+ timeout: options.timeout || this._defaultTimeout,
40
+ maxMemory: options.maxMemory || 128 * 1024 * 1024, // 128MB
41
+ maxNetworkCalls: options.maxNetworkCalls || 100,
42
+ maxDomOperations: options.maxDomOperations || 1000,
43
+ allowedDomains: options.allowedDomains || ['*'],
44
+ blockedSelectors: options.blockedSelectors || [],
45
+ },
46
+
47
+ // Runtime state
48
+ state: 'created',
49
+ usage: {
50
+ networkCalls: 0,
51
+ domOperations: 0,
52
+ startedAt: null,
53
+ completedAt: null,
54
+ },
55
+
56
+ // Permission boundaries
57
+ capabilities: new Set(options.capabilities || []),
58
+
59
+ // Audit trail
60
+ audit: [],
61
+
62
+ // Isolated key-value store
63
+ store: new Map(),
64
+
65
+ createdAt: Date.now(),
66
+ };
67
+
68
+ this._sandboxes.set(sandbox.id, sandbox);
69
+ this._stats.created++;
70
+ return sandbox;
71
+ }
72
+
73
+ /**
74
+ * Execute a function within a sandbox
75
+ */
76
+ async execute(sandboxId, fn) {
77
+ const sandbox = this._sandboxes.get(sandboxId);
78
+ if (!sandbox) throw new Error(`Sandbox not found: ${sandboxId}`);
79
+ if (sandbox.state !== 'created' && sandbox.state !== 'running') {
80
+ throw new Error(`Sandbox ${sandboxId} is in state ${sandbox.state}, cannot execute`);
81
+ }
82
+
83
+ sandbox.state = 'running';
84
+ sandbox.usage.startedAt = Date.now();
85
+
86
+ // Create scoped context for the function
87
+ const context = this._createContext(sandbox);
88
+
89
+ try {
90
+ const result = await _withTimeout(fn(context), sandbox.limits.timeout);
91
+ sandbox.state = 'completed';
92
+ sandbox.usage.completedAt = Date.now();
93
+ this._stats.completed++;
94
+
95
+ sandbox.audit.push({
96
+ action: 'complete',
97
+ timestamp: Date.now(),
98
+ duration: sandbox.usage.completedAt - sandbox.usage.startedAt,
99
+ });
100
+
101
+ return { success: true, result, sandbox: this._getSandboxSummary(sandbox) };
102
+ } catch (err) {
103
+ sandbox.state = err.message.includes('timed out') ? 'timeout' : 'failed';
104
+ sandbox.usage.completedAt = Date.now();
105
+
106
+ if (sandbox.state === 'timeout') this._stats.timedOut++;
107
+ else this._stats.failed++;
108
+
109
+ sandbox.audit.push({
110
+ action: sandbox.state,
111
+ timestamp: Date.now(),
112
+ error: err.message,
113
+ });
114
+
115
+ return { success: false, error: err.message, sandbox: this._getSandboxSummary(sandbox) };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Create a scoped execution context
121
+ */
122
+ _createContext(sandbox) {
123
+ const self = this;
124
+ return {
125
+ taskId: sandbox.taskId,
126
+ agentId: sandbox.agentId,
127
+ siteId: sandbox.siteId,
128
+
129
+ // Capability check
130
+ hasCapability(cap) {
131
+ return sandbox.capabilities.has(cap);
132
+ },
133
+
134
+ requireCapability(cap) {
135
+ if (!sandbox.capabilities.has(cap)) {
136
+ throw new Error(`Sandbox lacks capability: ${cap}`);
137
+ }
138
+ },
139
+
140
+ // Domain check
141
+ checkDomain(domain) {
142
+ if (sandbox.limits.allowedDomains[0] === '*') return true;
143
+ return sandbox.limits.allowedDomains.some(d => domain.endsWith(d));
144
+ },
145
+
146
+ // Resource tracking
147
+ trackNetworkCall() {
148
+ sandbox.usage.networkCalls++;
149
+ if (sandbox.usage.networkCalls > sandbox.limits.maxNetworkCalls) {
150
+ throw new Error('Network call limit exceeded');
151
+ }
152
+ },
153
+
154
+ trackDomOperation() {
155
+ sandbox.usage.domOperations++;
156
+ if (sandbox.usage.domOperations > sandbox.limits.maxDomOperations) {
157
+ throw new Error('DOM operation limit exceeded');
158
+ }
159
+ },
160
+
161
+ // Isolated store
162
+ set(key, value) { sandbox.store.set(key, value); },
163
+ get(key) { return sandbox.store.get(key); },
164
+
165
+ // Audit
166
+ log(action, details) {
167
+ sandbox.audit.push({ action, details, timestamp: Date.now() });
168
+ },
169
+
170
+ // Selector validation
171
+ checkSelector(selector) {
172
+ for (const blocked of sandbox.limits.blockedSelectors) {
173
+ if (selector.includes(blocked)) {
174
+ throw new Error(`Selector blocked by sandbox policy: ${blocked}`);
175
+ }
176
+ }
177
+ return true;
178
+ },
179
+
180
+ // Read sandbox time remaining
181
+ get timeRemaining() {
182
+ if (!sandbox.usage.startedAt) return sandbox.limits.timeout;
183
+ return Math.max(0, sandbox.limits.timeout - (Date.now() - sandbox.usage.startedAt));
184
+ },
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Get sandbox summary (safe to expose)
190
+ */
191
+ _getSandboxSummary(sandbox) {
192
+ return {
193
+ id: sandbox.id,
194
+ taskId: sandbox.taskId,
195
+ state: sandbox.state,
196
+ usage: { ...sandbox.usage },
197
+ auditCount: sandbox.audit.length,
198
+ duration: sandbox.usage.completedAt
199
+ ? sandbox.usage.completedAt - sandbox.usage.startedAt
200
+ : null,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Destroy a sandbox and free resources
206
+ */
207
+ destroy(sandboxId) {
208
+ const sandbox = this._sandboxes.get(sandboxId);
209
+ if (sandbox) {
210
+ sandbox.store.clear();
211
+ this._sandboxes.delete(sandboxId);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Get audit trail for a sandbox
217
+ */
218
+ getAudit(sandboxId) {
219
+ const sandbox = this._sandboxes.get(sandboxId);
220
+ return sandbox ? [...sandbox.audit] : [];
221
+ }
222
+
223
+ /**
224
+ * List active sandboxes
225
+ */
226
+ listActive() {
227
+ const active = [];
228
+ for (const [, sb] of this._sandboxes) {
229
+ if (sb.state === 'created' || sb.state === 'running') {
230
+ active.push(this._getSandboxSummary(sb));
231
+ }
232
+ }
233
+ return active;
234
+ }
235
+
236
+ getStats() {
237
+ return { ...this._stats, active: this._sandboxes.size };
238
+ }
239
+
240
+ /**
241
+ * Cleanup completed/failed sandboxes older than maxAge
242
+ */
243
+ cleanup(maxAge = 3600_000) {
244
+ const cutoff = Date.now() - maxAge;
245
+ let cleaned = 0;
246
+ for (const [id, sb] of this._sandboxes) {
247
+ if (sb.state !== 'created' && sb.state !== 'running' && sb.createdAt < cutoff) {
248
+ sb.store.clear();
249
+ this._sandboxes.delete(id);
250
+ cleaned++;
251
+ }
252
+ }
253
+ return cleaned;
254
+ }
255
+ }
256
+
257
+ function _withTimeout(promise, ms) {
258
+ if (!ms || ms <= 0) return promise;
259
+ return new Promise((resolve, reject) => {
260
+ const timer = setTimeout(() => reject(new Error(`Sandbox execution timed out after ${ms}ms`)), ms);
261
+ promise.then(r => { clearTimeout(timer); resolve(r); })
262
+ .catch(e => { clearTimeout(timer); reject(e); });
263
+ });
264
+ }
265
+
266
+ module.exports = { ExecutionSandbox };