deadbyte-mcp 0.15.1 → 0.15.3

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 (96) hide show
  1. package/MANIFEST.SHA256 +95 -64
  2. package/PROVENANCE.json +6 -5
  3. package/README.txt +36 -20
  4. package/RELEASE.json +24 -8
  5. package/SBOM.cdx.json +3 -3
  6. package/controller/README.TXT +2 -2
  7. package/controller/deadbyte-controller.ps1 +50 -2
  8. package/controller/deadbyte-process-policy.json +63 -1
  9. package/docs/ARCHITECTURE.md +3 -4
  10. package/package.json +88 -86
  11. package/scripts/autonomous-build.ps1 +1 -0
  12. package/scripts/autonomous-tests.ps1 +1 -0
  13. package/scripts/check-agent-backend-r46.mjs +77 -0
  14. package/scripts/check-atomic-resource-r46.mjs +84 -0
  15. package/scripts/check-autonomous-race-r46.mjs +160 -0
  16. package/scripts/check-coding-task-deadline-r46.mjs +59 -0
  17. package/scripts/check-deadline-cancellation-r46.mjs +49 -0
  18. package/scripts/check-process-deadline-r46.mjs +52 -0
  19. package/scripts/check-r46-background.mjs +5 -0
  20. package/scripts/deferred-slot-operation-r46.mjs +126 -0
  21. package/scripts/extract-r46-failures.mjs +5 -0
  22. package/scripts/final-closure-r46.mjs +98 -0
  23. package/scripts/final-closure-verify-r46.mjs +119 -0
  24. package/scripts/full-regression-r46.mjs +28 -0
  25. package/scripts/gate-windows-r46.ps1 +58 -0
  26. package/scripts/inspect-r46-active.mjs +2 -0
  27. package/scripts/r47-core-failures.mjs +1 -0
  28. package/scripts/refresh-process-policy.mjs +64 -7
  29. package/scripts/release-manifest.mjs +1 -1
  30. package/scripts/release-parity-r46.mjs +187 -0
  31. package/scripts/run-r46-full-regression-background.mjs +7 -0
  32. package/scripts/run-r46-windows-gate-background.mjs +6 -0
  33. package/scripts/run-r46-windows-gate-background.ps1 +10 -0
  34. package/scripts/windows-gate-evidence-r46.mjs +111 -0
  35. package/scripts/workload-contract-regression-r47.mjs +14 -0
  36. package/scripts/workload-stream-regression.mjs +16 -0
  37. package/scripts/workload-substrate-r47-regression.mjs +12 -0
  38. package/src/agent-backend-r44.mjs +75 -10
  39. package/src/atomic-mutation.mjs +55 -0
  40. package/src/autonomous-mcp-tools.mjs +4 -3
  41. package/src/autonomous-output-contracts.mjs +16 -3
  42. package/src/autonomous-runtime.mjs +83 -30
  43. package/src/capability-ledger.mjs +1 -0
  44. package/src/coding-mcp-tools.mjs +2 -2
  45. package/src/coding-output-contracts.mjs +18 -4
  46. package/src/coding-plane.mjs +3 -24
  47. package/src/coding-search.mjs +11 -2
  48. package/src/coding-task-runtime.mjs +41 -6
  49. package/src/compact-gateway-mcp-tools.mjs +7 -5
  50. package/src/deadbyte-cli.mjs +7 -3
  51. package/src/deadline-cancellation.mjs +83 -0
  52. package/src/machine-fs-mcp-tools.mjs +3 -3
  53. package/src/machine-fs-output-contracts.mjs +1 -1
  54. package/src/machine-fs-read-worker.mjs +46 -0
  55. package/src/machine-fs-smoke-client.mjs +1 -1
  56. package/src/machine-fs.mjs +66 -33
  57. package/src/mcp-server.mjs +10 -0
  58. package/src/mcp-smoke-client.mjs +4 -1
  59. package/src/observation-runtime.mjs +25 -4
  60. package/src/observation-smoke-client.mjs +1 -1
  61. package/src/process-mcp-tools.mjs +29 -8
  62. package/src/process-output-contracts.mjs +43 -3
  63. package/src/process-policy.mjs +3 -1
  64. package/src/process-runtime.mjs +152 -23
  65. package/src/process-smoke-client.mjs +67 -22
  66. package/src/release-generation.mjs +22 -3
  67. package/src/remote-device-r44.mjs +53 -15
  68. package/src/remote-mcp-smoke-client.mjs +1 -1
  69. package/src/resource-identity.mjs +59 -0
  70. package/src/runtime-update.mjs +526 -162
  71. package/src/stdio-console-guard.mjs +24 -0
  72. package/src/version.mjs +1 -1
  73. package/src/workload-substrate.mjs +87 -0
  74. package/test/cli-entrypoint.test.mjs +3 -3
  75. package/test/coding-runtime.test.mjs +12 -0
  76. package/test/coding-search-r36.test.mjs +7 -0
  77. package/test/compact-gateway-r36.test.mjs +28 -0
  78. package/test/core.test.mjs +1 -1
  79. package/test/full-max-catalog-r46.test.mjs +92 -0
  80. package/test/helpers/autonomous-r34-fixture.mjs +10 -1
  81. package/test/machine-fs-mcp-tools.test.mjs +4 -1
  82. package/test/machine-fs-runtime.test.mjs +18 -2
  83. package/test/observation-runtime.test.mjs +29 -2
  84. package/test/process-mcp-tools.test.mjs +27 -6
  85. package/test/process-policy-refresh.test.mjs +87 -10
  86. package/test/process-runtime.test.mjs +129 -2
  87. package/test/process-supervision.test.mjs +4 -2
  88. package/test/production-docs.test.mjs +3 -3
  89. package/test/r33-finalization.test.mjs +3 -3
  90. package/test/r45-finalization.test.mjs +1 -1
  91. package/test/release-generation.test.mjs +25 -11
  92. package/test/release-version.test.mjs +99 -44
  93. package/test/remote-device-r44.test.mjs +75 -0
  94. package/test/remote-r24-catalog.test.mjs +2 -2
  95. package/test/runtime-self-update.test.mjs +118 -1
  96. package/test/workload-substrate.test.mjs +22 -0
@@ -1,9 +1,26 @@
1
- import * as z from 'zod/v4';
1
+ import * as z from 'zod/v4';
2
2
  import { structuredToolResult } from './output-contracts.mjs';
3
3
 
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
 
@@ -51,6 +74,23 @@ export const processActionOutputSchema = z.strictObject({
51
74
  }
52
75
  });
53
76
 
77
+ export const processWriteOutputSchema = z.strictObject({
78
+ status: z.enum(['ok','denied','failed']),
79
+ error: z.string().min(1).max(4096).optional(),
80
+ logical_bytes: z.number().int().positive().optional(),
81
+ logical_sha256: hex64.optional(),
82
+ append_newline: z.boolean().optional(),
83
+ session: processSessionRecordSchema.optional()
84
+ }).superRefine((value, ctx) => {
85
+ if (value.status !== 'ok') {
86
+ if (!value.error) ctx.addIssue({ code: 'custom', path: ['error'], message: 'error required' });
87
+ return;
88
+ }
89
+ for (const key of ['logical_bytes','logical_sha256','append_newline','session']) {
90
+ if (value[key] === undefined) ctx.addIssue({ code: 'custom', path: [key], message: `${key} required` });
91
+ }
92
+ });
93
+
54
94
  export const processListOutputSchema = z.strictObject({
55
95
  status: z.enum(['ok','failed']),
56
96
  error: z.string().min(1).max(4096).optional(),
@@ -65,11 +105,11 @@ export const processReadOutputSchema = z.strictObject({
65
105
  session_id: hex64.optional(), offset: z.number().int().nonnegative().optional(),
66
106
  next_offset: z.number().int().nonnegative().optional(), earliest_offset: z.number().int().nonnegative().optional(),
67
107
  stream_head: z.number().int().nonnegative().optional(), stream_sha256: hex64.optional(),
68
- truncated: z.boolean().optional(), data_base64: z.string().max(400000).optional(),
108
+ truncated: z.boolean().optional(), wait_satisfied: z.boolean().optional(), data_base64: z.string().max(400000).optional(),
69
109
  state: processState.optional(), exit_code: z.number().int().nullable().optional()
70
110
  }).superRefine((value, ctx) => {
71
111
  if (value.status !== 'ok') { if (!value.error) ctx.addIssue({ code: 'custom', path: ['error'], message: 'error required' }); return; }
72
- for (const key of ['session_id','offset','next_offset','earliest_offset','stream_head','stream_sha256','truncated','data_base64','state','exit_code']) {
112
+ for (const key of ['session_id','offset','next_offset','earliest_offset','stream_head','stream_sha256','truncated','wait_satisfied','data_base64','state','exit_code']) {
73
113
  if (value[key] === undefined) ctx.addIssue({ code: 'custom', path: [key], message: `${key} required` });
74
114
  }
75
115
  });
@@ -63,6 +63,8 @@ function normalizePolicyObject(raw, policyPath) {
63
63
  if (envAllow.some(name => !/^[A-Za-z_][A-Za-z0-9_()]*$/.test(name))) throw new Error(`process profile '${id}' env_allow invalid`);
64
64
  const transport = value.transport ?? 'conpty';
65
65
  if (transport !== 'conpty' && transport !== 'pipe') throw new Error(`process profile '${id}' transport invalid`);
66
+ if (value.allow_raw_input !== undefined && typeof value.allow_raw_input !== 'boolean') throw new Error(`process profile '${id}' allow_raw_input invalid`);
67
+ const allowRawInput = value.allow_raw_input === true;
66
68
  const inputActions = {};
67
69
  if (value.input_actions !== undefined) {
68
70
  if (!value.input_actions || typeof value.input_actions !== 'object' || Array.isArray(value.input_actions)) throw new Error(`process profile '${id}' input_actions invalid`);
@@ -80,7 +82,7 @@ function normalizePolicyObject(raw, policyPath) {
80
82
  max_sessions: finiteInteger(value.max_sessions, `profiles.${id}.max_sessions`, 1, 32, 4),
81
83
  max_input_bytes: finiteInteger(value.max_input_bytes, `profiles.${id}.max_input_bytes`, 1, 1024 * 1024, 64 * 1024),
82
84
  max_retained_output_bytes: finiteInteger(value.max_retained_output_bytes, `profiles.${id}.max_retained_output_bytes`, 4096, 16 * 1024 * 1024, 1024 * 1024),
83
- transport, env_allow: [...new Set(envAllow)], input_actions: inputActions
85
+ transport, allow_raw_input: allowRawInput, env_allow: [...new Set(envAllow)], input_actions: inputActions
84
86
  };
85
87
  }
86
88
  if (Object.keys(profiles).length === 0) throw new Error('process policy requires at least one profile');
@@ -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,28 +54,75 @@ 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
  }
72
+ function retainedSlice(session, begin = 0, length = session.retained_output_bytes - begin) {
73
+ if (!Number.isInteger(begin) || begin < 0 || !Number.isInteger(length) || length < 0) throw new Error('invalid retained output slice');
74
+ if (begin >= session.retained_output_bytes || length === 0) return Buffer.alloc(0);
75
+ const end = Math.min(session.retained_output_bytes, begin + length);
76
+ const parts = [];
77
+ let cursor = 0;
78
+ for (const chunk of session.output_chunks) {
79
+ const next = cursor + chunk.length;
80
+ if (next <= begin) { cursor = next; continue; }
81
+ if (cursor >= end) break;
82
+ const from = Math.max(0, begin - cursor);
83
+ const to = Math.min(chunk.length, end - cursor);
84
+ if (to > from) parts.push(chunk.subarray(from, to));
85
+ cursor = next;
86
+ }
87
+ if (parts.length === 0) return Buffer.alloc(0);
88
+ if (parts.length === 1) return Buffer.from(parts[0]);
89
+ return Buffer.concat(parts, end - begin);
90
+ }
91
+ function retainedAll(session) {
92
+ return retainedSlice(session, 0, session.retained_output_bytes);
93
+ }
94
+ function retainedTail(session, maxBytes) {
95
+ const length = Math.min(maxBytes, session.retained_output_bytes);
96
+ return retainedSlice(session, session.retained_output_bytes - length, length);
97
+ }
98
+ function retainedIndexOf(session, needle, begin = 0) {
99
+ if (!Buffer.isBuffer(needle) || needle.length < 1) return -1;
100
+ return retainedSlice(session, begin, session.retained_output_bytes - begin).indexOf(needle);
101
+ }
63
102
  function appendOutput(session, chunk) {
64
103
  if (!chunk?.length) return;
65
104
  const bytes = Buffer.from(chunk);
105
+ session.output_stream?.append(null, { producer: 'process-host', channel: 'merged-output', sizeBytes: bytes.length });
66
106
  session.stream_sha256 = sha256Bytes(Buffer.concat([
67
107
  Buffer.from(session.stream_sha256, 'hex'), bytes
68
108
  ]));
69
109
  session.stream_head += bytes.length;
70
- session.output = Buffer.concat([session.output, bytes]);
71
- if (session.output.length > session.max_retained_output_bytes) {
72
- const drop = session.output.length - session.max_retained_output_bytes;
73
- session.output = session.output.subarray(drop);
74
- session.earliest_offset += drop;
110
+ session.output_chunks.push(bytes);
111
+ session.retained_output_bytes += bytes.length;
112
+ while (session.retained_output_bytes > session.max_retained_output_bytes && session.output_chunks.length > 0) {
113
+ const excess = session.retained_output_bytes - session.max_retained_output_bytes;
114
+ const first = session.output_chunks[0];
115
+ if (first.length <= excess) {
116
+ session.output_chunks.shift();
117
+ session.retained_output_bytes -= first.length;
118
+ session.earliest_offset += first.length;
119
+ continue;
120
+ }
121
+ session.output_chunks[0] = first.subarray(excess);
122
+ session.retained_output_bytes -= excess;
123
+ session.earliest_offset += excess;
75
124
  }
76
- const tail = session.output.subarray(Math.max(0, session.output.length - 4096)).toString('latin1');
125
+ const tail = retainedTail(session, 4096).toString('latin1');
77
126
  const enabledAt = tail.lastIndexOf('\x1b[?9001h');
78
127
  const disabledAt = tail.lastIndexOf('\x1b[?9001l');
79
128
  if (enabledAt >= 0 || disabledAt >= 0) session.win32_input_mode = enabledAt > disabledAt;
@@ -166,9 +215,10 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
166
215
  child_pid:started.detail.child_pid, creation_filetime:String(started.detail.creation_filetime),
167
216
  state:terminal.type, exit_code:Number.isInteger(terminal.detail.exit_code) ? terminal.detail.exit_code : null,
168
217
  earliest_offset:terminal.detail.earliest_offset, stream_head:terminal.detail.stream_head,
169
- stream_sha256:terminal.detail.stream_sha256, output, control_buffer:Buffer.alloc(0),
218
+ stream_sha256:terminal.detail.stream_sha256, output_chunks:[output], retained_output_bytes:output.length, control_buffer:Buffer.alloc(0),
170
219
  journal_head_sha256:checked.head_sha256, journal_path:path.join(policy.state_dir,name), event_count:checked.event_count,
171
- 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,
172
222
  protocol_error:null, transport:profile.transport, win32_input_mode:false, host:null, close_promise:Promise.resolve(),
173
223
  recovered_terminal:true
174
224
  });
@@ -242,7 +292,8 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
242
292
  }
243
293
  }
244
294
 
245
- 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');
246
297
  await hydrateTerminalSessions();
247
298
  const grant = await assertProcessArmed(policy, profileId, rootId);
248
299
  const profile = policy.profiles[profileId];
@@ -262,18 +313,37 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
262
313
  const session = {
263
314
  session_id: sessionId, job_id: randomBytes(32).toString('hex'), profile_id: profileId, root_id: rootId,
264
315
  cwd_relative: cwdInfo.relative, child_pid: null, creation_filetime: null, state: 'starting', exit_code: null,
265
- earliest_offset: 0, stream_head: 0, stream_sha256: ZERO_SHA256, output: Buffer.alloc(0), control_buffer: Buffer.alloc(0),
316
+ earliest_offset: 0, stream_head: 0, stream_sha256: ZERO_SHA256, output_chunks: [], retained_output_bytes: 0, control_buffer: Buffer.alloc(0),
266
317
  journal_head_sha256: ZERO_SHA256, journal_path: path.join(policy.state_dir, `${sessionId}.jsonl`), event_count: 0,
267
- 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(),
268
322
  transport: profile.transport,
269
323
  win32_input_mode: false
270
324
  };
271
325
  await recheckProcessAuthority(policy, grant, profileId, rootId);
326
+ const composed = composeAbortSignal({ deadline: session.deadline, cancellation: session.cancellation });
327
+ session.abort_signal = composed;
272
328
  const host = spawn(policy.native_host.path,
273
329
  [profile.executable, cwdInfo.resolved, session.job_id, `--deadbyte-transport=${profile.transport}`, ...profile.prefix_args, ...args],
274
330
  { cwd: cwdInfo.resolved, env: processEnvironment(profile), windowsHide: true, shell: false, stdio: ['pipe','pipe','pipe'] });
275
331
  session.host = host;
276
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();
277
347
  const ready = new Promise((resolve, reject) => { session.ready_resolve = resolve; session.ready_reject = reject; });
278
348
  host.stdout.on('data', chunk => parseHostOutput(session, chunk));
279
349
  host.stderr.on('data', chunk => { session.stderr = (session.stderr + chunk.toString('utf8')).slice(-16384); });
@@ -283,23 +353,34 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
283
353
  parseHostOutput(session, null, true);
284
354
  if (session.state === 'starting') session.ready_reject?.(new Error(`process host exited before READY: ${session.stderr}`));
285
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
+ });
286
363
  const terminalState = session.terminate_requested ? 'terminated' : 'exited';
287
364
  try {
288
365
  const outputFile = `${session.session_id}.stream.bin`;
289
- const retainedOutputSha256 = sha256Bytes(session.output);
290
- await writeFile(path.join(policy.state_dir, outputFile), session.output, { flag:'wx' });
366
+ const retainedOutput = retainedAll(session);
367
+ const retainedOutputSha256 = sha256Bytes(retainedOutput);
368
+ await writeFile(path.join(policy.state_dir, outputFile), retainedOutput, { flag:'wx' });
291
369
  await appendEvent(runtime, session, terminalState, {
292
370
  recovery_schema:TERMINAL_RECOVERY_SCHEMA,
293
371
  exit_code:session.exit_code,
294
372
  earliest_offset:session.earliest_offset,
295
373
  stream_head:session.stream_head,
296
374
  stream_sha256:session.stream_sha256,
297
- retained_output_bytes:session.output.length,
375
+ retained_output_bytes:session.retained_output_bytes,
298
376
  retained_output_sha256:retainedOutputSha256,
377
+ termination:session.termination,
378
+ deadline:session.deadline,
299
379
  output_file:outputFile
300
380
  });
301
381
  session.state = terminalState;
302
382
  } catch {}
383
+ composed.close();
303
384
  resolve();
304
385
  });
305
386
  });
@@ -311,17 +392,57 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
311
392
  return publicSession(session);
312
393
  }
313
394
 
314
- async function read({ sessionId, offset = 0, maxBytes = 65536 }) {
395
+ async function read({ sessionId, offset = 0, maxBytes = 65536, waitMs = 0, waitFor = null }) {
315
396
  const session = await sessionFor(sessionId);
316
- if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > 262144) throw new Error('invalid process read bounds');
317
- if (offset > session.stream_head) throw new Error('process read offset is beyond stream head');
318
- const truncated = offset < session.earliest_offset;
319
- const effective = Math.max(offset, session.earliest_offset);
397
+ if (!Number.isInteger(offset) || offset < -(16 * 1024 * 1024) || !Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > 262144 ||
398
+ !Number.isInteger(waitMs) || waitMs < 0 || waitMs > 30000) throw new Error('invalid process read bounds');
399
+ if (waitFor !== null && (typeof waitFor !== 'string' || waitFor.length < 1 || waitFor.includes('\0') || Buffer.byteLength(waitFor, 'utf8') > 256)) {
400
+ throw new Error('invalid process wait_for literal');
401
+ }
402
+ const requested = offset < 0 ? Math.max(0, session.stream_head + offset) : offset;
403
+ if (requested > session.stream_head) throw new Error('process read offset is beyond stream head');
404
+ const baseline = session.stream_head;
405
+ const literal = waitFor === null ? null : Buffer.from(waitFor, 'utf8');
406
+ const satisfied = () => {
407
+ if (literal === null) return session.stream_head > baseline || session.state !== 'running';
408
+ const effectiveSearch = Math.max(requested, session.earliest_offset);
409
+ const begin = effectiveSearch - session.earliest_offset;
410
+ return retainedIndexOf(session, literal, begin) >= 0;
411
+ };
412
+ if (waitMs > 0 && session.state === 'running' && !satisfied()) {
413
+ const deadline = Date.now() + waitMs;
414
+ while (session.state === 'running' && !satisfied() && Date.now() < deadline) {
415
+ await new Promise(resolve => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
416
+ }
417
+ }
418
+ const effectiveRequested = offset < 0 ? Math.max(0, session.stream_head + offset) : requested;
419
+ const truncated = effectiveRequested < session.earliest_offset;
420
+ const effective = Math.max(effectiveRequested, session.earliest_offset);
320
421
  const begin = effective - session.earliest_offset;
321
- const data = session.output.subarray(begin, begin + maxBytes);
422
+ const data = retainedSlice(session, begin, maxBytes);
322
423
  return { session_id: session.session_id, offset: effective, next_offset: effective + data.length,
323
424
  earliest_offset: session.earliest_offset, stream_head: session.stream_head, stream_sha256: session.stream_sha256,
324
- truncated, data_base64: data.toString('base64'), state: session.state, exit_code: session.exit_code };
425
+ truncated, wait_satisfied: waitFor === null ? (session.stream_head > baseline || waitMs === 0 || session.state !== 'running') : satisfied(),
426
+ data_base64: data.toString('base64'), state: session.state, exit_code: session.exit_code };
427
+ }
428
+
429
+ async function write({ sessionId, input, appendNewline = true }) {
430
+ const session = await sessionFor(sessionId);
431
+ if (session.state !== 'running') throw new Error('process write requires a running session');
432
+ const grant = await assertProcessArmed(policy, session.profile_id, session.root_id);
433
+ const profile = policy.profiles[session.profile_id];
434
+ if (!profile?.allow_raw_input) throw new Error(`process profile '${session.profile_id}' does not allow raw input`);
435
+ if (session.transport !== 'pipe' || profile.transport !== 'pipe') throw new Error('process raw input requires pipe transport');
436
+ if (typeof input !== 'string' || input.includes('\0')) throw new Error('process raw input must be a NUL-free string');
437
+ if (typeof appendNewline !== 'boolean') throw new Error('process appendNewline must be boolean');
438
+ const logicalBytes = Buffer.from(appendNewline ? `${input}\r\n` : input, 'utf8');
439
+ if (logicalBytes.length < 1 || logicalBytes.length > profile.max_input_bytes) throw new Error('process raw input exceeds profile input bound');
440
+ if (!session.host?.stdin || session.host.stdin.destroyed || !session.host.stdin.writable) throw new Error('process session input is not writable');
441
+ await recheckProcessAuthority(policy, grant, session.profile_id, session.root_id);
442
+ await new Promise((resolve, reject) => session.host.stdin.write(logicalBytes, error => error ? reject(error) : resolve()));
443
+ const logicalSha256 = sha256Bytes(logicalBytes);
444
+ await appendEvent(runtime, session, 'input', { logical_bytes: logicalBytes.length, logical_sha256: logicalSha256, append_newline: appendNewline });
445
+ return { logical_bytes: logicalBytes.length, logical_sha256: logicalSha256, append_newline: appendNewline, session: publicSession(session) };
325
446
  }
326
447
 
327
448
  async function status({ sessionId }) { return publicSession(await sessionFor(sessionId)); }
@@ -348,11 +469,19 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
348
469
  await appendEvent(runtime, session, 'action', { action_id: actionId, logical_bytes: logicalBytes.length, logical_sha256: logicalSha256 });
349
470
  return { action_id: actionId, logical_bytes: logicalBytes.length, logical_sha256: logicalSha256, session: publicSession(session) };
350
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
+ }
351
480
  async function terminate({ sessionId }) {
352
481
  const session = await sessionFor(sessionId);
353
482
  if (['exited','terminated'].includes(session.state)) return publicSession(session);
354
483
  session.terminate_requested = true;
355
- session.host.kill();
484
+ if (!session.host.killed) session.host.kill();
356
485
  await session.close_promise;
357
486
  return publicSession(session);
358
487
  }
@@ -383,5 +512,5 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
383
512
  return { ok: true, event_count: sequence, head_sha256: previous };
384
513
  }
385
514
 
386
- return { policy, start, read, status, list, action, terminate, verifyEventJournal };
515
+ return { policy, start, read, write, status, list, action, cancel, terminate, verifyEventJournal };
387
516
  }
@@ -29,7 +29,7 @@ const policy = await loadProcessPolicy(smokePolicyPath);
29
29
  const keys = await generateTrustKeypair({ privateKeyPath: path.join(smokeTrust, 'private.pem'), publicKeyPath: path.join(smokeTrust, 'public.pem') });
30
30
  await mkdir(path.dirname(policy.grant_file), { recursive: true });
31
31
  const grant = { schema: PROCESS_GRANT_SCHEMA, policy_sha256: policy.policy_sha256,
32
- instance_nonce: randomBytes(32).toString('hex'), profile_ids: ['ping_hold'], root_ids: ['core'],
32
+ instance_nonce: randomBytes(32).toString('hex'), profile_ids: ['ping_hold','powershell_repl'], root_ids: ['core'],
33
33
  granted_at_utc: new Date().toISOString() };
34
34
  await writeFile(policy.grant_file, `${JSON.stringify(grant, null, 2)}\n`, { flag: 'wx' });
35
35
 
@@ -37,8 +37,10 @@ const client = new Client({ name: 'deadbyte-process-smoke', version: '0.8.0' },
37
37
  { versionNegotiation: { mode: { pin: '2026-07-28' } } });
38
38
  const transport = new StdioClientTransport({
39
39
  command: process.execPath, args: [serverPath], cwd: repoRoot,
40
- env: { ...getDefaultEnvironment(), DEADBYTE_PROCESS_RUNTIME: '1', DEADBYTE_PROCESS_POLICY: smokePolicyPath,
41
- DEADBYTE_SIGNING_KEY: keys.privateKeyPath, DEADBYTE_VERIFY_KEY: keys.publicKeyPath },
40
+ env: { ...getDefaultEnvironment(), DEADBYTE_PRIVILEGED_POLICY: '', DEADBYTE_MACHINE_RUNTIME:'0', DEADBYTE_AGENT_RUNTIME:'0',
41
+ DEADBYTE_PROCESS_RUNTIME: '1', DEADBYTE_PROCESS_POLICY: smokePolicyPath, DEADBYTE_CODING_RUNTIME:'0', DEADBYTE_MACHINE_FS_RUNTIME:'0',
42
+ DEADBYTE_OBSERVATION_RUNTIME:'0', DEADBYTE_DESKTOP_RUNTIME:'0', DEADBYTE_AUTONOMOUS_RUNTIME:'0', DEADBYTE_AUDIT_RUNTIME:'0',
43
+ DEADBYTE_CAPABILITY_PROFILE:'full', DEADBYTE_SIGNING_KEY: keys.privateKeyPath, DEADBYTE_VERIFY_KEY: keys.publicKeyPath },
42
44
  stderr: 'pipe'
43
45
  });
44
46
  const stderrChunks = [];
@@ -51,12 +53,13 @@ const structured = (result, label) => {
51
53
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
52
54
  let sessionId = null;
53
55
  let journalPath = null;
56
+ let replJournalPath = null;
54
57
  try {
55
58
  await client.connect(transport);
56
59
  const listed = await client.listTools();
57
60
  const names = listed.tools.map(tool => tool.name).sort();
58
61
  const expected = ['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open',
59
- 'process_session_action','process_session_list','process_session_read','process_session_start','process_session_status','process_session_terminate'].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();
60
63
  if (JSON.stringify(names) !== JSON.stringify(expected)) throw new Error(`process smoke catalog mismatch: ${JSON.stringify(names)}`);
61
64
  const started = structured(await client.callTool({ name: 'process_session_start', arguments: {
62
65
  profile_id: 'ping_hold', root_id: 'core', cwd: '.', args: []
@@ -93,31 +96,72 @@ try {
93
96
  const terminated = structured(await client.callTool({ name: 'process_session_terminate', arguments: { session_id: sessionId } }), 'process_session_terminate');
94
97
  if (terminated.status !== 'ok' || terminated.session.state !== 'terminated') throw new Error(`process terminate failed: ${JSON.stringify(terminated)}`);
95
98
  const finalHead = terminated.session.journal_head_sha256;
99
+
100
+ const replStarted = structured(await client.callTool({ name: 'process_session_start', arguments: {
101
+ profile_id: 'powershell_repl', root_id: 'core', cwd: '.', args: []
102
+ } }), 'process_session_start powershell_repl');
103
+ const replSessionId = replStarted.session?.session_id;
104
+ if (replStarted.status !== 'ok' || !/^[0-9a-f]{64}$/.test(replSessionId ?? '')) throw new Error('powershell repl start malformed');
105
+ replJournalPath = path.join(policy.state_dir, `${replSessionId}.jsonl`);
106
+ const firstWrite = structured(await client.callTool({ name: 'process_session_write', arguments: {
107
+ session_id: replSessionId, input: "$x=41;[Console]::WriteLine('DB_PUBLIC_REPL_SET')", append_newline: true
108
+ } }), 'process_session_write set');
109
+ if (firstWrite.status !== 'ok' || firstWrite.session?.state !== 'running') throw new Error('powershell repl first write failed');
110
+ const beforeSecond = firstWrite.session.stream_head;
111
+ const secondWrite = structured(await client.callTool({ name: 'process_session_write', arguments: {
112
+ session_id: replSessionId, input: "[Console]::WriteLine('DB_PUBLIC_REPL_VALUE=' + ($x+1))", append_newline: true
113
+ } }), 'process_session_write value');
114
+ if (secondWrite.status !== 'ok') throw new Error('powershell repl second write failed');
115
+ const replRead = structured(await client.callTool({ name: 'process_session_read', arguments: {
116
+ session_id: replSessionId, offset: beforeSecond, max_bytes: 65536, wait_ms: 5000, wait_for: 'DB_PUBLIC_REPL_VALUE=42'
117
+ } }), 'process_session_read repl');
118
+ const replText = Buffer.from(replRead.data_base64 ?? '', 'base64').toString('utf8');
119
+ if (replRead.status !== 'ok' || replRead.wait_satisfied !== true || !replText.includes('DB_PUBLIC_REPL_VALUE=42')) {
120
+ throw new Error(`powershell repl state persistence proof failed: ${JSON.stringify(replRead)}`);
121
+ }
122
+ const replTail = structured(await client.callTool({ name: 'process_session_read', arguments: {
123
+ session_id: replSessionId, offset: -4096, max_bytes: 4096, wait_ms: 0, wait_for: null
124
+ } }), 'process_session_read tail');
125
+ if (!Buffer.from(replTail.data_base64 ?? '', 'base64').toString('utf8').includes('DB_PUBLIC_REPL_VALUE=42')) {
126
+ throw new Error('powershell repl tail read proof failed');
127
+ }
128
+ const replTerminated = structured(await client.callTool({ name: 'process_session_terminate', arguments: { session_id: replSessionId } }), 'process_session_terminate repl');
129
+ if (replTerminated.status !== 'ok' || replTerminated.session.state !== 'terminated') throw new Error('powershell repl terminate failed');
130
+ const replJournalText = await readFile(replJournalPath, 'utf8');
131
+ if (/DB_PUBLIC_REPL_SET|DB_PUBLIC_REPL_VALUE|\$x=41/.test(replJournalText)) throw new Error('powershell repl journal leaked raw input payload');
132
+ const replFinalHead = replTerminated.session.journal_head_sha256;
96
133
  await client.close();
97
134
 
98
135
  const verifyKey = await loadPublicVerifyKey(keys.publicKeyPath);
99
- const records = (await readFile(journalPath, 'utf8')).split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
100
- let previous = ZERO_SHA256;
101
- let sequence = 0;
102
- for (const record of records) {
103
- const { event_sha256: eventSha, key_id: keyId, signature, ...unsigned } = record;
104
- if (unsigned.session_id !== sessionId || unsigned.sequence !== sequence + 1 || unsigned.previous_event_sha256 !== previous || unsigned.policy_sha256 !== policy.policy_sha256) {
105
- throw new Error('process journal chain mismatch');
136
+ const verifyJournal = async (targetSessionId, targetJournalPath, expectedHead, minimumEvents) => {
137
+ const records = (await readFile(targetJournalPath, 'utf8')).split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
138
+ let previous = ZERO_SHA256;
139
+ let sequence = 0;
140
+ for (const record of records) {
141
+ const { event_sha256: eventSha, key_id: keyId, signature, ...unsigned } = record;
142
+ if (unsigned.session_id !== targetSessionId || unsigned.sequence !== sequence + 1 || unsigned.previous_event_sha256 !== previous || unsigned.policy_sha256 !== policy.policy_sha256) {
143
+ throw new Error('process journal chain mismatch');
144
+ }
145
+ const canonical = Buffer.from(canonicalJson(unsigned), 'utf8');
146
+ const observed = sha256(canonical);
147
+ if (eventSha !== observed || keyId !== verifyKey.keyId || !cryptoVerify(null, Buffer.concat([EVENT_DOMAIN, canonical]), verifyKey.publicKey, Buffer.from(signature, 'base64'))) {
148
+ throw new Error('process journal signature mismatch');
149
+ }
150
+ previous = observed;
151
+ sequence += 1;
106
152
  }
107
- const canonical = Buffer.from(canonicalJson(unsigned), 'utf8');
108
- const observed = sha256(canonical);
109
- if (eventSha !== observed || keyId !== verifyKey.keyId || !cryptoVerify(null, Buffer.concat([EVENT_DOMAIN, canonical]), verifyKey.publicKey, Buffer.from(signature, 'base64'))) {
110
- throw new Error('process journal signature mismatch');
111
- }
112
- previous = observed;
113
- sequence += 1;
114
- }
115
- if (sequence < 3 || previous !== finalHead) throw new Error('process journal head mismatch');
153
+ if (sequence < minimumEvents || previous !== expectedHead) throw new Error('process journal head mismatch');
154
+ return { sequence, head: previous };
155
+ };
156
+ const pingVerified = await verifyJournal(sessionId, journalPath, finalHead, 3);
157
+ const replVerified = await verifyJournal(replSessionId, replJournalPath, replFinalHead, 4);
116
158
  if ((await readFile(journalPath, 'utf8')).includes('DEADBYTE_ACTION_PING')) throw new Error('process journal leaked action payload');
117
- console.log(JSON.stringify({ status: 'passed', protocol_revision: '2026-07-28', tool_count: 12,
159
+ console.log(JSON.stringify({ status: 'passed', protocol_revision: '2026-07-28', tool_count: 13,
118
160
  profile_id: 'ping_hold', session_id: sessionId, observed_output_bytes: read.stream_head,
119
161
  action_id: action.action_id, logical_bytes: action.logical_bytes, logical_sha256: action.logical_sha256,
120
- final_state: terminated.session.state, event_count: sequence, journal_head_sha256: previous,
162
+ final_state: terminated.session.state, event_count: pingVerified.sequence, journal_head_sha256: pingVerified.head,
163
+ persistent_repl: { profile_id:'powershell_repl', session_id:replSessionId, state_persisted:true, wait_for:true, tail_read:true,
164
+ raw_input_not_journaled:true, event_count:replVerified.sequence, journal_head_sha256:replVerified.head },
121
165
  policy_sha256: policy.policy_sha256, key_id: verifyKey.keyId, signature_valid: true }, null, 2));
122
166
  } catch (error) {
123
167
  const stderr = Buffer.concat(stderrChunks).toString('utf8').trim();
@@ -128,5 +172,6 @@ try {
128
172
  await client.close().catch(() => {});
129
173
  await rm(policy.grant_file, { force: true }).catch(() => {});
130
174
  if (journalPath) await rm(journalPath, { force: true }).catch(() => {});
175
+ if (replJournalPath) await rm(replJournalPath, { force: true }).catch(() => {});
131
176
  await rm(smokeTrust, { recursive: true, force: true }).catch(() => {});
132
177
  }
@@ -105,7 +105,7 @@ function rebindHostPolicy(raw,{developmentRoot,stateRoot}){
105
105
  return policy;
106
106
  }
107
107
 
108
- async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot}){
108
+ async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot,systemRoot}){
109
109
  if(!raw) return null;
110
110
  const policy=clone(raw);
111
111
  policy.grant_file=path.join(stateRoot,'runtime','process-grant.json');
@@ -116,6 +116,25 @@ async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot}){
116
116
  const nativeHostStat=await lstat(nativeHostPath);
117
117
  if(!nativeHostStat.isFile()||nativeHostStat.isSymbolicLink()) throw new Error('process native host invalid in release slot');
118
118
  policy.native_host={path:nativeHostPath,sha256:sha256(await readFile(nativeHostPath))};
119
+
120
+ const systemProfiles=[
121
+ ['ping_hold',['System32','cmd.exe']],
122
+ ['powershell',['System32','WindowsPowerShell','v1.0','powershell.exe']],
123
+ ['powershell_repl',['System32','WindowsPowerShell','v1.0','powershell.exe']]
124
+ ];
125
+ const needsSystemRoot=systemProfiles.some(([id])=>policy.profiles?.[id]);
126
+ if(needsSystemRoot&&(typeof systemRoot!=='string'||systemRoot.length<1)) {
127
+ throw new Error('process system profiles require SystemRoot/WINDIR');
128
+ }
129
+ for(const [id,parts] of systemProfiles){
130
+ const profile=policy.profiles?.[id];
131
+ if(!profile) continue;
132
+ const executable=path.join(path.resolve(systemRoot),...parts);
133
+ const stat=await lstat(executable);
134
+ if(!stat.isFile()||stat.isSymbolicLink()) throw new Error(`process profile '${id}' system executable invalid`);
135
+ profile.executable=executable;
136
+ profile.executable_sha256=sha256(await readFile(executable));
137
+ }
119
138
  return policy;
120
139
  }
121
140
 
@@ -199,7 +218,7 @@ function renderControllerConfig({currentConfig,coreRoot,workspaceRoot,processPol
199
218
  '}',''
200
219
  ].join('\r\n');
201
220
  }
202
- export async function createGenerationBundle({stateRoot,releaseRoot,developmentRoot,workspaceRoot,releaseId,currentConfig,processPolicy,desktopPolicy,codingPolicy,autonomousPolicy,hostPolicy}){
221
+ export async function createGenerationBundle({stateRoot,releaseRoot,developmentRoot,workspaceRoot,releaseId,currentConfig,processPolicy,desktopPolicy,codingPolicy,autonomousPolicy,hostPolicy,systemRoot=process.env.SystemRoot??process.env.WINDIR??null}){
203
222
  const state=path.resolve(stateRoot),release=path.resolve(releaseRoot),development=path.resolve(developmentRoot),workspace=path.resolve(workspaceRoot);
204
223
  if(typeof releaseId!=='string'||!/^[A-Za-z0-9._-]{1,96}$/.test(releaseId)) throw new Error('generation release id invalid');
205
224
  if(typeof currentConfig?.Bridge?.FileSha256!=='string'||!HEX64.test(currentConfig.Bridge.FileSha256)) throw new Error('generation Bridge FileSha256 invalid');
@@ -255,7 +274,7 @@ export async function createGenerationBundle({stateRoot,releaseRoot,developmentR
255
274
  const autonomousBytes=jsonBytes(autonomous);
256
275
  const host=rebindHostPolicy(hostPolicy,{developmentRoot:development,stateRoot:state});
257
276
  const hostBytes=host?jsonBytes(host):null;
258
- const processBound=await rebindProcessPolicy(processPolicy,{developmentRoot:development,stateRoot:state,releaseRoot:release});
277
+ const processBound=await rebindProcessPolicy(processPolicy,{developmentRoot:development,stateRoot:state,releaseRoot:release,systemRoot});
259
278
  const processBytes=processBound?jsonBytes(processBound):null;
260
279
  const desktopBound=await rebindDesktopPolicy(desktopPolicy,{stateRoot:state,releaseRoot:release});
261
280
  const desktopBytes=desktopBound?jsonBytes(desktopBound):null;