@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.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/package.json +70 -0
  4. package/src/hooks/builtin-hooks.mjs +296 -0
  5. package/src/hooks/condition-cache.mjs +109 -0
  6. package/src/hooks/condition-evaluator.mjs +722 -0
  7. package/src/hooks/define-hook.mjs +211 -0
  8. package/src/hooks/effect-sandbox-worker.mjs +170 -0
  9. package/src/hooks/effect-sandbox.mjs +517 -0
  10. package/src/hooks/file-resolver.mjs +387 -0
  11. package/src/hooks/hook-chain-compiler.mjs +236 -0
  12. package/src/hooks/hook-executor-batching.mjs +277 -0
  13. package/src/hooks/hook-executor.mjs +465 -0
  14. package/src/hooks/hook-management.mjs +202 -0
  15. package/src/hooks/hook-scheduler.mjs +413 -0
  16. package/src/hooks/knowledge-hook-engine.mjs +358 -0
  17. package/src/hooks/knowledge-hook-manager.mjs +269 -0
  18. package/src/hooks/observability.mjs +531 -0
  19. package/src/hooks/policy-pack.mjs +572 -0
  20. package/src/hooks/quad-pool.mjs +249 -0
  21. package/src/hooks/quality-metrics.mjs +544 -0
  22. package/src/hooks/security/error-sanitizer.mjs +257 -0
  23. package/src/hooks/security/path-validator.mjs +194 -0
  24. package/src/hooks/security/sandbox-restrictions.mjs +331 -0
  25. package/src/hooks/telemetry.mjs +167 -0
  26. package/src/index.mjs +101 -0
  27. package/src/security/sandbox/browser-executor.mjs +220 -0
  28. package/src/security/sandbox/detector.mjs +342 -0
  29. package/src/security/sandbox/isolated-vm-executor.mjs +373 -0
  30. package/src/security/sandbox/vm2-executor.mjs +217 -0
  31. package/src/security/sandbox/worker-executor-runtime.mjs +74 -0
  32. package/src/security/sandbox/worker-executor.mjs +212 -0
  33. 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
+ }