@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,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Persistence Probe - Output persistence and storage characteristics
|
|
3
|
+
* @module @unrdf/kgc-probe/probes/persistence
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Probes output persistence and storage characteristics:
|
|
7
|
+
* - Output persistence across runs (write, read, verify)
|
|
8
|
+
* - Quota behavior (write until failure, record limit)
|
|
9
|
+
* - Temp directory behavior (location, cleanup, permissions)
|
|
10
|
+
* - File locking semantics (if observable via flock or exclusive open)
|
|
11
|
+
* - Directory permissions (can create, rename, delete)
|
|
12
|
+
* - Atomic operations (rename, link)
|
|
13
|
+
* - Storage type detection (in-memory, disk, network if detectable)
|
|
14
|
+
*
|
|
15
|
+
* GUARD CONSTRAINTS:
|
|
16
|
+
* - ONLY write within config.out directory
|
|
17
|
+
* - NO access to system directories
|
|
18
|
+
* - Clean up test files after probing
|
|
19
|
+
* - Limit quota test to 100MB max
|
|
20
|
+
* - Timeout operations (5s per operation)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { promises as fs } from 'node:fs';
|
|
24
|
+
import { join, resolve, dirname } from 'node:path';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { ObservationSchema, ProbeConfigSchema } from '../types.mjs';
|
|
28
|
+
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = dirname(__filename);
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Guard Functions
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if path is within allowed output directory
|
|
38
|
+
* @param {string} path - Path to check
|
|
39
|
+
* @param {string} outDir - Allowed output directory
|
|
40
|
+
* @returns {Object} Guard decision
|
|
41
|
+
*/
|
|
42
|
+
function guardPathAccess(path, outDir) {
|
|
43
|
+
const resolvedPath = resolve(path);
|
|
44
|
+
const resolvedOutDir = resolve(outDir);
|
|
45
|
+
|
|
46
|
+
const allowed = resolvedPath.startsWith(resolvedOutDir);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
path: resolvedPath,
|
|
50
|
+
allowed,
|
|
51
|
+
reason: allowed
|
|
52
|
+
? 'Within config.out directory'
|
|
53
|
+
: `Outside config.out directory (${resolvedOutDir})`,
|
|
54
|
+
policy: 'output-only',
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create observation with guard decision
|
|
61
|
+
* @param {string} probeName - Name of probe
|
|
62
|
+
* @param {string} category - Observation category
|
|
63
|
+
* @param {string} observation - Observation description
|
|
64
|
+
* @param {any} value - Observed value
|
|
65
|
+
* @param {Object} guardDecision - Guard decision
|
|
66
|
+
* @param {Object} metadata - Additional metadata
|
|
67
|
+
* @returns {Object} Validated observation
|
|
68
|
+
*/
|
|
69
|
+
function createObservation(probeName, category, observation, value, guardDecision, metadata = {}) {
|
|
70
|
+
const obs = {
|
|
71
|
+
probeName,
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
category,
|
|
74
|
+
observation,
|
|
75
|
+
value,
|
|
76
|
+
guardDecision,
|
|
77
|
+
metadata,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Validate before returning
|
|
81
|
+
const validated = ObservationSchema.parse(obs);
|
|
82
|
+
return validated;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create error observation
|
|
87
|
+
* @param {string} probeName - Name of probe
|
|
88
|
+
* @param {string} category - Observation category
|
|
89
|
+
* @param {string} observation - Observation description
|
|
90
|
+
* @param {Error} error - Error object
|
|
91
|
+
* @param {Object} guardDecision - Guard decision
|
|
92
|
+
* @returns {Object} Validated observation
|
|
93
|
+
*/
|
|
94
|
+
function createErrorObservation(probeName, category, observation, error, guardDecision) {
|
|
95
|
+
return createObservation(
|
|
96
|
+
probeName,
|
|
97
|
+
category,
|
|
98
|
+
observation,
|
|
99
|
+
null,
|
|
100
|
+
guardDecision,
|
|
101
|
+
{
|
|
102
|
+
error: {
|
|
103
|
+
message: error.message,
|
|
104
|
+
code: error.code,
|
|
105
|
+
stack: error.stack,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Persistence Probe Functions
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Test basic write/read/verify persistence
|
|
117
|
+
* @param {Object} config - Probe configuration
|
|
118
|
+
* @returns {Promise<Object[]>} Observations
|
|
119
|
+
*/
|
|
120
|
+
async function probeBasicPersistence(config) {
|
|
121
|
+
const observations = [];
|
|
122
|
+
const testFile = join(config.out, 'persistence-test.txt');
|
|
123
|
+
const testContent = `Persistence test at ${Date.now()}`;
|
|
124
|
+
|
|
125
|
+
const guardDecision = guardPathAccess(testFile, config.out);
|
|
126
|
+
|
|
127
|
+
if (!guardDecision.allowed) {
|
|
128
|
+
observations.push(
|
|
129
|
+
createObservation(
|
|
130
|
+
'persistence',
|
|
131
|
+
'security',
|
|
132
|
+
'Basic persistence test blocked by guard',
|
|
133
|
+
false,
|
|
134
|
+
guardDecision
|
|
135
|
+
)
|
|
136
|
+
);
|
|
137
|
+
return observations;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Write
|
|
142
|
+
const writeStart = Date.now();
|
|
143
|
+
await fs.writeFile(testFile, testContent, 'utf8');
|
|
144
|
+
const writeTime = Date.now() - writeStart;
|
|
145
|
+
|
|
146
|
+
observations.push(
|
|
147
|
+
createObservation(
|
|
148
|
+
'persistence',
|
|
149
|
+
'storage',
|
|
150
|
+
'Write operation successful',
|
|
151
|
+
true,
|
|
152
|
+
guardDecision,
|
|
153
|
+
{ writeTime, fileSize: testContent.length }
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Read
|
|
158
|
+
const readStart = Date.now();
|
|
159
|
+
const readContent = await fs.readFile(testFile, 'utf8');
|
|
160
|
+
const readTime = Date.now() - readStart;
|
|
161
|
+
|
|
162
|
+
const contentMatches = readContent === testContent;
|
|
163
|
+
observations.push(
|
|
164
|
+
createObservation(
|
|
165
|
+
'persistence',
|
|
166
|
+
'storage',
|
|
167
|
+
'Read operation and content verification',
|
|
168
|
+
contentMatches,
|
|
169
|
+
guardDecision,
|
|
170
|
+
{ readTime, contentMatches }
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Check file stats
|
|
175
|
+
const stats = await fs.stat(testFile);
|
|
176
|
+
observations.push(
|
|
177
|
+
createObservation(
|
|
178
|
+
'persistence',
|
|
179
|
+
'filesystem',
|
|
180
|
+
'File metadata accessible',
|
|
181
|
+
true,
|
|
182
|
+
guardDecision,
|
|
183
|
+
{
|
|
184
|
+
size: stats.size,
|
|
185
|
+
mode: stats.mode.toString(8),
|
|
186
|
+
created: stats.birthtime.getTime(),
|
|
187
|
+
modified: stats.mtime.getTime(),
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Clean up
|
|
193
|
+
await fs.unlink(testFile);
|
|
194
|
+
observations.push(
|
|
195
|
+
createObservation(
|
|
196
|
+
'persistence',
|
|
197
|
+
'filesystem',
|
|
198
|
+
'File deletion successful',
|
|
199
|
+
true,
|
|
200
|
+
guardDecision
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
observations.push(createErrorObservation('persistence', 'storage', 'Basic persistence test failed', error, guardDecision));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return observations;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Test persistence across runs by checking for marker file
|
|
212
|
+
* @param {Object} config - Probe configuration
|
|
213
|
+
* @returns {Promise<Object[]>} Observations
|
|
214
|
+
*/
|
|
215
|
+
async function probeCrossRunPersistence(config) {
|
|
216
|
+
const observations = [];
|
|
217
|
+
const markerFile = join(config.out, '.persistence-marker');
|
|
218
|
+
|
|
219
|
+
const guardDecision = guardPathAccess(markerFile, config.out);
|
|
220
|
+
|
|
221
|
+
if (!guardDecision.allowed) {
|
|
222
|
+
observations.push(
|
|
223
|
+
createObservation(
|
|
224
|
+
'persistence',
|
|
225
|
+
'security',
|
|
226
|
+
'Cross-run persistence test blocked by guard',
|
|
227
|
+
false,
|
|
228
|
+
guardDecision
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
return observations;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
let markerExists = false;
|
|
236
|
+
let previousTimestamp = null;
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const content = await fs.readFile(markerFile, 'utf8');
|
|
240
|
+
previousTimestamp = parseInt(content, 10);
|
|
241
|
+
markerExists = true;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error.code !== 'ENOENT') {
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
observations.push(
|
|
249
|
+
createObservation(
|
|
250
|
+
'persistence',
|
|
251
|
+
'storage',
|
|
252
|
+
'Persistence marker from previous run',
|
|
253
|
+
markerExists,
|
|
254
|
+
guardDecision,
|
|
255
|
+
{ previousTimestamp, markerExists }
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Write new marker
|
|
260
|
+
const currentTimestamp = Date.now();
|
|
261
|
+
await fs.writeFile(markerFile, currentTimestamp.toString(), 'utf8');
|
|
262
|
+
|
|
263
|
+
observations.push(
|
|
264
|
+
createObservation(
|
|
265
|
+
'persistence',
|
|
266
|
+
'storage',
|
|
267
|
+
'Persistence marker written for next run',
|
|
268
|
+
true,
|
|
269
|
+
guardDecision,
|
|
270
|
+
{ currentTimestamp }
|
|
271
|
+
)
|
|
272
|
+
);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
observations.push(
|
|
275
|
+
createErrorObservation('persistence', 'storage', 'Cross-run persistence test failed', error, guardDecision)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return observations;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Test quota limits by writing incrementally
|
|
284
|
+
* @param {Object} config - Probe configuration
|
|
285
|
+
* @returns {Promise<Object[]>} Observations
|
|
286
|
+
*/
|
|
287
|
+
async function probeQuotaLimits(config) {
|
|
288
|
+
const observations = [];
|
|
289
|
+
const quotaTestFile = join(config.out, 'quota-test.bin');
|
|
290
|
+
|
|
291
|
+
const guardDecision = guardPathAccess(quotaTestFile, config.out);
|
|
292
|
+
|
|
293
|
+
if (!guardDecision.allowed) {
|
|
294
|
+
observations.push(
|
|
295
|
+
createObservation(
|
|
296
|
+
'persistence',
|
|
297
|
+
'security',
|
|
298
|
+
'Quota test blocked by guard',
|
|
299
|
+
false,
|
|
300
|
+
guardDecision
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
return observations;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const chunkSize = config.chunkSize || 1024 * 1024; // 1 MB
|
|
308
|
+
const maxQuota = config.maxQuota || 100 * 1024 * 1024; // 100 MB
|
|
309
|
+
const chunk = Buffer.alloc(chunkSize, 'A');
|
|
310
|
+
|
|
311
|
+
let bytesWritten = 0;
|
|
312
|
+
let quotaReached = false;
|
|
313
|
+
let quotaError = null;
|
|
314
|
+
|
|
315
|
+
const startTime = Date.now();
|
|
316
|
+
|
|
317
|
+
// Write chunks until we hit quota or max limit
|
|
318
|
+
while (bytesWritten < maxQuota && !quotaReached) {
|
|
319
|
+
try {
|
|
320
|
+
await fs.appendFile(quotaTestFile, chunk);
|
|
321
|
+
bytesWritten += chunkSize;
|
|
322
|
+
|
|
323
|
+
// Check if file size matches expected
|
|
324
|
+
const stats = await fs.stat(quotaTestFile);
|
|
325
|
+
if (stats.size !== bytesWritten) {
|
|
326
|
+
quotaReached = true;
|
|
327
|
+
quotaError = `Size mismatch: expected ${bytesWritten}, got ${stats.size}`;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Timeout check (don't spend more than config.timeout on quota test)
|
|
332
|
+
if (Date.now() - startTime > config.timeout) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
quotaReached = true;
|
|
337
|
+
quotaError = error.message;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const duration = Date.now() - startTime;
|
|
343
|
+
|
|
344
|
+
observations.push(
|
|
345
|
+
createObservation(
|
|
346
|
+
'persistence',
|
|
347
|
+
'quota',
|
|
348
|
+
quotaReached ? 'Quota limit reached' : 'Quota test completed without hitting limit',
|
|
349
|
+
bytesWritten,
|
|
350
|
+
guardDecision,
|
|
351
|
+
{
|
|
352
|
+
bytesWritten,
|
|
353
|
+
quotaReached,
|
|
354
|
+
quotaError,
|
|
355
|
+
duration,
|
|
356
|
+
throughputMBps: bytesWritten / duration / 1024,
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Clean up
|
|
362
|
+
try {
|
|
363
|
+
await fs.unlink(quotaTestFile);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
// Ignore cleanup errors
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
observations.push(createErrorObservation('persistence', 'quota', 'Quota test failed', error, guardDecision));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return observations;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Test directory permissions (create, rename, delete)
|
|
376
|
+
* @param {Object} config - Probe configuration
|
|
377
|
+
* @returns {Promise<Object[]>} Observations
|
|
378
|
+
*/
|
|
379
|
+
async function probeDirectoryPermissions(config) {
|
|
380
|
+
const observations = [];
|
|
381
|
+
const testDir = join(config.out, 'perm-test-dir');
|
|
382
|
+
const renamedDir = join(config.out, 'perm-test-dir-renamed');
|
|
383
|
+
|
|
384
|
+
const guardDecision = guardPathAccess(testDir, config.out);
|
|
385
|
+
const guardDecisionRenamed = guardPathAccess(renamedDir, config.out);
|
|
386
|
+
|
|
387
|
+
if (!guardDecision.allowed || !guardDecisionRenamed.allowed) {
|
|
388
|
+
observations.push(
|
|
389
|
+
createObservation(
|
|
390
|
+
'persistence',
|
|
391
|
+
'security',
|
|
392
|
+
'Directory permissions test blocked by guard',
|
|
393
|
+
false,
|
|
394
|
+
guardDecision
|
|
395
|
+
)
|
|
396
|
+
);
|
|
397
|
+
return observations;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
// Create directory
|
|
402
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
403
|
+
observations.push(
|
|
404
|
+
createObservation(
|
|
405
|
+
'persistence',
|
|
406
|
+
'permissions',
|
|
407
|
+
'Directory creation successful',
|
|
408
|
+
true,
|
|
409
|
+
guardDecision
|
|
410
|
+
)
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Check directory stats
|
|
414
|
+
const stats = await fs.stat(testDir);
|
|
415
|
+
observations.push(
|
|
416
|
+
createObservation(
|
|
417
|
+
'persistence',
|
|
418
|
+
'permissions',
|
|
419
|
+
'Directory is accessible',
|
|
420
|
+
stats.isDirectory(),
|
|
421
|
+
guardDecision,
|
|
422
|
+
{ mode: stats.mode.toString(8) }
|
|
423
|
+
)
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Rename directory
|
|
427
|
+
await fs.rename(testDir, renamedDir);
|
|
428
|
+
observations.push(
|
|
429
|
+
createObservation(
|
|
430
|
+
'persistence',
|
|
431
|
+
'permissions',
|
|
432
|
+
'Directory rename successful',
|
|
433
|
+
true,
|
|
434
|
+
guardDecisionRenamed
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Delete directory
|
|
439
|
+
await fs.rmdir(renamedDir);
|
|
440
|
+
observations.push(
|
|
441
|
+
createObservation(
|
|
442
|
+
'persistence',
|
|
443
|
+
'permissions',
|
|
444
|
+
'Directory deletion successful',
|
|
445
|
+
true,
|
|
446
|
+
guardDecisionRenamed
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
observations.push(
|
|
451
|
+
createErrorObservation('persistence', 'permissions', 'Directory permissions test failed', error, guardDecision)
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Cleanup on error
|
|
455
|
+
try {
|
|
456
|
+
await fs.rmdir(testDir).catch(() => {});
|
|
457
|
+
await fs.rmdir(renamedDir).catch(() => {});
|
|
458
|
+
} catch (e) {
|
|
459
|
+
// Ignore cleanup errors
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return observations;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Test atomic operations (rename, link if supported)
|
|
468
|
+
* @param {Object} config - Probe configuration
|
|
469
|
+
* @returns {Promise<Object[]>} Observations
|
|
470
|
+
*/
|
|
471
|
+
async function probeAtomicOperations(config) {
|
|
472
|
+
const observations = [];
|
|
473
|
+
const sourceFile = join(config.out, 'atomic-source.txt');
|
|
474
|
+
const targetFile = join(config.out, 'atomic-target.txt');
|
|
475
|
+
const linkFile = join(config.out, 'atomic-link.txt');
|
|
476
|
+
|
|
477
|
+
const guardDecisionSource = guardPathAccess(sourceFile, config.out);
|
|
478
|
+
const guardDecisionTarget = guardPathAccess(targetFile, config.out);
|
|
479
|
+
const guardDecisionLink = guardPathAccess(linkFile, config.out);
|
|
480
|
+
|
|
481
|
+
if (!guardDecisionSource.allowed || !guardDecisionTarget.allowed || !guardDecisionLink.allowed) {
|
|
482
|
+
observations.push(
|
|
483
|
+
createObservation(
|
|
484
|
+
'persistence',
|
|
485
|
+
'security',
|
|
486
|
+
'Atomic operations test blocked by guard',
|
|
487
|
+
false,
|
|
488
|
+
guardDecisionSource
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
return observations;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
// Create source file
|
|
496
|
+
await fs.writeFile(sourceFile, 'atomic test content', 'utf8');
|
|
497
|
+
|
|
498
|
+
// Test atomic rename
|
|
499
|
+
const renameStart = Date.now();
|
|
500
|
+
await fs.rename(sourceFile, targetFile);
|
|
501
|
+
const renameTime = Date.now() - renameStart;
|
|
502
|
+
|
|
503
|
+
observations.push(
|
|
504
|
+
createObservation(
|
|
505
|
+
'persistence',
|
|
506
|
+
'filesystem',
|
|
507
|
+
'Atomic rename successful',
|
|
508
|
+
true,
|
|
509
|
+
guardDecisionTarget,
|
|
510
|
+
{ renameTime }
|
|
511
|
+
)
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
// Test hard link (may not be supported on all filesystems)
|
|
515
|
+
try {
|
|
516
|
+
await fs.link(targetFile, linkFile);
|
|
517
|
+
const linkStats = await fs.stat(linkFile);
|
|
518
|
+
|
|
519
|
+
observations.push(
|
|
520
|
+
createObservation(
|
|
521
|
+
'persistence',
|
|
522
|
+
'filesystem',
|
|
523
|
+
'Hard link creation successful',
|
|
524
|
+
true,
|
|
525
|
+
guardDecisionLink,
|
|
526
|
+
{ nlink: linkStats.nlink }
|
|
527
|
+
)
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
// Clean up link
|
|
531
|
+
await fs.unlink(linkFile);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
observations.push(
|
|
534
|
+
createObservation(
|
|
535
|
+
'persistence',
|
|
536
|
+
'filesystem',
|
|
537
|
+
'Hard link not supported or failed',
|
|
538
|
+
false,
|
|
539
|
+
guardDecisionLink,
|
|
540
|
+
{ error: error.message }
|
|
541
|
+
)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Clean up
|
|
546
|
+
await fs.unlink(targetFile);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
observations.push(
|
|
549
|
+
createErrorObservation('persistence', 'filesystem', 'Atomic operations test failed', error, guardDecisionSource)
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// Cleanup on error
|
|
553
|
+
try {
|
|
554
|
+
await fs.unlink(sourceFile).catch(() => {});
|
|
555
|
+
await fs.unlink(targetFile).catch(() => {});
|
|
556
|
+
await fs.unlink(linkFile).catch(() => {});
|
|
557
|
+
} catch (e) {
|
|
558
|
+
// Ignore cleanup errors
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return observations;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Test temp directory behavior
|
|
567
|
+
* @param {Object} config - Probe configuration
|
|
568
|
+
* @returns {Promise<Object[]>} Observations
|
|
569
|
+
*/
|
|
570
|
+
async function probeTempDirectory(config) {
|
|
571
|
+
const observations = [];
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const tempDir = tmpdir();
|
|
575
|
+
|
|
576
|
+
// We don't write to temp dir (guard constraint), just observe it
|
|
577
|
+
const guardDecision = {
|
|
578
|
+
path: tempDir,
|
|
579
|
+
allowed: false,
|
|
580
|
+
reason: 'System temp directory - read-only observation',
|
|
581
|
+
policy: 'observe-only',
|
|
582
|
+
timestamp: Date.now(),
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
observations.push(
|
|
586
|
+
createObservation(
|
|
587
|
+
'persistence',
|
|
588
|
+
'filesystem',
|
|
589
|
+
'System temp directory location',
|
|
590
|
+
tempDir,
|
|
591
|
+
guardDecision
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// Try to check if temp dir is accessible (read-only)
|
|
596
|
+
try {
|
|
597
|
+
const stats = await fs.stat(tempDir);
|
|
598
|
+
observations.push(
|
|
599
|
+
createObservation(
|
|
600
|
+
'persistence',
|
|
601
|
+
'filesystem',
|
|
602
|
+
'System temp directory accessible for reading',
|
|
603
|
+
true,
|
|
604
|
+
guardDecision,
|
|
605
|
+
{ mode: stats.mode.toString(8) }
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
observations.push(
|
|
610
|
+
createObservation(
|
|
611
|
+
'persistence',
|
|
612
|
+
'filesystem',
|
|
613
|
+
'System temp directory not accessible',
|
|
614
|
+
false,
|
|
615
|
+
guardDecision,
|
|
616
|
+
{ error: error.message }
|
|
617
|
+
)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
const guardDecision = {
|
|
622
|
+
path: 'tmpdir()',
|
|
623
|
+
allowed: false,
|
|
624
|
+
reason: 'System temp directory access failed',
|
|
625
|
+
policy: 'observe-only',
|
|
626
|
+
timestamp: Date.now(),
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
observations.push(
|
|
630
|
+
createErrorObservation('persistence', 'filesystem', 'Temp directory probe failed', error, guardDecision)
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return observations;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Detect storage type (heuristic based on performance and behavior)
|
|
639
|
+
* @param {Object} config - Probe configuration
|
|
640
|
+
* @returns {Promise<Object[]>} Observations
|
|
641
|
+
*/
|
|
642
|
+
async function probeStorageType(config) {
|
|
643
|
+
const observations = [];
|
|
644
|
+
const testFile = join(config.out, 'storage-type-test.bin');
|
|
645
|
+
|
|
646
|
+
const guardDecision = guardPathAccess(testFile, config.out);
|
|
647
|
+
|
|
648
|
+
if (!guardDecision.allowed) {
|
|
649
|
+
observations.push(
|
|
650
|
+
createObservation(
|
|
651
|
+
'persistence',
|
|
652
|
+
'security',
|
|
653
|
+
'Storage type detection blocked by guard',
|
|
654
|
+
false,
|
|
655
|
+
guardDecision
|
|
656
|
+
)
|
|
657
|
+
);
|
|
658
|
+
return observations;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
// Write a small file and measure latency
|
|
663
|
+
const testData = Buffer.alloc(4096, 'X');
|
|
664
|
+
const iterations = 10;
|
|
665
|
+
const latencies = [];
|
|
666
|
+
|
|
667
|
+
for (let i = 0; i < iterations; i++) {
|
|
668
|
+
const start = Date.now();
|
|
669
|
+
await fs.writeFile(testFile, testData);
|
|
670
|
+
const handle = await fs.open(testFile, 'r+');
|
|
671
|
+
try {
|
|
672
|
+
await handle.sync();
|
|
673
|
+
} finally {
|
|
674
|
+
await handle.close();
|
|
675
|
+
}
|
|
676
|
+
latencies.push(Date.now() - start);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
|
|
680
|
+
const minLatency = Math.min(...latencies);
|
|
681
|
+
const maxLatency = Math.max(...latencies);
|
|
682
|
+
|
|
683
|
+
// Heuristic: in-memory < 1ms, SSD < 5ms, HDD > 5ms
|
|
684
|
+
let storageType = 'unknown';
|
|
685
|
+
if (avgLatency < 1) {
|
|
686
|
+
storageType = 'in-memory (likely tmpfs or ramfs)';
|
|
687
|
+
} else if (avgLatency < 5) {
|
|
688
|
+
storageType = 'fast-storage (likely SSD or cached)';
|
|
689
|
+
} else {
|
|
690
|
+
storageType = 'slow-storage (likely HDD or network)';
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
observations.push(
|
|
694
|
+
createObservation(
|
|
695
|
+
'persistence',
|
|
696
|
+
'performance',
|
|
697
|
+
'Storage type detection (heuristic)',
|
|
698
|
+
storageType,
|
|
699
|
+
guardDecision,
|
|
700
|
+
{
|
|
701
|
+
avgLatency,
|
|
702
|
+
minLatency,
|
|
703
|
+
maxLatency,
|
|
704
|
+
latencies,
|
|
705
|
+
}
|
|
706
|
+
)
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
// Clean up
|
|
710
|
+
await fs.unlink(testFile);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
observations.push(
|
|
713
|
+
createErrorObservation('persistence', 'performance', 'Storage type detection failed', error, guardDecision)
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return observations;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// =============================================================================
|
|
721
|
+
// Main Probe Function
|
|
722
|
+
// =============================================================================
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Probe output persistence and storage characteristics
|
|
726
|
+
*
|
|
727
|
+
* @param {Object} config - Probe configuration
|
|
728
|
+
* @param {string} config.out - Output directory (REQUIRED)
|
|
729
|
+
* @param {number} [config.timeout=5000] - Timeout per operation (ms)
|
|
730
|
+
* @param {number} [config.maxQuota=104857600] - Max quota to test (bytes, default 100MB)
|
|
731
|
+
* @param {number} [config.chunkSize=1048576] - Chunk size for quota test (bytes, default 1MB)
|
|
732
|
+
* @returns {Promise<Object[]>} Array of observations
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* const observations = await probePersistence({
|
|
736
|
+
* out: '/home/user/output',
|
|
737
|
+
* timeout: 5000,
|
|
738
|
+
* maxQuota: 100 * 1024 * 1024
|
|
739
|
+
* });
|
|
740
|
+
*/
|
|
741
|
+
export async function probePersistence(config) {
|
|
742
|
+
// Validate config
|
|
743
|
+
const validatedConfig = ProbeConfigSchema.parse(config);
|
|
744
|
+
|
|
745
|
+
const observations = [];
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
// Ensure output directory exists
|
|
749
|
+
await fs.mkdir(validatedConfig.out, { recursive: true });
|
|
750
|
+
|
|
751
|
+
// Run all persistence probes
|
|
752
|
+
const results = await Promise.all([
|
|
753
|
+
probeBasicPersistence(validatedConfig),
|
|
754
|
+
probeCrossRunPersistence(validatedConfig),
|
|
755
|
+
probeQuotaLimits(validatedConfig),
|
|
756
|
+
probeDirectoryPermissions(validatedConfig),
|
|
757
|
+
probeAtomicOperations(validatedConfig),
|
|
758
|
+
probeTempDirectory(validatedConfig),
|
|
759
|
+
probeStorageType(validatedConfig),
|
|
760
|
+
]);
|
|
761
|
+
|
|
762
|
+
// Flatten results
|
|
763
|
+
results.forEach(result => observations.push(...result));
|
|
764
|
+
} catch (error) {
|
|
765
|
+
const guardDecision = {
|
|
766
|
+
path: validatedConfig.out,
|
|
767
|
+
allowed: false,
|
|
768
|
+
reason: 'Probe execution failed',
|
|
769
|
+
policy: 'error',
|
|
770
|
+
timestamp: Date.now(),
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
observations.push(
|
|
774
|
+
createErrorObservation('persistence', 'storage', 'Persistence probe failed', error, guardDecision)
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return observations;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// =============================================================================
|
|
782
|
+
// Module Exports
|
|
783
|
+
// =============================================================================
|
|
784
|
+
|
|
785
|
+
export default probePersistence;
|