@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.
- package/README.md +414 -0
- package/package.json +81 -0
- package/src/agents/index.mjs +1402 -0
- package/src/artifact.mjs +405 -0
- package/src/cli.mjs +932 -0
- package/src/config.mjs +115 -0
- package/src/guards.mjs +1213 -0
- package/src/index.mjs +347 -0
- package/src/merge.mjs +196 -0
- package/src/observation.mjs +193 -0
- package/src/orchestrator.mjs +315 -0
- package/src/probe.mjs +58 -0
- package/src/probes/CONCURRENCY-PROBE.md +256 -0
- package/src/probes/README.md +275 -0
- package/src/probes/concurrency.mjs +1175 -0
- package/src/probes/filesystem.mjs +731 -0
- package/src/probes/filesystem.test.mjs +244 -0
- package/src/probes/network.mjs +503 -0
- package/src/probes/performance.mjs +816 -0
- package/src/probes/persistence.mjs +785 -0
- package/src/probes/runtime.mjs +589 -0
- package/src/probes/tooling.mjs +454 -0
- package/src/probes/tooling.test.mjs +372 -0
- package/src/probes/verify-execution.mjs +131 -0
- package/src/probes/verify-guards.mjs +73 -0
- package/src/probes/wasm.mjs +715 -0
- package/src/receipt.mjs +197 -0
- package/src/receipts/index.mjs +813 -0
- package/src/reporter.example.mjs +223 -0
- package/src/reporter.mjs +555 -0
- package/src/reporters/markdown.mjs +355 -0
- package/src/reporters/rdf.mjs +383 -0
- package/src/storage/index.mjs +827 -0
- package/src/types.mjs +1028 -0
- package/src/utils/errors.mjs +397 -0
- package/src/utils/index.mjs +32 -0
- package/src/utils/logger.mjs +236 -0
- package/src/vocabulary.ttl +169 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tooling Surface Probe - Safe API-based tool detection
|
|
3
|
+
* @module @unrdf/kgc-probe/probes/tooling
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: This probe uses ONLY safe APIs with explicit allowlisting.
|
|
6
|
+
* NO arbitrary command execution. All commands timeout at 5s.
|
|
7
|
+
*
|
|
8
|
+
* Allowlist: ['git', 'node', 'npm', 'pnpm', 'which']
|
|
9
|
+
*
|
|
10
|
+
* @agent Agent 8 - Tooling Surface Probe (KGC Probe Swarm)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFile } from 'node:child_process';
|
|
14
|
+
import { promisify } from 'node:util';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// SCHEMAS
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Observation schema - represents a single probe measurement
|
|
25
|
+
* @typedef {Object} Observation
|
|
26
|
+
* @property {string} method - Probe method identifier (e.g., "tooling.git_version")
|
|
27
|
+
* @property {Record<string, any>} inputs - Input parameters used for probing
|
|
28
|
+
* @property {Record<string, any>} outputs - Observed outputs/measurements
|
|
29
|
+
* @property {string} [guardDecision] - Guard decision: "allowed", "denied", "unknown"
|
|
30
|
+
* @property {Record<string, any>} [metadata] - Additional metadata
|
|
31
|
+
*/
|
|
32
|
+
const ObservationSchema = z.object({
|
|
33
|
+
method: z.string().min(1),
|
|
34
|
+
inputs: z.record(z.any()),
|
|
35
|
+
outputs: z.record(z.any()),
|
|
36
|
+
guardDecision: z.enum(['allowed', 'denied', 'unknown']).optional(),
|
|
37
|
+
metadata: z.record(z.any()).optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Probe configuration schema
|
|
42
|
+
* @typedef {Object} ProbeConfig
|
|
43
|
+
* @property {number} [timeout] - Command timeout in milliseconds (default: 5000)
|
|
44
|
+
* @property {boolean} [strict] - Strict mode - fail on any error (default: false)
|
|
45
|
+
*/
|
|
46
|
+
const ProbeConfigSchema = z.object({
|
|
47
|
+
timeout: z.number().int().positive().max(10000).default(5000),
|
|
48
|
+
strict: z.boolean().default(false),
|
|
49
|
+
}).default({});
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// GUARD: COMMAND ALLOWLIST (POKA-YOKE)
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* CRITICAL: Allowlisted commands - ONLY these can be executed
|
|
57
|
+
* NO shell metacharacters. NO arbitrary commands.
|
|
58
|
+
*/
|
|
59
|
+
const ALLOWED_COMMANDS = new Set(['git', 'node', 'npm', 'pnpm', 'which']);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Guard function - validates command is allowlisted
|
|
63
|
+
* @param {string} command - Command to validate
|
|
64
|
+
* @returns {boolean} True if allowed, false otherwise
|
|
65
|
+
*/
|
|
66
|
+
function isCommandAllowed(command) {
|
|
67
|
+
return ALLOWED_COMMANDS.has(command);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validates command arguments for shell metacharacters
|
|
72
|
+
* @param {string[]} args - Command arguments
|
|
73
|
+
* @returns {boolean} True if safe, false if potentially dangerous
|
|
74
|
+
*/
|
|
75
|
+
function argsAreSafe(args) {
|
|
76
|
+
// No shell metacharacters allowed
|
|
77
|
+
const dangerous = /[;&|`$<>(){}[\]\\'"]/;
|
|
78
|
+
return args.every(arg => !dangerous.test(arg));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// SAFE COMMAND EXECUTION
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Executes a command with strict safety guards
|
|
87
|
+
* @param {string} command - Command name (must be allowlisted)
|
|
88
|
+
* @param {string[]} args - Command arguments (no shell metacharacters)
|
|
89
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
90
|
+
* @returns {Promise<{stdout: string, stderr: string, success: boolean, guardDecision: string}>}
|
|
91
|
+
*/
|
|
92
|
+
async function safeExec(command, args, timeout) {
|
|
93
|
+
// Guard 1: Command allowlist check
|
|
94
|
+
if (!isCommandAllowed(command)) {
|
|
95
|
+
return {
|
|
96
|
+
stdout: '',
|
|
97
|
+
stderr: `Command '${command}' not in allowlist`,
|
|
98
|
+
success: false,
|
|
99
|
+
guardDecision: 'denied',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Guard 2: Argument safety check
|
|
104
|
+
if (!argsAreSafe(args)) {
|
|
105
|
+
return {
|
|
106
|
+
stdout: '',
|
|
107
|
+
stderr: 'Arguments contain shell metacharacters',
|
|
108
|
+
success: false,
|
|
109
|
+
guardDecision: 'denied',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Execute with timeout (no shell, no stdin, capture stdout/stderr only)
|
|
114
|
+
try {
|
|
115
|
+
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
116
|
+
timeout,
|
|
117
|
+
maxBuffer: 1024 * 1024, // 1MB max output
|
|
118
|
+
shell: false, // CRITICAL: NO shell execution
|
|
119
|
+
windowsHide: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
stdout: stdout.trim(),
|
|
124
|
+
stderr: stderr.trim(),
|
|
125
|
+
success: true,
|
|
126
|
+
guardDecision: 'allowed',
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Command failed or timed out
|
|
130
|
+
return {
|
|
131
|
+
stdout: error.stdout?.trim() || '',
|
|
132
|
+
stderr: error.stderr?.trim() || error.message,
|
|
133
|
+
success: false,
|
|
134
|
+
guardDecision: error.code === 'ETIMEDOUT' ? 'unknown' : 'allowed',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// TOOL DETECTION
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Probes for git availability and version
|
|
145
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
146
|
+
* @returns {Promise<Observation>}
|
|
147
|
+
*/
|
|
148
|
+
async function probeGit(timeout) {
|
|
149
|
+
const result = await safeExec('git', ['--version'], timeout);
|
|
150
|
+
|
|
151
|
+
const outputs = {};
|
|
152
|
+
const metadata = {};
|
|
153
|
+
|
|
154
|
+
if (result.success && result.stdout) {
|
|
155
|
+
// Parse version: "git version 2.34.1" -> "2.34.1"
|
|
156
|
+
const match = result.stdout.match(/git version ([\d.]+)/);
|
|
157
|
+
if (match) {
|
|
158
|
+
outputs.version = match[1];
|
|
159
|
+
outputs.available = true;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
outputs.available = false;
|
|
163
|
+
metadata.error = result.stderr || 'Command failed';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
method: 'tooling.git_version',
|
|
168
|
+
inputs: { command: 'git', args: ['--version'] },
|
|
169
|
+
outputs,
|
|
170
|
+
guardDecision: result.guardDecision,
|
|
171
|
+
metadata,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Probes for Node.js availability and version
|
|
177
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
178
|
+
* @returns {Promise<Observation>}
|
|
179
|
+
*/
|
|
180
|
+
async function probeNode(timeout) {
|
|
181
|
+
const result = await safeExec('node', ['--version'], timeout);
|
|
182
|
+
|
|
183
|
+
const outputs = {};
|
|
184
|
+
const metadata = {};
|
|
185
|
+
|
|
186
|
+
if (result.success && result.stdout) {
|
|
187
|
+
// Parse version: "v18.17.0" -> "18.17.0"
|
|
188
|
+
const version = result.stdout.replace(/^v/, '');
|
|
189
|
+
outputs.version = version;
|
|
190
|
+
outputs.available = true;
|
|
191
|
+
} else {
|
|
192
|
+
outputs.available = false;
|
|
193
|
+
metadata.error = result.stderr || 'Command failed';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
method: 'tooling.node_version',
|
|
198
|
+
inputs: { command: 'node', args: ['--version'] },
|
|
199
|
+
outputs,
|
|
200
|
+
guardDecision: result.guardDecision,
|
|
201
|
+
metadata,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Probes for npm availability and version
|
|
207
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
208
|
+
* @returns {Promise<Observation>}
|
|
209
|
+
*/
|
|
210
|
+
async function probeNpm(timeout) {
|
|
211
|
+
const result = await safeExec('npm', ['--version'], timeout);
|
|
212
|
+
|
|
213
|
+
const outputs = {};
|
|
214
|
+
const metadata = {};
|
|
215
|
+
|
|
216
|
+
if (result.success && result.stdout) {
|
|
217
|
+
outputs.version = result.stdout;
|
|
218
|
+
outputs.available = true;
|
|
219
|
+
} else {
|
|
220
|
+
outputs.available = false;
|
|
221
|
+
metadata.error = result.stderr || 'Command failed';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
method: 'tooling.npm_version',
|
|
226
|
+
inputs: { command: 'npm', args: ['--version'] },
|
|
227
|
+
outputs,
|
|
228
|
+
guardDecision: result.guardDecision,
|
|
229
|
+
metadata,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Probes for pnpm availability and version
|
|
235
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
236
|
+
* @returns {Promise<Observation>}
|
|
237
|
+
*/
|
|
238
|
+
async function probePnpm(timeout) {
|
|
239
|
+
const result = await safeExec('pnpm', ['--version'], timeout);
|
|
240
|
+
|
|
241
|
+
const outputs = {};
|
|
242
|
+
const metadata = {};
|
|
243
|
+
|
|
244
|
+
if (result.success && result.stdout) {
|
|
245
|
+
outputs.version = result.stdout;
|
|
246
|
+
outputs.available = true;
|
|
247
|
+
} else {
|
|
248
|
+
outputs.available = false;
|
|
249
|
+
metadata.error = result.stderr || 'Command failed';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
method: 'tooling.pnpm_version',
|
|
254
|
+
inputs: { command: 'pnpm', args: ['--version'] },
|
|
255
|
+
outputs,
|
|
256
|
+
guardDecision: result.guardDecision,
|
|
257
|
+
metadata,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Probes for shell availability
|
|
263
|
+
* Uses 'which' to detect shell binaries (sh, bash)
|
|
264
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
265
|
+
* @returns {Promise<Observation[]>}
|
|
266
|
+
*/
|
|
267
|
+
async function probeShells(timeout) {
|
|
268
|
+
const shells = ['sh', 'bash'];
|
|
269
|
+
const observations = [];
|
|
270
|
+
|
|
271
|
+
for (const shell of shells) {
|
|
272
|
+
const result = await safeExec('which', [shell], timeout);
|
|
273
|
+
|
|
274
|
+
const outputs = {};
|
|
275
|
+
const metadata = {};
|
|
276
|
+
|
|
277
|
+
if (result.success && result.stdout) {
|
|
278
|
+
outputs.path = result.stdout;
|
|
279
|
+
outputs.available = true;
|
|
280
|
+
} else {
|
|
281
|
+
outputs.available = false;
|
|
282
|
+
metadata.reason = result.stderr || 'Not found';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
observations.push({
|
|
286
|
+
method: `tooling.shell_${shell}`,
|
|
287
|
+
inputs: { command: 'which', args: [shell] },
|
|
288
|
+
outputs,
|
|
289
|
+
guardDecision: result.guardDecision,
|
|
290
|
+
metadata,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return observations;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Probes for build tools (make, cmake)
|
|
299
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
300
|
+
* @returns {Promise<Observation[]>}
|
|
301
|
+
*/
|
|
302
|
+
async function probeBuildTools(timeout) {
|
|
303
|
+
// NOTE: make and cmake are NOT in allowlist
|
|
304
|
+
// Return observations with guardDecision: "denied"
|
|
305
|
+
const tools = ['make', 'cmake'];
|
|
306
|
+
const observations = [];
|
|
307
|
+
|
|
308
|
+
for (const tool of tools) {
|
|
309
|
+
observations.push({
|
|
310
|
+
method: `tooling.build_${tool}`,
|
|
311
|
+
inputs: { command: tool },
|
|
312
|
+
outputs: { available: false },
|
|
313
|
+
guardDecision: 'denied',
|
|
314
|
+
metadata: { reason: 'not_in_allowlist' },
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return observations;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Detects package manager in use
|
|
323
|
+
* Checks for lock files and tool availability
|
|
324
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
325
|
+
* @returns {Promise<Observation>}
|
|
326
|
+
*/
|
|
327
|
+
async function detectPackageManager(timeout) {
|
|
328
|
+
const outputs = {};
|
|
329
|
+
const metadata = {};
|
|
330
|
+
|
|
331
|
+
// Check npm
|
|
332
|
+
const npmResult = await safeExec('npm', ['--version'], timeout);
|
|
333
|
+
if (npmResult.success) {
|
|
334
|
+
outputs.npm = { available: true, version: npmResult.stdout };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check pnpm
|
|
338
|
+
const pnpmResult = await safeExec('pnpm', ['--version'], timeout);
|
|
339
|
+
if (pnpmResult.success) {
|
|
340
|
+
outputs.pnpm = { available: true, version: pnpmResult.stdout };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Determine primary (prefer pnpm if both available)
|
|
344
|
+
if (outputs.pnpm?.available) {
|
|
345
|
+
outputs.primary = 'pnpm';
|
|
346
|
+
} else if (outputs.npm?.available) {
|
|
347
|
+
outputs.primary = 'npm';
|
|
348
|
+
} else {
|
|
349
|
+
outputs.primary = 'none';
|
|
350
|
+
metadata.warning = 'No package manager detected';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
method: 'tooling.package_manager',
|
|
355
|
+
inputs: {},
|
|
356
|
+
outputs,
|
|
357
|
+
guardDecision: 'allowed',
|
|
358
|
+
metadata,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// MAIN PROBE FUNCTION
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Probes tooling surface using ONLY safe APIs
|
|
368
|
+
*
|
|
369
|
+
* Returns observations for:
|
|
370
|
+
* - Available CLI tools (git, node, npm, pnpm)
|
|
371
|
+
* - Tool versions
|
|
372
|
+
* - Shell availability (sh, bash)
|
|
373
|
+
* - Package manager detection
|
|
374
|
+
* - Build tool availability (denied due to allowlist)
|
|
375
|
+
*
|
|
376
|
+
* GUARD CONSTRAINTS:
|
|
377
|
+
* - ONLY allowlisted commands: ['git', 'node', 'npm', 'pnpm', 'which']
|
|
378
|
+
* - NO shell metacharacters
|
|
379
|
+
* - Each command: timeout 5s (default), no stdin, capture stdout only
|
|
380
|
+
* - If execution unavailable: returns observations with guardDecision: "unknown"
|
|
381
|
+
*
|
|
382
|
+
* @param {ProbeConfig} [config] - Probe configuration
|
|
383
|
+
* @returns {Promise<Observation[]>} Array of observations
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* const observations = await probeTooling({ timeout: 5000 });
|
|
387
|
+
* observations.forEach(obs => {
|
|
388
|
+
* console.log(`${obs.method}: ${JSON.stringify(obs.outputs)}`);
|
|
389
|
+
* });
|
|
390
|
+
*/
|
|
391
|
+
export async function probeTooling(config = {}) {
|
|
392
|
+
// Validate config
|
|
393
|
+
const validatedConfig = ProbeConfigSchema.parse(config);
|
|
394
|
+
const { timeout } = validatedConfig;
|
|
395
|
+
|
|
396
|
+
const observations = [];
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
// Probe all tools in parallel
|
|
400
|
+
const [
|
|
401
|
+
gitObs,
|
|
402
|
+
nodeObs,
|
|
403
|
+
npmObs,
|
|
404
|
+
pnpmObs,
|
|
405
|
+
shellObs,
|
|
406
|
+
buildObs,
|
|
407
|
+
pkgMgrObs,
|
|
408
|
+
] = await Promise.all([
|
|
409
|
+
probeGit(timeout),
|
|
410
|
+
probeNode(timeout),
|
|
411
|
+
probeNpm(timeout),
|
|
412
|
+
probePnpm(timeout),
|
|
413
|
+
probeShells(timeout),
|
|
414
|
+
probeBuildTools(timeout),
|
|
415
|
+
detectPackageManager(timeout),
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
// Collect all observations
|
|
419
|
+
observations.push(gitObs, nodeObs, npmObs, pnpmObs);
|
|
420
|
+
observations.push(...shellObs);
|
|
421
|
+
observations.push(...buildObs);
|
|
422
|
+
observations.push(pkgMgrObs);
|
|
423
|
+
|
|
424
|
+
} catch (error) {
|
|
425
|
+
// Process execution unavailable or catastrophic failure
|
|
426
|
+
observations.push({
|
|
427
|
+
method: 'tooling.execution_error',
|
|
428
|
+
inputs: {},
|
|
429
|
+
outputs: {},
|
|
430
|
+
guardDecision: 'unknown',
|
|
431
|
+
metadata: {
|
|
432
|
+
reason: 'process_execution_unavailable',
|
|
433
|
+
error: error.message,
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Validate all observations
|
|
439
|
+
return observations.map(obs => ObservationSchema.parse(obs));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// EXPORTS
|
|
444
|
+
// ============================================================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Re-export schemas for external validation
|
|
448
|
+
*/
|
|
449
|
+
export { ObservationSchema, ProbeConfigSchema };
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Re-export guard functions for testing
|
|
453
|
+
*/
|
|
454
|
+
export { isCommandAllowed, argsAreSafe, safeExec };
|