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.
Files changed (67) hide show
  1. package/MANIFEST.SHA256 +66 -41
  2. package/PROVENANCE.json +3 -3
  3. package/RELEASE.json +3 -3
  4. package/package.json +1 -1
  5. package/scripts/autonomous-build.ps1 +1 -0
  6. package/scripts/autonomous-tests.ps1 +1 -0
  7. package/scripts/check-agent-backend-r46.mjs +77 -0
  8. package/scripts/check-atomic-resource-r46.mjs +84 -0
  9. package/scripts/check-autonomous-race-r46.mjs +160 -0
  10. package/scripts/check-coding-task-deadline-r46.mjs +59 -0
  11. package/scripts/check-deadline-cancellation-r46.mjs +49 -0
  12. package/scripts/check-process-deadline-r46.mjs +52 -0
  13. package/scripts/check-r46-background.mjs +5 -0
  14. package/scripts/extract-r46-failures.mjs +5 -0
  15. package/scripts/full-regression-r46.mjs +20 -12
  16. package/scripts/gate-windows-r46.ps1 +2 -0
  17. package/scripts/inspect-r46-active.mjs +2 -0
  18. package/scripts/r47-core-failures.mjs +1 -0
  19. package/scripts/release-manifest.mjs +1 -1
  20. package/scripts/run-r46-full-regression-background.mjs +7 -0
  21. package/scripts/run-r46-windows-gate-background.mjs +6 -0
  22. package/scripts/run-r46-windows-gate-background.ps1 +10 -0
  23. package/scripts/windows-gate-evidence-r46.mjs +4 -3
  24. package/scripts/workload-contract-regression-r47.mjs +14 -0
  25. package/scripts/workload-stream-regression.mjs +16 -0
  26. package/scripts/workload-substrate-r47-regression.mjs +12 -0
  27. package/src/agent-backend-r44.mjs +75 -10
  28. package/src/atomic-mutation.mjs +55 -0
  29. package/src/autonomous-mcp-tools.mjs +4 -3
  30. package/src/autonomous-output-contracts.mjs +16 -3
  31. package/src/autonomous-runtime.mjs +83 -30
  32. package/src/coding-mcp-tools.mjs +6 -6
  33. package/src/coding-output-contracts.mjs +20 -5
  34. package/src/coding-plane.mjs +3 -24
  35. package/src/coding-search.mjs +52 -15
  36. package/src/coding-task-runtime.mjs +41 -6
  37. package/src/compact-gateway-mcp-tools.mjs +3 -3
  38. package/src/deadline-cancellation.mjs +83 -0
  39. package/src/fuzzy-search-core.mjs +117 -0
  40. package/src/fuzzy-search-worker.mjs +8 -0
  41. package/src/fuzzy-search.mjs +34 -0
  42. package/src/machine-fs-mcp-tools.mjs +1 -1
  43. package/src/machine-fs-output-contracts.mjs +1 -1
  44. package/src/machine-fs-smoke-client.mjs +1 -1
  45. package/src/machine-fs.mjs +10 -29
  46. package/src/mcp-server.mjs +6 -0
  47. package/src/observation-smoke-client.mjs +1 -1
  48. package/src/process-mcp-tools.mjs +11 -3
  49. package/src/process-output-contracts.mjs +23 -0
  50. package/src/process-runtime.mjs +54 -5
  51. package/src/process-smoke-client.mjs +1 -1
  52. package/src/resource-identity.mjs +59 -0
  53. package/src/runtime-update.mjs +526 -162
  54. package/src/workload-substrate.mjs +87 -0
  55. package/test/coding-runtime.test.mjs +12 -0
  56. package/test/coding-search-r36.test.mjs +7 -0
  57. package/test/full-max-catalog-r46.test.mjs +2 -2
  58. package/test/fuzzy-search-r46.test.mjs +54 -0
  59. package/test/helpers/autonomous-r34-fixture.mjs +10 -1
  60. package/test/machine-fs-mcp-tools.test.mjs +2 -0
  61. package/test/machine-fs-runtime.test.mjs +6 -0
  62. package/test/process-mcp-tools.test.mjs +5 -2
  63. package/test/process-runtime.test.mjs +18 -1
  64. package/test/r45-finalization.test.mjs +1 -1
  65. package/test/release-version.test.mjs +1 -1
  66. package/test/runtime-self-update.test.mjs +118 -1
  67. 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().nonnegative().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) }),
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',
@@ -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
- const before = await regularState(target);
275
- checkExpectedFile(before, expectedSha256);
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 < 0) throw new Error('machine fs read offset must be nonnegative integer');
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
- if (offset > info.size) throw new Error('machine fs read offset exceeds file size');
355
- const length = Math.min(maxBytes, info.size - offset);
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, next_offset: offset + buffer.length, file_size: info.size,
364
- eof: offset + buffer.length >= info.size, data_base64: buffer.toString('base64'),
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
  };
@@ -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
 
@@ -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
+ }