@unrdf/hooks 5.0.1
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/LICENSE +21 -0
- package/README.md +86 -0
- package/package.json +70 -0
- package/src/hooks/builtin-hooks.mjs +296 -0
- package/src/hooks/condition-cache.mjs +109 -0
- package/src/hooks/condition-evaluator.mjs +722 -0
- package/src/hooks/define-hook.mjs +211 -0
- package/src/hooks/effect-sandbox-worker.mjs +170 -0
- package/src/hooks/effect-sandbox.mjs +517 -0
- package/src/hooks/file-resolver.mjs +387 -0
- package/src/hooks/hook-chain-compiler.mjs +236 -0
- package/src/hooks/hook-executor-batching.mjs +277 -0
- package/src/hooks/hook-executor.mjs +465 -0
- package/src/hooks/hook-management.mjs +202 -0
- package/src/hooks/hook-scheduler.mjs +413 -0
- package/src/hooks/knowledge-hook-engine.mjs +358 -0
- package/src/hooks/knowledge-hook-manager.mjs +269 -0
- package/src/hooks/observability.mjs +531 -0
- package/src/hooks/policy-pack.mjs +572 -0
- package/src/hooks/quad-pool.mjs +249 -0
- package/src/hooks/quality-metrics.mjs +544 -0
- package/src/hooks/security/error-sanitizer.mjs +257 -0
- package/src/hooks/security/path-validator.mjs +194 -0
- package/src/hooks/security/sandbox-restrictions.mjs +331 -0
- package/src/hooks/telemetry.mjs +167 -0
- package/src/index.mjs +101 -0
- package/src/security/sandbox/browser-executor.mjs +220 -0
- package/src/security/sandbox/detector.mjs +342 -0
- package/src/security/sandbox/isolated-vm-executor.mjs +373 -0
- package/src/security/sandbox/vm2-executor.mjs +217 -0
- package/src/security/sandbox/worker-executor-runtime.mjs +74 -0
- package/src/security/sandbox/worker-executor.mjs +212 -0
- package/src/security/sandbox-adapter.mjs +141 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Worker Thread Executor
|
|
3
|
+
* @module security/sandbox/worker-executor
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Fallback sandbox executor using Worker threads (Node.js 12+).
|
|
7
|
+
* Provides process-level isolation with:
|
|
8
|
+
* - Separate V8 context per worker
|
|
9
|
+
* - Memory isolation (separate heap)
|
|
10
|
+
* - Timeout controls
|
|
11
|
+
* - Message-based communication
|
|
12
|
+
* - OpenTelemetry instrumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Worker } from 'worker_threads';
|
|
16
|
+
import { trace } from '@opentelemetry/api';
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { dirname, join } from 'path';
|
|
20
|
+
|
|
21
|
+
const tracer = trace.getTracer('worker-executor');
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Worker Thread Executor
|
|
27
|
+
*/
|
|
28
|
+
export class WorkerExecutor {
|
|
29
|
+
/**
|
|
30
|
+
* @param {Object} [config] - Executor configuration
|
|
31
|
+
* @param {number} [config.timeout=5000] - Execution timeout in ms
|
|
32
|
+
* @param {number} [config.memoryLimit=128] - Memory limit in MB (soft limit)
|
|
33
|
+
* @param {Array<string>} [config.allowedGlobals] - Allowed global variables
|
|
34
|
+
* @param {boolean} [config.strictMode=true] - Enable strict mode
|
|
35
|
+
*/
|
|
36
|
+
constructor(config = {}) {
|
|
37
|
+
this.config = {
|
|
38
|
+
timeout: config.timeout || 5000,
|
|
39
|
+
memoryLimit: config.memoryLimit || 128,
|
|
40
|
+
allowedGlobals: config.allowedGlobals || ['console', 'Date', 'Math', 'JSON'],
|
|
41
|
+
strictMode: config.strictMode !== false,
|
|
42
|
+
...config,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** @type {Map<string, Worker>} */
|
|
46
|
+
this.workers = new Map();
|
|
47
|
+
|
|
48
|
+
this.executionCount = 0;
|
|
49
|
+
this.totalDuration = 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute code in worker thread
|
|
54
|
+
* @param {string|Function} code - Code to execute
|
|
55
|
+
* @param {Object} [context] - Execution context
|
|
56
|
+
* @param {Object} [options] - Execution options
|
|
57
|
+
* @returns {Promise<Object>} Execution result
|
|
58
|
+
*/
|
|
59
|
+
async run(code, context = {}, options = {}) {
|
|
60
|
+
return tracer.startActiveSpan('security.worker.execute', async span => {
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
const executionId = randomUUID();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
span.setAttributes({
|
|
66
|
+
'security.executor.type': 'worker',
|
|
67
|
+
'security.execution.id': executionId,
|
|
68
|
+
'security.timeout': options.timeout || this.config.timeout,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Convert function to string if needed
|
|
72
|
+
const codeString = typeof code === 'function' ? code.toString() : code;
|
|
73
|
+
|
|
74
|
+
// Wrap code in strict mode if enabled
|
|
75
|
+
const wrappedCode = this.config.strictMode ? `"use strict";\n${codeString}` : codeString;
|
|
76
|
+
|
|
77
|
+
// Create worker promise
|
|
78
|
+
const result = await new Promise((resolve, reject) => {
|
|
79
|
+
// Create worker first
|
|
80
|
+
const worker = new Worker(join(__dirname, 'worker-executor-runtime.mjs'), {
|
|
81
|
+
workerData: {
|
|
82
|
+
code: wrappedCode,
|
|
83
|
+
context,
|
|
84
|
+
config: {
|
|
85
|
+
allowedGlobals: this.config.allowedGlobals,
|
|
86
|
+
strictMode: this.config.strictMode,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
resourceLimits: {
|
|
90
|
+
maxOldGenerationSizeMb: this.config.memoryLimit,
|
|
91
|
+
maxYoungGenerationSizeMb: Math.floor(this.config.memoryLimit / 4),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Then set up timeout that uses worker
|
|
96
|
+
const timeout = setTimeout(() => {
|
|
97
|
+
worker.terminate();
|
|
98
|
+
this.workers.delete(executionId);
|
|
99
|
+
reject(new Error(`Worker execution timeout after ${this.config.timeout}ms`));
|
|
100
|
+
}, options.timeout || this.config.timeout);
|
|
101
|
+
|
|
102
|
+
this.workers.set(executionId, worker);
|
|
103
|
+
|
|
104
|
+
worker.on('message', message => {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
this.workers.delete(executionId);
|
|
107
|
+
worker.terminate();
|
|
108
|
+
|
|
109
|
+
if (message.success) {
|
|
110
|
+
resolve(message);
|
|
111
|
+
} else {
|
|
112
|
+
reject(new Error(message.error || 'Worker execution failed'));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
worker.on('error', error => {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
this.workers.delete(executionId);
|
|
119
|
+
worker.terminate();
|
|
120
|
+
reject(error);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
worker.on('exit', code => {
|
|
124
|
+
if (code !== 0 && !this.workers.has(executionId)) {
|
|
125
|
+
// Already handled by message or error
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
this.workers.delete(executionId);
|
|
130
|
+
reject(new Error(`Worker exited with code ${code}`));
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const duration = Date.now() - startTime;
|
|
135
|
+
this.executionCount++;
|
|
136
|
+
this.totalDuration += duration;
|
|
137
|
+
|
|
138
|
+
span.setAttributes({
|
|
139
|
+
'security.execution.duration': duration,
|
|
140
|
+
'security.execution.success': true,
|
|
141
|
+
});
|
|
142
|
+
span.setStatus({ code: 1 }); // OK
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
success: true,
|
|
146
|
+
result: result.result,
|
|
147
|
+
duration,
|
|
148
|
+
executionId,
|
|
149
|
+
memoryUsed: result.memoryUsed || { used: 0 },
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const duration = Date.now() - startTime;
|
|
153
|
+
|
|
154
|
+
span.recordException(error);
|
|
155
|
+
span.setAttributes({
|
|
156
|
+
'security.execution.duration': duration,
|
|
157
|
+
'security.execution.success': false,
|
|
158
|
+
'security.error.message': error.message,
|
|
159
|
+
});
|
|
160
|
+
span.setStatus({ code: 2, message: error.message });
|
|
161
|
+
|
|
162
|
+
// Categorize error
|
|
163
|
+
let errorType = 'unknown';
|
|
164
|
+
if (error.message?.includes('timeout')) {
|
|
165
|
+
errorType = 'timeout';
|
|
166
|
+
} else if (error.message?.includes('memory')) {
|
|
167
|
+
errorType = 'memory_limit';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
span.setAttribute('security.error.type', errorType);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: error.message,
|
|
175
|
+
errorType,
|
|
176
|
+
duration,
|
|
177
|
+
executionId,
|
|
178
|
+
};
|
|
179
|
+
} finally {
|
|
180
|
+
span.end();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get executor statistics
|
|
187
|
+
* @returns {Object}
|
|
188
|
+
*/
|
|
189
|
+
getStats() {
|
|
190
|
+
return {
|
|
191
|
+
type: 'worker',
|
|
192
|
+
config: this.config,
|
|
193
|
+
executionCount: this.executionCount,
|
|
194
|
+
averageDuration: this.executionCount > 0 ? this.totalDuration / this.executionCount : 0,
|
|
195
|
+
activeWorkers: this.workers.size,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Cleanup all workers
|
|
201
|
+
*/
|
|
202
|
+
async cleanup() {
|
|
203
|
+
for (const [executionId, worker] of this.workers.entries()) {
|
|
204
|
+
try {
|
|
205
|
+
await worker.terminate();
|
|
206
|
+
} catch (err) {
|
|
207
|
+
// Ignore termination errors
|
|
208
|
+
}
|
|
209
|
+
this.workers.delete(executionId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox adapter to abstract execution engine (isolated-vm preferred, vm2 deprecated).
|
|
3
|
+
* Automatically detects and uses the best available executor.
|
|
4
|
+
*/
|
|
5
|
+
import { detectBestExecutor, createExecutor } from './sandbox/detector.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
export class SandboxAdapter {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} [options]
|
|
13
|
+
* @param {string} [options.engine] - Force specific engine ('isolated-vm', 'worker', 'vm2', 'browser')
|
|
14
|
+
* @param {number} [options.timeoutMs] - Execution timeout in milliseconds
|
|
15
|
+
* @param {number} [options.memoryLimit] - Memory limit in MB
|
|
16
|
+
* @param {Object} [options.sandbox] - Sandbox context
|
|
17
|
+
* @param {boolean} [options.strictMode] - Enable strict mode
|
|
18
|
+
*/
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.options = {
|
|
21
|
+
timeoutMs: options.timeoutMs || 1000,
|
|
22
|
+
memoryLimit: options.memoryLimit || 128,
|
|
23
|
+
sandbox: options.sandbox || {},
|
|
24
|
+
strictMode: options.strictMode !== false,
|
|
25
|
+
...options,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
this.engine = options.engine || null;
|
|
29
|
+
this.executor = null;
|
|
30
|
+
this.initialized = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initialize executor (lazy initialization)
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
async _initialize() {
|
|
38
|
+
if (this.initialized) return;
|
|
39
|
+
|
|
40
|
+
// Detect best executor if not explicitly set
|
|
41
|
+
if (!this.engine) {
|
|
42
|
+
this.engine = await detectBestExecutor({
|
|
43
|
+
preferIsolatedVm: true,
|
|
44
|
+
allowVm2: process.env.UNRDF_ALLOW_VM2 === '1', // Only allow if explicitly enabled
|
|
45
|
+
allowBrowser: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create executor instance
|
|
50
|
+
this.executor = await createExecutor(this.engine, {
|
|
51
|
+
timeout: this.options.timeoutMs,
|
|
52
|
+
memoryLimit: this.options.memoryLimit,
|
|
53
|
+
strictMode: this.options.strictMode,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.initialized = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Execute untrusted code and return result.
|
|
61
|
+
* For sync-style usage with simple executors (vm2), returns value directly.
|
|
62
|
+
* For async executors, returns a Promise.
|
|
63
|
+
* @param {string} code
|
|
64
|
+
* @returns {any|Promise<any>}
|
|
65
|
+
*/
|
|
66
|
+
run(code) {
|
|
67
|
+
// Try to use sync execution path if already initialized
|
|
68
|
+
if (this.initialized && this.executor && typeof this.executor.runSync === 'function') {
|
|
69
|
+
try {
|
|
70
|
+
return this.executor.runSync(code, this.options.sandbox, {
|
|
71
|
+
timeout: this.options.timeoutMs,
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Async path - initialize if needed, then execute
|
|
79
|
+
return (async () => {
|
|
80
|
+
await this._initialize();
|
|
81
|
+
|
|
82
|
+
const result = await this.executor.run(code, this.options.sandbox, {
|
|
83
|
+
timeout: this.options.timeoutMs,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!result || !result.success) {
|
|
87
|
+
throw new Error((result && result.error) || 'Sandbox execution failed');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result.result;
|
|
91
|
+
})();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get executor type
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
getEngine() {
|
|
99
|
+
if (!this.initialized && !this.engine) {
|
|
100
|
+
// Force synchronous initialization for getter
|
|
101
|
+
const result = detectBestExecutor({
|
|
102
|
+
preferIsolatedVm: false,
|
|
103
|
+
allowVm2: process.env.UNRDF_ALLOW_VM2 === '1',
|
|
104
|
+
allowBrowser: true,
|
|
105
|
+
});
|
|
106
|
+
// If result is a promise, we can't wait for it in a sync getter
|
|
107
|
+
// Just return 'vm2' as default for now
|
|
108
|
+
return typeof result === 'string' ? result : 'vm2';
|
|
109
|
+
}
|
|
110
|
+
return this.engine;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get executor statistics
|
|
115
|
+
* @returns {Object}
|
|
116
|
+
*/
|
|
117
|
+
getStats() {
|
|
118
|
+
if (!this.executor) {
|
|
119
|
+
return { engine: this.engine, initialized: false };
|
|
120
|
+
}
|
|
121
|
+
return this.executor.getStats();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cleanup executor resources
|
|
126
|
+
*/
|
|
127
|
+
async cleanup() {
|
|
128
|
+
if (this.executor && this.executor.cleanup) {
|
|
129
|
+
await this.executor.cleanup();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create sandbox adapter with auto-detection
|
|
136
|
+
* @param {Object} [options]
|
|
137
|
+
* @returns {SandboxAdapter}
|
|
138
|
+
*/
|
|
139
|
+
export function createSandboxAdapter(options = {}) {
|
|
140
|
+
return new SandboxAdapter(options);
|
|
141
|
+
}
|