@probelabs/probe 0.6.0-rc331 → 0.6.0-rc332
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/bin/binaries/{probe-v0.6.0-rc331-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc332-aarch64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc331-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc332-aarch64-unknown-linux-musl.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc331-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc332-x86_64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc331-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc332-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc331-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc332-x86_64-unknown-linux-musl.tar.gz} +0 -0
- package/build/agent/ProbeAgent.d.ts +105 -4
- package/build/agent/ProbeAgent.js +209 -12
- package/build/agent/bashExecutor.js +36 -101
- package/build/agent/engines/codex.js +367 -88
- package/build/agent/engines/governed-answer-failure.js +152 -0
- package/build/agent/engines/governed-codex-profile.js +198 -0
- package/build/agent/governance/acknowledgedJsonlChannel.js +328 -0
- package/build/agent/governance/atomicTerminalReceipt.js +188 -0
- package/build/agent/governance/index.d.ts +130 -0
- package/build/agent/governance/index.js +8 -0
- package/build/agent/mcp/built-in-server.js +152 -53
- package/build/agent/mcp/index.d.ts +65 -0
- package/build/agent/mcp/index.js +6 -1
- package/build/agent/probeTool.js +1 -1
- package/build/agent/processSupervisor.js +351 -0
- package/build/agent/tools.js +14 -8
- package/build/index.js +2 -0
- package/cjs/agent/ProbeAgent.cjs +13238 -11968
- package/cjs/index.cjs +75955 -74126
- package/index.d.ts +149 -4
- package/package.json +6 -2
- package/src/agent/ProbeAgent.d.ts +105 -4
- package/src/agent/ProbeAgent.js +209 -12
- package/src/agent/bashExecutor.js +36 -101
- package/src/agent/engines/codex.js +367 -88
- package/src/agent/engines/governed-answer-failure.js +152 -0
- package/src/agent/engines/governed-codex-profile.js +198 -0
- package/src/agent/governance/acknowledgedJsonlChannel.js +328 -0
- package/src/agent/governance/atomicTerminalReceipt.js +188 -0
- package/src/agent/governance/index.d.ts +130 -0
- package/src/agent/governance/index.js +8 -0
- package/src/agent/mcp/built-in-server.js +152 -53
- package/src/agent/mcp/index.d.ts +65 -0
- package/src/agent/mcp/index.js +6 -1
- package/src/agent/probeTool.js +1 -1
- package/src/agent/processSupervisor.js +351 -0
- package/src/agent/tools.js +14 -8
- package/src/index.js +2 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acknowledged JSONL channel exposed by the package governance subpath.
|
|
3
|
+
*
|
|
4
|
+
* Semantics are derived from the accepted ReqProof EXP-0171 cooperative
|
|
5
|
+
* channel at commit dcc888120cfbeb04f8bfe59147f272c2396723e0. This is the
|
|
6
|
+
* reusable channel primitive only, not a port of that experiment's runner.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PassThrough } from 'stream';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_FRAME_BYTE_CAP = 256;
|
|
12
|
+
const DEFAULT_TOTAL_BYTE_CAP = 1024;
|
|
13
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 50;
|
|
14
|
+
const DEFAULT_DEADLINE_MS = 400;
|
|
15
|
+
const DEFAULT_HIGH_WATER_MARK = 16 * 1024;
|
|
16
|
+
|
|
17
|
+
function positiveInteger(value, fallback, name) {
|
|
18
|
+
if (value === undefined) return fallback;
|
|
19
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
20
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nonNegativeNumber(value, fallback, name) {
|
|
26
|
+
if (value === undefined) return fallback;
|
|
27
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
28
|
+
throw new TypeError(`${name} must be a non-negative finite number`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createAcknowledgedJsonlChannel(options = {}) {
|
|
34
|
+
if (typeof options.onRecord !== 'function') {
|
|
35
|
+
throw new TypeError('onRecord must be a function');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const frameByteCap = positiveInteger(options.frameByteCap, DEFAULT_FRAME_BYTE_CAP, 'frameByteCap');
|
|
39
|
+
const totalByteCap = positiveInteger(options.totalByteCap, DEFAULT_TOTAL_BYTE_CAP, 'totalByteCap');
|
|
40
|
+
const idleTimeoutMs = nonNegativeNumber(options.idleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS, 'idleTimeoutMs');
|
|
41
|
+
const deadlineMs = nonNegativeNumber(options.deadlineMs, DEFAULT_DEADLINE_MS, 'deadlineMs');
|
|
42
|
+
const highWaterMark = positiveInteger(options.highWaterMark, DEFAULT_HIGH_WATER_MARK, 'highWaterMark');
|
|
43
|
+
|
|
44
|
+
const stream = new PassThrough({ highWaterMark });
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const pendingAcks = new Set();
|
|
47
|
+
const pendingWrites = new Set();
|
|
48
|
+
const timers = new Set();
|
|
49
|
+
let partial = Buffer.alloc(0);
|
|
50
|
+
let totalBytes = 0;
|
|
51
|
+
let frames = 0;
|
|
52
|
+
let acknowledgements = 0;
|
|
53
|
+
let accepting = true;
|
|
54
|
+
let eof = false;
|
|
55
|
+
let settled = false;
|
|
56
|
+
let cleaned = false;
|
|
57
|
+
let cleanupPromise = null;
|
|
58
|
+
let failure = null;
|
|
59
|
+
let firstFailureCount = 0;
|
|
60
|
+
let laterFailureCount = 0;
|
|
61
|
+
let abortCount = 0;
|
|
62
|
+
let backpressureCount = 0;
|
|
63
|
+
let drainWaiters = 0;
|
|
64
|
+
let idleTimer = null;
|
|
65
|
+
let deadlineTimer = null;
|
|
66
|
+
let resolveResult;
|
|
67
|
+
|
|
68
|
+
const result = new Promise(resolve => { resolveResult = resolve; });
|
|
69
|
+
const closed = new Promise(resolve => stream.once('close', resolve));
|
|
70
|
+
|
|
71
|
+
const clearOwnedTimer = timer => {
|
|
72
|
+
if (!timer) return;
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
timers.delete(timer);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const ownedTimer = (delay, callback) => {
|
|
78
|
+
if (delay === 0) return null;
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
timers.delete(timer);
|
|
81
|
+
callback();
|
|
82
|
+
}, delay);
|
|
83
|
+
timers.add(timer);
|
|
84
|
+
return timer;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const clearTimers = () => {
|
|
88
|
+
clearOwnedTimer(idleTimer);
|
|
89
|
+
clearOwnedTimer(deadlineTimer);
|
|
90
|
+
idleTimer = null;
|
|
91
|
+
deadlineTimer = null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const settle = classification => {
|
|
95
|
+
if (settled) return;
|
|
96
|
+
settled = true;
|
|
97
|
+
accepting = false;
|
|
98
|
+
clearTimers();
|
|
99
|
+
resolveResult(Object.freeze({
|
|
100
|
+
classification,
|
|
101
|
+
error: failure?.message ?? null,
|
|
102
|
+
frames,
|
|
103
|
+
acknowledgements,
|
|
104
|
+
eof
|
|
105
|
+
}));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const fail = (classification, error) => {
|
|
109
|
+
if (failure) {
|
|
110
|
+
laterFailureCount += 1;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
failure = {
|
|
114
|
+
classification,
|
|
115
|
+
message: error instanceof Error ? error.message : String(error ?? classification)
|
|
116
|
+
};
|
|
117
|
+
firstFailureCount += 1;
|
|
118
|
+
abortCount += 1;
|
|
119
|
+
accepting = false;
|
|
120
|
+
controller.abort(failure);
|
|
121
|
+
settle(classification);
|
|
122
|
+
return true;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const maybePass = () => {
|
|
126
|
+
if (eof && pendingAcks.size === 0 && !failure) settle('PASS');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const touchIdle = () => {
|
|
130
|
+
clearOwnedTimer(idleTimer);
|
|
131
|
+
idleTimer = ownedTimer(idleTimeoutMs, () => fail('FAIL_IDLE_TIMEOUT'));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const observeAck = value => {
|
|
135
|
+
const underlying = Promise.resolve(value);
|
|
136
|
+
let active = true;
|
|
137
|
+
let resolveOwned;
|
|
138
|
+
const owned = new Promise(resolve => { resolveOwned = resolve; });
|
|
139
|
+
const finish = acknowledged => {
|
|
140
|
+
if (!active) return;
|
|
141
|
+
active = false;
|
|
142
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
143
|
+
if (acknowledged) acknowledgements += 1;
|
|
144
|
+
resolveOwned();
|
|
145
|
+
};
|
|
146
|
+
const onAbort = () => finish(false);
|
|
147
|
+
|
|
148
|
+
pendingAcks.add(owned);
|
|
149
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
150
|
+
underlying.then(
|
|
151
|
+
() => finish(true),
|
|
152
|
+
error => {
|
|
153
|
+
fail('FAIL_ACK', error);
|
|
154
|
+
finish(false);
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
if (controller.signal.aborted) onAbort();
|
|
158
|
+
owned.then(() => {
|
|
159
|
+
pendingAcks.delete(owned);
|
|
160
|
+
maybePass();
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const acceptFrame = bytes => {
|
|
165
|
+
let record;
|
|
166
|
+
try {
|
|
167
|
+
record = JSON.parse(bytes.toString('utf8'));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
fail('FAIL_PARSE', error);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const keys = record && typeof record === 'object' ? Object.keys(record) : [];
|
|
173
|
+
if (keys.join(',') !== 'id,value' ||
|
|
174
|
+
!Number.isSafeInteger(record.id) || record.id < 0 || typeof record.value !== 'string' ||
|
|
175
|
+
!bytes.equals(Buffer.from(JSON.stringify(record), 'utf8'))) {
|
|
176
|
+
fail('FAIL_PARSE', 'frame is not canonical JSON');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
frames += 1;
|
|
180
|
+
try {
|
|
181
|
+
observeAck(options.onRecord(record, controller.signal));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
fail('FAIL_ACK', error);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const ingest = chunk => {
|
|
188
|
+
if (!accepting) return;
|
|
189
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
190
|
+
let offset = 0;
|
|
191
|
+
while (offset < bytes.length && accepting) {
|
|
192
|
+
const newline = bytes.indexOf(10, offset);
|
|
193
|
+
const end = newline === -1 ? bytes.length : newline;
|
|
194
|
+
const segment = bytes.subarray(offset, end);
|
|
195
|
+
if (partial.length + segment.length > frameByteCap) {
|
|
196
|
+
fail('FAIL_FRAME_CAP');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (segment.length > 0) partial = Buffer.concat([partial, segment]);
|
|
200
|
+
if (newline === -1) break;
|
|
201
|
+
const frame = partial;
|
|
202
|
+
partial = Buffer.alloc(0);
|
|
203
|
+
acceptFrame(frame);
|
|
204
|
+
offset = newline + 1;
|
|
205
|
+
}
|
|
206
|
+
if (accepting) touchIdle();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const onData = chunk => {
|
|
210
|
+
try { ingest(chunk); } catch (error) { fail('FAIL_SOURCE', error); }
|
|
211
|
+
};
|
|
212
|
+
const onEnd = () => {
|
|
213
|
+
eof = true;
|
|
214
|
+
clearOwnedTimer(idleTimer);
|
|
215
|
+
idleTimer = null;
|
|
216
|
+
if (partial.length > 0) fail('FAIL_PARTIAL_EOF');
|
|
217
|
+
else maybePass();
|
|
218
|
+
};
|
|
219
|
+
const onError = error => fail('FAIL_SOURCE', error);
|
|
220
|
+
|
|
221
|
+
stream.on('data', onData);
|
|
222
|
+
stream.once('end', onEnd);
|
|
223
|
+
stream.on('error', onError);
|
|
224
|
+
touchIdle();
|
|
225
|
+
deadlineTimer = ownedTimer(deadlineMs, () => fail('FAIL_DEADLINE'));
|
|
226
|
+
|
|
227
|
+
const trackWrite = operation => {
|
|
228
|
+
pendingWrites.add(operation);
|
|
229
|
+
operation.finally(() => pendingWrites.delete(operation));
|
|
230
|
+
return operation;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const write = input => {
|
|
234
|
+
if (!accepting) return Promise.resolve(false);
|
|
235
|
+
let byteLength;
|
|
236
|
+
if (Buffer.isBuffer(input) || input instanceof Uint8Array) byteLength = input.byteLength;
|
|
237
|
+
else if (typeof input === 'string') byteLength = Buffer.byteLength(input);
|
|
238
|
+
else throw new TypeError('write input must be a Buffer, string, or Uint8Array');
|
|
239
|
+
if (totalBytes + byteLength > totalByteCap) {
|
|
240
|
+
fail('FAIL_TOTAL_CAP');
|
|
241
|
+
return Promise.resolve(false);
|
|
242
|
+
}
|
|
243
|
+
const bytes = Buffer.from(input);
|
|
244
|
+
totalBytes += byteLength;
|
|
245
|
+
let resolveCallback;
|
|
246
|
+
const callbackDone = new Promise(resolve => { resolveCallback = resolve; });
|
|
247
|
+
let resolveDrain = () => {};
|
|
248
|
+
let drained = null;
|
|
249
|
+
|
|
250
|
+
stream.cork();
|
|
251
|
+
const accepted = stream.write(bytes, error => {
|
|
252
|
+
if (error) fail('FAIL_SOURCE', error);
|
|
253
|
+
resolveCallback();
|
|
254
|
+
});
|
|
255
|
+
if (!accepted) {
|
|
256
|
+
backpressureCount += 1;
|
|
257
|
+
drainWaiters += 1;
|
|
258
|
+
drained = new Promise(resolve => {
|
|
259
|
+
resolveDrain = () => {
|
|
260
|
+
stream.off('drain', onDrain);
|
|
261
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
262
|
+
drainWaiters -= 1;
|
|
263
|
+
resolve();
|
|
264
|
+
};
|
|
265
|
+
const onDrain = () => resolveDrain();
|
|
266
|
+
const onAbort = () => resolveDrain();
|
|
267
|
+
stream.once('drain', onDrain);
|
|
268
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
queueMicrotask(() => stream.uncork());
|
|
272
|
+
|
|
273
|
+
return trackWrite(Promise.all([callbackDone, drained ?? Promise.resolve()])
|
|
274
|
+
.then(() => accepting || eof));
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const end = () => {
|
|
278
|
+
if (stream.writableEnded || stream.destroyed) return Promise.resolve();
|
|
279
|
+
let resolveEnd;
|
|
280
|
+
const operation = new Promise(resolve => { resolveEnd = resolve; });
|
|
281
|
+
pendingWrites.add(operation);
|
|
282
|
+
stream.end(() => resolveEnd());
|
|
283
|
+
operation.finally(() => pendingWrites.delete(operation));
|
|
284
|
+
return operation;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const snapshot = () => Object.freeze({
|
|
288
|
+
accepting,
|
|
289
|
+
eof,
|
|
290
|
+
frames,
|
|
291
|
+
acknowledgements,
|
|
292
|
+
totalBytes,
|
|
293
|
+
partialBytes: partial.length,
|
|
294
|
+
pending: pendingAcks.size,
|
|
295
|
+
writes: pendingWrites.size,
|
|
296
|
+
timers: timers.size,
|
|
297
|
+
drainWaiters,
|
|
298
|
+
backpressureCount,
|
|
299
|
+
firstFailureCount,
|
|
300
|
+
laterFailureCount,
|
|
301
|
+
abortCount,
|
|
302
|
+
listeners: ['data', 'end', 'error', 'close', 'drain']
|
|
303
|
+
.reduce((count, event) => count + stream.listenerCount(event), 0)
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const cleanup = () => {
|
|
307
|
+
if (cleanupPromise) return cleanupPromise;
|
|
308
|
+
cleanupPromise = (async () => {
|
|
309
|
+
clearTimers();
|
|
310
|
+
if (pendingAcks.size > 0 && !failure) fail('FAIL_CLEANUP');
|
|
311
|
+
if (!stream.writableEnded && !stream.destroyed) {
|
|
312
|
+
if (failure) stream.destroy();
|
|
313
|
+
else await end();
|
|
314
|
+
}
|
|
315
|
+
await Promise.allSettled([...pendingWrites]);
|
|
316
|
+
await Promise.allSettled([...pendingAcks]);
|
|
317
|
+
if (!stream.destroyed) stream.destroy();
|
|
318
|
+
await closed;
|
|
319
|
+
stream.off('data', onData);
|
|
320
|
+
stream.off('end', onEnd);
|
|
321
|
+
stream.off('error', onError);
|
|
322
|
+
cleaned = true;
|
|
323
|
+
})();
|
|
324
|
+
return cleanupPromise;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return Object.freeze({ write, end, result, cleanup, snapshot: () => ({ ...snapshot(), cleaned }) });
|
|
328
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarded atomic terminal-receipt writer exposed by the package governance subpath.
|
|
3
|
+
*
|
|
4
|
+
* Semantics are derived from the accepted ReqProof EXP-0164 finalizer at
|
|
5
|
+
* source commit dc1a80476c89699e4d9a4921b6ef5d7f980a3c60. The caller owns a
|
|
6
|
+
* unique attempt directory and a single writer.
|
|
7
|
+
* Publication is process-observed atomicity, not power-loss durability for the
|
|
8
|
+
* directory entry: this narrow primitive intentionally does not fsync the directory.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { constants } from 'fs';
|
|
12
|
+
import { lstat, open, readFile, realpath, rename, unlink } from 'fs/promises';
|
|
13
|
+
import { basename, dirname, join, resolve } from 'path';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_NAME = 'receipt.json';
|
|
16
|
+
const DEFAULT_MAX_BYTES = 16_384;
|
|
17
|
+
const CLEANUP_ATTEMPTS = 3;
|
|
18
|
+
|
|
19
|
+
function safeName(name) {
|
|
20
|
+
return typeof name === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) &&
|
|
21
|
+
Buffer.byteLength(name) <= 255 && basename(name) === name;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function absent(path) {
|
|
25
|
+
try {
|
|
26
|
+
return { absent: false, info: await lstat(path) };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code === 'ENOENT') return { absent: true, info: null };
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function guardDirectory(directory) {
|
|
34
|
+
if (typeof directory !== 'string' || directory !== resolve(directory)) {
|
|
35
|
+
throw new TypeError('directory must be an absolute canonical path');
|
|
36
|
+
}
|
|
37
|
+
const info = await lstat(directory);
|
|
38
|
+
if (!info.isDirectory() || info.isSymbolicLink() || await realpath(directory) !== directory) {
|
|
39
|
+
throw new Error('directory must be an existing canonical nonsymlink directory');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function guardChild(path, directory, expectedName) {
|
|
44
|
+
if (dirname(path) !== directory || basename(path) !== expectedName) {
|
|
45
|
+
throw new Error('receipt path must be an exact immediate child');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function requireAbsent(path, label) {
|
|
50
|
+
const observed = await absent(path);
|
|
51
|
+
if (!observed.absent) {
|
|
52
|
+
const kind = observed.info.isSymbolicLink() ? 'symlink' : 'occupied path';
|
|
53
|
+
throw new Error(`${label} ${kind} is forbidden`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function combinedFailure(primary, cleanupErrors, cleanupComplete) {
|
|
58
|
+
if (cleanupErrors.length === 0) return primary;
|
|
59
|
+
const error = new AggregateError(
|
|
60
|
+
[primary, ...cleanupErrors],
|
|
61
|
+
cleanupComplete
|
|
62
|
+
? 'receipt operation failed after cleanup recovery'
|
|
63
|
+
: 'receipt operation and owned cleanup failed',
|
|
64
|
+
{ cause: primary }
|
|
65
|
+
);
|
|
66
|
+
Object.defineProperties(error, {
|
|
67
|
+
primary: { value: primary, enumerable: true },
|
|
68
|
+
cleanupErrors: { value: Object.freeze([...cleanupErrors]), enumerable: true },
|
|
69
|
+
cleanupComplete: { value: cleanupComplete, enumerable: true }
|
|
70
|
+
});
|
|
71
|
+
return error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function measuredBytes(bytes) {
|
|
75
|
+
if (Buffer.isBuffer(bytes) || bytes instanceof Uint8Array) return bytes.byteLength;
|
|
76
|
+
if (typeof bytes === 'string') return Buffer.byteLength(bytes);
|
|
77
|
+
throw new TypeError('bytes must be a Buffer, string, or Uint8Array');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function writeAtomicTerminalReceipt({
|
|
81
|
+
directory,
|
|
82
|
+
name = DEFAULT_NAME,
|
|
83
|
+
bytes,
|
|
84
|
+
maxBytes = DEFAULT_MAX_BYTES
|
|
85
|
+
} = {}) {
|
|
86
|
+
if (!safeName(name) || !safeName(`${name}.tmp`)) {
|
|
87
|
+
throw new TypeError('name must be a safe basename');
|
|
88
|
+
}
|
|
89
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
90
|
+
throw new TypeError('maxBytes must be a positive safe integer');
|
|
91
|
+
}
|
|
92
|
+
const byteLength = measuredBytes(bytes);
|
|
93
|
+
if (byteLength > maxBytes) throw new Error('receipt exceeds maxBytes');
|
|
94
|
+
|
|
95
|
+
await guardDirectory(directory);
|
|
96
|
+
const finalPath = join(directory, name);
|
|
97
|
+
const temporaryPath = join(directory, `${name}.tmp`);
|
|
98
|
+
guardChild(finalPath, directory, name);
|
|
99
|
+
guardChild(temporaryPath, directory, `${name}.tmp`);
|
|
100
|
+
await requireAbsent(finalPath, 'final receipt');
|
|
101
|
+
await requireAbsent(temporaryPath, 'temporary receipt');
|
|
102
|
+
|
|
103
|
+
const payload = Buffer.from(bytes);
|
|
104
|
+
let handle;
|
|
105
|
+
let createdTemporary = false;
|
|
106
|
+
let renamed = false;
|
|
107
|
+
let result;
|
|
108
|
+
let primary;
|
|
109
|
+
try {
|
|
110
|
+
handle = await open(
|
|
111
|
+
temporaryPath,
|
|
112
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
|
113
|
+
0o600
|
|
114
|
+
);
|
|
115
|
+
createdTemporary = true;
|
|
116
|
+
let offset = 0;
|
|
117
|
+
while (offset < payload.length) {
|
|
118
|
+
const { bytesWritten } = await handle.write(payload, offset, payload.length - offset, offset);
|
|
119
|
+
if (bytesWritten <= 0) throw new Error('receipt write made no progress');
|
|
120
|
+
offset += bytesWritten;
|
|
121
|
+
}
|
|
122
|
+
await handle.sync();
|
|
123
|
+
await handle.chmod(0o600);
|
|
124
|
+
await handle.close();
|
|
125
|
+
handle = undefined;
|
|
126
|
+
|
|
127
|
+
await guardDirectory(directory);
|
|
128
|
+
await requireAbsent(finalPath, 'final receipt');
|
|
129
|
+
const temporary = await lstat(temporaryPath);
|
|
130
|
+
if (!temporary.isFile() || temporary.isSymbolicLink() ||
|
|
131
|
+
(temporary.mode & 0o777) !== 0o600 || temporary.size !== byteLength) {
|
|
132
|
+
throw new Error('temporary receipt failed guarded validation');
|
|
133
|
+
}
|
|
134
|
+
await rename(temporaryPath, finalPath);
|
|
135
|
+
renamed = true;
|
|
136
|
+
|
|
137
|
+
const info = await lstat(finalPath);
|
|
138
|
+
const actual = await readFile(finalPath);
|
|
139
|
+
if (!info.isFile() || info.isSymbolicLink() || (info.mode & 0o777) !== 0o600 ||
|
|
140
|
+
info.size !== byteLength || actual.length !== byteLength || !actual.equals(payload)) {
|
|
141
|
+
throw new Error('published receipt failed exact validation');
|
|
142
|
+
}
|
|
143
|
+
result = Object.freeze({ bytes: actual, mode: info.mode & 0o777, size: info.size });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
primary = error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (primary) {
|
|
149
|
+
const cleanupErrors = [];
|
|
150
|
+
let cleanupComplete = false;
|
|
151
|
+
let temporaryAbsent = !createdTemporary || renamed;
|
|
152
|
+
for (let attempt = 1; attempt <= CLEANUP_ATTEMPTS && !cleanupComplete; attempt += 1) {
|
|
153
|
+
if (handle) {
|
|
154
|
+
try {
|
|
155
|
+
await handle.close();
|
|
156
|
+
handle = undefined;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
cleanupErrors.push(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!handle && createdTemporary && !renamed) {
|
|
162
|
+
try {
|
|
163
|
+
await guardDirectory(directory);
|
|
164
|
+
guardChild(temporaryPath, directory, `${name}.tmp`);
|
|
165
|
+
const temporary = await absent(temporaryPath);
|
|
166
|
+
if (!temporary.absent) {
|
|
167
|
+
if (!temporary.info.isFile() || temporary.info.isSymbolicLink()) {
|
|
168
|
+
throw new Error('owned temporary receipt changed type during cleanup');
|
|
169
|
+
}
|
|
170
|
+
await unlink(temporaryPath);
|
|
171
|
+
}
|
|
172
|
+
if (!(await absent(temporaryPath)).absent) {
|
|
173
|
+
throw new Error('owned temporary receipt remains after cleanup');
|
|
174
|
+
}
|
|
175
|
+
temporaryAbsent = true;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
cleanupErrors.push(error);
|
|
178
|
+
temporaryAbsent = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
cleanupComplete = !handle && temporaryAbsent;
|
|
182
|
+
}
|
|
183
|
+
if (!cleanupComplete) cleanupErrors.push(new Error('owned receipt cleanup remained incomplete'));
|
|
184
|
+
throw combinedFailure(primary, cleanupErrors, cleanupComplete);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
export type GovernedSignalScope = 'child' | 'process-group';
|
|
4
|
+
export type GovernedProcessClassification =
|
|
5
|
+
| 'exited'
|
|
6
|
+
| 'terminated'
|
|
7
|
+
| 'aborted'
|
|
8
|
+
| 'execution_timeout'
|
|
9
|
+
| 'output_overflow'
|
|
10
|
+
| 'cleanup_timeout'
|
|
11
|
+
| 'spawn_error';
|
|
12
|
+
|
|
13
|
+
export interface GovernedProcessSpec {
|
|
14
|
+
command: string;
|
|
15
|
+
args?: string[];
|
|
16
|
+
cwd?: string;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
executionTimeoutMs?: number;
|
|
20
|
+
terminationGraceMs?: number;
|
|
21
|
+
cleanupTimeoutMs?: number;
|
|
22
|
+
stdoutByteCap?: number;
|
|
23
|
+
stderrByteCap?: number;
|
|
24
|
+
signalScope?: GovernedSignalScope;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GovernedProcessBarrierState {
|
|
28
|
+
close: boolean;
|
|
29
|
+
stdoutEOF: boolean;
|
|
30
|
+
stderrEOF: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GovernedProcessObservation {
|
|
34
|
+
sequence: number;
|
|
35
|
+
fact: string;
|
|
36
|
+
[detail: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface GovernedProcessReceipt {
|
|
40
|
+
id: string;
|
|
41
|
+
classification: GovernedProcessClassification;
|
|
42
|
+
reason: string | null;
|
|
43
|
+
error?: string;
|
|
44
|
+
stdout: string;
|
|
45
|
+
stderr: string;
|
|
46
|
+
stdoutBytes: number;
|
|
47
|
+
stderrBytes: number;
|
|
48
|
+
exitCode: number | null;
|
|
49
|
+
signal: NodeJS.Signals | null;
|
|
50
|
+
barriers: Readonly<GovernedProcessBarrierState>;
|
|
51
|
+
observed: readonly Readonly<GovernedProcessObservation>[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GovernedProcessHandle {
|
|
55
|
+
readonly id: string;
|
|
56
|
+
terminate(reason?: string): Promise<GovernedProcessReceipt>;
|
|
57
|
+
readonly result: Promise<GovernedProcessReceipt>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function spawnGovernedProcess(spec: GovernedProcessSpec): GovernedProcessHandle;
|
|
61
|
+
|
|
62
|
+
export interface AcknowledgedJsonlRecord {
|
|
63
|
+
id: number;
|
|
64
|
+
value: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface AcknowledgedJsonlChannelOptions {
|
|
68
|
+
onRecord(record: AcknowledgedJsonlRecord, signal: AbortSignal): unknown | PromiseLike<unknown>;
|
|
69
|
+
frameByteCap?: number;
|
|
70
|
+
totalByteCap?: number;
|
|
71
|
+
idleTimeoutMs?: number;
|
|
72
|
+
deadlineMs?: number;
|
|
73
|
+
highWaterMark?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface AcknowledgedJsonlChannelResult {
|
|
77
|
+
classification: string;
|
|
78
|
+
error: string | null;
|
|
79
|
+
frames: number;
|
|
80
|
+
acknowledgements: number;
|
|
81
|
+
eof: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AcknowledgedJsonlChannelSnapshot {
|
|
85
|
+
accepting: boolean;
|
|
86
|
+
eof: boolean;
|
|
87
|
+
frames: number;
|
|
88
|
+
acknowledgements: number;
|
|
89
|
+
totalBytes: number;
|
|
90
|
+
partialBytes: number;
|
|
91
|
+
pending: number;
|
|
92
|
+
writes: number;
|
|
93
|
+
timers: number;
|
|
94
|
+
drainWaiters: number;
|
|
95
|
+
backpressureCount: number;
|
|
96
|
+
firstFailureCount: number;
|
|
97
|
+
laterFailureCount: number;
|
|
98
|
+
abortCount: number;
|
|
99
|
+
listeners: number;
|
|
100
|
+
cleaned: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface AcknowledgedJsonlChannel {
|
|
104
|
+
write(input: Buffer | Uint8Array | string): Promise<boolean>;
|
|
105
|
+
end(): Promise<void>;
|
|
106
|
+
readonly result: Promise<Readonly<AcknowledgedJsonlChannelResult>>;
|
|
107
|
+
cleanup(): Promise<void>;
|
|
108
|
+
snapshot(): AcknowledgedJsonlChannelSnapshot;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createAcknowledgedJsonlChannel(
|
|
112
|
+
options: AcknowledgedJsonlChannelOptions
|
|
113
|
+
): Readonly<AcknowledgedJsonlChannel>;
|
|
114
|
+
|
|
115
|
+
export interface AtomicTerminalReceiptOptions {
|
|
116
|
+
directory: string;
|
|
117
|
+
name?: string;
|
|
118
|
+
bytes: Buffer | Uint8Array | string;
|
|
119
|
+
maxBytes?: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface AtomicTerminalReceiptResult {
|
|
123
|
+
bytes: Buffer;
|
|
124
|
+
mode: number;
|
|
125
|
+
size: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function writeAtomicTerminalReceipt(
|
|
129
|
+
options: AtomicTerminalReceiptOptions
|
|
130
|
+
): Promise<Readonly<AtomicTerminalReceiptResult>>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public ESM entry point for Probe's accepted process-governance primitives.
|
|
3
|
+
* This module deliberately has no dependency on the package root or dotenv.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { spawnGovernedProcess } from '../processSupervisor.js';
|
|
7
|
+
export { createAcknowledgedJsonlChannel } from './acknowledgedJsonlChannel.js';
|
|
8
|
+
export { writeAtomicTerminalReceipt } from './atomicTerminalReceipt.js';
|