@unrdf/kgc-runtime 26.4.2
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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Plugin Isolation - Capability-based security for plugins
|
|
3
|
+
* @module @unrdf/kgc-runtime/plugin-isolation
|
|
4
|
+
* @description Provides sandboxing and capability whitelist for plugin execution
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default capability whitelist
|
|
11
|
+
* Plugins can only access these capabilities unless explicitly granted more
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_WHITELIST = [
|
|
14
|
+
'receipt:generate',
|
|
15
|
+
'receipt:validate',
|
|
16
|
+
'schema:validate',
|
|
17
|
+
'tool:register',
|
|
18
|
+
'bounds:check',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Blocked capabilities (never allowed)
|
|
23
|
+
*/
|
|
24
|
+
const BLOCKED_CAPABILITIES = [
|
|
25
|
+
'filesystem:write',
|
|
26
|
+
'filesystem:delete',
|
|
27
|
+
'network:http',
|
|
28
|
+
'network:socket',
|
|
29
|
+
'process:spawn',
|
|
30
|
+
'process:exit',
|
|
31
|
+
'eval:code',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Capability schema
|
|
36
|
+
*/
|
|
37
|
+
const CapabilitySchema = z.string().regex(
|
|
38
|
+
/^[a-z_-]+:[a-z_-]+$/,
|
|
39
|
+
'Capability must be in format "category:action"'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Plugin sandbox configuration schema
|
|
44
|
+
*/
|
|
45
|
+
const SandboxConfigSchema = z.object({
|
|
46
|
+
/** Whitelisted capabilities */
|
|
47
|
+
whitelist: z.array(CapabilitySchema).default([...DEFAULT_WHITELIST]),
|
|
48
|
+
|
|
49
|
+
/** Additional capabilities to grant */
|
|
50
|
+
additionalCapabilities: z.array(CapabilitySchema).optional(),
|
|
51
|
+
|
|
52
|
+
/** Whether to enforce strict mode (reject any non-whitelisted) */
|
|
53
|
+
strictMode: z.boolean().default(true),
|
|
54
|
+
|
|
55
|
+
/** Maximum memory usage (bytes) */
|
|
56
|
+
maxMemory: z.number().int().positive().optional(),
|
|
57
|
+
|
|
58
|
+
/** Maximum execution time (milliseconds) */
|
|
59
|
+
maxExecutionTime: z.number().int().positive().default(30000),
|
|
60
|
+
|
|
61
|
+
/** Whether to allow custom receipt types */
|
|
62
|
+
allowCustomReceipts: z.boolean().default(false),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Plugin Isolation Manager - Enforces capability-based security
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* import { PluginIsolation } from '@unrdf/kgc-runtime/plugin-isolation';
|
|
70
|
+
* const isolation = new PluginIsolation({
|
|
71
|
+
* whitelist: ['receipt:generate', 'schema:validate'],
|
|
72
|
+
* strictMode: true
|
|
73
|
+
* });
|
|
74
|
+
* const allowed = isolation.checkCapability('receipt:generate'); // true
|
|
75
|
+
* const denied = isolation.checkCapability('filesystem:write'); // false
|
|
76
|
+
*/
|
|
77
|
+
export class PluginIsolation {
|
|
78
|
+
/**
|
|
79
|
+
* Create new plugin isolation manager
|
|
80
|
+
* @param {Object} config - Sandbox configuration
|
|
81
|
+
*/
|
|
82
|
+
constructor(config = {}) {
|
|
83
|
+
this.config = SandboxConfigSchema.parse(config);
|
|
84
|
+
|
|
85
|
+
// Build complete whitelist
|
|
86
|
+
this.whitelist = new Set([
|
|
87
|
+
...this.config.whitelist,
|
|
88
|
+
...(this.config.additionalCapabilities || []),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// Blocked capabilities always rejected
|
|
92
|
+
this.blocklist = new Set(BLOCKED_CAPABILITIES);
|
|
93
|
+
|
|
94
|
+
/** @type {Array<Object>} Access log */
|
|
95
|
+
this.accessLog = [];
|
|
96
|
+
|
|
97
|
+
/** @type {Map<string, number>} Capability usage counter */
|
|
98
|
+
this.usageStats = new Map();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a capability is allowed
|
|
103
|
+
*
|
|
104
|
+
* @param {string} capability - Capability to check (format: "category:action")
|
|
105
|
+
* @returns {boolean} True if allowed
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* const allowed = isolation.checkCapability('receipt:generate');
|
|
109
|
+
* console.log(allowed); // true or false
|
|
110
|
+
*/
|
|
111
|
+
checkCapability(capability) {
|
|
112
|
+
// Validate format
|
|
113
|
+
try {
|
|
114
|
+
CapabilitySchema.parse(capability);
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Always deny blocked capabilities
|
|
120
|
+
if (this.blocklist.has(capability)) {
|
|
121
|
+
this._logAccess(capability, false, 'blocked');
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check whitelist
|
|
126
|
+
const allowed = this.whitelist.has(capability);
|
|
127
|
+
|
|
128
|
+
// In strict mode, deny if not explicitly whitelisted
|
|
129
|
+
if (this.config.strictMode && !allowed) {
|
|
130
|
+
this._logAccess(capability, false, 'not_whitelisted');
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Log successful access
|
|
135
|
+
if (allowed) {
|
|
136
|
+
this._logAccess(capability, true, 'allowed');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return allowed;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Request a capability (throws if denied)
|
|
144
|
+
*
|
|
145
|
+
* @param {string} capability - Capability to request
|
|
146
|
+
* @throws {Error} If capability is denied
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* isolation.requestCapability('receipt:generate'); // OK
|
|
150
|
+
* isolation.requestCapability('filesystem:write'); // throws Error
|
|
151
|
+
*/
|
|
152
|
+
requestCapability(capability) {
|
|
153
|
+
if (!this.checkCapability(capability)) {
|
|
154
|
+
throw new Error(`Capability denied: ${capability}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Grant additional capability
|
|
160
|
+
*
|
|
161
|
+
* @param {string} capability - Capability to grant
|
|
162
|
+
* @throws {Error} If capability is blocked
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* isolation.grantCapability('custom:action');
|
|
166
|
+
*/
|
|
167
|
+
grantCapability(capability) {
|
|
168
|
+
CapabilitySchema.parse(capability);
|
|
169
|
+
|
|
170
|
+
if (this.blocklist.has(capability)) {
|
|
171
|
+
throw new Error(`Cannot grant blocked capability: ${capability}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.whitelist.add(capability);
|
|
175
|
+
this._logAccess(capability, true, 'granted');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Revoke a capability
|
|
180
|
+
*
|
|
181
|
+
* @param {string} capability - Capability to revoke
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* isolation.revokeCapability('custom:action');
|
|
185
|
+
*/
|
|
186
|
+
revokeCapability(capability) {
|
|
187
|
+
this.whitelist.delete(capability);
|
|
188
|
+
this._logAccess(capability, false, 'revoked');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get whitelisted capabilities
|
|
193
|
+
*
|
|
194
|
+
* @returns {string[]} Array of whitelisted capabilities
|
|
195
|
+
*/
|
|
196
|
+
getWhitelist() {
|
|
197
|
+
return Array.from(this.whitelist);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get blocked capabilities
|
|
202
|
+
*
|
|
203
|
+
* @returns {string[]} Array of blocked capabilities
|
|
204
|
+
*/
|
|
205
|
+
getBlocklist() {
|
|
206
|
+
return Array.from(this.blocklist);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get access log
|
|
211
|
+
*
|
|
212
|
+
* @param {Object} filters - Optional filters
|
|
213
|
+
* @param {string} filters.capability - Filter by capability
|
|
214
|
+
* @param {boolean} filters.allowed - Filter by allowed status
|
|
215
|
+
* @returns {Array<Object>} Filtered access log
|
|
216
|
+
*/
|
|
217
|
+
getAccessLog(filters = {}) {
|
|
218
|
+
let log = this.accessLog;
|
|
219
|
+
|
|
220
|
+
if (filters.capability) {
|
|
221
|
+
log = log.filter(entry => entry.capability === filters.capability);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (filters.allowed !== undefined) {
|
|
225
|
+
log = log.filter(entry => entry.allowed === filters.allowed);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return log;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get usage statistics
|
|
233
|
+
*
|
|
234
|
+
* @returns {Object} Usage stats by capability
|
|
235
|
+
*/
|
|
236
|
+
getUsageStats() {
|
|
237
|
+
return Object.fromEntries(this.usageStats);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Create isolated execution context
|
|
242
|
+
*
|
|
243
|
+
* @param {Function} fn - Function to execute
|
|
244
|
+
* @param {Array<string>} requiredCapabilities - Required capabilities
|
|
245
|
+
* @returns {Promise<any>} Execution result
|
|
246
|
+
* @throws {Error} If any required capability is denied
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* const result = await isolation.executeIsolated(
|
|
250
|
+
* async () => { return { data: 42 }; },
|
|
251
|
+
* ['receipt:generate']
|
|
252
|
+
* );
|
|
253
|
+
*/
|
|
254
|
+
async executeIsolated(fn, requiredCapabilities = []) {
|
|
255
|
+
// Check all required capabilities upfront
|
|
256
|
+
for (const capability of requiredCapabilities) {
|
|
257
|
+
this.requestCapability(capability);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const startTime = Date.now();
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Execute with timeout
|
|
264
|
+
const result = await this._executeWithTimeout(fn, this.config.maxExecutionTime);
|
|
265
|
+
|
|
266
|
+
const duration = Date.now() - startTime;
|
|
267
|
+
|
|
268
|
+
// Log execution
|
|
269
|
+
this._logExecution(requiredCapabilities, true, duration);
|
|
270
|
+
|
|
271
|
+
return result;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const duration = Date.now() - startTime;
|
|
274
|
+
this._logExecution(requiredCapabilities, false, duration, error.message);
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Validate plugin manifest capabilities
|
|
281
|
+
*
|
|
282
|
+
* @param {Array<string>} requestedCapabilities - Capabilities from plugin manifest
|
|
283
|
+
* @returns {Object} Validation result { allowed: [], denied: [], blocked: [] }
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* const validation = isolation.validatePluginCapabilities([
|
|
287
|
+
* 'receipt:generate',
|
|
288
|
+
* 'filesystem:write'
|
|
289
|
+
* ]);
|
|
290
|
+
* console.log(validation.denied); // ['filesystem:write']
|
|
291
|
+
*/
|
|
292
|
+
validatePluginCapabilities(requestedCapabilities) {
|
|
293
|
+
const allowed = [];
|
|
294
|
+
const denied = [];
|
|
295
|
+
const blocked = [];
|
|
296
|
+
|
|
297
|
+
for (const capability of requestedCapabilities) {
|
|
298
|
+
if (this.blocklist.has(capability)) {
|
|
299
|
+
blocked.push(capability);
|
|
300
|
+
} else if (this.whitelist.has(capability)) {
|
|
301
|
+
allowed.push(capability);
|
|
302
|
+
} else {
|
|
303
|
+
denied.push(capability);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { allowed, denied, blocked };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ===== Private Methods =====
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Log capability access
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
_logAccess(capability, allowed, reason) {
|
|
317
|
+
this.accessLog.push({
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
capability,
|
|
320
|
+
allowed,
|
|
321
|
+
reason,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Update usage stats
|
|
325
|
+
if (allowed) {
|
|
326
|
+
const count = this.usageStats.get(capability) || 0;
|
|
327
|
+
this.usageStats.set(capability, count + 1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Log execution
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
_logExecution(capabilities, success, duration, error = null) {
|
|
336
|
+
this.accessLog.push({
|
|
337
|
+
timestamp: Date.now(),
|
|
338
|
+
type: 'execution',
|
|
339
|
+
capabilities,
|
|
340
|
+
success,
|
|
341
|
+
duration,
|
|
342
|
+
error,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Execute function with timeout
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
async _executeWithTimeout(fn, timeout) {
|
|
351
|
+
return Promise.race([
|
|
352
|
+
fn(),
|
|
353
|
+
new Promise((_, reject) =>
|
|
354
|
+
setTimeout(() => reject(new Error('Execution timeout')), timeout)
|
|
355
|
+
),
|
|
356
|
+
]);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Create a new plugin isolation manager
|
|
362
|
+
*
|
|
363
|
+
* @param {Object} config - Sandbox configuration
|
|
364
|
+
* @returns {PluginIsolation} New isolation manager
|
|
365
|
+
*/
|
|
366
|
+
export function createPluginIsolation(config = {}) {
|
|
367
|
+
return new PluginIsolation(config);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Create a public API proxy that only exposes whitelisted methods
|
|
372
|
+
*
|
|
373
|
+
* @param {Object} api - Full API object
|
|
374
|
+
* @param {Array<string>} whitelist - Whitelisted method names
|
|
375
|
+
* @returns {Object} Proxy object with only whitelisted methods
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* const publicAPI = createPublicAPI(fullAPI, ['generate', 'validate']);
|
|
379
|
+
*/
|
|
380
|
+
export function createPublicAPI(api, whitelist) {
|
|
381
|
+
const proxy = {};
|
|
382
|
+
|
|
383
|
+
for (const method of whitelist) {
|
|
384
|
+
if (typeof api[method] === 'function') {
|
|
385
|
+
proxy[method] = api[method].bind(api);
|
|
386
|
+
} else if (api[method] !== undefined) {
|
|
387
|
+
proxy[method] = api[method];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return Object.freeze(proxy);
|
|
392
|
+
}
|