@unrdf/knowledge-engine 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 +84 -0
- package/package.json +64 -0
- package/src/browser-shims.mjs +343 -0
- package/src/browser.mjs +910 -0
- package/src/canonicalize.mjs +414 -0
- package/src/condition-cache.mjs +109 -0
- package/src/condition-evaluator.mjs +722 -0
- package/src/dark-matter-core.mjs +742 -0
- package/src/define-hook.mjs +213 -0
- package/src/effect-sandbox-browser.mjs +283 -0
- package/src/effect-sandbox-worker.mjs +170 -0
- package/src/effect-sandbox.mjs +517 -0
- package/src/engines/index.mjs +11 -0
- package/src/engines/rdf-engine.mjs +299 -0
- package/src/file-resolver.mjs +387 -0
- package/src/hook-executor-batching.mjs +277 -0
- package/src/hook-executor.mjs +870 -0
- package/src/hook-management.mjs +150 -0
- package/src/index.mjs +93 -0
- package/src/ken-parliment.mjs +119 -0
- package/src/ken.mjs +149 -0
- package/src/knowledge-engine/builtin-rules.mjs +190 -0
- package/src/knowledge-engine/inference-engine.mjs +418 -0
- package/src/knowledge-engine/knowledge-engine.mjs +317 -0
- package/src/knowledge-engine/pattern-dsl.mjs +142 -0
- package/src/knowledge-engine/pattern-matcher.mjs +215 -0
- package/src/knowledge-engine/rules.mjs +184 -0
- package/src/knowledge-engine.mjs +319 -0
- package/src/knowledge-hook-engine.mjs +360 -0
- package/src/knowledge-hook-manager.mjs +469 -0
- package/src/knowledge-substrate-core.mjs +927 -0
- package/src/lite.mjs +222 -0
- package/src/lockchain-writer-browser.mjs +414 -0
- package/src/lockchain-writer.mjs +602 -0
- package/src/monitoring/andon-signals.mjs +775 -0
- package/src/observability.mjs +531 -0
- package/src/parse.mjs +290 -0
- package/src/performance-optimizer.mjs +678 -0
- package/src/policy-pack.mjs +572 -0
- package/src/query-cache.mjs +116 -0
- package/src/query-optimizer.mjs +1051 -0
- package/src/query.mjs +306 -0
- package/src/reason.mjs +350 -0
- package/src/resolution-layer.mjs +506 -0
- package/src/schemas.mjs +1063 -0
- package/src/security/error-sanitizer.mjs +257 -0
- package/src/security/path-validator.mjs +194 -0
- package/src/security/sandbox-restrictions.mjs +331 -0
- package/src/security-validator.mjs +389 -0
- package/src/store-cache.mjs +137 -0
- package/src/telemetry.mjs +167 -0
- package/src/transaction.mjs +810 -0
- package/src/utils/adaptive-monitor.mjs +746 -0
- package/src/utils/circuit-breaker.mjs +513 -0
- package/src/utils/edge-case-handler.mjs +503 -0
- package/src/utils/memory-manager.mjs +498 -0
- package/src/utils/ring-buffer.mjs +282 -0
- package/src/validate.mjs +319 -0
- package/src/validators/index.mjs +338 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 80/20 Knowledge Hook definition contract for autonomic systems.
|
|
3
|
+
* @module newco/defineHook
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* This module provides the `defineHook` function, the sole entry point for
|
|
7
|
+
* defining a Knowledge Hook. The contract enforces critical principles for
|
|
8
|
+
* autonomic, deterministic, and provable systems:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Conditions are Addressed, Not Embedded**: The `when` clause MUST
|
|
11
|
+
* reference an external, content-addressed SPARQL or SHACL file.
|
|
12
|
+
* This forbids inline query strings, ensuring that governance logic is
|
|
13
|
+
* a verifiable, standalone artifact.
|
|
14
|
+
*
|
|
15
|
+
* 2. **Reflex Arc Lifecycle**: The `before`, `run`, and `after` functions
|
|
16
|
+
* provide a minimal, complete lifecycle for autonomic reflexes:
|
|
17
|
+
* - `before`: A pre-condition gate for payload normalization or cancellation.
|
|
18
|
+
* - `run`: The core effect or analysis.
|
|
19
|
+
* - `after`: A post-execution step for auditing and cleanup.
|
|
20
|
+
*
|
|
21
|
+
* 3. **Declarative Configuration**: Determinism and receipting strategies
|
|
22
|
+
* are declared as metadata, not implemented imperatively within the hook.
|
|
23
|
+
*
|
|
24
|
+
* 4. **Comprehensive Validation**: Uses Zod schemas for complete type safety
|
|
25
|
+
* and validation of all hook components.
|
|
26
|
+
*
|
|
27
|
+
* This API is designed to feel familiar to users of Nitro's `defineTask`
|
|
28
|
+
* while being fundamentally adapted for a knowledge-native, policy-first runtime.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A content-addressed file reference. This is the only way to specify a
|
|
33
|
+
* condition, ensuring that governance logic is a verifiable artifact and
|
|
34
|
+
* not an inline string.
|
|
35
|
+
*
|
|
36
|
+
* @typedef {Object} Ref
|
|
37
|
+
* @property {string} uri - The URI of the resource, typically a file path like "file://hooks/compliance/largeTx.ask.rq".
|
|
38
|
+
* @property {string} sha256 - The SHA-256 hash of the file's content, for integrity and provenance.
|
|
39
|
+
* @property {'application/sparql-query'|'text/shacl'|'text/turtle'} mediaType - The MIME type of the resource.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Descriptive metadata for cataloging, discovery, and tooling.
|
|
44
|
+
*
|
|
45
|
+
* @typedef {Object} HookMeta
|
|
46
|
+
* @property {string} name - A unique, human-readable name for the hook (e.g., "compliance:largeTx").
|
|
47
|
+
* @property {string} [description] - A brief explanation of the hook's purpose.
|
|
48
|
+
* @property {string[]} [ontology] - A list of ontology prefixes this hook relates to (e.g., ["fibo", "prov"]).
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Defines the knowledge surface the hook observes during its condition evaluation.
|
|
53
|
+
*
|
|
54
|
+
* @typedef {Object} HookChannel
|
|
55
|
+
* @property {string[]} [graphs] - An array of named graph IRIs or labels to scope the query.
|
|
56
|
+
* @property {'before'|'after'|'delta'} [view] - The state of the graph to evaluate against. 'before' is the state before the delta is applied, 'after' is the state after, and 'delta' is a graph containing only the additions and removals.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The declarative trigger condition for the hook, based on a content-addressed reference.
|
|
61
|
+
*
|
|
62
|
+
* @typedef {Object} HookCondition
|
|
63
|
+
* @property {'sparql-ask'|'sparql-select'|'shacl'} kind - The type of evaluation to perform.
|
|
64
|
+
* @property {Ref} ref - The content-addressed reference to the SPARQL or SHACL file.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The execution context passed to all lifecycle functions, providing access
|
|
69
|
+
* to the graph and environment.
|
|
70
|
+
*
|
|
71
|
+
* @typedef {Object} HookContext
|
|
72
|
+
* @property {any} [graph] - The active RDF/JS Store or Dataset instance.
|
|
73
|
+
* @property {Object.<string, unknown>} [env] - Environment variables or runtime configuration.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The data payload for a hook event.
|
|
78
|
+
*
|
|
79
|
+
* @typedef {Object.<string, unknown>} HookPayload
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The event object passed to each lifecycle function of a hook.
|
|
84
|
+
*
|
|
85
|
+
* @typedef {Object} HookEvent
|
|
86
|
+
* @property {string} name - The name of the hook being executed.
|
|
87
|
+
* @property {HookPayload} payload - The input data for the event.
|
|
88
|
+
* @property {HookContext} context - The execution context.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The standardized result of a hook's `run` or `after` function.
|
|
93
|
+
*
|
|
94
|
+
* @typedef {Object} HookResult
|
|
95
|
+
* @property {any} [result] - The primary output or return value of the hook.
|
|
96
|
+
* @property {any} [assertions] - Optional RDF quads to be added to the graph as a result of the hook's execution.
|
|
97
|
+
* @property {any} [deltas] - An optional, more complex delta (additions/removals) to be applied.
|
|
98
|
+
* @property {boolean} [cancelled] - Indicates if the hook was cancelled during the `before` phase.
|
|
99
|
+
* @property {string} [reason] - The reason for cancellation.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The complete, 80/20 contract for a Knowledge Hook. This object defines
|
|
104
|
+
* the hook's identity, its trigger condition, its autonomic behavior, and
|
|
105
|
+
* its operational guarantees.
|
|
106
|
+
*
|
|
107
|
+
* @typedef {Object} KnowledgeHook
|
|
108
|
+
* @property {HookMeta} meta - Essential metadata for the hook.
|
|
109
|
+
* @property {HookChannel} [channel] - The observation channel for the condition.
|
|
110
|
+
* @property {HookCondition} when - The declarative, file-based trigger condition.
|
|
111
|
+
* @property {{ seed?: number }} [determinism] - Configuration for deterministic operations. Defaults to a fixed seed of 42.
|
|
112
|
+
* @property {{ anchor?: ('git-notes'|'none') }} [receipt] - Strategy for anchoring the transaction receipt. Defaults to 'none'.
|
|
113
|
+
* @property {(e: HookEvent) => Promise<Partial<HookPayload>|{cancel:true,reason?:string}> | Partial<HookPayload>|{cancel:true,reason?:string}} [before] - A function that runs before the condition is checked. It can modify the payload or cancel the execution.
|
|
114
|
+
* @property {(e: HookEvent) => Promise<HookResult> | HookResult} run - The main execution body of the hook.
|
|
115
|
+
* @property {(e: HookEvent & { result?: any, cancelled?: boolean, reason?: string }) => Promise<HookResult> | HookResult} [after] - A function that runs after the `run` phase, for cleanup or auditing.
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Defines a Knowledge Hook, validating its structure and enforcing the
|
|
120
|
+
* 80/20 contract for autonomic systems using comprehensive Zod validation.
|
|
121
|
+
*
|
|
122
|
+
* @template T
|
|
123
|
+
* @param {KnowledgeHook} def - The hook definition object.
|
|
124
|
+
* @returns {KnowledgeHook} The validated and normalized hook definition.
|
|
125
|
+
*/
|
|
126
|
+
import { createKnowledgeHook } from './schemas.mjs';
|
|
127
|
+
import { defaultSecurityValidator } from './security-validator.mjs';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
*
|
|
131
|
+
*/
|
|
132
|
+
export function defineHook(def) {
|
|
133
|
+
// Use comprehensive Zod validation
|
|
134
|
+
const validatedHook = createKnowledgeHook(def);
|
|
135
|
+
|
|
136
|
+
// Apply security validation (warn only, don't block)
|
|
137
|
+
// Security will be enforced at execution time via sandbox
|
|
138
|
+
const securityValidation = defaultSecurityValidator.validateKnowledgeHook(validatedHook);
|
|
139
|
+
if (!securityValidation.valid) {
|
|
140
|
+
// Log warning in development (can't modify frozen object, so just log)
|
|
141
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
142
|
+
console.warn(
|
|
143
|
+
`[Security Warning] Hook "${validatedHook.meta.name}": ${securityValidation.blockReason}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return validatedHook;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ------------------------- Example (Happy Path) ------------------------- */
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* This is an example of how to use `defineHook` to create a compliance gate.
|
|
155
|
+
* It demonstrates all the core features of the 80/20 contract.
|
|
156
|
+
*/
|
|
157
|
+
export const exampleComplianceHook = defineHook({
|
|
158
|
+
meta: {
|
|
159
|
+
name: 'compliance:largeTx',
|
|
160
|
+
description:
|
|
161
|
+
'Alerts and creates an audit trail when a financial transaction exceeds a certain threshold.',
|
|
162
|
+
ontology: ['fibo'],
|
|
163
|
+
},
|
|
164
|
+
channel: {
|
|
165
|
+
graphs: ['urn:graph:fibo:prod'],
|
|
166
|
+
view: 'delta',
|
|
167
|
+
},
|
|
168
|
+
when: {
|
|
169
|
+
kind: 'sparql-ask',
|
|
170
|
+
ref: {
|
|
171
|
+
uri: 'file://hooks/compliance/largeTx.ask.rq',
|
|
172
|
+
sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', // Example hash
|
|
173
|
+
mediaType: 'application/sparql-query',
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
determinism: { seed: 42 },
|
|
177
|
+
receipt: { anchor: 'git-notes' },
|
|
178
|
+
|
|
179
|
+
async before({ payload }) {
|
|
180
|
+
if (!payload || typeof payload.amount !== 'number' || payload.amount <= 0) {
|
|
181
|
+
return {
|
|
182
|
+
cancel: true,
|
|
183
|
+
reason: 'Invalid or non-positive transaction amount.',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// Normalize payload for the `run` step
|
|
187
|
+
return { ...payload, validatedAt: new Date().toISOString() };
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
async run({ payload }) {
|
|
191
|
+
console.log(
|
|
192
|
+
`[RUN] Processing large transaction of ${payload.amount} validated at ${payload.validatedAt}`
|
|
193
|
+
);
|
|
194
|
+
// The main result could be an alert object, an event to be emitted, etc.
|
|
195
|
+
return {
|
|
196
|
+
result: { status: 'alert-dispatched', amount: payload.amount },
|
|
197
|
+
// This hook also generates a new piece of knowledge (an audit triple).
|
|
198
|
+
assertions: [
|
|
199
|
+
/* an RDF quad like: [ a:tx, prov:wasGeneratedBy, this:hook ] */
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
async after({ result, cancelled, reason }) {
|
|
205
|
+
if (cancelled) {
|
|
206
|
+
console.log(`[AFTER] Hook execution was cancelled. Reason: ${reason}`);
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`[AFTER] Hook successfully completed with status: ${result?.result?.status}`);
|
|
209
|
+
}
|
|
210
|
+
// The 'after' hook always returns a result for logging/receipt purposes.
|
|
211
|
+
return { result: { finalStatus: cancelled ? 'cancelled' : 'completed' } };
|
|
212
|
+
},
|
|
213
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Browser-compatible Effect Sandbox
|
|
3
|
+
* @module effect-sandbox-browser
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Browser-compatible version of the effect sandbox that uses Web Workers
|
|
7
|
+
* instead of Node.js worker threads for secure hook execution.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomUUID, Worker } from './browser-shims.mjs';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Schema for sandbox configuration
|
|
15
|
+
*/
|
|
16
|
+
const SandboxConfigSchema = z.object({
|
|
17
|
+
type: z.enum(['worker', 'global']).default('worker'), // Only worker mode in browser
|
|
18
|
+
timeout: z.number().int().positive().max(30000).default(5000), // Shorter timeout for browser
|
|
19
|
+
memoryLimit: z
|
|
20
|
+
.number()
|
|
21
|
+
.int()
|
|
22
|
+
.positive()
|
|
23
|
+
.max(100 * 1024 * 1024)
|
|
24
|
+
.default(10 * 1024 * 1024), // 10MB default
|
|
25
|
+
allowedGlobals: z
|
|
26
|
+
.array(z.string())
|
|
27
|
+
.default(['console', 'Date', 'Math', 'JSON', 'Array', 'Object']),
|
|
28
|
+
enableNetwork: z.boolean().default(false),
|
|
29
|
+
enableFileSystem: z.boolean().default(false),
|
|
30
|
+
strictMode: z.boolean().default(true),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Schema for sandbox execution context
|
|
35
|
+
*/
|
|
36
|
+
const SandboxContextSchema = z.object({
|
|
37
|
+
event: z.any(),
|
|
38
|
+
store: z.any(),
|
|
39
|
+
delta: z.any(),
|
|
40
|
+
metadata: z.record(z.any()).optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Schema for sandbox execution result
|
|
45
|
+
*/
|
|
46
|
+
const SandboxResultSchema = z.object({
|
|
47
|
+
success: z.boolean(),
|
|
48
|
+
result: z.any().optional(),
|
|
49
|
+
error: z.string().optional(),
|
|
50
|
+
duration: z.number().nonnegative(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Browser-compatible Effect Sandbox
|
|
55
|
+
*/
|
|
56
|
+
export class EffectSandbox {
|
|
57
|
+
/**
|
|
58
|
+
*
|
|
59
|
+
*/
|
|
60
|
+
constructor(config = {}) {
|
|
61
|
+
this.config = SandboxConfigSchema.parse(config);
|
|
62
|
+
this.workers = new Map();
|
|
63
|
+
this.executionCount = 0;
|
|
64
|
+
this.totalExecutions = 0;
|
|
65
|
+
this.totalDuration = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Execute a hook effect in the sandbox
|
|
70
|
+
* @param {Function} effect - The effect function to execute
|
|
71
|
+
* @param {Object} context - Execution context
|
|
72
|
+
* @param {Object} [options] - Execution options
|
|
73
|
+
* @returns {Promise<Object>} Execution result
|
|
74
|
+
*/
|
|
75
|
+
async executeEffect(effect, context, options = {}) {
|
|
76
|
+
const executionId = randomUUID();
|
|
77
|
+
const startTime = Date.now();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Validate context
|
|
81
|
+
const validatedContext = SandboxContextSchema.parse(context);
|
|
82
|
+
|
|
83
|
+
let result;
|
|
84
|
+
switch (this.config.type) {
|
|
85
|
+
case 'worker':
|
|
86
|
+
result = await this._executeInWorker(effect, validatedContext, executionId, options);
|
|
87
|
+
break;
|
|
88
|
+
case 'global':
|
|
89
|
+
// Not recommended for browser, but allowed for testing
|
|
90
|
+
result = await this._executeInGlobal(effect, validatedContext, executionId, options);
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
throw new Error(`Unsupported sandbox type: ${this.config.type}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const duration = Date.now() - startTime;
|
|
97
|
+
this.totalExecutions++;
|
|
98
|
+
this.totalDuration += duration;
|
|
99
|
+
|
|
100
|
+
const validatedResult = SandboxResultSchema.parse({
|
|
101
|
+
...result,
|
|
102
|
+
duration,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return validatedResult;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const duration = Date.now() - startTime;
|
|
108
|
+
this.totalExecutions++;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
error: error.message,
|
|
113
|
+
duration,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Execute effect in Web Worker
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
async _executeInWorker(effect, context, executionId, _options) {
|
|
123
|
+
const workerScript = this._createWorkerScript(effect);
|
|
124
|
+
|
|
125
|
+
const worker = new Worker(workerScript, {
|
|
126
|
+
name: `effect-sandbox-${executionId}`,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const timeout = setTimeout(() => {
|
|
131
|
+
worker.terminate();
|
|
132
|
+
reject(new Error(`Worker execution timeout after ${this.config.timeout}ms`));
|
|
133
|
+
}, this.config.timeout);
|
|
134
|
+
|
|
135
|
+
worker.onmessage = event => {
|
|
136
|
+
clearTimeout(timeout);
|
|
137
|
+
worker.terminate();
|
|
138
|
+
|
|
139
|
+
if (event.data.error) {
|
|
140
|
+
reject(new Error(event.data.error));
|
|
141
|
+
} else {
|
|
142
|
+
resolve(event.data);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
worker.onerror = error => {
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
worker.terminate();
|
|
149
|
+
reject(new Error(`Worker error: ${error.message}`));
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
worker.postMessage({ context, config: this.config });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Execute effect in global scope (not recommended for production)
|
|
158
|
+
* @private
|
|
159
|
+
*/
|
|
160
|
+
async _executeInGlobal(effect, context, _executionId, _options) {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
try {
|
|
163
|
+
const result = effect(context, {
|
|
164
|
+
emitEvent: event => console.log('Event:', event),
|
|
165
|
+
log: message => console.log(message),
|
|
166
|
+
assert: (condition, message) => {
|
|
167
|
+
if (!condition) throw new Error(message || 'Assertion failed');
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (result instanceof Promise) {
|
|
172
|
+
result.then(resolve).catch(reject);
|
|
173
|
+
} else {
|
|
174
|
+
resolve({ success: true, result });
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
resolve({ success: false, error: error.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create worker script from effect function
|
|
184
|
+
* @private
|
|
185
|
+
*/
|
|
186
|
+
_createWorkerScript(effect) {
|
|
187
|
+
const _allowedGlobals = this.config._allowedGlobals.join(', ');
|
|
188
|
+
|
|
189
|
+
return `
|
|
190
|
+
// Worker script for effect sandbox
|
|
191
|
+
const effect = ${effect.toString()};
|
|
192
|
+
|
|
193
|
+
// Sandbox globals
|
|
194
|
+
${this.config.allowedGlobals.map(global => `const ${global} = self.${global};`).join('\n')}
|
|
195
|
+
|
|
196
|
+
// Safe console
|
|
197
|
+
const console = {
|
|
198
|
+
log: (...args) => self.postMessage({ type: 'console', level: 'log', args }),
|
|
199
|
+
warn: (...args) => self.postMessage({ type: 'console', level: 'warn', args }),
|
|
200
|
+
error: (...args) => self.postMessage({ type: 'console', level: 'error', args }),
|
|
201
|
+
info: (...args) => self.postMessage({ type: 'console', level: 'info', args })
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Safe API implementations
|
|
205
|
+
const emitEvent = (event) => {
|
|
206
|
+
self.postMessage({ type: 'event', event });
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const log = (message) => {
|
|
210
|
+
self.postMessage({ type: 'log', message });
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const assert = (condition, message) => {
|
|
214
|
+
throw new Error(message || 'Assertion failed');
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Message handler
|
|
218
|
+
self.onmessage = async (event) => {
|
|
219
|
+
try {
|
|
220
|
+
const { context, config } = event.data;
|
|
221
|
+
|
|
222
|
+
const result = await effect(context, {
|
|
223
|
+
emitEvent,
|
|
224
|
+
log,
|
|
225
|
+
assert
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
self.postMessage({
|
|
229
|
+
success: true,
|
|
230
|
+
result,
|
|
231
|
+
type: 'result'
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
self.postMessage({
|
|
235
|
+
success: false,
|
|
236
|
+
error: error.message,
|
|
237
|
+
type: 'error'
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get sandbox statistics
|
|
246
|
+
* @returns {Object} Statistics
|
|
247
|
+
*/
|
|
248
|
+
getStats() {
|
|
249
|
+
return {
|
|
250
|
+
type: this.config.type,
|
|
251
|
+
activeWorkers: this.workers.size,
|
|
252
|
+
totalExecutions: this.totalExecutions,
|
|
253
|
+
averageDuration: this.totalExecutions > 0 ? this.totalDuration / this.totalExecutions : 0,
|
|
254
|
+
config: this.config,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Clear sandbox state
|
|
260
|
+
*/
|
|
261
|
+
clear() {
|
|
262
|
+
// Terminate all workers
|
|
263
|
+
for (const worker of this.workers.values()) {
|
|
264
|
+
worker.terminate();
|
|
265
|
+
}
|
|
266
|
+
this.workers.clear();
|
|
267
|
+
|
|
268
|
+
this.executionCount = 0;
|
|
269
|
+
this.totalExecutions = 0;
|
|
270
|
+
this.totalDuration = 0;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Create a new effect sandbox
|
|
276
|
+
* @param {Object} [config] - Sandbox configuration
|
|
277
|
+
* @returns {EffectSandbox} New sandbox instance
|
|
278
|
+
*/
|
|
279
|
+
export function createEffectSandbox(config = {}) {
|
|
280
|
+
return new EffectSandbox(config);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export default EffectSandbox;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Worker thread implementation for effect sandbox
|
|
3
|
+
* @module effect-sandbox-worker
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Worker thread implementation for secure hook effect execution.
|
|
7
|
+
* This file runs in a separate worker thread to isolate hook execution.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { parentPort, workerData, isMainThread } from 'worker_threads';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Worker thread entry point
|
|
14
|
+
*/
|
|
15
|
+
if (!isMainThread) {
|
|
16
|
+
const { effect, context, executionId, config, _options } = workerData;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Create safe execution environment
|
|
20
|
+
const safeGlobals = createSafeGlobals(context, config);
|
|
21
|
+
|
|
22
|
+
// Create safe effect function
|
|
23
|
+
const safeEffect = createSafeEffect(effect, safeGlobals);
|
|
24
|
+
|
|
25
|
+
// Execute effect with timeout
|
|
26
|
+
const result = await executeWithTimeout(safeEffect, context, config.timeout);
|
|
27
|
+
|
|
28
|
+
// Send result back to main thread
|
|
29
|
+
parentPort.postMessage({
|
|
30
|
+
success: true,
|
|
31
|
+
result: result.result,
|
|
32
|
+
assertions: result.assertions || [],
|
|
33
|
+
events: result.events || [],
|
|
34
|
+
executionId,
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// Send error back to main thread
|
|
38
|
+
parentPort.postMessage({
|
|
39
|
+
success: false,
|
|
40
|
+
error: error.message,
|
|
41
|
+
executionId,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create safe globals for worker execution
|
|
48
|
+
* @param {Object} context - Execution context
|
|
49
|
+
* @param {Object} config - Sandbox configuration
|
|
50
|
+
* @returns {Object} Safe globals
|
|
51
|
+
*/
|
|
52
|
+
function createSafeGlobals(context, config) {
|
|
53
|
+
const globals = {};
|
|
54
|
+
|
|
55
|
+
// Add context data
|
|
56
|
+
globals.event = context.event;
|
|
57
|
+
globals.store = context.store;
|
|
58
|
+
globals.delta = context.delta;
|
|
59
|
+
globals.metadata = context.metadata || {};
|
|
60
|
+
|
|
61
|
+
// Add safe console
|
|
62
|
+
globals.console = {
|
|
63
|
+
log: message => console.log(`[Worker] ${message}`),
|
|
64
|
+
warn: message => console.warn(`[Worker] ${message}`),
|
|
65
|
+
error: message => console.error(`[Worker] ${message}`),
|
|
66
|
+
info: message => console.info(`[Worker] ${message}`),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Add safe functions
|
|
70
|
+
globals.emitEvent = eventData => {
|
|
71
|
+
if (!context.events) context.events = [];
|
|
72
|
+
context.events.push(eventData);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
globals.log = (message, level = 'info') => {
|
|
76
|
+
console.log(`[Sandbox ${level.toUpperCase()}] ${message}`);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
globals.assert = (subject, predicate, object, graph) => {
|
|
80
|
+
if (!context.assertions) context.assertions = [];
|
|
81
|
+
context.assertions.push({ subject, predicate, object, graph });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Add allowed globals
|
|
85
|
+
for (const globalName of config.allowedGlobals || []) {
|
|
86
|
+
if (globalName === 'Date') globals.Date = Date;
|
|
87
|
+
if (globalName === 'Math') globals.Math = Math;
|
|
88
|
+
if (globalName === 'JSON') globals.JSON = JSON;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return globals;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create safe effect function
|
|
96
|
+
* @param {string} effectCode - Effect function code
|
|
97
|
+
* @param {Object} safeGlobals - Safe globals
|
|
98
|
+
* @returns {Function} Safe effect function
|
|
99
|
+
*/
|
|
100
|
+
function createSafeEffect(effectCode, safeGlobals) {
|
|
101
|
+
// Create function with safe globals
|
|
102
|
+
const safeFunction = new Function(
|
|
103
|
+
...Object.keys(safeGlobals),
|
|
104
|
+
`
|
|
105
|
+
"use strict";
|
|
106
|
+
|
|
107
|
+
// Override dangerous globals
|
|
108
|
+
const process = undefined;
|
|
109
|
+
const require = undefined;
|
|
110
|
+
const module = undefined;
|
|
111
|
+
const exports = undefined;
|
|
112
|
+
const __dirname = undefined;
|
|
113
|
+
const __filename = undefined;
|
|
114
|
+
|
|
115
|
+
// Create effect function
|
|
116
|
+
const effect = ${effectCode};
|
|
117
|
+
|
|
118
|
+
// Return wrapper that captures assertions and events
|
|
119
|
+
return function(context) {
|
|
120
|
+
const result = effect(context);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
result,
|
|
124
|
+
assertions: context.assertions || [],
|
|
125
|
+
events: context.events || []
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return safeFunction(...Object.values(safeGlobals));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Execute function with timeout
|
|
136
|
+
* @param {Function} fn - Function to execute
|
|
137
|
+
* @param {Object} context - Execution context
|
|
138
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
139
|
+
* @returns {Promise<Object>} Execution result
|
|
140
|
+
*/
|
|
141
|
+
async function executeWithTimeout(fn, context, timeout) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const timeoutId = setTimeout(() => {
|
|
144
|
+
reject(new Error(`Execution timeout after ${timeout}ms`));
|
|
145
|
+
}, timeout);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const result = fn(context);
|
|
149
|
+
|
|
150
|
+
// Handle both sync and async results
|
|
151
|
+
if (result && typeof result.then === 'function') {
|
|
152
|
+
result
|
|
153
|
+
.then(res => {
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
resolve(res);
|
|
156
|
+
})
|
|
157
|
+
.catch(err => {
|
|
158
|
+
clearTimeout(timeoutId);
|
|
159
|
+
reject(err);
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
clearTimeout(timeoutId);
|
|
163
|
+
resolve(result);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
clearTimeout(timeoutId);
|
|
167
|
+
reject(error);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|