@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.
Files changed (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. 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
+ }