deadbyte-mcp 0.15.2 → 0.15.4
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/MANIFEST.SHA256 +66 -41
- package/PROVENANCE.json +3 -3
- package/RELEASE.json +3 -3
- package/package.json +1 -1
- package/scripts/autonomous-build.ps1 +1 -0
- package/scripts/autonomous-tests.ps1 +1 -0
- package/scripts/check-agent-backend-r46.mjs +77 -0
- package/scripts/check-atomic-resource-r46.mjs +84 -0
- package/scripts/check-autonomous-race-r46.mjs +160 -0
- package/scripts/check-coding-task-deadline-r46.mjs +59 -0
- package/scripts/check-deadline-cancellation-r46.mjs +49 -0
- package/scripts/check-process-deadline-r46.mjs +52 -0
- package/scripts/check-r46-background.mjs +5 -0
- package/scripts/extract-r46-failures.mjs +5 -0
- package/scripts/full-regression-r46.mjs +20 -12
- package/scripts/gate-windows-r46.ps1 +2 -0
- package/scripts/inspect-r46-active.mjs +2 -0
- package/scripts/r47-core-failures.mjs +1 -0
- package/scripts/release-manifest.mjs +1 -1
- package/scripts/run-r46-full-regression-background.mjs +7 -0
- package/scripts/run-r46-windows-gate-background.mjs +6 -0
- package/scripts/run-r46-windows-gate-background.ps1 +10 -0
- package/scripts/windows-gate-evidence-r46.mjs +4 -3
- package/scripts/workload-contract-regression-r47.mjs +14 -0
- package/scripts/workload-stream-regression.mjs +16 -0
- package/scripts/workload-substrate-r47-regression.mjs +12 -0
- package/src/agent-backend-r44.mjs +75 -10
- package/src/atomic-mutation.mjs +55 -0
- package/src/autonomous-mcp-tools.mjs +4 -3
- package/src/autonomous-output-contracts.mjs +16 -3
- package/src/autonomous-runtime.mjs +83 -30
- package/src/coding-mcp-tools.mjs +6 -6
- package/src/coding-output-contracts.mjs +20 -5
- package/src/coding-plane.mjs +3 -24
- package/src/coding-search.mjs +52 -15
- package/src/coding-task-runtime.mjs +41 -6
- package/src/compact-gateway-mcp-tools.mjs +3 -3
- package/src/deadline-cancellation.mjs +83 -0
- package/src/fuzzy-search-core.mjs +117 -0
- package/src/fuzzy-search-worker.mjs +8 -0
- package/src/fuzzy-search.mjs +34 -0
- package/src/machine-fs-mcp-tools.mjs +1 -1
- package/src/machine-fs-output-contracts.mjs +1 -1
- package/src/machine-fs-smoke-client.mjs +1 -1
- package/src/machine-fs.mjs +10 -29
- package/src/mcp-server.mjs +6 -0
- package/src/observation-smoke-client.mjs +1 -1
- package/src/process-mcp-tools.mjs +11 -3
- package/src/process-output-contracts.mjs +23 -0
- package/src/process-runtime.mjs +54 -5
- package/src/process-smoke-client.mjs +1 -1
- package/src/resource-identity.mjs +59 -0
- package/src/runtime-update.mjs +526 -162
- package/src/workload-substrate.mjs +87 -0
- package/test/coding-runtime.test.mjs +12 -0
- package/test/coding-search-r36.test.mjs +7 -0
- package/test/full-max-catalog-r46.test.mjs +2 -2
- package/test/fuzzy-search-r46.test.mjs +54 -0
- package/test/helpers/autonomous-r34-fixture.mjs +10 -1
- package/test/machine-fs-mcp-tools.test.mjs +2 -0
- package/test/machine-fs-runtime.test.mjs +6 -0
- package/test/process-mcp-tools.test.mjs +5 -2
- package/test/process-runtime.test.mjs +18 -1
- package/test/r45-finalization.test.mjs +1 -1
- package/test/release-version.test.mjs +1 -1
- package/test/runtime-self-update.test.mjs +118 -1
- package/test/workload-substrate.test.mjs +22 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
export const FUZZY_SEARCH_TIMEOUT_MS=30000;
|
|
4
|
+
const WORKER_URL=new URL('./fuzzy-search-worker.mjs',import.meta.url);
|
|
5
|
+
|
|
6
|
+
export function runFuzzySearchInWorker(text,query,timeoutMs=FUZZY_SEARCH_TIMEOUT_MS){
|
|
7
|
+
if(!Number.isInteger(timeoutMs)||timeoutMs<100||timeoutMs>FUZZY_SEARCH_TIMEOUT_MS)throw new Error('fuzzy timeout_ms must be in [100, 30000]');
|
|
8
|
+
return new Promise((resolve,reject)=>{
|
|
9
|
+
const worker=new Worker(WORKER_URL,{type:'module',workerData:{text,query},execArgv:[]});
|
|
10
|
+
worker.unref?.();
|
|
11
|
+
let settled=false;
|
|
12
|
+
let timer;
|
|
13
|
+
const finish=(fn,value)=>{
|
|
14
|
+
if(settled)return;
|
|
15
|
+
settled=true;
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
fn(value);
|
|
18
|
+
};
|
|
19
|
+
timer=setTimeout(()=>{
|
|
20
|
+
worker.terminate().catch(()=>{});
|
|
21
|
+
finish(reject,new Error(`fuzzy search timed out after ${timeoutMs}ms`));
|
|
22
|
+
},timeoutMs);
|
|
23
|
+
timer.unref?.();
|
|
24
|
+
worker.once('message',message=>{
|
|
25
|
+
if(message?.ok)finish(resolve,message.result);
|
|
26
|
+
else finish(reject,new Error(`fuzzy search worker failed: ${String(message?.error??'unknown error')}`));
|
|
27
|
+
worker.terminate().catch(()=>{});
|
|
28
|
+
});
|
|
29
|
+
worker.once('error',error=>finish(reject,error));
|
|
30
|
+
worker.once('exit',code=>{
|
|
31
|
+
if(code!==0)finish(reject,new Error(`fuzzy search worker exited with code ${code}`));
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -32,7 +32,7 @@ export function registerMachineFsTools(server, { runtime }) {
|
|
|
32
32
|
});
|
|
33
33
|
server.registerTool('machine_fs_read', {
|
|
34
34
|
description:'Read one bounded byte range from a root-relative regular file.',
|
|
35
|
-
inputSchema:z.strictObject({ root_id:id, path:rel, offset:z.number().int().
|
|
35
|
+
inputSchema:z.strictObject({ root_id:id, path:rel, offset:z.number().int().min(-4*1024*1024).max(4*1024*1024).default(0), max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536), hash_file:z.boolean().default(false), timeout_ms:z.number().int().min(100).max(180000).default(30000) }),
|
|
36
36
|
outputSchema:machineFsReadOutputSchema
|
|
37
37
|
}, async ({root_id,path,offset,max_bytes,hash_file,timeout_ms}) => {
|
|
38
38
|
try { return ok(machineFsReadOutputSchema, await runtime.read({rootId:root_id,relativePath:path,offset,maxBytes:max_bytes,hashFile:hash_file,timeoutMs:timeout_ms})); }
|
|
@@ -30,7 +30,7 @@ export const machineFsListOutputSchema = z.strictObject({
|
|
|
30
30
|
export const machineFsReadOutputSchema = z.strictObject({
|
|
31
31
|
status: statusOk, error,
|
|
32
32
|
root_id: z.string().min(1).max(64).optional(), path: pathText.optional(),
|
|
33
|
-
offset: z.number().int().nonnegative().optional(), next_offset: z.number().int().nonnegative().optional(),
|
|
33
|
+
offset: z.number().int().nonnegative().optional(), requested_offset: z.number().int().optional(), next_offset: z.number().int().nonnegative().optional(),
|
|
34
34
|
file_size: z.number().int().nonnegative().optional(), eof: z.boolean().optional(),
|
|
35
35
|
data_base64: z.string().max(12 * 1024 * 1024).optional(), data_sha256: hex64.optional(),
|
|
36
36
|
file_sha256: hex64.nullable().optional(), timeout_ms: z.number().int().min(100).max(180000).optional()
|
|
@@ -56,7 +56,7 @@ const transport = new StdioClientTransport({
|
|
|
56
56
|
});
|
|
57
57
|
const stderrChunks = [];
|
|
58
58
|
transport.stderr?.on('data', chunk => stderrChunks.push(Buffer.from(chunk)));
|
|
59
|
-
const baseTools = ['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open'];
|
|
59
|
+
const baseTools = ['contained_transform','evidence_verify','file_read','host_exec','host_file_read','host_file_write','proof_execute','skill_catalog','workspace_open'];
|
|
60
60
|
const codingTools = [
|
|
61
61
|
'coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read',
|
|
62
62
|
'coding_receipt_verify','coding_search','coding_session_status','coding_stat',
|
package/src/machine-fs.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { assertCodingArmed, recheckCodingAuthority } from './coding-plane.mjs';
|
|
|
9
9
|
import { isInside, validatePortableRelativePath } from './path-policy.mjs';
|
|
10
10
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
11
11
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
12
|
+
import { atomicMutation } from './atomic-mutation.mjs';
|
|
12
13
|
|
|
13
14
|
export const MACHINE_FS_RECEIPT_SCHEMA = 'deadbyte.machine-fs-receipt.v1';
|
|
14
15
|
const RECEIPT_DOMAIN = Buffer.from('DEADBYTE-MACHINE-FS-RECEIPT-V1\0', 'utf8');
|
|
@@ -271,29 +272,8 @@ function requestSha(operation, args) {
|
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
async function atomicWrite(target, bytes, expectedSha256, authorityCheck = null) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const temp = path.join(path.dirname(target), `.${path.basename(target)}.deadbyte-machine-fs-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
|
|
277
|
-
let handle;
|
|
278
|
-
try {
|
|
279
|
-
if (authorityCheck) await authorityCheck();
|
|
280
|
-
handle = await open(temp, 'wx', 0o600);
|
|
281
|
-
await handle.writeFile(bytes);
|
|
282
|
-
await handle.sync();
|
|
283
|
-
await handle.close();
|
|
284
|
-
handle = null;
|
|
285
|
-
checkExpectedFile(await regularState(target), expectedSha256);
|
|
286
|
-
if (authorityCheck) await authorityCheck();
|
|
287
|
-
await rename(temp, target);
|
|
288
|
-
} catch (error) {
|
|
289
|
-
if (handle) await handle.close().catch(() => {});
|
|
290
|
-
await rm(temp, { force: true }).catch(() => {});
|
|
291
|
-
throw error;
|
|
292
|
-
}
|
|
293
|
-
const after = await regularState(target);
|
|
294
|
-
const expectedAfter = sha256Bytes(bytes);
|
|
295
|
-
if (!after.exists || after.sha256 !== expectedAfter) throw new Error('machine fs post-commit hash mismatch');
|
|
296
|
-
return { before, after };
|
|
275
|
+
return atomicMutation({ target, bytes, expected: expectedSha256, readState: regularState,
|
|
276
|
+
assertExpected: checkExpectedFile, authorityCheck, tempTag: 'deadbyte-machine-fs' });
|
|
297
277
|
}
|
|
298
278
|
|
|
299
279
|
export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath, readWorkerPath = machineFsReadWorkerPath }) {
|
|
@@ -344,24 +324,25 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath,
|
|
|
344
324
|
return { status: 'ok', root_id: rootId, path: found.relative, entries, next_cursor: nextCursor };
|
|
345
325
|
}
|
|
346
326
|
async function readRange({ rootId, relativePath, offset = 0, maxBytes = 65536, hashFile = false, timeoutMs = 30000 }) {
|
|
347
|
-
if (!Number.isInteger(offset) || offset <
|
|
327
|
+
if (!Number.isInteger(offset) || offset < -policy.max_read_bytes || offset > policy.max_read_bytes) throw new Error(`machine fs read offset must be in [-${policy.max_read_bytes}, ${policy.max_read_bytes}]`);
|
|
348
328
|
if (!Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > policy.max_read_bytes) throw new Error(`machine fs max_bytes must be in [1, ${policy.max_read_bytes}]`);
|
|
349
329
|
if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 180000) throw new Error('machine fs read timeout_ms must be in [100, 180000]');
|
|
350
330
|
const grant = await assertCodingArmed(policy, rootId);
|
|
351
331
|
const found = await resolveExisting(policy, grant, rootId, relativePath, 'read', false);
|
|
352
332
|
const info = await lstat(found.resolved);
|
|
353
333
|
if (!info.isFile()) throw new Error('machine fs read target must be a regular file');
|
|
354
|
-
|
|
355
|
-
|
|
334
|
+
const effectiveOffset = offset < 0 ? Math.max(0, info.size + offset) : offset;
|
|
335
|
+
if (effectiveOffset > info.size) throw new Error('machine fs read offset exceeds file size');
|
|
336
|
+
const length = Math.min(maxBytes, info.size - effectiveOffset);
|
|
356
337
|
await recheckCodingAuthority(policy, grant, rootId);
|
|
357
|
-
const worker = await boundedReadWorker({ file: found.resolved, offset, length, hashFile, timeoutMs, workerPath: readWorkerPath });
|
|
338
|
+
const worker = await boundedReadWorker({ file: found.resolved, offset: effectiveOffset, length, hashFile, timeoutMs, workerPath: readWorkerPath });
|
|
358
339
|
const buffer = Buffer.from(worker.data_base64, 'base64');
|
|
359
340
|
if (buffer.length !== worker.bytes_read || buffer.length > length) throw new Error('machine fs read worker byte count mismatch');
|
|
360
341
|
await recheckCodingAuthority(policy, grant, rootId);
|
|
361
342
|
return {
|
|
362
343
|
status: 'ok', root_id: rootId, path: found.relative,
|
|
363
|
-
offset,
|
|
364
|
-
eof:
|
|
344
|
+
offset: effectiveOffset, requested_offset: offset, next_offset: effectiveOffset + buffer.length, file_size: info.size,
|
|
345
|
+
eof: effectiveOffset + buffer.length >= info.size, data_base64: buffer.toString('base64'),
|
|
365
346
|
data_sha256: sha256Bytes(buffer), file_sha256: worker.file_sha256,
|
|
366
347
|
timeout_ms: timeoutMs
|
|
367
348
|
};
|
package/src/mcp-server.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { DEADBYTE_VERSION } from './version.mjs';
|
|
|
15
15
|
import { currentIngressAdapterMode, TUNNEL_STDIO_PROBE_COMPAT_MODE } from './ingress-mode.mjs';
|
|
16
16
|
import { installTunnelStdioProbeCompat } from './tunnel-stdio-compat.mjs';
|
|
17
17
|
import { installStdioConsoleGuard } from './stdio-console-guard.mjs';
|
|
18
|
+
import { recoverInterruptedSlotUpdates } from './runtime-update.mjs';
|
|
18
19
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
19
20
|
import { assertHostArmed, hostExec, hostExecInline, hostExecNative, hostFileRead, hostFileWrite, loadHostPolicy } from './host-plane.mjs';
|
|
20
21
|
import { createMachineRuntime } from './machine-identity.mjs';
|
|
@@ -74,6 +75,11 @@ const verifyKeyPath = process.env.DEADBYTE_VERIFY_KEY ?? defaultPublicKeyPath();
|
|
|
74
75
|
const auditRuntimeMode = process.env.DEADBYTE_AUDIT_RUNTIME ?? '0';
|
|
75
76
|
if (!['0','1'].includes(auditRuntimeMode)) throw new Error('DEADBYTE_AUDIT_RUNTIME must be 0 or 1');
|
|
76
77
|
const runtimeStateRoot = path.resolve(process.env.DEADBYTE_STATE_ROOT || path.join(os.homedir(),'.deadbyte-mcp'));
|
|
78
|
+
const interruptedUpdateRecovery = await recoverInterruptedSlotUpdates({ stateRoot: runtimeStateRoot });
|
|
79
|
+
if (interruptedUpdateRecovery.launched > 0) {
|
|
80
|
+
console.error(`[runtime-update-recovery] launched ${interruptedUpdateRecovery.launched} detached supervisor(s)`);
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
const surfaceProfile = capabilityProfileFromEnv(process.env);
|
|
78
84
|
const localPrivilegedPolicyPath = path.join(packageRoot, 'controller', 'deadbyte-host-policy.json');
|
|
79
85
|
const privilegedPolicyExplicit = Object.hasOwn(process.env, 'DEADBYTE_PRIVILEGED_POLICY');
|
|
@@ -51,7 +51,7 @@ const transport=new StdioClientTransport({command:process.execPath,args:[serverP
|
|
|
51
51
|
env:{...getDefaultEnvironment(),DEADBYTE_MACHINE_RUNTIME:'1',DEADBYTE_CODING_RUNTIME:'1',DEADBYTE_OBSERVATION_RUNTIME:'1',
|
|
52
52
|
DEADBYTE_CODING_POLICY:policyPath,DEADBYTE_STATE_ROOT:state,DEADBYTE_SIGNING_KEY:keys.privateKeyPath,DEADBYTE_VERIFY_KEY:keys.publicKeyPath},stderr:'pipe'});
|
|
53
53
|
const stderr=[]; transport.stderr?.on('data',chunk=>stderr.push(Buffer.from(chunk)));
|
|
54
|
-
const base=['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open'];
|
|
54
|
+
const base=['contained_transform','evidence_verify','file_read','host_exec','host_file_read','host_file_write','proof_execute','skill_catalog','workspace_open'];
|
|
55
55
|
const machine=['machine_list','machine_ping','machine_revoke','machine_status'];
|
|
56
56
|
const coding=['coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read','coding_receipt_verify','coding_search',
|
|
57
57
|
'coding_session_status','coding_stat','coding_symbol_definition','coding_symbol_diagnostics','coding_symbol_implementations','coding_symbol_outline','coding_symbol_references','coding_symbol_rename_preview','coding_symbol_replace',
|
|
@@ -22,10 +22,10 @@ export function registerProcessTools(server, { runtime }) {
|
|
|
22
22
|
description: 'Start one locally registered process profile inside an approved root. Executable identity comes only from local policy.',
|
|
23
23
|
inputSchema: z.strictObject({
|
|
24
24
|
profile_id: id, root_id: id, cwd: z.string().min(1).max(1024).default('.'),
|
|
25
|
-
args: z.array(z.string().max(8192)).max(64).default([])
|
|
25
|
+
args: z.array(z.string().max(8192)).max(64).default([]), timeout_ms: z.number().int().min(100).max(900000).nullable().default(null)
|
|
26
26
|
}), outputSchema: processStartOutputSchema
|
|
27
|
-
}, async ({ profile_id, root_id, cwd, args }) => {
|
|
28
|
-
try { return ok(processStartOutputSchema, { status: 'ok', session: await runtime.start({ profileId: profile_id, rootId: root_id, cwd, args }) }); }
|
|
27
|
+
}, async ({ profile_id, root_id, cwd, args, timeout_ms }) => {
|
|
28
|
+
try { return ok(processStartOutputSchema, { status: 'ok', session: await runtime.start({ profileId: profile_id, rootId: root_id, cwd, args, timeoutMs: timeout_ms }) }); }
|
|
29
29
|
catch (error) { return fail(processStartOutputSchema, 'denied', error); }
|
|
30
30
|
});
|
|
31
31
|
server.registerTool('process_session_read', {
|
|
@@ -66,6 +66,14 @@ export function registerProcessTools(server, { runtime }) {
|
|
|
66
66
|
try { return ok(processListOutputSchema, { status: 'ok', ...(await runtime.list()) }); }
|
|
67
67
|
catch (error) { return fail(processListOutputSchema, 'failed', error); }
|
|
68
68
|
});
|
|
69
|
+
server.registerTool('process_session_cancel', {
|
|
70
|
+
description: 'Request cooperative cancellation of one DEADBYTE-owned process session. Cancellation is recorded separately from physical termination.',
|
|
71
|
+
inputSchema: z.strictObject({ session_id: hex64, reason: z.string().min(1).max(256).default('operator_request') }), outputSchema: processTerminateOutputSchema
|
|
72
|
+
}, async ({ session_id, reason }) => {
|
|
73
|
+
try { return ok(processTerminateOutputSchema, { status: 'ok', session: await runtime.cancel({ sessionId: session_id, reason }) }); }
|
|
74
|
+
catch (error) { return fail(processTerminateOutputSchema, 'denied', error); }
|
|
75
|
+
});
|
|
76
|
+
|
|
69
77
|
server.registerTool('process_session_terminate', {
|
|
70
78
|
description: 'Terminate one DEADBYTE-owned process session through its owned host boundary. Numeric PID is never an input.',
|
|
71
79
|
inputSchema: z.strictObject({ session_id: hex64 }), outputSchema: processTerminateOutputSchema
|
|
@@ -4,6 +4,23 @@ import { structuredToolResult } from './output-contracts.mjs';
|
|
|
4
4
|
const hex64 = z.string().regex(/^[0-9a-f]{64}$/);
|
|
5
5
|
const digits = z.string().regex(/^[0-9]+$/);
|
|
6
6
|
const processState = z.enum(['running','exited','terminated']);
|
|
7
|
+
const termination = z.strictObject({
|
|
8
|
+
deadline_exceeded:z.boolean(), cancellation_requested:z.boolean(), physical_termination:z.boolean(), verified:z.boolean(),
|
|
9
|
+
terminal_semantics:z.string().min(1).max(64)
|
|
10
|
+
});
|
|
11
|
+
const deadline = z.strictObject({
|
|
12
|
+
schema:z.literal('deadbyte.deadline.v1'), id:hex64.nullable(), created_at_ms:z.number(), timeout_ms:z.number().int().positive().nullable(), deadline_ms:z.number().nullable()
|
|
13
|
+
});
|
|
14
|
+
const streamCoverage = z.strictObject({
|
|
15
|
+
logical_first_seq:z.number().int().min(1), logical_last_seq:z.number().int().min(0),
|
|
16
|
+
retained_first_seq:z.number().int().min(1), retained_last_seq:z.number().int().min(0),
|
|
17
|
+
complete:z.boolean(), truncated:z.boolean(),
|
|
18
|
+
missing_ranges:z.array(z.strictObject({from:z.number().int().min(1),to:z.number().int().min(0)})).max(100000),
|
|
19
|
+
logical_item_count:z.number().int().min(0), retained_item_count:z.number().int().min(0),
|
|
20
|
+
logical_bytes:z.number().int().min(0), retained_bytes:z.number().int().min(0),
|
|
21
|
+
dropped_items:z.number().int().min(0), dropped_bytes:z.number().int().min(0),
|
|
22
|
+
status:z.enum(['open','closed'])
|
|
23
|
+
});
|
|
7
24
|
|
|
8
25
|
export const processSessionRecordSchema = z.strictObject({
|
|
9
26
|
session_id: hex64,
|
|
@@ -17,6 +34,12 @@ export const processSessionRecordSchema = z.strictObject({
|
|
|
17
34
|
earliest_offset: z.number().int().nonnegative(),
|
|
18
35
|
stream_head: z.number().int().nonnegative(),
|
|
19
36
|
stream_sha256: hex64,
|
|
37
|
+
stream_id: hex64.nullable(),
|
|
38
|
+
stream_generation: z.number().int().min(1).nullable(),
|
|
39
|
+
stream_next_seq: z.number().int().min(1).nullable(),
|
|
40
|
+
stream_coverage: streamCoverage.nullable(),
|
|
41
|
+
termination: termination.nullable(),
|
|
42
|
+
deadline: deadline.nullable(),
|
|
20
43
|
journal_head_sha256: hex64
|
|
21
44
|
});
|
|
22
45
|
|
package/src/process-runtime.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { canonicalJson } from './canonical-json.mjs';
|
|
|
6
6
|
import { isInside, validatePortableRelativePath } from './path-policy.mjs';
|
|
7
7
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
8
8
|
import { assertProcessArmed, recheckProcessAuthority } from './process-policy.mjs';
|
|
9
|
+
import { createSequenceStream } from './workload-substrate.mjs';
|
|
10
|
+
import { classifyTermination, composeAbortSignal, createCancellationToken, createDeadline } from './deadline-cancellation.mjs';
|
|
9
11
|
|
|
10
12
|
const EVENT_SCHEMA = 'deadbyte.process-event.v1';
|
|
11
13
|
const EVENT_DOMAIN = Buffer.from('DEADBYTE-PROCESS-EVENT-V1\0', 'utf8');
|
|
@@ -52,11 +54,18 @@ async function resolveCwd(policy, grant, profile, rootId, cwd) {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function publicSession(session) {
|
|
57
|
+
const stream = session.recovered_terminal ? null : session.output_stream;
|
|
55
58
|
return {
|
|
56
59
|
session_id: session.session_id, profile_id: session.profile_id, root_id: session.root_id,
|
|
57
60
|
cwd: session.cwd_relative, child_pid: session.child_pid, creation_filetime: session.creation_filetime,
|
|
58
61
|
state: session.state, exit_code: session.exit_code, earliest_offset: session.earliest_offset,
|
|
59
62
|
stream_head: session.stream_head, stream_sha256: session.stream_sha256,
|
|
63
|
+
stream_id: stream?.streamId ?? null,
|
|
64
|
+
stream_generation: stream?.generation ?? null,
|
|
65
|
+
stream_next_seq: stream?.snapshot().next_seq ?? null,
|
|
66
|
+
stream_coverage: stream?.coverage() ?? null,
|
|
67
|
+
termination: session.termination ?? null,
|
|
68
|
+
deadline: session.deadline ?? null,
|
|
60
69
|
journal_head_sha256: session.journal_head_sha256
|
|
61
70
|
};
|
|
62
71
|
}
|
|
@@ -93,6 +102,7 @@ function retainedIndexOf(session, needle, begin = 0) {
|
|
|
93
102
|
function appendOutput(session, chunk) {
|
|
94
103
|
if (!chunk?.length) return;
|
|
95
104
|
const bytes = Buffer.from(chunk);
|
|
105
|
+
session.output_stream?.append(null, { producer: 'process-host', channel: 'merged-output', sizeBytes: bytes.length });
|
|
96
106
|
session.stream_sha256 = sha256Bytes(Buffer.concat([
|
|
97
107
|
Buffer.from(session.stream_sha256, 'hex'), bytes
|
|
98
108
|
]));
|
|
@@ -207,7 +217,8 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
207
217
|
earliest_offset:terminal.detail.earliest_offset, stream_head:terminal.detail.stream_head,
|
|
208
218
|
stream_sha256:terminal.detail.stream_sha256, output_chunks:[output], retained_output_bytes:output.length, control_buffer:Buffer.alloc(0),
|
|
209
219
|
journal_head_sha256:checked.head_sha256, journal_path:path.join(policy.state_dir,name), event_count:checked.event_count,
|
|
210
|
-
max_retained_output_bytes:profile.max_retained_output_bytes, stderr:'', terminate_requested:terminal.type==='terminated',
|
|
220
|
+
max_retained_output_bytes:profile.max_retained_output_bytes, output_stream:createSequenceStream({streamId:sessionId,maxItems:1000000,maxBytes:profile.max_retained_output_bytes}), stderr:'', terminate_requested:terminal.type==='terminated',
|
|
221
|
+
termination:terminal.detail?.termination ?? null, deadline:terminal.detail?.deadline ?? null, cancellation:null,
|
|
211
222
|
protocol_error:null, transport:profile.transport, win32_input_mode:false, host:null, close_promise:Promise.resolve(),
|
|
212
223
|
recovered_terminal:true
|
|
213
224
|
});
|
|
@@ -281,7 +292,8 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
281
292
|
}
|
|
282
293
|
}
|
|
283
294
|
|
|
284
|
-
async function start({ profileId, rootId, cwd = '.', args = [] }) {
|
|
295
|
+
async function start({ profileId, rootId, cwd = '.', args = [], timeoutMs = null }) {
|
|
296
|
+
if (timeoutMs !== null && (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 900000)) throw new Error('process timeout_ms must be in [100, 900000] or null');
|
|
285
297
|
await hydrateTerminalSessions();
|
|
286
298
|
const grant = await assertProcessArmed(policy, profileId, rootId);
|
|
287
299
|
const profile = policy.profiles[profileId];
|
|
@@ -303,16 +315,35 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
303
315
|
cwd_relative: cwdInfo.relative, child_pid: null, creation_filetime: null, state: 'starting', exit_code: null,
|
|
304
316
|
earliest_offset: 0, stream_head: 0, stream_sha256: ZERO_SHA256, output_chunks: [], retained_output_bytes: 0, control_buffer: Buffer.alloc(0),
|
|
305
317
|
journal_head_sha256: ZERO_SHA256, journal_path: path.join(policy.state_dir, `${sessionId}.jsonl`), event_count: 0,
|
|
306
|
-
max_retained_output_bytes: profile.max_retained_output_bytes, stderr: '', terminate_requested: false, protocol_error: null,
|
|
318
|
+
max_retained_output_bytes: profile.max_retained_output_bytes, output_stream: createSequenceStream({streamId: sessionId, maxItems: 1000000, maxBytes: profile.max_retained_output_bytes}), stderr: '', terminate_requested: false, protocol_error: null,
|
|
319
|
+
deadline: createDeadline({ timeoutMs, id: sessionId }),
|
|
320
|
+
cancellation: createCancellationToken({ id: sessionId }),
|
|
321
|
+
termination: classifyTermination(),
|
|
307
322
|
transport: profile.transport,
|
|
308
323
|
win32_input_mode: false
|
|
309
324
|
};
|
|
310
325
|
await recheckProcessAuthority(policy, grant, profileId, rootId);
|
|
326
|
+
const composed = composeAbortSignal({ deadline: session.deadline, cancellation: session.cancellation });
|
|
327
|
+
session.abort_signal = composed;
|
|
311
328
|
const host = spawn(policy.native_host.path,
|
|
312
329
|
[profile.executable, cwdInfo.resolved, session.job_id, `--deadbyte-transport=${profile.transport}`, ...profile.prefix_args, ...args],
|
|
313
330
|
{ cwd: cwdInfo.resolved, env: processEnvironment(profile), windowsHide: true, shell: false, stdio: ['pipe','pipe','pipe'] });
|
|
314
331
|
session.host = host;
|
|
315
332
|
sessions.set(sessionId, session);
|
|
333
|
+
const abortProcess = () => {
|
|
334
|
+
const reason = String(composed.signal.reason ?? 'cancelled');
|
|
335
|
+
const snapshot = session.cancellation.snapshot();
|
|
336
|
+
session.termination = classifyTermination({
|
|
337
|
+
deadlineExceeded: reason === 'deadline_exceeded',
|
|
338
|
+
cancellationRequested: snapshot.requested,
|
|
339
|
+
physicallyTerminated: false,
|
|
340
|
+
verified: false
|
|
341
|
+
});
|
|
342
|
+
session.terminate_requested = true;
|
|
343
|
+
if (!host.killed) host.kill();
|
|
344
|
+
};
|
|
345
|
+
composed.signal.addEventListener('abort', abortProcess, { once: true });
|
|
346
|
+
if (composed.signal.aborted) abortProcess();
|
|
316
347
|
const ready = new Promise((resolve, reject) => { session.ready_resolve = resolve; session.ready_reject = reject; });
|
|
317
348
|
host.stdout.on('data', chunk => parseHostOutput(session, chunk));
|
|
318
349
|
host.stderr.on('data', chunk => { session.stderr = (session.stderr + chunk.toString('utf8')).slice(-16384); });
|
|
@@ -322,6 +353,13 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
322
353
|
parseHostOutput(session, null, true);
|
|
323
354
|
if (session.state === 'starting') session.ready_reject?.(new Error(`process host exited before READY: ${session.stderr}`));
|
|
324
355
|
session.exit_code ??= Number.isInteger(code) ? code : null;
|
|
356
|
+
const snapshot = session.cancellation.snapshot();
|
|
357
|
+
session.termination = classifyTermination({
|
|
358
|
+
deadlineExceeded: session.deadline.deadline_ms !== null && Date.now() >= session.deadline.deadline_ms && session.terminate_requested,
|
|
359
|
+
cancellationRequested: snapshot.requested,
|
|
360
|
+
physicallyTerminated: session.terminate_requested,
|
|
361
|
+
verified: true
|
|
362
|
+
});
|
|
325
363
|
const terminalState = session.terminate_requested ? 'terminated' : 'exited';
|
|
326
364
|
try {
|
|
327
365
|
const outputFile = `${session.session_id}.stream.bin`;
|
|
@@ -336,10 +374,13 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
336
374
|
stream_sha256:session.stream_sha256,
|
|
337
375
|
retained_output_bytes:session.retained_output_bytes,
|
|
338
376
|
retained_output_sha256:retainedOutputSha256,
|
|
377
|
+
termination:session.termination,
|
|
378
|
+
deadline:session.deadline,
|
|
339
379
|
output_file:outputFile
|
|
340
380
|
});
|
|
341
381
|
session.state = terminalState;
|
|
342
382
|
} catch {}
|
|
383
|
+
composed.close();
|
|
343
384
|
resolve();
|
|
344
385
|
});
|
|
345
386
|
});
|
|
@@ -428,11 +469,19 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
428
469
|
await appendEvent(runtime, session, 'action', { action_id: actionId, logical_bytes: logicalBytes.length, logical_sha256: logicalSha256 });
|
|
429
470
|
return { action_id: actionId, logical_bytes: logicalBytes.length, logical_sha256: logicalSha256, session: publicSession(session) };
|
|
430
471
|
}
|
|
472
|
+
async function cancel({ sessionId, reason = 'operator_request' }) {
|
|
473
|
+
const session = await sessionFor(sessionId);
|
|
474
|
+
if (typeof reason !== 'string' || reason.length < 1 || reason.length > 256) throw new Error('process cancellation reason invalid');
|
|
475
|
+
if (['exited','terminated'].includes(session.state)) return publicSession(session);
|
|
476
|
+
session.cancellation.request(reason);
|
|
477
|
+
await session.close_promise;
|
|
478
|
+
return publicSession(session);
|
|
479
|
+
}
|
|
431
480
|
async function terminate({ sessionId }) {
|
|
432
481
|
const session = await sessionFor(sessionId);
|
|
433
482
|
if (['exited','terminated'].includes(session.state)) return publicSession(session);
|
|
434
483
|
session.terminate_requested = true;
|
|
435
|
-
session.host.kill();
|
|
484
|
+
if (!session.host.killed) session.host.kill();
|
|
436
485
|
await session.close_promise;
|
|
437
486
|
return publicSession(session);
|
|
438
487
|
}
|
|
@@ -463,5 +512,5 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
463
512
|
return { ok: true, event_count: sequence, head_sha256: previous };
|
|
464
513
|
}
|
|
465
514
|
|
|
466
|
-
return { policy, start, read, write, status, list, action, terminate, verifyEventJournal };
|
|
515
|
+
return { policy, start, read, write, status, list, action, cancel, terminate, verifyEventJournal };
|
|
467
516
|
}
|
|
@@ -59,7 +59,7 @@ try {
|
|
|
59
59
|
const listed = await client.listTools();
|
|
60
60
|
const names = listed.tools.map(tool => tool.name).sort();
|
|
61
61
|
const expected = ['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open',
|
|
62
|
-
'process_session_action','process_session_list','process_session_read','process_session_start','process_session_status','process_session_terminate','process_session_write'].sort();
|
|
62
|
+
'process_session_action','process_session_cancel','process_session_list','process_session_read','process_session_start','process_session_status','process_session_terminate','process_session_write'].sort();
|
|
63
63
|
if (JSON.stringify(names) !== JSON.stringify(expected)) throw new Error(`process smoke catalog mismatch: ${JSON.stringify(names)}`);
|
|
64
64
|
const started = structured(await client.callTool({ name: 'process_session_start', arguments: {
|
|
65
65
|
profile_id: 'ping_hold', root_id: 'core', cwd: '.', args: []
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
4
|
+
|
|
5
|
+
function hashObject(value) {
|
|
6
|
+
const json = JSON.stringify(value);
|
|
7
|
+
return createHash('sha256').update(Buffer.from(json, 'utf8')).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function requiredString(value, name, max = 512) {
|
|
11
|
+
if (typeof value !== 'string' || value.length < 1 || value.length > max) throw new Error(`resource identity ${name} invalid`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createResourceIdentity({
|
|
16
|
+
rootId,
|
|
17
|
+
rootGeneration = null,
|
|
18
|
+
relativePath,
|
|
19
|
+
canonicalPath,
|
|
20
|
+
kind,
|
|
21
|
+
exists,
|
|
22
|
+
size = 0,
|
|
23
|
+
contentSha256 = null,
|
|
24
|
+
objectIdentitySha256 = null
|
|
25
|
+
}) {
|
|
26
|
+
requiredString(rootId, 'root_id', 64);
|
|
27
|
+
requiredString(relativePath, 'relative_path');
|
|
28
|
+
requiredString(canonicalPath, 'canonical_path', 4096);
|
|
29
|
+
requiredString(kind, 'kind', 32);
|
|
30
|
+
if (rootGeneration !== null && typeof rootGeneration !== 'string') throw new Error('resource identity root_generation invalid');
|
|
31
|
+
if (typeof exists !== 'boolean') throw new Error('resource identity exists invalid');
|
|
32
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new Error('resource identity size invalid');
|
|
33
|
+
if (contentSha256 !== null && !SHA256_RE.test(contentSha256)) throw new Error('resource identity content_sha256 invalid');
|
|
34
|
+
if (objectIdentitySha256 !== null && !SHA256_RE.test(objectIdentitySha256)) throw new Error('resource identity object_identity_sha256 invalid');
|
|
35
|
+
|
|
36
|
+
const location = {
|
|
37
|
+
schema: 'deadbyte.resource-location.v1',
|
|
38
|
+
root_id: rootId,
|
|
39
|
+
root_generation: rootGeneration,
|
|
40
|
+
relative_path: relativePath.replaceAll('\\', '/'),
|
|
41
|
+
canonical_path: canonicalPath,
|
|
42
|
+
kind,
|
|
43
|
+
exists
|
|
44
|
+
};
|
|
45
|
+
const resourceIdentitySha256 = hashObject(location);
|
|
46
|
+
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
...location,
|
|
49
|
+
size,
|
|
50
|
+
content_sha256: contentSha256,
|
|
51
|
+
object_identity_sha256: objectIdentitySha256,
|
|
52
|
+
resource_identity_sha256: resourceIdentitySha256
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function contentIdentitySha256(contentSha256) {
|
|
57
|
+
if (contentSha256 !== null && !SHA256_RE.test(contentSha256)) throw new Error('content_sha256 invalid');
|
|
58
|
+
return contentSha256;
|
|
59
|
+
}
|