deadbyte-mcp 0.15.36 → 0.15.37

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 (56) hide show
  1. package/MANIFEST.SHA256 +55 -27
  2. package/PROVENANCE.json +114 -87
  3. package/README.txt +12 -12
  4. package/RELEASE.json +214 -187
  5. package/SBOM.cdx.json +10 -25
  6. package/TEST-MATRIX.json +297 -258
  7. package/bridge/package.json +1 -1
  8. package/bridge/src/bridge-health-r59.mjs +4 -3
  9. package/bridge/src/bridge.mjs +71 -14
  10. package/bridge/src/canonical-json-r60.mjs +13 -0
  11. package/bridge/src/reconnect-delay-r60.mjs +31 -0
  12. package/bridge/src/remote-call-claims-r60.mjs +157 -0
  13. package/bridge/src/remote-dispatch-r60.mjs +75 -0
  14. package/controller/deadbyte-controller.ps1 +6 -1
  15. package/controller/deadbyte-process-policy.json +4 -4
  16. package/docs/R59-PROVENANCE.json +87 -0
  17. package/docs/R59-RELEASE.json +187 -0
  18. package/docs/R59-SBOM.cdx.json +68 -0
  19. package/docs/R59-TEST-MATRIX.json +258 -0
  20. package/docs/R59-bridge-package.json +13 -0
  21. package/docs/R59-package-lock.json +184 -0
  22. package/docs/R59-package.json +106 -0
  23. package/docs/R59-version.mjs +6 -0
  24. package/docs/R60-TASK-CONTRACT.md +22 -0
  25. package/package.json +5 -3
  26. package/scripts/full-regression-r60.mjs +32 -0
  27. package/scripts/gate-windows-r60-prepublish.ps1 +74 -0
  28. package/src/capability-snapshot.mjs +6 -2
  29. package/src/deadbyte-cli.mjs +80 -34
  30. package/src/remote-authority.mjs +56 -0
  31. package/src/remote-enforcement-probe-r60.mjs +17 -0
  32. package/src/remote-live-log.mjs +19 -2
  33. package/src/remote-log-decode-r60.mjs +32 -0
  34. package/src/remote-mcp-machine-smoke-client.mjs +19 -1
  35. package/src/remote-session-guardian.mjs +20 -3
  36. package/src/tool-audit.mjs +5 -0
  37. package/src/version.mjs +1 -1
  38. package/test/r48-release-contract.test.mjs +1 -1
  39. package/test/r51-runtime-coherence.test.mjs +2 -2
  40. package/test/r58-cleanup-retention.test.mjs +9 -3
  41. package/test/r58-concurrent-recovery.test.mjs +9 -3
  42. package/test/r58-controller-live-local.test.mjs +22 -3
  43. package/test/r58-stale-owner.test.mjs +2 -2
  44. package/test/r59-release-contract.test.mjs +6 -6
  45. package/test/r60-authority-coherence.test.mjs +36 -0
  46. package/test/r60-authority-enforcement.test.mjs +28 -0
  47. package/test/r60-bridge-dispatch.test.mjs +119 -0
  48. package/test/r60-clock-discipline.test.mjs +15 -0
  49. package/test/r60-live-timeline.test.mjs +29 -0
  50. package/test/r60-reconnect-errors.test.mjs +21 -0
  51. package/test/r60-reconnect-jitter.test.mjs +18 -0
  52. package/test/r60-release-contract.test.mjs +81 -0
  53. package/test/r60-remote-call-recovery.test.mjs +170 -0
  54. package/test/r60-windows-log-decode.test.mjs +25 -0
  55. package/test/remote-live-log-r37.test.mjs +1 -1
  56. package/test/remote-session-cli.test.mjs +200 -43
@@ -20,6 +20,8 @@ import { windowsPowerShellEnvironment } from './runtime-update.mjs';
20
20
  import { resolveStateRoot } from './state-root.mjs';
21
21
  import {foregroundIdentityMatches,queryForegroundIdentity,validForegroundIdentity} from './foreground-process-identity.mjs';
22
22
  import {acquireForegroundSessionLock} from './foreground-session-lock.mjs';
23
+ import { AUTHORITY_PLANES, authorityVector, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation, authorityBanner } from './remote-authority.mjs';
24
+ import { decodeRemoteLogBytes, boundedRemoteDiagnostic, safeRemoteLogPrefixLength } from './remote-log-decode-r60.mjs';
23
25
 
24
26
  const REMOTE_COMMANDS=new Set([
25
27
  'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
@@ -27,17 +29,9 @@ const REMOTE_COMMANDS=new Set([
27
29
  'armdesktop','disarmdesktop','desktopstatus',
28
30
  'armcoding','disarmcoding','codingstatus','codingtest','armautonomous','disarmautonomous','autonomousstatus','autonomoustest'
29
31
  ]);
30
- const SESSION_ARM=['armprocess','armcoding','armautonomous'];
31
32
  const REMOTE_STATUS_TIMEOUT_MS=30000;
32
33
  const REMOTE_START_TIMEOUT_MS=150000;
33
- const SESSION_STATUS=[
34
- ['processstatus',/^Process plane\s*:\s*ARMED\b/m],
35
- ['codingstatus',/^Coding plane\s*:\s*ARMED\b/m],
36
- ['autonomousstatus',/^Autonomous\s*:\s*ARMED\b/m],
37
- ['desktopstatus',/^Desktop plane\s*:\s*DISARMED\b/m],
38
- ['hoststatus',/^Host plane\s*:\s*DISARMED\b/m]
39
- ];
40
- const SESSION_CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','park'];
34
+ const SESSION_CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'];
41
35
  const SUPERVISOR_COMMANDS=Object.freeze({
42
36
  start:'supervisorstart',stop:'supervisorstop',status:'supervisorstatus',recover:'supervisorrecover'
43
37
  });
@@ -55,6 +49,9 @@ function printHelp(){
55
49
  ` npx -y ${exact} doctor # verify current package/release truth`,
56
50
  ` npx -y ${exact} mcp # stdio MCP server for local clients`,
57
51
  ` npx -y ${exact} remote # unified foreground device session + live timeline`,
52
+ ` npx -y ${exact} remote --desktop # session with Desktop authority; Host remains disarmed`,
53
+ ` npx -y ${exact} remote --host # session with Host authority; Desktop remains disarmed`,
54
+ ` npx -y ${exact} remote --full # explicit five-plane foreground session`,
58
55
  ` npx -y ${exact} remote --resume <cursor> # replay missed results, then reconnect`,
59
56
  ` npx -y ${exact} remote --trace # show trace/span identity without payload detail`,
60
57
  ` npx -y ${exact} remote --filter <expr> # presentation filter: tool/category/status/trace`,
@@ -75,8 +72,7 @@ async function remoteContext(){
75
72
  return {root,launcher,powershell};
76
73
  }
77
74
  function boundedSingleLine(value,maxChars=2048){
78
- const line=String(value??'').replace(/[\r\n\t]+/g,' ').replace(/\s{2,}/g,' ').trim();
79
- return line.length<=maxChars?line:`${line.slice(0,maxChars)}...`;
75
+ return boundedRemoteDiagnostic(value,maxChars);
80
76
  }
81
77
  async function readActiveRuntimeBinding(root){
82
78
  const file=path.join(root,'active-release.json');
@@ -158,11 +154,16 @@ async function readTailBounded(file,maxBytes=4096){
158
154
  if(length===0) return '';
159
155
  const buffer=Buffer.alloc(length);
160
156
  const handle=await open(file,'r');
161
- try{await handle.read(buffer,0,length,info.size-length);}finally{await handle.close();}
162
- return boundedSingleLine(buffer.toString('utf8'),maxBytes);
157
+ let encoding=null;
158
+ try{
159
+ const header=Buffer.alloc(2);await handle.read(header,0,2,0);
160
+ if(header[0]===0xff&&header[1]===0xfe)encoding='utf16le';
161
+ await handle.read(buffer,0,length,info.size-length);
162
+ }finally{await handle.close();}
163
+ return boundedSingleLine(decodeRemoteLogBytes(file,buffer,{encoding}),maxBytes);
163
164
  }catch(error){if(error?.code==='ENOENT') return '';throw error;}
164
165
  }
165
- function invokeController(ctx,command,{capture=false,allowFailure=false,echoCaptured=true,timeoutMs=null,discardOutput=false,redirectOutput=null}={}){
166
+ function invokeController(ctx,command,{capture=false,allowFailure=false,echoCaptured=true,timeoutMs=null,discardOutput=false,redirectOutput=null,json=false,probeMode=null}={}){
166
167
  if(capture&&discardOutput) throw new Error('invokeController cannot capture and discard output together');
167
168
  if(capture&&redirectOutput) throw new Error('invokeController cannot capture and redirect output together');
168
169
  if(discardOutput&&redirectOutput) throw new Error('invokeController cannot discard and redirect output together');
@@ -175,9 +176,10 @@ function invokeController(ctx,command,{capture=false,allowFailure=false,echoCapt
175
176
  stderrFd=openSync(redirectOutput.stderr_path,'w');
176
177
  stdio=['ignore',stdoutFd,stderrFd];
177
178
  }else stdio=capture?['ignore','pipe','pipe']:'inherit';
178
- const options={stdio,encoding:capture?'utf8':undefined,windowsHide:false,shell:false,env:windowsPowerShellEnvironment()};
179
+ const options={stdio,encoding:capture?'utf8':undefined,windowsHide:false,shell:false,
180
+ env:windowsPowerShellEnvironment(probeMode?{...process.env,DEADBYTE_REMOTE_PROBE_MODE:probeMode}:process.env)};
179
181
  if(Number.isInteger(timeoutMs)&&timeoutMs>0) options.timeout=timeoutMs;
180
- result=spawnSync(ctx.powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',ctx.launcher,command],options);
182
+ result=spawnSync(ctx.powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',ctx.launcher,command,...(json?['-Json']:[])],options);
181
183
  }finally{
182
184
  if(stdoutFd!==null) closeSync(stdoutFd);
183
185
  if(stderrFd!==null) closeSync(stderrFd);
@@ -193,6 +195,10 @@ function invokeController(ctx,command,{capture=false,allowFailure=false,echoCapt
193
195
  if(status!==0&&!allowFailure) throw new Error(`remote controller '${command}' failed exit=${status}`);
194
196
  return {status,stdout,stderr};
195
197
  }
198
+ function observedAuthority(ctx){
199
+ const result=invokeController(ctx,'status',{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS,json:true});
200
+ return parseAuthorityObservation(result.stdout);
201
+ }
196
202
  function controllerRuntimeReady(text){
197
203
  const value=String(text||'');
198
204
  return /^DEADBYTE MCP\s*=\s*ONLINE\s*$/m.test(value)&&
@@ -278,13 +284,23 @@ async function replayRemoteResults(root,cursor,{displayMode='unicode'}={}){
278
284
  async function readAppend(file,offset){
279
285
  let info;try{info=await stat(file);}catch(error){if(error?.code==='ENOENT') return {offset:0,text:''};throw error;}
280
286
  let start=offset;
281
- if(info.size<start) start=0;
287
+ const reset=info.size<start;
288
+ if(reset) start=0;
282
289
  if(info.size===start) return {offset:start,text:''};
283
- const length=info.size-start;
284
- const buffer=Buffer.alloc(length);
285
290
  const handle=await open(file,'r');
286
- try{await handle.read(buffer,0,length,start);}finally{await handle.close();}
287
- return {offset:info.size,text:buffer.toString('utf8')};
291
+ let encoding=null,buffer,bytesRead=0;
292
+ try{
293
+ const header=Buffer.alloc(2);await handle.read(header,0,2,0);
294
+ if(header[0]===0xff&&header[1]===0xfe&&/\.log$/i.test(file)){
295
+ encoding='utf16le';if(start%2!==0)start=Math.max(0,start-1);
296
+ }
297
+ const length=Math.min(1024*1024,info.size-start);
298
+ if(length===0)return {offset:start,text:''};
299
+ buffer=Buffer.alloc(length);
300
+ ({bytesRead}=await handle.read(buffer,0,length,start));
301
+ }finally{await handle.close();}
302
+ const complete=safeRemoteLogPrefixLength(buffer.subarray(0,bytesRead),encoding??'utf8');
303
+ return {offset:start+complete,text:decodeRemoteLogBytes(file,buffer.subarray(0,complete),{encoding}),reset};
288
304
  }
289
305
  async function snapshotRemoteLogOffsets(root){
290
306
  const offsets=new Map();
@@ -314,6 +330,7 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
314
330
  ? '[LOG] Verbose live timeline. Full sanitized request detail enabled; Ctrl+C disconnects and revokes authority.\n'
315
331
  : '[LOG] Human live timeline. Transport IN/OUT hidden; full replay/audit detail remains on disk. Ctrl+C disconnects and revokes authority.\n');
316
332
  const activeOffsets=offsets??await snapshotRemoteLogOffsets(root);
333
+ const partialLines=new Map();
317
334
  let liveFiles=[...activeOffsets.keys()];
318
335
  let nextDiscoveryAt=0;
319
336
  while(!signal.aborted){
@@ -326,7 +343,13 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
326
343
  const prior=activeOffsets.has(file)?activeOffsets.get(file):0;
327
344
  const next=await readAppend(file,prior);
328
345
  activeOffsets.set(file,next.offset);
329
- for(const line of next.text.split(/\r?\n/)){
346
+ if(next.reset)partialLines.delete(file);
347
+ if(!next.text)continue;
348
+ const lines=((partialLines.get(file)??'')+next.text).split(/\r?\n/);
349
+ partialLines.set(file,lines.pop()??'');
350
+ if(Buffer.byteLength(partialLines.get(file),'utf8')>1024*1024) throw new Error(`remote log line exceeds bounded decode limit: ${path.basename(file)}`);
351
+ for(const line of lines){
352
+ if(Buffer.byteLength(line,'utf8')>1024*1024) throw new Error(`remote log line exceeds bounded decode limit: ${path.basename(file)}`);
330
353
  const auditEvent=parseAuditLogLine(file,line);
331
354
  const timeline=auditTimelineRecord(file,line,auditEvent);
332
355
  let recorded=null;
@@ -363,6 +386,15 @@ async function cleanupRemoteSession(ctx,{displayMode='unicode'}={}){
363
386
  catch(error){failed.push(command);writeRemote(process.stderr,`${remoteTag('warn',displayMode)} cleanup ${command}: ${String(error?.message||error)}\n`,displayMode);}
364
387
  }
365
388
  if(failed.length) throw new Error(`remote session cleanup failed; ownership marker retained: ${failed.join(',')}`);
389
+ let snapshot;
390
+ try{snapshot=assertAuthorityVector(observedAuthority(ctx),Object.fromEntries(AUTHORITY_PLANES.map(plane=>[plane,false])));}
391
+ catch(error){throw new Error(`remote session cleanup verification failed; ownership marker retained: ${String(error?.message||error)}`);}
392
+ if(snapshot.state==='ONLINE'){
393
+ try{invokeController(ctx,'machinetest',{capture:true,echoCaptured:false,timeoutMs:REMOTE_START_TIMEOUT_MS,probeMode:'revoked'});}
394
+ catch(error){throw new Error(`remote session enforcement cleanup verification failed; ownership marker retained: ${String(error?.message||error)}`);}
395
+ }
396
+ const parked=invokeController(ctx,'park',{allowFailure:true,capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
397
+ if(parked.status!==0) throw new Error(`remote session park failed exit=${parked.status}; ownership marker retained`);
366
398
  }
367
399
  const GUARDIAN_SCRIPT=fileURLToPath(new URL('./remote-session-guardian.mjs',import.meta.url));
368
400
  function remoteSessionMarker(root){return path.join(root,'runtime','remote-session.json');}
@@ -454,7 +486,7 @@ async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
454
486
  const same=current?.schema==='deadbyte.remote-session.v2'&&current.owner_pid===marker.owner_pid&&current.session_nonce===marker.session_nonce;
455
487
  if(same) await rm(remoteSessionMarker(ctx.root),{force:true});
456
488
  }
457
- async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression=''}={}){
489
+ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default'}={}){
458
490
  const display=prepareRemoteConsole();
459
491
  const displayMode=display.mode;
460
492
  const ctx=await remoteContext();
@@ -524,21 +556,31 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
524
556
  }
525
557
  process.stdout.write(`${formatRemoteLifecycle('bridge_connected',{displayMode})}\n`);
526
558
  process.stdout.write(`${formatRemoteLifecycle('cloud_connected',{displayMode})}\n`);
527
- for(const command of SESSION_ARM){
559
+ const desired=authorityVector(mode);
560
+ const priorAuthority=observedAuthority(ctx);
561
+ for(const plane of AUTHORITY_PLANES.filter(name=>!desired[name]&&priorAuthority.authority[name].armed)){
562
+ const command=`disarm${plane}`;
563
+ writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Revoking unwanted ${plane} authority...\n`,displayMode);
564
+ invokeController(ctx,command,{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
565
+ }
566
+ for(const plane of AUTHORITY_PLANES.filter(name=>desired[name])){
567
+ const command=`arm${plane}`;
528
568
  writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Arming ${command.slice(3)}...\n`,displayMode);
529
569
  invokeController(ctx,command,{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
530
570
  }
531
571
  const url=startText.match(/^URL\s*:\s*(\S+)/m)?.[1]||'';
572
+ writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Verifying exact ${mode} authority vector...\n`,displayMode);
573
+ const current=await readActiveRuntimeBinding(ctx.root);
574
+ const authority=assertAuthorityVector(assertRuntimeReadyObservation(observedAuthority(ctx),{
575
+ coreRoot:current?.release_root??null
576
+ }),desired);
577
+ process.stdout.write('[BOOT] Verifying public signed MCP execution path...\n');
578
+ invokeController(ctx,'machinetest',{capture:true,echoCaptured:false,timeoutMs:REMOTE_START_TIMEOUT_MS,probeMode:mode});
579
+ ready=true;
580
+ writeRemote(process.stdout,`${remoteTag('auth',displayMode)} ${authorityBanner(authority)}\n`,displayMode);
532
581
  process.stdout.write('[OK] ONLINE\n');
533
582
  process.stdout.write(`${formatRemoteLifecycle('ready',{displayMode})}\n`);
534
583
  if(url) writeRemote(process.stdout,`URL: ${url}\n`,displayMode);
535
- writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Verifying process, coding, autonomous; desktop + host must stay DISARMED...\n`,displayMode);
536
- for(const [command,pattern] of SESSION_STATUS){
537
- const result=invokeController(ctx,command,{capture:true,echoCaptured:false});
538
- if(!pattern.test(result.stdout)) throw new Error(`remote session authority verification failed: ${command} state mismatch`);
539
- }
540
- ready=true;
541
- writeRemote(process.stdout,`${remoteTag('auth',displayMode)} ARMED: process, coding, autonomous | desktop, host DISARMED\n`,displayMode);
542
584
  await logTask;
543
585
  }finally{
544
586
  const wasAborted=aborter.signal.aborted;
@@ -556,12 +598,16 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
556
598
  }
557
599
  }
558
600
  async function runRemote(args){
559
- let verbose=false,trace=false,filterExpression='',resumeCursor=null;
601
+ let verbose=false,trace=false,filterExpression='',resumeCursor=null,mode='default';
560
602
  const positional=[];
561
603
  for(let index=0;index<args.length;index++){
562
604
  const arg=args[index];
563
605
  if(arg==='--verbose'||arg==='-v'){verbose=true;continue;}
564
606
  if(arg==='--trace'){trace=true;continue;}
607
+ if(['--desktop','--host','--full'].includes(arg)){
608
+ if(mode!=='default') throw new Error('remote authority modes are mutually exclusive');
609
+ mode=arg.slice(2);continue;
610
+ }
565
611
  if(arg==='--filter'){
566
612
  if(filterExpression) throw new Error('remote --filter may be specified once');
567
613
  if(index+1>=args.length) throw new Error('remote --filter requires an expression');
@@ -574,8 +620,8 @@ async function runRemote(args){
574
620
  }
575
621
  positional.push(arg);
576
622
  }
577
- const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null;
578
- if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression});return;}
623
+ const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null||mode!=='default';
624
+ if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression,mode});return;}
579
625
  if(presentation) throw new Error('remote presentation flags are valid only for foreground session');
580
626
  if(positional.length>1) throw new Error('remote accepts one controller command');
581
627
  const command=positional[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
@@ -0,0 +1,56 @@
1
+ import path from 'node:path';
2
+
3
+ export const AUTHORITY_PLANES=Object.freeze(['process','coding','autonomous','desktop','host']);
4
+
5
+ export function authorityVector(mode='default'){
6
+ if(!['default','desktop','host','full'].includes(mode)) throw new Error('remote authority mode invalid');
7
+ return Object.freeze({
8
+ process:true,coding:true,autonomous:true,
9
+ desktop:mode==='desktop'||mode==='full',
10
+ host:mode==='host'||mode==='full'
11
+ });
12
+ }
13
+
14
+ export function parseAuthorityObservation(raw){
15
+ let value;
16
+ try{value=typeof raw==='string'?JSON.parse(raw.replace(/^\uFEFF/,'')):raw;}
17
+ catch{throw new Error('controller observation JSON invalid');}
18
+ if(value?.schema!=='deadbyte.controller-observation.v1'||!value.authority||typeof value.authority!=='object'||Array.isArray(value.authority))
19
+ throw new Error('controller observation schema invalid');
20
+ const authority={};
21
+ for(const plane of AUTHORITY_PLANES){
22
+ const entry=value.authority[plane];
23
+ if(!entry||typeof entry!=='object'||typeof entry.armed!=='boolean'||typeof entry.configured!=='boolean'||typeof entry.reason!=='string')
24
+ throw new Error(`controller authority ${plane} observation invalid`);
25
+ authority[plane]=Object.freeze({configured:entry.configured,armed:entry.armed,reason:entry.reason.slice(0,128)});
26
+ }
27
+ return Object.freeze({...value,authority:Object.freeze(authority)});
28
+ }
29
+
30
+ export function assertAuthorityVector(snapshot,expected){
31
+ for(const plane of AUTHORITY_PLANES){
32
+ const entry=snapshot?.authority?.[plane];
33
+ if(!entry||typeof expected?.[plane]!=='boolean') throw new Error(`authority ${plane} observation or expectation invalid`);
34
+ if(entry.armed!==expected[plane]) throw new Error(`${plane} authority expected ${expected[plane]?'ARMED':'DISARMED'}; observed ${entry.armed?'ARMED':'DISARMED'} (${entry.reason})`);
35
+ if(expected[plane]&&!entry.configured) throw new Error(`${plane} authority unconfigured`);
36
+ }
37
+ return snapshot;
38
+ }
39
+
40
+ export function authorityBanner(snapshot){
41
+ return AUTHORITY_PLANES.map(plane=>`${plane} ${snapshot.authority[plane].armed?'ARMED':'DISARMED'}`).join(' | ');
42
+ }
43
+
44
+ export function assertRuntimeReadyObservation(snapshot,{coreRoot=null}={}){
45
+ if(snapshot?.state!=='ONLINE') throw new Error(`runtime state not ONLINE: ${String(snapshot?.state)}`);
46
+ for(const name of ['bridge','core','cloud']){
47
+ if(snapshot[name]?.alive!==true||snapshot[name]?.reason!=='verified') throw new Error(`${name} runtime identity unverified`);
48
+ }
49
+ if(snapshot.local_ready!==true) throw new Error('local MCP readiness unverified');
50
+ if(snapshot.supervisor?.running!==true) throw new Error('autonomous supervisor not running');
51
+ if(coreRoot!==null){
52
+ if(typeof snapshot.core_root!=='string'||path.win32.resolve(snapshot.core_root).toLowerCase()!==path.win32.resolve(coreRoot).toLowerCase())
53
+ throw new Error('core root does not match active release');
54
+ }
55
+ return snapshot;
56
+ }
@@ -0,0 +1,17 @@
1
+ import { authorityVector } from './remote-authority.mjs';
2
+
3
+ export async function probeRemoteEnforcement({client,mode='default'}={}){
4
+ if(!client||typeof client.callTool!=='function')throw new Error('remote enforcement client invalid');
5
+ const expected=mode==='revoked'?{desktop:false,host:false}:authorityVector(mode);
6
+ const desktop=await client.callTool({name:'desktop_control',arguments:{op:'status'}});
7
+ const desktopValue=desktop?.structuredContent;
8
+ const desktopArmed=desktop?.isError===false&&desktopValue?.status==='ok'&&desktopValue?.result?.armed===true;
9
+ const desktopUnavailable=desktop?.isError===true&&desktopValue?.status==='unavailable';
10
+ if(expected.desktop?!desktopArmed:desktopArmed||(!desktopUnavailable&&desktopValue?.result?.armed!==false))
11
+ throw new Error('Desktop enforcement does not match requested authority');
12
+ const host=await client.callTool({name:'host_file_read',arguments:{root_id:'core',path:'package.json',encoding:'utf8'}});
13
+ const hostArmed=host?.isError===false&&host?.structuredContent?.status==='ok'&&/^[0-9a-f]{64}$/.test(String(host.structuredContent.sha256??''));
14
+ const hostDenied=host?.isError===true&&host?.structuredContent?.status==='denied';
15
+ if(expected.host?!hostArmed:!hostDenied) throw new Error('Host enforcement does not match requested authority');
16
+ return Object.freeze({desktop_armed:desktopArmed,host_armed:hostArmed});
17
+ }
@@ -319,19 +319,21 @@ export function createLiveTimelineWriter({stream=process.stdout,displayMode='uni
319
319
  return Object.freeze({write,flush,pending(){return active?{...active}:null;}});
320
320
  }
321
321
 
322
- export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000}={}){
322
+ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,foldHeartbeats=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000}={}){
323
323
  if(typeof clock!=='function') throw new Error('live timeline clock must be a function');
324
324
  if(!REMOTE_DISPLAY_MODES.includes(displayMode)) throw new Error('remote display mode invalid');
325
325
  if(!Number.isInteger(heartbeatBurstLimit)||heartbeatBurstLimit<2||heartbeatBurstLimit>100) throw new Error('heartbeatBurstLimit invalid');
326
326
  if(!Number.isInteger(heartbeatWindowMs)||heartbeatWindowMs<250||heartbeatWindowMs>60000) throw new Error('heartbeatWindowMs invalid');
327
327
  if(!rootPaths||typeof rootPaths!=='object'||Array.isArray(rootPaths)) throw new Error('live timeline rootPaths invalid');
328
328
  if(typeof trace!=='boolean') throw new Error('live timeline trace invalid');
329
+ if(typeof foldHeartbeats!=='boolean') throw new Error('live timeline foldHeartbeats invalid');
329
330
  if(!Number.isInteger(slowCallMs)||slowCallMs<100||slowCallMs>600000) throw new Error('live timeline slowCallMs invalid');
330
331
  const pending=new Map();
331
332
  const completed=new Set();
332
333
  const toolCounts=new Map();
333
334
  const durationSamples=[];
334
335
  let resultCount=0,okCount=0,errorCount=0,unknownCount=0,mutationCount=0,totalDurationMs=0,slowest=null,burst=null,foldedHeartbeatLines=0,peakInFlight=0;
336
+ let lastCapability=null,suppressedCapability=0,suppressedCapabilityReported=false;
335
337
 
336
338
  function eventPrefix(event,sequence,{displayTool=null,icon=null}={}){
337
339
  const seq=Number.isSafeInteger(sequence)&&sequence>0?'#'+sequence+' ':'';
@@ -419,7 +421,18 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,he
419
421
  addStats(event,span);
420
422
  const error=event.outcome?.is_error===true;
421
423
  const nowMs=eventMs(event,clock);
422
- if(HEARTBEAT_TOOLS.has(tool)&&!error){
424
+ if(tool==='capability_manifest'&&!error){
425
+ const snapshot=event.outcome?.snapshot_id,epoch=event.outcome?.state_epoch;
426
+ if(/^[0-9a-f]{64}$/.test(String(snapshot??''))&&Number.isInteger(epoch)&&epoch>0){
427
+ const identity=`${snapshot}:${epoch}`;
428
+ if(identity===lastCapability){suppressedCapability++;return null;}
429
+ lastCapability=identity;
430
+ const prior=flushBurst();
431
+ const line=resultLine(event,span,sequence)+` snapshot=${snapshot.slice(0,8)} epoch=${epoch}`;
432
+ return prior?[prior,line]:line;
433
+ }
434
+ }
435
+ if(foldHeartbeats&&HEARTBEAT_TOOLS.has(tool)&&!error){
423
436
  const duration=Number(event.outcome?.duration_ms);
424
437
  if(burst&&burst.tool===tool&&nowMs-burst.last_ms<=heartbeatWindowMs){
425
438
  burst.count++;
@@ -449,6 +462,10 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,he
449
462
  const lines=[];
450
463
  const folded=flushBurst();
451
464
  if(folded) lines.push(folded);
465
+ if(suppressedCapability>0&&!suppressedCapabilityReported){
466
+ lines.push(remoteDisplayText(`capability_manifest ×${suppressedCapability} unchanged suppressed`,displayMode));
467
+ suppressedCapabilityReported=true;
468
+ }
452
469
  for(const item of pending.values()) lines.push(remoteDisplayText((displayMode==='ascii'?'[WARN]':'⚠')+' ['+shortSpanId(item.request_id)+'] OUTCOME_UNKNOWN '+item.tool+' — '+suffix+'; inspect state before retry',displayMode));
453
470
  pending.clear();
454
471
  return lines;
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+
3
+ export function safeRemoteLogPrefixLength(bytes,encoding='utf8'){
4
+ if(!Buffer.isBuffer(bytes))throw new Error('remote log prefix bytes invalid');
5
+ if(encoding==='utf16le'){
6
+ let size=bytes.length-(bytes.length%2);
7
+ if(size>=2){const last=bytes.readUInt16LE(size-2);if(last>=0xd800&&last<=0xdbff)size-=2;}
8
+ return size;
9
+ }
10
+ if(encoding!=='utf8')throw new Error('remote log prefix encoding invalid');
11
+ let index=bytes.length-1;
12
+ while(index>=0&&(bytes[index]&0xc0)===0x80)index--;
13
+ if(index<0)return 0;
14
+ const lead=bytes[index];
15
+ const needed=lead<0x80?1:lead>=0xc2&&lead<=0xdf?2:lead>=0xe0&&lead<=0xef?3:lead>=0xf0&&lead<=0xf4?4:1;
16
+ return bytes.length-index<needed?index:bytes.length;
17
+ }
18
+
19
+ export function decodeRemoteLogBytes(file,bytes,{encoding=null}={}){
20
+ if(typeof file!=='string'||!Buffer.isBuffer(bytes))throw new Error('remote log decoder input invalid');
21
+ const knownLog=/\.log$/i.test(path.basename(file));
22
+ if(!knownLog)return bytes.toString('utf8');
23
+ const utf16=encoding==='utf16le'||(bytes.length>=2&&bytes[0]===0xff&&bytes[1]===0xfe);
24
+ if(!utf16)return bytes.toString('utf8').replace(/^\uFEFF/,'');
25
+ return bytes.toString('utf16le').replace(/^\uFEFF/,'');
26
+ }
27
+
28
+ export function boundedRemoteDiagnostic(value,maxChars=2048){
29
+ if(!Number.isInteger(maxChars)||maxChars<32||maxChars>8192)throw new Error('remote diagnostic bound invalid');
30
+ const line=String(value??'').replace(/\x1b\[[0-9;]*[A-Za-z]/g,'').replace(/[\x00-\x1f\x7f]+/g,' ').replace(/\s{2,}/g,' ').trim();
31
+ return line.length<=maxChars?line:`${line.slice(0,maxChars)}...`;
32
+ }
@@ -1,11 +1,17 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
2
4
  import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
3
5
  import { verifyMachinePingPublic } from './machine-identity.mjs';
6
+ import { parseActivePointer } from './release-slot.mjs';
7
+ import { probeRemoteEnforcement } from './remote-enforcement-probe-r60.mjs';
4
8
 
5
9
  const endpointText = process.argv[2];
6
10
  if (!endpointText) throw new Error('usage: node src/remote-mcp-machine-smoke-client.mjs https://host/mcp/path');
7
11
  const endpoint = new URL(endpointText);
8
12
  if (endpoint.protocol !== 'https:') throw new Error('machine live gate requires HTTPS');
13
+ const enforcementMode=process.argv[3]??null;
14
+ if(enforcementMode!==null&&!['default','desktop','host','full','revoked'].includes(enforcementMode))throw new Error('remote enforcement probe mode invalid');
9
15
 
10
16
  const machineTools = ['machine_list', 'machine_ping', 'machine_revoke', 'machine_status'];
11
17
  const client = new Client(
@@ -49,6 +55,17 @@ try {
49
55
  }
50
56
  const signatureVerified = await verifyMachinePingPublic({ ping, publicKeySpkiBase64: status.public_key_spki_base64 });
51
57
  if (!signatureVerified) throw new Error('machine ping public Ed25519 verification failed');
58
+ if(enforcementMode!==null){
59
+ const stateRoot=process.env.DEADBYTE_STATE_ROOT;
60
+ if(!stateRoot||!path.isAbsolute(stateRoot))throw new Error('remote enforcement probe state root missing');
61
+ const pointer=parseActivePointer(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
62
+ for(const [actual,expected] of [
63
+ [status.release_id,pointer.release_id],
64
+ [status.release_manifest_sha256,pointer.manifest_sha256],
65
+ [status.generation_manifest_sha256,pointer.generation_manifest_sha256]
66
+ ])if(actual!==expected)throw new Error('signed machine status does not bind active release/generation');
67
+ await probeRemoteEnforcement({client,mode:enforcementMode});
68
+ }
52
69
 
53
70
  console.log(JSON.stringify({
54
71
  status: 'passed',
@@ -59,7 +76,8 @@ try {
59
76
  release_id: status.release_id,
60
77
  release_manifest_sha256: status.release_manifest_sha256,
61
78
  generation_manifest_sha256: status.generation_manifest_sha256,
62
- signature_verified: true
79
+ signature_verified: true,
80
+ ...(enforcementMode?{enforcement_mode:enforcementMode,enforcement_verified:true}:{})
63
81
  }));
64
82
  } finally {
65
83
  await client.close().catch(() => {});
@@ -6,8 +6,9 @@ import path from 'node:path';
6
6
  import { setTimeout as sleep } from 'node:timers/promises';
7
7
  import { windowsPowerShellEnvironment } from './runtime-update.mjs';
8
8
  import { resolveStateRoot } from './state-root.mjs';
9
+ import { AUTHORITY_PLANES, parseAuthorityObservation, assertAuthorityVector } from './remote-authority.mjs';
9
10
 
10
- const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','park'];
11
+ const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'];
11
12
  // This inherited IPC channel is an OS-owned parent lifetime handle, not a PID
12
13
  // poll. A reused PID cannot keep a new foreground guardian alive.
13
14
  let ownerDisconnected=false;
@@ -42,12 +43,23 @@ function markerMatches(marker,{ownerPid,nonce}){
42
43
  marker?.owner_pid===ownerPid&&
43
44
  marker?.session_nonce===nonce;
44
45
  }
45
- function invokeCleanup(powershell,launcher,command,stateRoot){
46
+ function invokeCleanup(powershell,launcher,command,stateRoot,probeMode=null){
46
47
  const result=spawnSync(powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',launcher,command],{
47
- stdio:'ignore',windowsHide:true,shell:false,env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:stateRoot}),timeout:30000
48
+ stdio:'ignore',windowsHide:true,shell:false,env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:stateRoot,
49
+ ...(probeMode?{DEADBYTE_REMOTE_PROBE_MODE:probeMode}:{})}),timeout:probeMode?150000:30000
48
50
  });
49
51
  return !result.error&&result.status===0;
50
52
  }
53
+ function verifyRevoked(powershell,launcher,stateRoot){
54
+ const result=spawnSync(powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',launcher,'status','-Json'],{
55
+ stdio:['ignore','pipe','pipe'],encoding:'utf8',windowsHide:true,shell:false,
56
+ env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:stateRoot}),timeout:30000
57
+ });
58
+ if(result.error||result.status!==0) return null;
59
+ try{
60
+ return assertAuthorityVector(parseAuthorityObservation(result.stdout),Object.fromEntries(AUTHORITY_PLANES.map(plane=>[plane,false])));
61
+ }catch{return null;}
62
+ }
51
63
 
52
64
  async function main(){
53
65
  if(process.platform!=='win32') return;
@@ -82,6 +94,11 @@ async function main(){
82
94
  if(!invokeCleanup(powershell,launcher,command,root)) failed.push(command);
83
95
  }
84
96
  if(failed.length) throw new Error('guardian cleanup failed; ownership marker retained: '+failed.join(','));
97
+ const observed=verifyRevoked(powershell,launcher,root);
98
+ if(!observed) throw new Error('guardian authority cleanup unverified; ownership marker retained');
99
+ if(observed.state==='ONLINE'&&!invokeCleanup(powershell,launcher,'machinetest',root,'revoked'))
100
+ throw new Error('guardian runtime enforcement cleanup unverified; ownership marker retained');
101
+ if(!invokeCleanup(powershell,launcher,'park',root)) throw new Error('guardian park failed; ownership marker retained');
85
102
  const current=await readMarker(markerPath);
86
103
  if(current&&markerMatches(current,{ownerPid,nonce})) await rm(markerPath,{force:true});
87
104
  return;
@@ -116,7 +116,12 @@ function summarizeOutcome(result,error,durationMs){
116
116
  const structured=result?.structuredContent;
117
117
  const status=typeof structured?.status==='string'?structured.status:(result?.isError===true?'error':'ok');
118
118
  const isError=result?.isError===true;
119
+ const capability=structured?.result;
120
+ const snapshot=capability?.schema==='deadbyte.capability-manifest.v3'&&HEX64.test(String(capability.snapshot_id??''))&&
121
+ Number.isInteger(capability.state_epoch)&&capability.state_epoch>0
122
+ ?{snapshot_id:capability.snapshot_id,state_epoch:capability.state_epoch}:{};
119
123
  return {status:String(status).slice(0,128),is_error:isError,duration_ms:duration,receipt_sha256s:outcomeReceiptHashes(result),
124
+ ...snapshot,
120
125
  ...(isError?{reason:resultOutcomeReason(result)}:{})};
121
126
  }
122
127
 
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.36';
1
+ export const DEADBYTE_VERSION = '0.15.37';
2
2
  export const MCP_PROTOCOL_VERSION = '2026-07-28';
3
3
  export const RECEIPT_SCHEMA_V1 = 'deadbyte.receipt.v1';
4
4
  export const RECEIPT_SCHEMA_V2 = 'deadbyte.receipt.v2';
@@ -5,7 +5,7 @@ import { DEADBYTE_VERSION } from '../src/version.mjs';
5
5
 
6
6
  const root = new URL('../', import.meta.url);
7
7
  const read = rel => readFile(new URL(rel, root), 'utf8');
8
- const json = rel => read(rel).then(JSON.parse);
8
+ const json = rel => read(rel).then(raw => JSON.parse(raw.replace(/^\uFEFF/,'')));
9
9
 
10
10
  test('R48 compatibility release contract remains coherent in the current successor', async () => {
11
11
  const [pkg, lock, release, provenance, matrix] = await Promise.all([
@@ -53,7 +53,7 @@ test('R51 bare remote wires coherence before live generation binding and deploy
53
53
  test('R51 foreground authority contract explicitly verifies and cleans desktop plus host',async()=>{
54
54
  const {readFile}=await import('node:fs/promises');
55
55
  const cli=await readFile(new URL('../src/deadbyte-cli.mjs',import.meta.url),'utf8');
56
- assert.match(cli,/\['desktopstatus',\/\^Desktop plane/);
56
+ assert.match(cli,/assertAuthorityVector\(assertRuntimeReadyObservation\(observedAuthority/);
57
57
  assert.match(cli,/SESSION_CLEANUP=.*disarmdesktop.*disarmhost/s);
58
- assert.match(cli,/desktop, host DISARMED/);
58
+ assert.match(cli,/authorityBanner\(authority\)/);
59
59
  });
@@ -11,7 +11,12 @@ test('R58 foreground cleanup failure retains ownership evidence for recovery', {
11
11
  await mkdir(base,{recursive:true});
12
12
  const root=await mkdtemp(path.join(base,'cleanup-'));
13
13
  await mkdir(path.join(root,'bin'));
14
- await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command)
14
+ await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command,[switch]$Json)
15
+ if ($Command -eq 'status' -and $Json) {
16
+ $on=@{configured=$true;armed=$true;reason='standing-grant'}; $off=@{configured=$true;armed=$false;reason='disarmed'}
17
+ @{schema='deadbyte.controller-observation.v1';state='ONLINE';core_root='fixture';local_ready=$true;bridge=@{alive=$true;reason='verified'};core=@{alive=$true;reason='verified'};cloud=@{alive=$true;reason='verified'};supervisor=@{running=$true};authority=@{process=$on;coding=$on;autonomous=$on;desktop=$off;host=$off}} | ConvertTo-Json -Depth 5 -Compress
18
+ exit 0
19
+ }
15
20
  switch ($Command) {
16
21
  'status' { Write-Output 'DEADBYTE MCP = ONLINE'; Write-Output 'Bridge PID : 1001 [verified]'; Write-Output 'Cloud PID : 1002 [verified]'; Write-Output 'Local ready : True'; Write-Output 'Loop supervisor: RUNNING [verified]' }
17
22
  'processstatus' { Write-Output 'Process plane : ARMED [standing-grant]' }
@@ -30,8 +35,9 @@ exit 0
30
35
  const closed=new Promise(resolve=>child.once('close',code=>resolve(code)));
31
36
  try {
32
37
  const deadline=Date.now()+12000;
33
- while(!stdout.includes('ARMED: process, coding, autonomous')&&child.exitCode===null&&Date.now()<deadline) await delay(50);
34
- assert.match(stdout,/ARMED: process, coding, autonomous/,stderr);
38
+ while(!stdout.includes('[READY]')&&child.exitCode===null&&Date.now()<deadline) await delay(50);
39
+ assert.match(stdout,/\[READY\]/,stderr);
40
+ assert.match(stdout,/\[AUTH\] process ARMED \| coding ARMED \| autonomous ARMED \| desktop DISARMED \| host DISARMED/,stderr);
35
41
  child.stdin.end();
36
42
  assert.notEqual(await closed,0,'cleanup failure must not report a successful foreground exit');
37
43
  assert.match(stderr,/cleanup disarmprocess exit=9/);
@@ -11,9 +11,15 @@ test('R58 simultaneous stale recovery cannot both revoke the shared authority',
11
11
  const root=await mkdtemp(path.join(base,'concurrent-recovery-'));await mkdir(path.join(root,'bin'));await mkdir(path.join(root,'runtime'));
12
12
  const trace=path.join(root,'trace.txt'),markerPath=path.join(root,'runtime','remote-session.json');
13
13
  await writeFile(markerPath,JSON.stringify({schema:'deadbyte.remote-session.v1',pid:2147483000,started_at_utc:'2000-01-01T00:00:00.000Z'}));
14
- await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command)
14
+ await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command,[switch]$Json)
15
15
  $ErrorActionPreference='Stop'
16
16
  for($i=0;$i -lt 40;$i++){try{[IO.File]::AppendAllText('${trace.replaceAll("'","''")}',$Command+[Environment]::NewLine);break}catch{if($i -eq 39){throw};Start-Sleep -Milliseconds 10}}
17
+ if ($Command -match '^(arm|disarm)(process|coding|autonomous|desktop|host)$') { $file=Join-Path $env:DEADBYTE_STATE_ROOT ('runtime/' + $Matches[2] + '.armed'); if ($Matches[1] -eq 'arm') { [IO.File]::WriteAllText($file,'1') } else { Remove-Item -LiteralPath $file -Force -ErrorAction SilentlyContinue } }
18
+ if ($Command -eq 'status' -and $Json) {
19
+ $authority=@{};foreach($name in @('process','coding','autonomous','desktop','host')) { $armed=Test-Path -LiteralPath (Join-Path $env:DEADBYTE_STATE_ROOT ('runtime/' + $name + '.armed'));$authority[$name]=@{configured=$true;armed=[bool]$armed;reason=$(if($armed){'standing-grant'}else{'disarmed'})} }
20
+ @{schema='deadbyte.controller-observation.v1';state='ONLINE';core_root='fixture';local_ready=$true;bridge=@{alive=$true;reason='verified'};core=@{alive=$true;reason='verified'};cloud=@{alive=$true;reason='verified'};supervisor=@{running=$true};authority=$authority} | ConvertTo-Json -Depth 5 -Compress
21
+ exit 0
22
+ }
17
23
  switch($Command){
18
24
  'status' {Write-Output 'DEADBYTE MCP = ONLINE';Write-Output 'Bridge PID : 1001 [verified]';Write-Output 'Cloud PID : 1002 [verified]';Write-Output 'Local ready : True';Write-Output 'Loop supervisor: RUNNING [verified]'}
19
25
  'processstatus' {Write-Output 'Process plane : ARMED [standing-grant]'}
@@ -42,9 +48,9 @@ exit 0
42
48
  const o={stdout:'',stderr:''};c.stdout.on('data',b=>{o.stdout+=b;});c.stderr.on('data',b=>{o.stderr+=b;});o.exit=new Promise(resolve=>c.once('exit',resolve));children.push(c);outputs.push(o);
43
49
  }
44
50
  const until=Date.now()+12000;let commands=[];
45
- while(Date.now()<until){try{commands=(await readFile(trace,'utf8')).trim().split(/\r?\n/);}catch{}if(commands.includes('hoststatus'))break;await delay(50);}
51
+ while(Date.now()<until){try{commands=(await readFile(trace,'utf8')).trim().split(/\r?\n/);}catch{}if(outputs.some(o=>o.stdout.includes('[READY]')))break;await delay(50);}
46
52
  await writeFile(path.join(root,'observation.json'),JSON.stringify({commands,outputs:outputs.map(o=>({stdout:o.stdout,stderr:o.stderr})),simulation:'concurrent stale marker recovery with delayed second park'}),{flag:'wx'});
47
- assert.ok(commands.includes('hoststatus'),'a recovered foreground must become ready');
53
+ assert.ok(outputs.some(o=>o.stdout.includes('[READY]')),'a recovered foreground must become ready');
48
54
  for(const cmd of ['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'])assert.equal(commands.filter(x=>x===cmd).length,1,`stale recovery must serialize ${cmd}`);
49
55
  }finally{
50
56
  for(const c of children)if(c.exitCode===null)c.stdin.end();