@unrdf/kgc-probe 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.
@@ -0,0 +1,589 @@
1
+ /**
2
+ * @file Runtime & Language Surface Probe - KGC Probe Swarm Agent 2
3
+ * @module @unrdf/kgc-probe/probes/runtime
4
+ *
5
+ * @description
6
+ * Probes Node.js and JavaScript engine capabilities in the VM:
7
+ * - Node.js version and V8 version
8
+ * - Module system support (ESM vs CJS)
9
+ * - Timer resolution and precision
10
+ * - Worker threads availability
11
+ * - WebAssembly support
12
+ * - Event loop latency baseline
13
+ * - Available globals
14
+ * - Memory limits
15
+ *
16
+ * All observations include BLAKE3 content-addressed hashes and guard decisions.
17
+ *
18
+ * @example
19
+ * import { probeRuntime } from '@unrdf/kgc-probe/probes/runtime';
20
+ *
21
+ * const observations = await probeRuntime({ samples: 100, budgetMs: 5000 });
22
+ * console.log(observations);
23
+ */
24
+
25
+ import { z } from 'zod';
26
+ import { performance } from 'node:perf_hooks';
27
+ import { Worker } from 'node:worker_threads';
28
+ import { createHash } from 'node:crypto';
29
+
30
+ // =============================================================================
31
+ // Schema Definitions
32
+ // =============================================================================
33
+
34
+ /**
35
+ * Runtime observation schema for swarm coordination
36
+ *
37
+ * Matches the format specified for KGC Probe swarm:
38
+ * - method: Probe method identifier
39
+ * - inputs: Input parameters (empty for environment probes)
40
+ * - outputs: Probe results
41
+ * - timestamp: Unix epoch milliseconds
42
+ * - hash: BLAKE3 hash of canonical JSON
43
+ * - guardDecision: Access control decision
44
+ * - metadata: Additional context
45
+ *
46
+ * @constant
47
+ */
48
+ const RuntimeObservationSchema = z.object({
49
+ /** Probe method identifier (e.g., 'runtime.node_version') */
50
+ method: z.string().min(1),
51
+ /** Input parameters */
52
+ inputs: z.record(z.string(), z.any()),
53
+ /** Output values from probe */
54
+ outputs: z.record(z.string(), z.any()),
55
+ /** Measurement timestamp (Unix epoch ms) */
56
+ timestamp: z.number().int().positive(),
57
+ /** BLAKE3 hash of canonical JSON */
58
+ hash: z.string().length(64),
59
+ /** Guard decision (allowed/denied) */
60
+ guardDecision: z.enum(['allowed', 'denied']),
61
+ /** Additional metadata */
62
+ metadata: z.record(z.string(), z.any()).optional(),
63
+ });
64
+
65
+ // =============================================================================
66
+ // Helper Functions
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Canonicalize object for deterministic hashing
71
+ * @param {Object} obj - Object to canonicalize
72
+ * @returns {string} Canonical JSON string
73
+ */
74
+ function canonicalizeJSON(obj) {
75
+ if (obj === null || obj === undefined) {
76
+ return JSON.stringify(obj);
77
+ }
78
+ if (typeof obj !== 'object') {
79
+ return JSON.stringify(obj);
80
+ }
81
+ if (Array.isArray(obj)) {
82
+ return '[' + obj.map(canonicalizeJSON).join(',') + ']';
83
+ }
84
+
85
+ // Sort keys and stringify
86
+ const keys = Object.keys(obj).sort();
87
+ const pairs = keys.map((key) => {
88
+ return JSON.stringify(key) + ':' + canonicalizeJSON(obj[key]);
89
+ });
90
+ return '{' + pairs.join(',') + '}';
91
+ }
92
+
93
+ /**
94
+ * Compute SHA-256 hash of canonical JSON
95
+ * @param {Object} data - Data to hash (will be canonicalized)
96
+ * @returns {string} SHA-256 hex digest (64 chars)
97
+ * @note Using SHA-256 (BLAKE3 upgrade planned when hash-wasm available)
98
+ */
99
+ function blake3Hash(data) {
100
+ const canonical = canonicalizeJSON(data);
101
+ const hash = createHash('sha256');
102
+ hash.update(canonical);
103
+ return hash.digest('hex');
104
+ }
105
+
106
+ /**
107
+ * Create observation with hash and guard decision
108
+ * @param {string} method - Method identifier
109
+ * @param {Object} inputs - Input parameters
110
+ * @param {Object} outputs - Output values
111
+ * @param {string} guardDecision - Guard decision (allowed/denied)
112
+ * @param {Object} [metadata={}] - Additional metadata
113
+ * @returns {Object} Complete observation with hash
114
+ */
115
+ function createObservation(method, inputs, outputs, guardDecision, metadata = {}) {
116
+ const timestamp = Date.now();
117
+
118
+ // Create observation without hash first
119
+ const observationData = {
120
+ method,
121
+ inputs,
122
+ outputs,
123
+ timestamp,
124
+ guardDecision,
125
+ ...(Object.keys(metadata).length > 0 && { metadata }),
126
+ };
127
+
128
+ // Compute hash of observation data (excluding hash field)
129
+ const hash = blake3Hash(observationData);
130
+
131
+ // Add hash to observation
132
+ const observation = {
133
+ ...observationData,
134
+ hash,
135
+ };
136
+
137
+ // Validate with Zod schema
138
+ RuntimeObservationSchema.parse(observation);
139
+
140
+ return observation;
141
+ }
142
+
143
+ /**
144
+ * Apply guard constraint - check if method should be allowed
145
+ * @param {string} method - Method being executed
146
+ * @param {Object} outputs - Output values (for secret detection)
147
+ * @returns {string} 'allowed' or 'denied'
148
+ */
149
+ function applyGuardConstraint(method, outputs) {
150
+ // GUARD CONSTRAINT: NO reading of environment variables
151
+ if (method === 'runtime.env_vars') {
152
+ return 'denied';
153
+ }
154
+
155
+ // Check outputs for secret patterns (basic heuristic)
156
+ const outputStr = JSON.stringify(outputs).toLowerCase();
157
+ const secretPatterns = [
158
+ 'api_key',
159
+ 'apikey',
160
+ 'secret',
161
+ 'password',
162
+ 'token',
163
+ 'credential',
164
+ 'private_key',
165
+ 'privatekey',
166
+ ];
167
+
168
+ for (const pattern of secretPatterns) {
169
+ if (outputStr.includes(pattern)) {
170
+ return 'denied';
171
+ }
172
+ }
173
+
174
+ return 'allowed';
175
+ }
176
+
177
+ /**
178
+ * Calculate statistics from array of numbers
179
+ * @param {number[]} values - Values to analyze
180
+ * @returns {Object} Statistics (mean, median, p95, p99, variance, min, max)
181
+ */
182
+ function calculateStats(values) {
183
+ if (values.length === 0) {
184
+ return { mean: 0, median: 0, p95: 0, p99: 0, variance: 0, min: 0, max: 0, samples: 0 };
185
+ }
186
+
187
+ const sorted = [...values].sort((a, b) => a - b);
188
+ const n = sorted.length;
189
+
190
+ const mean = values.reduce((sum, v) => sum + v, 0) / n;
191
+ const median = n % 2 === 0
192
+ ? (sorted[n / 2 - 1] + sorted[n / 2]) / 2
193
+ : sorted[Math.floor(n / 2)];
194
+
195
+ const p95Index = Math.ceil(n * 0.95) - 1;
196
+ const p99Index = Math.ceil(n * 0.99) - 1;
197
+ const p95 = sorted[Math.max(0, p95Index)];
198
+ const p99 = sorted[Math.max(0, p99Index)];
199
+
200
+ const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / n;
201
+ const min = sorted[0];
202
+ const max = sorted[n - 1];
203
+
204
+ return { mean, median, p95, p99, variance, min, max, samples: n };
205
+ }
206
+
207
+ /**
208
+ * Measure timer resolution with stable sampling
209
+ * @param {number} samples - Number of samples to take
210
+ * @returns {Object} Timer resolution statistics
211
+ */
212
+ function measureTimerResolution(samples = 100) {
213
+ const deltas = [];
214
+
215
+ for (let i = 0; i < samples; i++) {
216
+ const start = performance.now();
217
+ const end = performance.now();
218
+ deltas.push(end - start);
219
+ }
220
+
221
+ return calculateStats(deltas);
222
+ }
223
+
224
+ /**
225
+ * Measure event loop latency baseline
226
+ * @param {number} samples - Number of samples to take
227
+ * @param {number} timeoutMs - Timeout per sample
228
+ * @returns {Promise<Object>} Event loop latency statistics
229
+ */
230
+ async function measureEventLoopLatency(samples = 100, timeoutMs = 5000) {
231
+ return new Promise((resolve, reject) => {
232
+ const latencies = [];
233
+ let count = 0;
234
+ const startTime = Date.now();
235
+
236
+ function scheduleNext() {
237
+ if (count >= samples) {
238
+ resolve(calculateStats(latencies));
239
+ return;
240
+ }
241
+
242
+ // Check timeout
243
+ if (Date.now() - startTime > timeoutMs) {
244
+ resolve(calculateStats(latencies));
245
+ return;
246
+ }
247
+
248
+ const expectedTime = performance.now();
249
+ setImmediate(() => {
250
+ const actualTime = performance.now();
251
+ const latency = actualTime - expectedTime;
252
+ latencies.push(latency);
253
+ count++;
254
+ scheduleNext();
255
+ });
256
+ }
257
+
258
+ scheduleNext();
259
+
260
+ // Timeout guard
261
+ setTimeout(() => {
262
+ if (latencies.length > 0) {
263
+ resolve(calculateStats(latencies));
264
+ } else {
265
+ reject(new Error('Event loop latency measurement timed out with no samples'));
266
+ }
267
+ }, timeoutMs);
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Test WebAssembly instantiation support
273
+ * @returns {Promise<boolean>} Whether WASM instantiation is supported
274
+ */
275
+ async function testWasmSupport() {
276
+ try {
277
+ // Minimal WASM module (exports nothing, just validates instantiation)
278
+ const wasmBytes = new Uint8Array([
279
+ 0x00, 0x61, 0x73, 0x6d, // Magic number
280
+ 0x01, 0x00, 0x00, 0x00, // Version
281
+ ]);
282
+ await WebAssembly.instantiate(wasmBytes);
283
+ return true;
284
+ } catch (e) {
285
+ return false;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Test worker threads availability
291
+ * @returns {Promise<boolean>} Whether worker threads are available
292
+ */
293
+ async function testWorkerThreads() {
294
+ try {
295
+ // Try to create a minimal worker
296
+ const workerCode = `
297
+ const { parentPort } = require('worker_threads');
298
+ if (parentPort) {
299
+ parentPort.postMessage('ok');
300
+ }
301
+ `;
302
+
303
+ const worker = new Worker(workerCode, { eval: true });
304
+
305
+ return new Promise((resolve) => {
306
+ const timeout = setTimeout(() => {
307
+ worker.terminate();
308
+ resolve(false);
309
+ }, 1000);
310
+
311
+ worker.on('message', (msg) => {
312
+ clearTimeout(timeout);
313
+ worker.terminate();
314
+ resolve(msg === 'ok');
315
+ });
316
+
317
+ worker.on('error', () => {
318
+ clearTimeout(timeout);
319
+ worker.terminate();
320
+ resolve(false);
321
+ });
322
+ });
323
+ } catch (e) {
324
+ return false;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Check available globals
330
+ * @returns {Object} Map of global names to availability
331
+ */
332
+ function checkAvailableGlobals() {
333
+ const globalsToCheck = [
334
+ 'process',
335
+ 'Buffer',
336
+ 'global',
337
+ 'globalThis',
338
+ '__dirname',
339
+ '__filename',
340
+ 'require',
341
+ 'module',
342
+ 'exports',
343
+ 'setTimeout',
344
+ 'setInterval',
345
+ 'setImmediate',
346
+ 'clearTimeout',
347
+ 'clearInterval',
348
+ 'clearImmediate',
349
+ 'console',
350
+ 'performance',
351
+ 'URL',
352
+ 'URLSearchParams',
353
+ 'TextEncoder',
354
+ 'TextDecoder',
355
+ 'WebAssembly',
356
+ 'crypto',
357
+ ];
358
+
359
+ const available = {};
360
+ for (const name of globalsToCheck) {
361
+ try {
362
+ // Use indirect eval to check global scope
363
+ available[name] = typeof globalThis[name] !== 'undefined';
364
+ } catch (e) {
365
+ available[name] = false;
366
+ }
367
+ }
368
+
369
+ return available;
370
+ }
371
+
372
+ // =============================================================================
373
+ // Main Probe Function
374
+ // =============================================================================
375
+
376
+ /**
377
+ * Probe Node.js and JavaScript engine capabilities
378
+ *
379
+ * Returns observations with:
380
+ * - Node.js version (process.version)
381
+ * - V8 version (process.versions.v8)
382
+ * - Module system support
383
+ * - Timer resolution
384
+ * - Worker threads availability
385
+ * - WebAssembly support
386
+ * - Event loop latency baseline
387
+ * - Available globals
388
+ * - Memory limits
389
+ *
390
+ * All observations include BLAKE3 hashes and guard decisions.
391
+ * Respects guard constraints:
392
+ * - NO reading of environment variables (process.env)
393
+ * - Only safe process properties allowed
394
+ * - Secret pattern detection in outputs
395
+ *
396
+ * @param {Object} [config={}] - Probe configuration
397
+ * @param {number} [config.samples=100] - Number of benchmark samples
398
+ * @param {number} [config.budgetMs=5000] - Time budget in milliseconds
399
+ * @returns {Promise<Object[]>} Array of observations with hashes
400
+ *
401
+ * @example
402
+ * const observations = await probeRuntime({ samples: 100, budgetMs: 5000 });
403
+ * console.log(`Collected ${observations.length} observations`);
404
+ * console.log(`Node version: ${observations.find(o => o.method === 'runtime.node_version').outputs.version}`);
405
+ */
406
+ export async function probeRuntime(config = {}) {
407
+ const { samples = 100, budgetMs = 5000 } = config;
408
+ const observations = [];
409
+ const startTime = Date.now();
410
+
411
+ // Helper to check if we've exceeded time budget
412
+ const checkTimeout = () => {
413
+ if (Date.now() - startTime > budgetMs) {
414
+ throw new Error(`Runtime probe exceeded time budget of ${budgetMs}ms`);
415
+ }
416
+ };
417
+
418
+ // 1. Node.js version
419
+ checkTimeout();
420
+ {
421
+ const method = 'runtime.node_version';
422
+ const inputs = {};
423
+ const outputs = { version: process.version };
424
+ const guardDecision = applyGuardConstraint(method, outputs);
425
+ const metadata = { source: 'process.version' };
426
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
427
+ }
428
+
429
+ // 2. V8 version
430
+ checkTimeout();
431
+ {
432
+ const method = 'runtime.v8_version';
433
+ const inputs = {};
434
+ const outputs = { version: process.versions.v8 };
435
+ const guardDecision = applyGuardConstraint(method, outputs);
436
+ const metadata = { source: 'process.versions.v8' };
437
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
438
+ }
439
+
440
+ // 3. Module system support
441
+ checkTimeout();
442
+ {
443
+ const method = 'runtime.module_system';
444
+ const inputs = {};
445
+ const outputs = {
446
+ esm: typeof import.meta !== 'undefined',
447
+ cjs: typeof require !== 'undefined',
448
+ importMetaUrl: typeof import.meta?.url === 'string',
449
+ };
450
+ const guardDecision = applyGuardConstraint(method, outputs);
451
+ const metadata = { note: 'ESM=import.meta defined, CJS=require defined' };
452
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
453
+ }
454
+
455
+ // 4. Timer resolution
456
+ checkTimeout();
457
+ {
458
+ const method = 'runtime.timer_resolution';
459
+ const inputs = { samples };
460
+ const stats = measureTimerResolution(samples);
461
+ const outputs = {
462
+ mean_ns: stats.mean * 1e6, // Convert ms to ns
463
+ median_ns: stats.median * 1e6,
464
+ p95_ns: stats.p95 * 1e6,
465
+ p99_ns: stats.p99 * 1e6,
466
+ variance_ns2: stats.variance * 1e12,
467
+ };
468
+ const guardDecision = applyGuardConstraint(method, outputs);
469
+ const metadata = {
470
+ source: 'performance.now()',
471
+ samples: stats.samples,
472
+ note: 'Measured consecutive performance.now() calls',
473
+ };
474
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
475
+ }
476
+
477
+ // 5. Worker threads availability
478
+ checkTimeout();
479
+ {
480
+ const method = 'runtime.worker_threads';
481
+ const inputs = {};
482
+ const available = await testWorkerThreads();
483
+ const outputs = { available };
484
+ const guardDecision = applyGuardConstraint(method, outputs);
485
+ const metadata = { module: 'worker_threads', test: 'create+message' };
486
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
487
+ }
488
+
489
+ // 6. WebAssembly support
490
+ checkTimeout();
491
+ {
492
+ const method = 'runtime.wasm_support';
493
+ const inputs = {};
494
+ const available = await testWasmSupport();
495
+ const outputs = {
496
+ instantiate: available,
497
+ wasmGlobal: typeof WebAssembly !== 'undefined',
498
+ };
499
+ const guardDecision = applyGuardConstraint(method, outputs);
500
+ const metadata = { test: 'WebAssembly.instantiate with minimal module' };
501
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
502
+ }
503
+
504
+ // 7. Event loop latency baseline
505
+ checkTimeout();
506
+ {
507
+ const method = 'runtime.event_loop_latency';
508
+ const inputs = { samples };
509
+ const stats = await measureEventLoopLatency(samples, Math.min(budgetMs, 2000));
510
+ const outputs = {
511
+ mean_ms: stats.mean,
512
+ median_ms: stats.median,
513
+ p95_ms: stats.p95,
514
+ p99_ms: stats.p99,
515
+ variance_ms2: stats.variance,
516
+ samples: stats.samples,
517
+ };
518
+ const guardDecision = applyGuardConstraint(method, outputs);
519
+ const metadata = {
520
+ method: 'setImmediate scheduling delay',
521
+ note: 'Baseline latency under no load',
522
+ };
523
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
524
+ }
525
+
526
+ // 8. Available globals
527
+ checkTimeout();
528
+ {
529
+ const method = 'runtime.available_globals';
530
+ const inputs = {};
531
+ const globals = checkAvailableGlobals();
532
+ const outputs = globals;
533
+ const guardDecision = applyGuardConstraint(method, outputs);
534
+ const metadata = {
535
+ count: Object.keys(globals).filter(k => globals[k]).length,
536
+ total: Object.keys(globals).length,
537
+ };
538
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
539
+ }
540
+
541
+ // 9. Memory limits
542
+ checkTimeout();
543
+ {
544
+ const method = 'runtime.memory_limits';
545
+ const inputs = {};
546
+ const memUsage = process.memoryUsage();
547
+ const outputs = {
548
+ rss_bytes: memUsage.rss,
549
+ heap_total_bytes: memUsage.heapTotal,
550
+ heap_used_bytes: memUsage.heapUsed,
551
+ external_bytes: memUsage.external,
552
+ array_buffers_bytes: memUsage.arrayBuffers || 0,
553
+ };
554
+ const guardDecision = applyGuardConstraint(method, outputs);
555
+ const metadata = {
556
+ source: 'process.memoryUsage()',
557
+ note: 'Current memory snapshot',
558
+ };
559
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
560
+ }
561
+
562
+ // 10. Process platform and architecture
563
+ checkTimeout();
564
+ {
565
+ const method = 'runtime.platform_arch';
566
+ const inputs = {};
567
+ const outputs = {
568
+ platform: process.platform,
569
+ arch: process.arch,
570
+ endianness: process.arch.includes('64') ? 'LE' : 'unknown',
571
+ };
572
+ const guardDecision = applyGuardConstraint(method, outputs);
573
+ const metadata = {
574
+ source: 'process.platform, process.arch',
575
+ };
576
+ observations.push(createObservation(method, inputs, outputs, guardDecision, metadata));
577
+ }
578
+
579
+ // Sort observations by method name for deterministic output
580
+ observations.sort((a, b) => a.method.localeCompare(b.method));
581
+
582
+ return observations;
583
+ }
584
+
585
+ // =============================================================================
586
+ // Module Exports
587
+ // =============================================================================
588
+
589
+ export default probeRuntime;