deadbyte-mcp 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/MANIFEST.SHA256 +109 -64
  2. package/PROVENANCE.json +11 -0
  3. package/R45-PARENT.json +18 -0
  4. package/README.txt +36 -20
  5. package/RELEASE.json +53 -0
  6. package/SBOM.cdx.json +46 -0
  7. package/TEST-MATRIX.json +31 -0
  8. package/bin/deadbyte-hardware-observer-r45.exe +0 -0
  9. package/bin/deadbyte-hardware-observer-r45.obj +0 -0
  10. package/controller/README.TXT +2 -2
  11. package/controller/deadbyte-controller.ps1 +51 -3
  12. package/controller/deadbyte-process-policy.json +63 -1
  13. package/docs/ARCHITECTURE.md +3 -4
  14. package/native/deadbyte-hardware-observer.cpp +308 -0
  15. package/package.json +14 -7
  16. package/scripts/autonomous-tests.ps1 +2 -2
  17. package/scripts/build-hardware-r45.ps1 +71 -0
  18. package/scripts/deferred-slot-operation-r45.mjs +126 -0
  19. package/scripts/deferred-slot-operation-r46.mjs +126 -0
  20. package/scripts/derive-r45-parent.mjs +24 -0
  21. package/scripts/derive-r45-release.mjs +72 -0
  22. package/scripts/derive-r45-tests.mjs +20 -0
  23. package/scripts/final-closure-r45.mjs +121 -0
  24. package/scripts/final-closure-r46.mjs +98 -0
  25. package/scripts/final-closure-verify-r45.mjs +220 -0
  26. package/scripts/final-closure-verify-r46.mjs +119 -0
  27. package/scripts/finalize-r45.mjs +219 -0
  28. package/scripts/full-regression-r45.mjs +20 -0
  29. package/scripts/full-regression-r46.mjs +20 -0
  30. package/scripts/full-regression-summary-r45.mjs +14 -0
  31. package/scripts/gate-windows-r45.ps1 +44 -0
  32. package/scripts/gate-windows-r46.ps1 +56 -0
  33. package/scripts/init-host-policy.ps1 +39 -1
  34. package/scripts/inspect-active-release-r45.mjs +20 -0
  35. package/scripts/refresh-process-policy.mjs +64 -7
  36. package/scripts/release-manifest.mjs +3 -1
  37. package/scripts/release-parity-r45.mjs +253 -0
  38. package/scripts/release-parity-r46.mjs +187 -0
  39. package/scripts/release-parity-tests-r45.ps1 +8 -0
  40. package/scripts/verify-r45-parent.mjs +142 -0
  41. package/scripts/windows-gate-evidence-r45.mjs +60 -0
  42. package/scripts/windows-gate-evidence-r46.mjs +110 -0
  43. package/src/autonomous-runtime.mjs +12 -7
  44. package/src/capability-ledger.mjs +2 -0
  45. package/src/compact-gateway-mcp-tools.mjs +11 -7
  46. package/src/deadbyte-cli.mjs +24 -12
  47. package/src/doctor-r45.mjs +65 -0
  48. package/src/host-plane.mjs +133 -85
  49. package/src/machine-fs-mcp-tools.mjs +3 -3
  50. package/src/machine-fs-output-contracts.mjs +1 -1
  51. package/src/machine-fs-read-worker.mjs +46 -0
  52. package/src/machine-fs.mjs +63 -10
  53. package/src/mcp-server.mjs +68 -23
  54. package/src/mcp-smoke-client.mjs +4 -1
  55. package/src/native-diff-v2.mjs +9 -4
  56. package/src/native-diff.mjs +29 -7
  57. package/src/observation-mcp-tools.mjs +5 -1
  58. package/src/observation-output-contracts.mjs +8 -0
  59. package/src/observation-runtime.mjs +57 -5
  60. package/src/observation-smoke-client.mjs +6 -2
  61. package/src/output-contracts.mjs +27 -11
  62. package/src/process-mcp-tools.mjs +18 -5
  63. package/src/process-output-contracts.mjs +20 -3
  64. package/src/process-policy.mjs +3 -1
  65. package/src/process-runtime.mjs +99 -19
  66. package/src/process-smoke-client.mjs +67 -22
  67. package/src/r45-compact-disarmed-probe.mjs +74 -0
  68. package/src/r45-full-surface-probe.mjs +34 -0
  69. package/src/release-generation.mjs +23 -3
  70. package/src/remote-device-r44.mjs +53 -15
  71. package/src/remote-mcp-r45-parity-client.mjs +206 -0
  72. package/src/remote-mcp-smoke-client.mjs +1 -1
  73. package/src/runtime-update.mjs +36 -8
  74. package/src/stdio-console-guard.mjs +24 -0
  75. package/src/version.mjs +1 -1
  76. package/test/agent-runtime.test.mjs +1 -0
  77. package/test/autonomous-loop.test.mjs +24 -0
  78. package/test/autonomous-runtime.test.mjs +1 -0
  79. package/test/cli-entrypoint.test.mjs +4 -4
  80. package/test/coding-plane.test.mjs +1 -0
  81. package/test/compact-gateway-r36.test.mjs +28 -0
  82. package/test/compact-max-catalog-r45.test.mjs +99 -0
  83. package/test/contained.test.mjs +23 -11
  84. package/test/controller.test.mjs +5 -1
  85. package/test/core.test.mjs +1 -1
  86. package/test/full-max-catalog-r45.test.mjs +91 -0
  87. package/test/full-max-catalog-r46.test.mjs +92 -0
  88. package/test/hardware-observation-r45.test.mjs +44 -0
  89. package/test/host-inline-r45.test.mjs +67 -0
  90. package/test/machine-fs-mcp-tools.test.mjs +4 -1
  91. package/test/machine-fs-runtime.test.mjs +18 -2
  92. package/test/machine-mcp-integration.test.mjs +1 -0
  93. package/test/observation-mcp-tools.test.mjs +4 -3
  94. package/test/observation-runtime.test.mjs +29 -2
  95. package/test/output-schema.test.mjs +1 -0
  96. package/test/package-boundary.test.mjs +1 -1
  97. package/test/process-mcp-tools.test.mjs +23 -5
  98. package/test/process-policy-refresh.test.mjs +87 -10
  99. package/test/process-runtime.test.mjs +111 -1
  100. package/test/process-supervision.test.mjs +4 -2
  101. package/test/production-docs.test.mjs +3 -3
  102. package/test/r33-closeout-regression.test.mjs +20 -19
  103. package/test/r33-finalization.test.mjs +3 -3
  104. package/test/r45-autonomous-quick-test.test.mjs +10 -0
  105. package/test/r45-finalization.test.mjs +84 -0
  106. package/test/release-generation.test.mjs +26 -11
  107. package/test/release-version.test.mjs +112 -46
  108. package/test/remote-device-r44.test.mjs +75 -0
  109. package/test/remote-r24-catalog.test.mjs +2 -2
  110. package/test/runtime-host-bootstrap-r45.test.mjs +46 -0
@@ -1,4 +1,4 @@
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}$/);
@@ -51,6 +51,23 @@ export const processActionOutputSchema = z.strictObject({
51
51
  }
52
52
  });
53
53
 
54
+ export const processWriteOutputSchema = z.strictObject({
55
+ status: z.enum(['ok','denied','failed']),
56
+ error: z.string().min(1).max(4096).optional(),
57
+ logical_bytes: z.number().int().positive().optional(),
58
+ logical_sha256: hex64.optional(),
59
+ append_newline: z.boolean().optional(),
60
+ session: processSessionRecordSchema.optional()
61
+ }).superRefine((value, ctx) => {
62
+ if (value.status !== 'ok') {
63
+ if (!value.error) ctx.addIssue({ code: 'custom', path: ['error'], message: 'error required' });
64
+ return;
65
+ }
66
+ for (const key of ['logical_bytes','logical_sha256','append_newline','session']) {
67
+ if (value[key] === undefined) ctx.addIssue({ code: 'custom', path: [key], message: `${key} required` });
68
+ }
69
+ });
70
+
54
71
  export const processListOutputSchema = z.strictObject({
55
72
  status: z.enum(['ok','failed']),
56
73
  error: z.string().min(1).max(4096).optional(),
@@ -65,11 +82,11 @@ export const processReadOutputSchema = z.strictObject({
65
82
  session_id: hex64.optional(), offset: z.number().int().nonnegative().optional(),
66
83
  next_offset: z.number().int().nonnegative().optional(), earliest_offset: z.number().int().nonnegative().optional(),
67
84
  stream_head: z.number().int().nonnegative().optional(), stream_sha256: hex64.optional(),
68
- truncated: z.boolean().optional(), data_base64: z.string().max(400000).optional(),
85
+ truncated: z.boolean().optional(), wait_satisfied: z.boolean().optional(), data_base64: z.string().max(400000).optional(),
69
86
  state: processState.optional(), exit_code: z.number().int().nullable().optional()
70
87
  }).superRefine((value, ctx) => {
71
88
  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']) {
89
+ for (const key of ['session_id','offset','next_offset','earliest_offset','stream_head','stream_sha256','truncated','wait_satisfied','data_base64','state','exit_code']) {
73
90
  if (value[key] === undefined) ctx.addIssue({ code: 'custom', path: [key], message: `${key} required` });
74
91
  }
75
92
  });
@@ -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');
@@ -60,6 +60,36 @@ function publicSession(session) {
60
60
  journal_head_sha256: session.journal_head_sha256
61
61
  };
62
62
  }
63
+ function retainedSlice(session, begin = 0, length = session.retained_output_bytes - begin) {
64
+ if (!Number.isInteger(begin) || begin < 0 || !Number.isInteger(length) || length < 0) throw new Error('invalid retained output slice');
65
+ if (begin >= session.retained_output_bytes || length === 0) return Buffer.alloc(0);
66
+ const end = Math.min(session.retained_output_bytes, begin + length);
67
+ const parts = [];
68
+ let cursor = 0;
69
+ for (const chunk of session.output_chunks) {
70
+ const next = cursor + chunk.length;
71
+ if (next <= begin) { cursor = next; continue; }
72
+ if (cursor >= end) break;
73
+ const from = Math.max(0, begin - cursor);
74
+ const to = Math.min(chunk.length, end - cursor);
75
+ if (to > from) parts.push(chunk.subarray(from, to));
76
+ cursor = next;
77
+ }
78
+ if (parts.length === 0) return Buffer.alloc(0);
79
+ if (parts.length === 1) return Buffer.from(parts[0]);
80
+ return Buffer.concat(parts, end - begin);
81
+ }
82
+ function retainedAll(session) {
83
+ return retainedSlice(session, 0, session.retained_output_bytes);
84
+ }
85
+ function retainedTail(session, maxBytes) {
86
+ const length = Math.min(maxBytes, session.retained_output_bytes);
87
+ return retainedSlice(session, session.retained_output_bytes - length, length);
88
+ }
89
+ function retainedIndexOf(session, needle, begin = 0) {
90
+ if (!Buffer.isBuffer(needle) || needle.length < 1) return -1;
91
+ return retainedSlice(session, begin, session.retained_output_bytes - begin).indexOf(needle);
92
+ }
63
93
  function appendOutput(session, chunk) {
64
94
  if (!chunk?.length) return;
65
95
  const bytes = Buffer.from(chunk);
@@ -67,13 +97,22 @@ function appendOutput(session, chunk) {
67
97
  Buffer.from(session.stream_sha256, 'hex'), bytes
68
98
  ]));
69
99
  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;
100
+ session.output_chunks.push(bytes);
101
+ session.retained_output_bytes += bytes.length;
102
+ while (session.retained_output_bytes > session.max_retained_output_bytes && session.output_chunks.length > 0) {
103
+ const excess = session.retained_output_bytes - session.max_retained_output_bytes;
104
+ const first = session.output_chunks[0];
105
+ if (first.length <= excess) {
106
+ session.output_chunks.shift();
107
+ session.retained_output_bytes -= first.length;
108
+ session.earliest_offset += first.length;
109
+ continue;
110
+ }
111
+ session.output_chunks[0] = first.subarray(excess);
112
+ session.retained_output_bytes -= excess;
113
+ session.earliest_offset += excess;
75
114
  }
76
- const tail = session.output.subarray(Math.max(0, session.output.length - 4096)).toString('latin1');
115
+ const tail = retainedTail(session, 4096).toString('latin1');
77
116
  const enabledAt = tail.lastIndexOf('\x1b[?9001h');
78
117
  const disabledAt = tail.lastIndexOf('\x1b[?9001l');
79
118
  if (enabledAt >= 0 || disabledAt >= 0) session.win32_input_mode = enabledAt > disabledAt;
@@ -166,7 +205,7 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
166
205
  child_pid:started.detail.child_pid, creation_filetime:String(started.detail.creation_filetime),
167
206
  state:terminal.type, exit_code:Number.isInteger(terminal.detail.exit_code) ? terminal.detail.exit_code : null,
168
207
  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),
208
+ stream_sha256:terminal.detail.stream_sha256, output_chunks:[output], retained_output_bytes:output.length, control_buffer:Buffer.alloc(0),
170
209
  journal_head_sha256:checked.head_sha256, journal_path:path.join(policy.state_dir,name), event_count:checked.event_count,
171
210
  max_retained_output_bytes:profile.max_retained_output_bytes, stderr:'', terminate_requested:terminal.type==='terminated',
172
211
  protocol_error:null, transport:profile.transport, win32_input_mode:false, host:null, close_promise:Promise.resolve(),
@@ -262,7 +301,7 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
262
301
  const session = {
263
302
  session_id: sessionId, job_id: randomBytes(32).toString('hex'), profile_id: profileId, root_id: rootId,
264
303
  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),
304
+ earliest_offset: 0, stream_head: 0, stream_sha256: ZERO_SHA256, output_chunks: [], retained_output_bytes: 0, control_buffer: Buffer.alloc(0),
266
305
  journal_head_sha256: ZERO_SHA256, journal_path: path.join(policy.state_dir, `${sessionId}.jsonl`), event_count: 0,
267
306
  max_retained_output_bytes: profile.max_retained_output_bytes, stderr: '', terminate_requested: false, protocol_error: null,
268
307
  transport: profile.transport,
@@ -286,15 +325,16 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
286
325
  const terminalState = session.terminate_requested ? 'terminated' : 'exited';
287
326
  try {
288
327
  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' });
328
+ const retainedOutput = retainedAll(session);
329
+ const retainedOutputSha256 = sha256Bytes(retainedOutput);
330
+ await writeFile(path.join(policy.state_dir, outputFile), retainedOutput, { flag:'wx' });
291
331
  await appendEvent(runtime, session, terminalState, {
292
332
  recovery_schema:TERMINAL_RECOVERY_SCHEMA,
293
333
  exit_code:session.exit_code,
294
334
  earliest_offset:session.earliest_offset,
295
335
  stream_head:session.stream_head,
296
336
  stream_sha256:session.stream_sha256,
297
- retained_output_bytes:session.output.length,
337
+ retained_output_bytes:session.retained_output_bytes,
298
338
  retained_output_sha256:retainedOutputSha256,
299
339
  output_file:outputFile
300
340
  });
@@ -311,17 +351,57 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
311
351
  return publicSession(session);
312
352
  }
313
353
 
314
- async function read({ sessionId, offset = 0, maxBytes = 65536 }) {
354
+ async function read({ sessionId, offset = 0, maxBytes = 65536, waitMs = 0, waitFor = null }) {
315
355
  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);
356
+ if (!Number.isInteger(offset) || offset < -(16 * 1024 * 1024) || !Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > 262144 ||
357
+ !Number.isInteger(waitMs) || waitMs < 0 || waitMs > 30000) throw new Error('invalid process read bounds');
358
+ if (waitFor !== null && (typeof waitFor !== 'string' || waitFor.length < 1 || waitFor.includes('\0') || Buffer.byteLength(waitFor, 'utf8') > 256)) {
359
+ throw new Error('invalid process wait_for literal');
360
+ }
361
+ const requested = offset < 0 ? Math.max(0, session.stream_head + offset) : offset;
362
+ if (requested > session.stream_head) throw new Error('process read offset is beyond stream head');
363
+ const baseline = session.stream_head;
364
+ const literal = waitFor === null ? null : Buffer.from(waitFor, 'utf8');
365
+ const satisfied = () => {
366
+ if (literal === null) return session.stream_head > baseline || session.state !== 'running';
367
+ const effectiveSearch = Math.max(requested, session.earliest_offset);
368
+ const begin = effectiveSearch - session.earliest_offset;
369
+ return retainedIndexOf(session, literal, begin) >= 0;
370
+ };
371
+ if (waitMs > 0 && session.state === 'running' && !satisfied()) {
372
+ const deadline = Date.now() + waitMs;
373
+ while (session.state === 'running' && !satisfied() && Date.now() < deadline) {
374
+ await new Promise(resolve => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
375
+ }
376
+ }
377
+ const effectiveRequested = offset < 0 ? Math.max(0, session.stream_head + offset) : requested;
378
+ const truncated = effectiveRequested < session.earliest_offset;
379
+ const effective = Math.max(effectiveRequested, session.earliest_offset);
320
380
  const begin = effective - session.earliest_offset;
321
- const data = session.output.subarray(begin, begin + maxBytes);
381
+ const data = retainedSlice(session, begin, maxBytes);
322
382
  return { session_id: session.session_id, offset: effective, next_offset: effective + data.length,
323
383
  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 };
384
+ truncated, wait_satisfied: waitFor === null ? (session.stream_head > baseline || waitMs === 0 || session.state !== 'running') : satisfied(),
385
+ data_base64: data.toString('base64'), state: session.state, exit_code: session.exit_code };
386
+ }
387
+
388
+ async function write({ sessionId, input, appendNewline = true }) {
389
+ const session = await sessionFor(sessionId);
390
+ if (session.state !== 'running') throw new Error('process write requires a running session');
391
+ const grant = await assertProcessArmed(policy, session.profile_id, session.root_id);
392
+ const profile = policy.profiles[session.profile_id];
393
+ if (!profile?.allow_raw_input) throw new Error(`process profile '${session.profile_id}' does not allow raw input`);
394
+ if (session.transport !== 'pipe' || profile.transport !== 'pipe') throw new Error('process raw input requires pipe transport');
395
+ if (typeof input !== 'string' || input.includes('\0')) throw new Error('process raw input must be a NUL-free string');
396
+ if (typeof appendNewline !== 'boolean') throw new Error('process appendNewline must be boolean');
397
+ const logicalBytes = Buffer.from(appendNewline ? `${input}\r\n` : input, 'utf8');
398
+ if (logicalBytes.length < 1 || logicalBytes.length > profile.max_input_bytes) throw new Error('process raw input exceeds profile input bound');
399
+ if (!session.host?.stdin || session.host.stdin.destroyed || !session.host.stdin.writable) throw new Error('process session input is not writable');
400
+ await recheckProcessAuthority(policy, grant, session.profile_id, session.root_id);
401
+ await new Promise((resolve, reject) => session.host.stdin.write(logicalBytes, error => error ? reject(error) : resolve()));
402
+ const logicalSha256 = sha256Bytes(logicalBytes);
403
+ await appendEvent(runtime, session, 'input', { logical_bytes: logicalBytes.length, logical_sha256: logicalSha256, append_newline: appendNewline });
404
+ return { logical_bytes: logicalBytes.length, logical_sha256: logicalSha256, append_newline: appendNewline, session: publicSession(session) };
325
405
  }
326
406
 
327
407
  async function status({ sessionId }) { return publicSession(await sessionFor(sessionId)); }
@@ -383,5 +463,5 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
383
463
  return { ok: true, event_count: sequence, head_sha256: previous };
384
464
  }
385
465
 
386
- return { policy, start, read, status, list, action, terminate, verifyEventJournal };
466
+ return { policy, start, read, write, status, list, action, terminate, verifyEventJournal };
387
467
  }
@@ -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_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
  }
@@ -0,0 +1,74 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
7
+ import { R45_COMPACT_GATEWAYS,R45_COMPACT_TOOL_LIMIT } from './remote-mcp-r45-parity-client.mjs';
8
+ import { loadDesktopPolicy } from './desktop-policy-r40.mjs';
9
+ import { verifyDesktopReceiptObject } from './desktop-runtime-r40.mjs';
10
+ import { loadPublicVerifyKey } from './trust.mjs';
11
+
12
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
13
+ const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
14
+ const verifyKeyPath=path.join(stateRoot,'trust','ed25519-public.pem');
15
+ function structured(result,label){
16
+ if(result.isError) throw new Error(label+' returned error: '+JSON.stringify(result.structuredContent??result.content));
17
+ if(!result.structuredContent||typeof result.structuredContent!=='object') throw new Error(label+' missing structured output');
18
+ return result.structuredContent;
19
+ }
20
+ function gateway(result,label){
21
+ const value=structured(result,label);
22
+ if(value.status!=='ok'||!value.result||typeof value.result!=='object') throw new Error(label+' gateway status='+String(value.status));
23
+ return value.result;
24
+ }
25
+ async function verifyDesktop(policy,verifyKey,id,sha,kind){
26
+ const receipt=JSON.parse(await readFile(path.join(policy.evidence_dir,id+'.json'),'utf8'));
27
+ const proof=verifyDesktopReceiptObject(receipt,{policySha256:policy.policy_sha256,publicKey:verifyKey.publicKey,keyId:verifyKey.keyId});
28
+ if(!proof.ok||proof.receipt_sha256!==sha||proof.kind!==kind) throw new Error('R45 desktop '+kind+' receipt verification failed');
29
+ }
30
+ export async function probeR45CompactDisarmed(endpointText){
31
+ const endpoint=new URL(endpointText);if(endpoint.protocol!=='https:')throw new Error('R45 compact disarmed probe requires HTTPS');
32
+ const pointer=JSON.parse(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
33
+ const desktopPolicy=await loadDesktopPolicy(path.join(pointer.generation_root,'desktop-policy.json'));
34
+ const verifyKey=await loadPublicVerifyKey(verifyKeyPath);
35
+ const client=new Client({name:'deadbyte-r45-compact-disarmed-probe',version:'0.12.0'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
36
+ const transport=new StreamableHTTPClientTransport(endpoint);
37
+ try{
38
+ await client.connect(transport);
39
+ const listed=await client.listTools(),names=listed.tools.map(t=>t.name).sort();
40
+ if(names.length!==R45_COMPACT_TOOL_LIMIT)throw new Error('R45 compact disarmed catalog must be exactly '+R45_COMPACT_TOOL_LIMIT+', got '+names.length);
41
+ for(const name of R45_COMPACT_GATEWAYS)if(!names.includes(name))throw new Error('R45 compact gateway missing '+name);
42
+ for(const tool of listed.tools)if(tool.outputSchema?.type!=='object'||tool.outputSchema?.additionalProperties!==false||!tool.outputSchema?.properties)throw new Error('tool '+tool.name+' missing strict output schema');
43
+ const manifest=gateway(await client.callTool({name:'capability_manifest',arguments:{}}),'capability_manifest');
44
+ if(manifest.schema!=='deadbyte.capability-manifest.v2'||manifest.profile!=='compact'||manifest.tool_count!==names.length||
45
+ JSON.stringify(manifest.tool_catalog)!==JSON.stringify(names))throw new Error('R45 compact manifest malformed');
46
+ const required=manifest.capabilities.filter(x=>x?.production===true&&x?.host_only!==true);
47
+ for(const item of required)if(typeof item.compact_tool!=='string'||!names.includes(item.compact_tool))throw new Error('compact route missing '+String(item.id));
48
+ const skillCatalog=structured(await client.callTool({name:'skill_catalog',arguments:{op:'list'}}),'skill_catalog');
49
+ if(skillCatalog.status!=='ok'||skillCatalog.op!=='list'||!skillCatalog.result||!Array.isArray(skillCatalog.result.skills)||
50
+ !/^[0-9a-f]{64}$/.test(String(skillCatalog.result.registry_sha256??'')))throw new Error('R45 skill catalog verification failed');
51
+ const coding=structured(await client.callTool({name:'coding_session_status',arguments:{}}),'coding status');
52
+ const autonomous=structured(await client.callTool({name:'autonomous_session_status',arguments:{}}),'autonomous status');
53
+ const desktop=gateway(await client.callTool({name:'desktop_control',arguments:{op:'status'}}),'desktop status');
54
+ if(coding.armed!==false||autonomous.coding_armed!==false||autonomous.autonomous_armed!==false||desktop.armed!==false)throw new Error('R45 closure requires Coding/Autonomous/Desktop DISARMED');
55
+ const observed=gateway(await client.callTool({name:'desktop_control',arguments:{op:'observe',preview:false}}),'desktop observe');
56
+ await verifyDesktop(desktopPolicy,verifyKey,observed.receipt_id,observed.receipt_sha256,'desktop_observation');
57
+ const grounded=gateway(await client.callTool({name:'desktop_control',arguments:{op:'target',frame_id:observed.frame.frame_id,uia_query:{kind:'root',value:'',max_results:1,ordinal:0}}}),'desktop target');
58
+ await verifyDesktop(desktopPolicy,verifyKey,grounded.grounding.receipt_id,grounded.grounding.receipt_sha256,'desktop_grounding');
59
+ const machine=gateway(await client.callTool({name:'machine_observe',arguments:{op:'machine_status'}}),'machine status');
60
+ const nonce='r45-final-'+randomBytes(16).toString('hex');
61
+ const ping=gateway(await client.callTool({name:'machine_observe',arguments:{op:'machine_ping',nonce}}),'machine ping');
62
+ if(ping.nonce!==nonce||ping.release_id!==machine.release_id||ping.release_manifest_sha256!==machine.release_manifest_sha256)throw new Error('R45 machine ping binding mismatch');
63
+ return {status:'passed',profile:'compact',tool_count:names.length,tools:names,catalog_sha256:sha256(Buffer.from(JSON.stringify(names),'utf8')),
64
+ strict_output_schemas:true,ledger_sha256:manifest.ledger_sha256,skill_catalog_verified:true,skill_count:skillCatalog.result.skills.length,capability_count:manifest.capabilities.length,
65
+ production_non_host_capabilities:required.length,capability_closure:true,coding_armed:false,autonomous_armed:false,desktop_armed:false,
66
+ desktop_observation_receipt_verified:true,desktop_grounding_receipt_verified:true,
67
+ desktop_frame_id:observed.frame.frame_id,desktop_target_id:grounded.target.target_id,
68
+ release_id:machine.release_id,release_manifest_sha256:machine.release_manifest_sha256,machine_status:machine,machine_ping:ping,nonce,
69
+ protocol_revision:client.getNegotiatedProtocolVersion()};
70
+ }finally{await client.close().catch(()=>{});}
71
+ }
72
+ const invoked=process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url);
73
+ if(invoked){const endpoint=process.argv[2];if(!endpoint)throw new Error('usage: node src/r45-compact-disarmed-probe.mjs https://host/mcp/path');
74
+ probeR45CompactDisarmed(endpoint).then(v=>console.log(JSON.stringify(v,null,2))).catch(e=>{console.error(e instanceof Error?e.stack:String(e));process.exitCode=1;});}
@@ -0,0 +1,34 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { Client } from '@modelcontextprotocol/client';
6
+ import { StdioClientTransport,getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { R45_EXPECTED_TOOLS } from './remote-mcp-r45-parity-client.mjs';
9
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
10
+ export async function probeR45FullSurface({packageRoot,generationRoot,stateRoot=path.join(os.homedir(),'.deadbyte-mcp')}={}){
11
+ const pkgRoot=packageRoot?path.resolve(packageRoot):path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
12
+ const gen=path.resolve(generationRoot),semanticPath=path.join(gen,'semantic-lsp-config.json');
13
+ const semanticSha=sha256(await readFile(semanticPath));
14
+ const env={...getDefaultEnvironment(),DEADBYTE_STATE_ROOT:path.resolve(stateRoot),DEADBYTE_CORE_ROOT:pkgRoot,
15
+ DEADBYTE_WORKSPACE_ROOT:path.join(pkgRoot,'workspaces'),DEADBYTE_CAPABILITY_PROFILE:'full',DEADBYTE_AUDIT_RUNTIME:'0',
16
+ DEADBYTE_MACHINE_RUNTIME:'1',DEADBYTE_AGENT_RUNTIME:'1',DEADBYTE_PROCESS_RUNTIME:'1',DEADBYTE_PROCESS_POLICY:path.join(gen,'process-policy.json'),
17
+ DEADBYTE_CODING_RUNTIME:'1',DEADBYTE_MACHINE_FS_RUNTIME:'1',DEADBYTE_OBSERVATION_RUNTIME:'1',
18
+ DEADBYTE_DESKTOP_RUNTIME:'1',DEADBYTE_DESKTOP_POLICY:path.join(gen,'desktop-policy.json'),
19
+ DEADBYTE_CODING_POLICY:path.join(gen,'coding-policy.json'),DEADBYTE_SEMANTIC_LSP_CONFIG:semanticPath,DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256:semanticSha,
20
+ DEADBYTE_AUTONOMOUS_RUNTIME:'1',DEADBYTE_AUTONOMOUS_POLICY:path.join(gen,'autonomous-policy.json'),
21
+ DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE:'external',DEADBYTE_PRIVILEGED_POLICY:path.join(gen,'host-policy.json')};
22
+ const client=new Client({name:'deadbyte-r45-full-surface-probe',version:'0.12.0'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
23
+ const transport=new StdioClientTransport({command:process.execPath,args:[path.join(pkgRoot,'src','mcp-server.mjs')],cwd:pkgRoot,env,stderr:'pipe'});
24
+ try{
25
+ await client.connect(transport);const listed=await client.listTools(),names=listed.tools.map(t=>t.name).sort();
26
+ if(JSON.stringify(names)!==JSON.stringify(R45_EXPECTED_TOOLS))throw new Error('R45 full catalog mismatch count='+names.length);
27
+ for(const tool of listed.tools)if(tool.outputSchema?.type!=='object'||tool.outputSchema?.additionalProperties!==false||!tool.outputSchema?.properties)throw new Error('R45 full tool '+tool.name+' missing strict output schema');
28
+ return {status:'passed',profile:'full',tool_count:names.length,tools:names,catalog_sha256:sha256(Buffer.from(JSON.stringify(names),'utf8')),
29
+ strict_output_schemas:true,desktop_tools:['desktop_status','desktop_observe','desktop_target','desktop_action'],protocol_revision:client.getNegotiatedProtocolVersion()};
30
+ }finally{await client.close().catch(()=>{});}
31
+ }
32
+ if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){const [packageRoot,generationRoot]=process.argv.slice(2);
33
+ if(!packageRoot||!generationRoot)throw new Error('usage: node src/r45-full-surface-probe.mjs <package-root> <generation-root>');
34
+ probeR45FullSurface({packageRoot,generationRoot}).then(v=>console.log(JSON.stringify(v,null,2))).catch(e=>{console.error(e instanceof Error?e.stack:String(e));process.exitCode=1;});}
@@ -84,6 +84,7 @@ async function rebindAutonomousPolicy(raw,{stateRoot,codingPolicy,codingPolicyBy
84
84
  if(id==='windows-release'){
85
85
  if(profile.kind!=='command'||rootId!=='core'||profile.command_id!=='powershell_script') throw new Error('windows release profile shape invalid');
86
86
  profile.script_path=await currentWindowsGateScript(rootPath);
87
+ profile.timeout_ms=600000;
87
88
  profile.description='Full Windows target release gate; runs only when finish is requested.';
88
89
  }
89
90
  const rel=safeRel(String(profile.script_path).replaceAll('\\','/'));
@@ -104,7 +105,7 @@ function rebindHostPolicy(raw,{developmentRoot,stateRoot}){
104
105
  return policy;
105
106
  }
106
107
 
107
- async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot}){
108
+ async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot,systemRoot}){
108
109
  if(!raw) return null;
109
110
  const policy=clone(raw);
110
111
  policy.grant_file=path.join(stateRoot,'runtime','process-grant.json');
@@ -115,6 +116,25 @@ async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot}){
115
116
  const nativeHostStat=await lstat(nativeHostPath);
116
117
  if(!nativeHostStat.isFile()||nativeHostStat.isSymbolicLink()) throw new Error('process native host invalid in release slot');
117
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
+ }
118
138
  return policy;
119
139
  }
120
140
 
@@ -198,7 +218,7 @@ function renderControllerConfig({currentConfig,coreRoot,workspaceRoot,processPol
198
218
  '}',''
199
219
  ].join('\r\n');
200
220
  }
201
- 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}){
202
222
  const state=path.resolve(stateRoot),release=path.resolve(releaseRoot),development=path.resolve(developmentRoot),workspace=path.resolve(workspaceRoot);
203
223
  if(typeof releaseId!=='string'||!/^[A-Za-z0-9._-]{1,96}$/.test(releaseId)) throw new Error('generation release id invalid');
204
224
  if(typeof currentConfig?.Bridge?.FileSha256!=='string'||!HEX64.test(currentConfig.Bridge.FileSha256)) throw new Error('generation Bridge FileSha256 invalid');
@@ -254,7 +274,7 @@ export async function createGenerationBundle({stateRoot,releaseRoot,developmentR
254
274
  const autonomousBytes=jsonBytes(autonomous);
255
275
  const host=rebindHostPolicy(hostPolicy,{developmentRoot:development,stateRoot:state});
256
276
  const hostBytes=host?jsonBytes(host):null;
257
- const processBound=await rebindProcessPolicy(processPolicy,{developmentRoot:development,stateRoot:state,releaseRoot:release});
277
+ const processBound=await rebindProcessPolicy(processPolicy,{developmentRoot:development,stateRoot:state,releaseRoot:release,systemRoot});
258
278
  const processBytes=processBound?jsonBytes(processBound):null;
259
279
  const desktopBound=await rebindDesktopPolicy(desktopPolicy,{stateRoot:state,releaseRoot:release});
260
280
  const desktopBytes=desktopBound?jsonBytes(desktopBound):null;