deadbyte-mcp 0.15.68 → 0.15.69

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 (83) hide show
  1. package/MANIFEST.SHA256 +81 -38
  2. package/PROVENANCE.json +35 -23
  3. package/README.txt +44 -31
  4. package/RELEASE.json +30 -17
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +25 -2
  7. package/bridge/package.json +1 -1
  8. package/controller/deadbyte-controller.ps1 +65 -1
  9. package/controller/deadbyte-process-policy.json +4 -4
  10. package/docs/R184-package-lock.json +97 -0
  11. package/docs/R186-OWNER-CONSENT-HANDOFF.txt +87 -0
  12. package/package.json +1 -1
  13. package/scripts/install-r186-owner-trust.ps1 +59 -0
  14. package/scripts/r175-focused-runner.mjs +38 -0
  15. package/scripts/r176-log-preview.mjs +18 -0
  16. package/scripts/r177-owner-trust-inventory.ps1 +36 -0
  17. package/scripts/r178-full-regression.mjs +21 -0
  18. package/scripts/r178-npm-ci.mjs +10 -0
  19. package/scripts/r178-npm-preflight.mjs +16 -0
  20. package/scripts/r178-release-gates-readonly.mjs +9 -0
  21. package/scripts/r178-shard-regression.mjs +25 -0
  22. package/scripts/r179-predecessor-registry.mjs +24 -0
  23. package/scripts/r179-protected-inventory.ps1 +21 -0
  24. package/scripts/r186-focused-runner.mjs +22 -0
  25. package/scripts/r186-owner-anchor-inspect.mjs +6 -0
  26. package/scripts/r186-owner-consent-check.mjs +86 -0
  27. package/scripts/r186-owner-grant-cli.mjs +45 -0
  28. package/scripts/r186-owner-key-local.mjs +22 -0
  29. package/scripts/r186-owner-review-export.mjs +34 -0
  30. package/src/deadbyte-cli.mjs +69 -56
  31. package/src/r174-owner-webauthn.mjs +176 -0
  32. package/src/r175-foreground-boundary.mjs +28 -0
  33. package/src/r186-owner-anchor.mjs +12 -0
  34. package/src/r186-owner-consent-preflight.mjs +63 -0
  35. package/src/r186-owner-grant-broker.mjs +96 -0
  36. package/src/r186-owner-local-signing.mjs +25 -0
  37. package/src/r186-owner-request.mjs +40 -0
  38. package/src/r186-owner-review-export.mjs +49 -0
  39. package/src/remote-live-log.mjs +81 -5
  40. package/src/remote-session-guardian.mjs +23 -0
  41. package/src/version.mjs +1 -1
  42. package/test/helpers/r175-active-pointer.mjs +28 -0
  43. package/test/helpers/release-lock.mjs +12 -0
  44. package/test/r129-sdk-security-baseline.test.mjs +2 -1
  45. package/test/r130-release-security-contract.test.mjs +11 -10
  46. package/test/r132-release-identity.test.mjs +12 -11
  47. package/test/r153-prepublish-identity.test.mjs +1 -1
  48. package/test/r155-default-live-log.test.mjs +17 -16
  49. package/test/r160-package-decision-manifest.test.mjs +1 -1
  50. package/test/r160-release-contract.test.mjs +4 -5
  51. package/test/r163-isolated-slot-rollback-restore.test.mjs +6 -6
  52. package/test/r165-live-cli-skew.test.mjs +6 -6
  53. package/test/r167-release-contract.test.mjs +15 -13
  54. package/test/r168-protected-entrypoint.test.mjs +5 -2
  55. package/test/r168-release-contract.test.mjs +4 -3
  56. package/test/r168-remote-one-shot-coherence.test.mjs +2 -1
  57. package/test/r172-release-contract.test.mjs +7 -7
  58. package/test/r173-release-contract.test.mjs +18 -17
  59. package/test/r174-owner-webauthn.test.mjs +144 -0
  60. package/test/r175-cli-flags.test.mjs +23 -0
  61. package/test/r175-foreground-boundary.test.mjs +87 -0
  62. package/test/r175-guardian-owner-boundary.test.mjs +56 -0
  63. package/test/r176-pretty-live-log.test.mjs +54 -0
  64. package/test/r177-windows-absolute-live-path.test.mjs +37 -0
  65. package/test/r179-absolute-path-live-log.test.mjs +40 -0
  66. package/test/r179-release-contract.test.mjs +62 -0
  67. package/test/r186-controller-owner-grant.test.mjs +20 -0
  68. package/test/r186-owner-consent-preflight.test.mjs +98 -0
  69. package/test/r186-owner-enrollment.test.mjs +20 -0
  70. package/test/r186-owner-grant-broker.test.mjs +85 -0
  71. package/test/r186-owner-local-signing.test.mjs +30 -0
  72. package/test/r186-owner-request.test.mjs +28 -0
  73. package/test/r186-owner-review-export.test.mjs +52 -0
  74. package/test/r48-release-contract.test.mjs +2 -1
  75. package/test/r51-runtime-coherence.test.mjs +4 -2
  76. package/test/r58-cleanup-retention.test.mjs +5 -12
  77. package/test/r58-concurrent-recovery.test.mjs +5 -1
  78. package/test/r58-stale-owner.test.mjs +9 -3
  79. package/test/r59-tool-surface-compat.test.mjs +1 -1
  80. package/test/remote-live-log-r37.test.mjs +2 -2
  81. package/test/remote-live-log-recovery-r36.test.mjs +1 -1
  82. package/test/remote-session-cli.test.mjs +92 -126
  83. package/node_modules/which/CHANGELOG.md +0 -166
@@ -11,7 +11,7 @@ const LIFECYCLE=Object.freeze({
11
11
  session_new:'[SESSION] New foreground session',
12
12
  session_restored:'♻️ [SESSION] Previous result stream replayed',
13
13
  ready:'✅ [READY] DEADBYTE MCP online',
14
- cleanup:'🔐 [AUTH] Revoking session authority...'
14
+ cleanup:'[SESSION] Closing observation session...'
15
15
  });
16
16
  const LIFECYCLE_ASCII=Object.freeze({
17
17
  device_start:'[DEVICE] Starting DEADBYTE MCP...',
@@ -20,7 +20,7 @@ const LIFECYCLE_ASCII=Object.freeze({
20
20
  session_new:'[SESSION] New foreground session',
21
21
  session_restored:'[SESSION] Previous result stream replayed',
22
22
  ready:'[READY] DEADBYTE MCP online',
23
- cleanup:'[AUTH] Revoking session authority...'
23
+ cleanup:'[SESSION] Closing observation session...'
24
24
  });
25
25
 
26
26
  function safeAtom(value){
@@ -104,13 +104,33 @@ function rootedPath(args,key='path',rootPaths={}){
104
104
  if(base){
105
105
  const flavor=/^(?:[A-Za-z]:[\\/]|\\\\)/.test(base)?path.win32:path.posix;
106
106
  const absolute=p==='.'?flavor.resolve(base):flavor.resolve(base,p);
107
+ const relative=flavor.relative(flavor.resolve(base),absolute);
108
+ // A display-only resolver must not misleadingly mark traversal as in-root.
109
+ if(relative==='..'||relative.startsWith('..'+flavor.sep)||flavor.isAbsolute(relative))
110
+ return '[outside root='+root+'] '+p;
107
111
  return absolute+' [root='+root+']';
108
112
  }
113
+ // We do not invent drive letters when the trusted runtime policy has
114
+ // not supplied the actual root mapping.
109
115
  return root+':'+p;
110
116
  }
111
117
  if(root) return 'root='+root;
112
118
  return p?'path='+p:'';
113
119
  }
120
+ function detailedDisplayPaths(args,rootPaths){
121
+ const safe=sanitizeAuditArgs(args??{});
122
+ const paths=[];
123
+ const entry=(key,label)=>{
124
+ if(!safePathAtom(safe?.[key]))return;
125
+ const value=rootedPath(safe,key,rootPaths)+(safe.root_id&&!safePathAtom(rootPaths?.[safe.root_id])?' [unresolved root]':'');
126
+ if(value)paths.push((label?label+': ':'')+value);
127
+ };
128
+ if(safe?.source_path&&safe?.destination_path){
129
+ entry('source_path','FROM');entry('destination_path','TO');
130
+ }else if(safe?.path)entry('path','');
131
+ else if(safe?.script_path)entry('script_path','SCRIPT');
132
+ return paths;
133
+ }
114
134
  function displayToolName(tool,args){
115
135
  const op=safeAtom(args?.op);
116
136
  if(op&&['machine_fs','coding_inspect','process_session','desktop_control','machine_observe'].includes(tool)) return tool+'.'+op;
@@ -399,10 +419,12 @@ export function createLiveTimelineWriter({stream=process.stdout,displayMode='uni
399
419
 
400
420
  export function defaultForegroundTimelineOptions({verbose=false}={}){
401
421
  if(typeof verbose!=='boolean')throw new Error('default live timeline verbosity invalid');
402
- return Object.freeze({commanderStyle:!verbose,foldHeartbeats:false});
422
+ // R176 default: readable emoji timeline with compact heartbeat groups.
423
+ // Commander/verbose presentation remains explicitly selectable by callers.
424
+ return Object.freeze({commanderStyle:false,foldHeartbeats:!verbose,prettyStyle:!verbose});
403
425
  }
404
426
 
405
- export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,foldHeartbeats=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000,commanderStyle=false}={}){
427
+ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,foldHeartbeats=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000,commanderStyle=false,prettyStyle=false}={}){
406
428
  if(typeof clock!=='function') throw new Error('live timeline clock must be a function');
407
429
  if(!REMOTE_DISPLAY_MODES.includes(displayMode)) throw new Error('remote display mode invalid');
408
430
  if(!Number.isInteger(heartbeatBurstLimit)||heartbeatBurstLimit<2||heartbeatBurstLimit>100) throw new Error('heartbeatBurstLimit invalid');
@@ -410,6 +432,7 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
410
432
  if(!rootPaths||typeof rootPaths!=='object'||Array.isArray(rootPaths)) throw new Error('live timeline rootPaths invalid');
411
433
  if(typeof trace!=='boolean') throw new Error('live timeline trace invalid');
412
434
  if(typeof commanderStyle!=='boolean') throw new Error('live timeline commanderStyle invalid');
435
+ if(typeof prettyStyle!=='boolean') throw new Error('live timeline prettyStyle invalid');
413
436
  if(typeof foldHeartbeats!=='boolean') throw new Error('live timeline foldHeartbeats invalid');
414
437
  if(!Number.isInteger(slowCallMs)||slowCallMs<100||slowCallMs>600000) throw new Error('live timeline slowCallMs invalid');
415
438
  const pending=new Map();
@@ -428,6 +451,23 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
428
451
  return visual+' '+displayClock(eventMs(event,clock))+' '+seq+'['+shortSpanId(event.request_id)+'] '+tool;
429
452
  }
430
453
  function traceSuffix(meta){return trace&&meta?' '+traceText(meta,displayMode):'';}
454
+ function prettyLine(event,tool,tag,detail='',glyph=null,displayPaths=[]){
455
+ const time=displayClock(eventMs(event,clock)).slice(0,8);
456
+ const title=tool.length>30?tool.slice(0,29)+'…':tool;
457
+ // Spaces avoid incorrect console-cell widths for double-width emoji.
458
+ const icon=glyph??iconForTool(tool,displayMode);
459
+ const leader=displayMode==='ascii'?' | ':' │ ';
460
+ const shortId=shortSpanId(event.request_id);
461
+ const label=title.padEnd(31,' ');
462
+ const headline=time+' '+icon+' '+tag.padEnd(8,' ')+' '+label+
463
+ leader+'['+shortId+']'+(detail?' '+clipped(detail,220):'');
464
+ if(!displayPaths.length)return remoteDisplayText(headline,displayMode);
465
+ const pathIcon=displayMode==='ascii'?'PATH: ':'📁 ';
466
+ // Paths are not shortened to an ambiguous basename: retain the Windows
467
+ // drive, directory tree and policy root. Respect the bounded input limit.
468
+ const descriptions=displayPaths.map(p=>' '+pathIcon+p);
469
+ return remoteDisplayText([headline,...descriptions].join('\n'),displayMode);
470
+ }
431
471
  function resultLine(event,span,sequence){
432
472
  const outcome=outcomeClass(event);
433
473
  const error=outcome==='error';
@@ -458,6 +498,16 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
458
498
  const prefixIcon=unknown?(displayMode==='ascii'?'WARN':'⚠️'):null;
459
499
  const evidence=receipt?' '+(displayMode==='ascii'?'RECEIPT':'🧾 receipt')+(displayMode==='ascii'?'=':'=')+receipt:'';
460
500
  const reasonSep=displayMode==='ascii'?' - ':' — ';
501
+ if(prettyStyle){
502
+ const tag=unknown?'UNKNOWN':pendingResult?'PENDING':error?(rawStatus==='denied'?'DENIED':'FAIL'):'PASS';
503
+ const glyph=displayMode==='ascii'?(unknown?'!':error?'X':pendingResult?'~':'+'):
504
+ unknown?'⚠️':error?'❌':pendingResult?'⏳':'✅';
505
+ const pathList=span?.display_paths??[];
506
+ const nonPathSummary=pathList.length?summary.replace(pathList[0],'').trim():summary;
507
+ const detail=[duration,trace&&span?.trace_meta?traceText(span.trace_meta,displayMode):'',nonPathSummary,statusDetail.trim(),receipt?'receipt='+receipt:'',
508
+ reason?reason:''].filter(Boolean).join(' | ');
509
+ return prettyLine(event,displayTool,tag,detail,glyph,pathList);
510
+ }
461
511
  if(commanderStyle){
462
512
  const tag=unknown?'OUTCOME_UNKNOWN':pendingResult?'pending':error?'failed':'completed';
463
513
  const state=rawStatus?' status='+rawStatus:'';
@@ -475,6 +525,11 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
475
525
  const endClock=displayClock(value.last_ms);
476
526
  const range=value.first_ms===value.last_ms?endClock:startClock.slice(0,8)+'–'+endClock.slice(0,8);
477
527
  if(value.count>1) foldedHeartbeatLines+=value.count-1;
528
+ if(prettyStyle){
529
+ return prettyLine({request_id:'polling',created_at_utc:new Date(value.last_ms).toISOString()},
530
+ value.tool,'FOLDED','×'+value.count+' avg '+formatDuration(avg),
531
+ displayMode==='ascii'?'H':'📡');
532
+ }
478
533
  const marker=displayMode==='ascii'?'OK':'✓';
479
534
  return remoteDisplayText(iconForTool(value.tool,displayMode)+' '+range+' '+seq+value.tool+' ×'+value.count+' avg '+formatDuration(avg)+' '+marker,displayMode);
480
535
  }
@@ -512,12 +567,13 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
512
567
  const args=event.args??{};
513
568
  const displayTool=displayToolName(tool,args);
514
569
  const summary=summarizeLiveToolArgs(displayTool,args,{rootPaths});
570
+ const displayPaths=prettyStyle?detailedDisplayPaths(args,rootPaths):[];
515
571
  const traceMeta=traceIdentity(event,args);
516
572
  if(pending.size>=MAX_PRESENTATION_IDENTITIES){
517
573
  pendingOverflow++;
518
574
  return pendingOverflow===1?remoteDisplayText('LIVELOG pending span cap reached; full audit remains on disk',displayMode):null;
519
575
  }
520
- pending.set(requestId,{request_id:requestId,tool,display_tool:displayTool,summary,trace_meta:traceMeta,started_at_ms:clock()});
576
+ pending.set(requestId,{request_id:requestId,tool,display_tool:displayTool,summary,display_paths:displayPaths,trace_meta:traceMeta,started_at_ms:clock()});
521
577
  peakInFlight=Math.max(peakInFlight,pending.size);
522
578
  if(commanderStyle){
523
579
  const safeArgs=sanitizeAuditArgs(args);
@@ -527,6 +583,9 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
527
583
  }
528
584
  if(verbose) return formatRemoteToolCall({...event,tool:displayTool},{clock,displayMode})+traceSuffix(traceMeta);
529
585
  if(visibleStart(displayTool)){
586
+ if(prettyStyle) return prettyLine(event,displayTool,'START',
587
+ [trace?traceText(traceMeta,displayMode):'',displayPaths.length?summary.replace(displayPaths[0],'').trim():summary].filter(Boolean).join(' | '),
588
+ null,displayPaths);
530
589
  const startMarker=displayMode==='ascii'?'START':'▶ START';
531
590
  return remoteDisplayText(eventPrefix(event,sequence,{displayTool})+traceSuffix(traceMeta)+' '+startMarker+(summary?' '+summary:''),displayMode);
532
591
  }
@@ -606,6 +665,23 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
606
665
  const p50=sorted.length?(sorted.length%2?sorted[(sorted.length-1)/2]:(sorted[sorted.length/2-1]+sorted[sorted.length/2])/2):null;
607
666
  const p95=sorted.length?sorted[Math.max(0,Math.ceil(sorted.length*0.95)-1)]:null;
608
667
  const p99=sorted.length?sorted[Math.max(0,Math.ceil(sorted.length*0.99)-1)]:null;
668
+ if(prettyStyle){
669
+ const ascii=displayMode==='ascii';
670
+ const lines=[
671
+ (ascii?'[SUMMARY]':'📊 SESSION')+' '+resultCount+' calls | '+okCount+' ok | '+
672
+ errorCount+(errorCount===1?' error | ':' errors | ')+unknownCount+' unknown'+(pendingCount?' | '+pendingCount+' pending':''),
673
+ (ascii?' work:':' 🛠 work:')+' '+mutationCount+' mutations | peak '+peakInFlight+' in-flight'+
674
+ (foldedHeartbeatLines?' | folded '+foldedHeartbeatLines+' heartbeats':''),
675
+ (ascii?' timing:':' ⏱ timing:')+' p50 '+(p50===null?'n/a':formatDuration(p50))+
676
+ ' | p95 '+(p95===null?'n/a':formatDuration(p95))+
677
+ ' | p99 '+(p99===null?'n/a':formatDuration(p99))+
678
+ (slowest?' | slowest '+slowest.tool+' '+formatDuration(slowest.duration_ms):''),
679
+ (ascii?' top:':' 🔝 top:')+(top?' '+top:' none')
680
+ ];
681
+ if(pendingOverflow)lines.push((ascii?' WARNING:':' ⚠️ WARNING:')+
682
+ ' '+pendingOverflow+' untracked presentation spans; inspect signed audit');
683
+ return remoteDisplayText(lines.join('\n'),displayMode);
684
+ }
609
685
  return remoteDisplayText((displayMode==='ascii'?'[SUMMARY]':'📊 [SUMMARY]')+' '+resultCount+' calls | '+okCount+' ok | '+
610
686
  errorCount+' error'+(errorCount===1?'':'s')+' | '+unknownCount+' unknown'+(pendingCount?' | '+pendingCount+' pending':'')+' | '+mutationCount+' mutation'+(mutationCount===1?'':'s')+
611
687
  ' | tool time '+(formatDuration(totalDurationMs)||'0ms')+
@@ -8,6 +8,7 @@ import { windowsPowerShellEnvironment } from './runtime-update.mjs';
8
8
  import { resolveStateRoot } from './state-root.mjs';
9
9
  import { AUTHORITY_PLANES, parseAuthorityObservation, assertAuthorityVector } from './remote-authority.mjs';
10
10
  import { readRemoteMarkerSnapshot, deletePinnedRemoteMarker } from './foreground-marker-cas-win32.mjs';
11
+ import {assertObserveOnlyMarker} from './r175-foreground-boundary.mjs';
11
12
 
12
13
  const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'];
13
14
  const TRUSTED_WINDOWS_ROOT='C:\\Windows';
@@ -68,6 +69,9 @@ async function main(){
68
69
  const ownerPid=Number(args['owner-pid']);
69
70
  const nonce=String(args['session-nonce']||'');
70
71
  const ipcOwner=args['owner-channel']==='ipc';
72
+ const observerGuard=args['authority-scope']==='observe-only';
73
+ if(args['authority-scope']!==undefined&&!observerGuard)
74
+ throw new Error('R175 guardian authority scope invalid');
71
75
  if(ipcOwner&&typeof process.send!=='function')throw new Error('guardian owner IPC channel missing');
72
76
  if(!Number.isInteger(ownerPid)||ownerPid<1) throw new Error('guardian owner pid invalid');
73
77
  if(ipcOwner&&process.ppid!==ownerPid)throw new Error('guardian IPC parent does not match declared owner');
@@ -85,6 +89,25 @@ async function main(){
85
89
  continue;
86
90
  }
87
91
 
92
+ // R179: a missing command-line flag is never permission to downgrade an
93
+ // observation-only marker to legacy broad authority revocation.
94
+ if(assertObserveOnlyMarker(marker)&&!observerGuard)
95
+ throw new Error('R179_OBSERVER_GUARDIAN_SCOPE_DOWNGRADE: owner grants retained');
96
+ if(observerGuard){
97
+ // Guardian scope is inherited from the parent process command line.
98
+ // Marker mismatch can never degrade into legacy unconditional revoke.
99
+ if(!assertObserveOnlyMarker(marker))
100
+ throw new Error('R175 observer guardian marker invalid; no grants touched');
101
+ // Observation-only sessions own the foreground marker, never the
102
+ // process/coding/host/desktop/autonomous grants or runtime.
103
+ const current=await readRemoteMarkerSnapshot(markerPath);
104
+ if(!current)return;
105
+ if(!markerMatches(current.marker,{ownerPid,nonce})||!assertObserveOnlyMarker(current.marker))return;
106
+ if(deletePinnedRemoteMarker(markerPath,current.sha256)!=='REMOVED')
107
+ throw new Error('R175 guardian monitor marker CAS failed; marker retained');
108
+ return;
109
+ }
110
+
88
111
  const failed=[];
89
112
  for(const command of CLEANUP){
90
113
  const latest=await readMarker(markerPath);
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.68';
1
+ export const DEADBYTE_VERSION = '0.15.69';
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';
@@ -0,0 +1,28 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {mkdir,readFile,writeFile} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const sha256=value=>createHash('sha256').update(value).digest('hex');
6
+
7
+ export async function installExactActivePointer({stateRoot,packageRoot,launcherPath}){
8
+ const manifestBytes=await readFile(path.join(packageRoot,'MANIFEST.SHA256'));
9
+ const manifestSha256=sha256(manifestBytes);
10
+ const pkg=JSON.parse(await readFile(path.join(packageRoot,'package.json'),'utf8'));
11
+ const generationRoot=path.join(stateRoot,'generations','test-current');
12
+ const configPath=path.join(generationRoot,'controller-config.psd1');
13
+ await mkdir(generationRoot,{recursive:true});
14
+ await writeFile(configPath,"@{ Label = 'test-current' }\r\n",'utf8');
15
+ const pointer={
16
+ schema:'deadbyte.active-release.v1',
17
+ release_id:`v${pkg.version}-${manifestSha256.slice(0,16)}`,
18
+ version:pkg.version,
19
+ manifest_sha256:manifestSha256,
20
+ release_root:path.resolve(packageRoot),
21
+ generation_root:generationRoot,
22
+ generation_manifest_sha256:sha256(await readFile(configPath)),
23
+ launcher_sha256:sha256(await readFile(launcherPath)),
24
+ config_path:configPath
25
+ };
26
+ await writeFile(path.join(stateRoot,'active-release.json'),`${JSON.stringify(pointer)}\n`,'utf8');
27
+ return pointer;
28
+ }
@@ -0,0 +1,12 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
4
+
5
+ export async function readReleaseLock(root){
6
+ const rootPath=root instanceof URL?fileURLToPath(root):root;
7
+ for(const relative of ['package-lock.json','docs/R184-package-lock.json']){
8
+ try{return JSON.parse(await readFile(path.join(rootPath,relative),'utf8'));}
9
+ catch(error){if(error?.code!=='ENOENT')throw error;}
10
+ }
11
+ throw new Error('R184_RELEASE_LOCK_EVIDENCE_MISSING');
12
+ }
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
3
3
  import {readFile} from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import {fileURLToPath} from 'node:url';
6
+ import {readReleaseLock} from './helpers/release-lock.mjs';
6
7
 
7
8
  const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
8
9
  async function json(file){return JSON.parse(await readFile(path.join(root,file),'utf8'));}
@@ -29,7 +30,7 @@ test('R129 shipped client and server SDK exclude all recorded 2.1.0 security adv
29
30
  });
30
31
 
31
32
  test('R129 lockfile resolves security-fixed SDK packages without prerelease versions',async()=>{
32
- const lock=await json('package-lock.json');
33
+ const lock=await readReleaseLock(root);
33
34
  const pins=['@modelcontextprotocol/client','@modelcontextprotocol/server'];
34
35
  for(const dep of pins){
35
36
  const version=lock.packages?.['node_modules/'+dep]?.version;
@@ -4,23 +4,24 @@ import {readFile} from 'node:fs/promises';
4
4
  import {fileURLToPath} from 'node:url';
5
5
  import path from 'node:path';
6
6
  import {DEADBYTE_VERSION} from '../src/version.mjs';
7
+ import {readReleaseLock} from './helpers/release-lock.mjs';
7
8
 
8
9
  const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
9
10
  const read=async file=>JSON.parse(await readFile(path.join(root,file),'utf8'));
10
- test('current successor retains the R130 security baseline with truthful R173 identity',async()=>{
11
+ test('current successor retains the R130 security baseline with truthful R179 identity',async()=>{
11
12
  const [pkg,release,provenance,bom,lock,matrix]=await Promise.all([
12
13
  read('package.json'),read('RELEASE.json'),read('PROVENANCE.json'),
13
- read('SBOM.cdx.json'),read('package-lock.json'),read('TEST-MATRIX.json')]);
14
+ read('SBOM.cdx.json'),readReleaseLock(root),read('TEST-MATRIX.json')]);
14
15
  assert.equal(pkg.name,'deadbyte-mcp');
15
16
  for(const actual of [pkg.version,release.version,provenance.version,matrix.version,bom.metadata.component.version,lock.version,lock.packages[''].version,DEADBYTE_VERSION])
16
- assert.equal(actual,'0.15.68');
17
- assert.equal(release.train,'R173');
18
- assert.equal(matrix.train,'R173');
19
- assert.equal(release.source_predecessor.version,'0.15.67');
20
- assert.equal(release.source_predecessor.train,'R172');
21
- assert.match(release.source_predecessor.source_path,/0\.15\.67-r172-final-remote-recovery/i);
22
- assert.equal(release.source_predecessor.manifest_status,'VERIFIED_FINAL_BYTES');
23
- assert.equal(release.runtime_predecessor.version,'0.15.61');
17
+ assert.equal(actual,'0.15.69');
18
+ assert.equal(release.train,'R179');
19
+ assert.equal(matrix.train,'R179');
20
+ assert.equal(release.source_predecessor.version,'0.15.68');
21
+ assert.equal(release.source_predecessor.train,'R173');
22
+ assert.match(release.source_predecessor.source_path,/deadbyte-mcp-0\.15\.68\.tgz/i);
23
+ assert.equal(release.source_predecessor.manifest_status,'ACTIVE_PRODUCTION_MANIFEST_VERIFIED');
24
+ assert.equal(release.runtime_predecessor.version,'0.15.68');
24
25
  assert.equal(provenance.r160.source_predecessor_manifest_status,'STALE_REJECTED');
25
26
  assert.equal(pkg.dependencies['@modelcontextprotocol/client'],'2.3.1');
26
27
  assert.equal(pkg.dependencies['@modelcontextprotocol/server'],'2.3.1');
@@ -4,28 +4,29 @@ import {readFile} from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import {fileURLToPath} from 'node:url';
6
6
  import {DEADBYTE_VERSION} from '../src/version.mjs';
7
+ import {readReleaseLock} from './helpers/release-lock.mjs';
7
8
 
8
9
  const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
9
10
  const read=async rel=>JSON.parse(await readFile(path.join(root,rel),'utf8'));
10
11
 
11
- test('R173 package identity preserves R132 evidence and published R172 source predecessor',async()=>{
12
+ test('R179 package identity preserves R132 evidence and published R173 source predecessor',async()=>{
12
13
  const [pkg,lock,release,prov,bom,matrix]=await Promise.all([
13
- read('package.json'),read('package-lock.json'),read('RELEASE.json'),
14
+ read('package.json'),readReleaseLock(root),read('RELEASE.json'),
14
15
  read('PROVENANCE.json'),read('SBOM.cdx.json'),read('TEST-MATRIX.json')]);
15
16
  for(const value of [pkg.version,lock.version,lock.packages[''].version,release.version,prov.version,bom.metadata.component.version,matrix.version,DEADBYTE_VERSION]){
16
17
  assert.equal(value,pkg.version);
17
18
  }
18
- assert.equal(release.train,'R173');
19
- assert.equal(matrix.train,'R173');
20
- assert.equal(release.source_predecessor.version,'0.15.67');
21
- assert.equal(release.source_predecessor.train,'R172');
22
- assert.match(release.source_predecessor.source_path,/0\.15\.67-r172-final-remote-recovery/i);
19
+ assert.equal(release.train,'R179');
20
+ assert.equal(matrix.train,'R179');
21
+ assert.equal(release.source_predecessor.version,'0.15.68');
22
+ assert.equal(release.source_predecessor.train,'R173');
23
+ assert.match(release.source_predecessor.source_path,/deadbyte-mcp-0\.15\.68\.tgz/i);
23
24
  assert.equal(prov.r160.source_predecessor_manifest_status,'STALE_REJECTED');
24
25
  assert.equal(release.r165.published_source_manifest_sha256,'4adf7d94682fbb71c4a91eb3147ecc48076bbc5d79cb67fcb875cde6a1bb4805');
25
26
  assert.equal(release.r165.published_source_tarball_sha256,'812e97609b7c51353ed25f2202129737e43bb4fbe0778c3adec43ec065352641');
26
- assert.equal(release.source_predecessor.tarball_sha256,'0fe761f035f38730a4717448da4217f55464c38cd1616126b5850ff44411cd6d');
27
- assert.equal(release.r173.source_predecessor_tarball_sha256,release.source_predecessor.tarball_sha256);
28
- assert.equal(release.runtime_predecessor.version,'0.15.61');
27
+ assert.equal(release.source_predecessor.tarball_sha256,'1a7f1fe05a06178b8d772a140be6e0470790b743cef690c3f8f8165cc61075bf');
28
+ assert.equal(release.r179.source_predecessor_tarball_sha256,release.source_predecessor.tarball_sha256);
29
+ assert.equal(release.runtime_predecessor.version,'0.15.68');
29
30
  assert.equal(prov.runtime_predecessor.release_id,release.runtime_predecessor.release_id);
30
31
  assert.equal(prov.runtime_predecessor.manifest_sha256,release.runtime_predecessor.manifest_sha256);
31
32
  assert.equal(prov.runtime_predecessor.generation_manifest_sha256,release.runtime_predecessor.generation_manifest_sha256);
@@ -36,7 +37,7 @@ test('R173 package identity preserves R132 evidence and published R172 source pr
36
37
  });
37
38
 
38
39
  test('R132 shipped SBOM matches the actual audited bundled MCP SDK',async()=>{
39
- const [pkg,bom,lock]=await Promise.all([read('package.json'),read('SBOM.cdx.json'),read('package-lock.json')]);
40
+ const [pkg,bom,lock]=await Promise.all([read('package.json'),read('SBOM.cdx.json'),readReleaseLock(root)]);
40
41
  for(const name of ['@modelcontextprotocol/client','@modelcontextprotocol/server','@modelcontextprotocol/core']){
41
42
  const entry=bom.components.find(v=>v.name===name);
42
43
  assert.ok(entry,'missing SBOM component: '+name);
@@ -26,7 +26,7 @@ test('R153 historical verifier refuses current successor package identity',async
26
26
  const state={pkg:JSON.parse(await readFile(path.join(root,'package.json'),'utf8')),
27
27
  release:JSON.parse(await readFile(path.join(root,'RELEASE.json'),'utf8')),
28
28
  bridge:JSON.parse(await readFile(path.join(root,'bridge/package.json'),'utf8'))};
29
- assert.equal(state.pkg.version,'0.15.68');
29
+ assert.equal(state.pkg.version,'0.15.69');
30
30
  assert.throws(()=>verifyR153Lineage(state),/package version/);
31
31
  await assert.rejects(verifyR153IdentityTree(root),/package version/);
32
32
  });
@@ -14,25 +14,25 @@ function defaultTracker(){
14
14
  return live.createLiveTimelineTracker({clock:now,...live.defaultForegroundTimelineOptions({verbose:false})});
15
15
  }
16
16
 
17
- test('R155 ordinary remote has per-call START and DONE without opt-in flag',()=>{
17
+ test('R176 default folds harmless status polling',()=>{
18
18
  const tracker=defaultTracker();
19
19
  const start=tracker.observe({event_type:'tool.request',request_id:'abc012345678',tool:'machine_status',args:{}},{sequence:1});
20
- assert.match(start,/Received tool call/);
21
- assert.match(start,/machine_status/);
20
+ assert.equal(start,null);
22
21
  const done=tracker.observe({event_type:'tool.result',request_id:'abc012345678',tool:'machine_status',outcome:{status:'ok',duration_ms:3}},{sequence:2});
23
- assert.match(done,/completed/);
24
- assert.match(done,/3ms/);
22
+ assert.equal(done,null);
23
+ assert.match(tracker.reconcileInterrupted().join('\n'),/machine_status.*×1/);
25
24
  });
26
25
 
27
- test('R155 ordinary remote does not fold successive status calls into a burst',()=>{
26
+ test('R176 status bursts collapse without losing event totals',()=>{
28
27
  const tracker=defaultTracker();
29
28
  for(let n=0;n<3;n++){
30
29
  const id='repeat-'+n;
31
- assert.match(tracker.observe({event_type:'tool.request',request_id:id,tool:'machine_ping',args:{}},{sequence:n*2+1}),/Received tool call/);
32
- assert.match(tracker.observe({event_type:'tool.result',request_id:id,tool:'machine_ping',outcome:{status:'ok',duration_ms:1}},{sequence:n*2+2}),/completed/);
30
+ assert.equal(tracker.observe({event_type:'tool.request',request_id:id,tool:'machine_ping',args:{}},{sequence:n*2+1}),null);
31
+ assert.equal(tracker.observe({event_type:'tool.result',request_id:id,tool:'machine_ping',outcome:{status:'ok',duration_ms:1}},{sequence:n*2+2}),null);
33
32
  }
33
+ assert.match(tracker.reconcileInterrupted().join('\n'),/machine_ping.*×3/);
34
34
  assert.match(tracker.summary(),/3 calls \| 3 ok/);
35
- assert.doesNotMatch(tracker.summary(),/folded [1-9]/);
35
+ assert.match(tracker.summary(),/folded 2 heartbeat/);
36
36
  });
37
37
 
38
38
  test('R155 ordinary remote keeps credentials and payload opaque by default',()=>{
@@ -42,7 +42,7 @@ test('R155 ordinary remote keeps credentials and payload opaque by default',()=>
42
42
  command_id:'node_script',path:'C:\\Safe\\test.js',
43
43
  metadata:{token:'Bearer should-not-appear'}
44
44
  }},{sequence:1});
45
- assert.match(line,/Received tool call/);
45
+ assert.match(line,/START/);
46
46
  assert.match(line,/node_script/);
47
47
  assert.doesNotMatch(line,/sk-secret-test|PRIVATE SOURCE CONTENT|should-not-appear/);
48
48
  });
@@ -51,10 +51,10 @@ test('R155 ordinary remote prints DENIED and UNKNOWN as failures rather than com
51
51
  const tracker=defaultTracker();
52
52
  tracker.observe({event_type:'tool.request',request_id:'one',tool:'host_file_read',args:{path:'a.txt'}},{sequence:1});
53
53
  const fail=tracker.observe({event_type:'tool.result',request_id:'one',tool:'host_file_read',outcome:{status:'denied',is_error:true,reason:'arm file missing'}},{sequence:2});
54
- assert.match(fail,/failed/);assert.match(fail,/arm file missing/);
54
+ assert.match(fail,/DENIED/);assert.match(fail,/arm file missing/);
55
55
  tracker.observe({event_type:'tool.request',request_id:'two',tool:'coding_file_write',args:{path:'x.txt'}},{sequence:3});
56
56
  const unknown=tracker.observe({event_type:'tool.result',request_id:'two',tool:'coding_file_write',outcome:{status:'interrupted',side_effect_state:'unknown'}},{sequence:4});
57
- assert.match(unknown,/OUTCOME_UNKNOWN/);
57
+ assert.match(unknown,/UNKNOWN/);
58
58
  });
59
59
 
60
60
  test('R155 ordinary remote has no new CLI live-log mode and wired default live behavior',()=>{
@@ -66,16 +66,17 @@ test('R155 ordinary remote has no new CLI live-log mode and wired default live b
66
66
  assert.match(source,/defaultForegroundTimelineOptions\(\{verbose\}\)/);
67
67
  });
68
68
 
69
- test('R155 unchanged capability polling still has a matching visible DONE for every CALL',()=>{
69
+ test('R176 same snapshot suppresses repeated capability messages',()=>{
70
70
  const tracker=defaultTracker();
71
71
  const snapshot='a'.repeat(64);
72
72
  for(let n=0;n<2;n++){
73
73
  const id='cap-'+n;
74
- assert.match(tracker.observe({event_type:'tool.request',request_id:id,tool:'capability_manifest',args:{}},{sequence:n*2+1}),/Received tool call/);
74
+ assert.equal(tracker.observe({event_type:'tool.request',request_id:id,tool:'capability_manifest',args:{}},{sequence:n*2+1}),null);
75
75
  const done=tracker.observe({event_type:'tool.result',request_id:id,tool:'capability_manifest',outcome:{status:'ok',snapshot_id:snapshot,state_epoch:1,duration_ms:2}},{sequence:n*2+2});
76
- assert.match(done,/completed/);
76
+ if(n===0) assert.match(done,/PASS/);
77
+ else assert.equal(done,null);
77
78
  }
78
- assert.match(tracker.summary(),/2 calls \\| 2 ok/);
79
+ assert.match(tracker.summary(),/2 calls \| 2 ok/);
79
80
  });
80
81
 
81
82
  test('R155 runtime-only stream reset does not claim audit corruption, audit reset remains severe',()=>{
@@ -59,7 +59,7 @@ test('R160 startup decision manifest rejects a newly required recovery module om
59
59
  test('R160 remote startup verifies decision bytes before evidence recovery and foreground claim',async()=>{
60
60
  const source=await readFile(new URL('../src/deadbyte-cli.mjs',import.meta.url),'utf8');
61
61
  const verifyAt=source.indexOf('await verifyPackageDecisionManifest({root:CURRENT_PACKAGE_ROOT,expectedVersion:DEADBYTE_VERSION})');
62
- const recoverAt=source.indexOf('const migrationContext=await recoverRemoteSession(ctx,{displayMode})');
62
+ const recoverAt=source.indexOf('await recoverRemoteSession(ctx,{displayMode})');
63
63
  const claimAt=source.indexOf('marker=await claimRemoteSession(ctx)');
64
64
  assert.ok(verifyAt>0&&verifyAt<recoverAt&&recoverAt<claimAt,
65
65
  'package decision manifest must be verified before recovery and claim');
@@ -8,12 +8,11 @@ const read=async rel=>JSON.parse(await readFile(path.join(root,rel),'utf8'));
8
8
 
9
9
  test('R160 historical contract remains distinct from the current package identity',async()=>{
10
10
  const [release,provenance]=await Promise.all([read('RELEASE.json'),read('PROVENANCE.json')]);
11
- assert.equal(release.runtime_predecessor.version,'0.15.61');
12
- assert.equal(release.runtime_predecessor.release_id,'v0.15.61-055d1ff0aa99e60b');
13
- assert.equal(release.runtime_predecessor.manifest_sha256,'055d1ff0aa99e60bbf9e9f136b02ebd4fffb8391712560f7607cff30af94dcdf');
14
- assert.equal(release.runtime_predecessor.generation_manifest_sha256,'f940e794daac2dda097a66301c4b85c4b29a2df7c669e549503c05a2a55ff6c4');
15
- assert.equal(release.r160.source_predecessor_train,'R159');
11
+ assert.equal(release.runtime_predecessor.version,'0.15.68');
12
+ assert.equal(release.runtime_predecessor.release_id,'v0.15.68-ff29dbb2fbb89862');
13
+ assert.equal(release.runtime_predecessor.manifest_sha256,'ff29dbb2fbb898625592afb47835a4a62ef289429f1e0a89a83242398cd98885');
16
14
  assert.equal(release.r160.runtime_predecessor_version,'0.15.61');
15
+ assert.equal(release.r160.source_predecessor_train,'R159');
17
16
  assert.equal(release.r160.public_tool_delta,0);
18
17
  assert.equal(release.r160.authority_scope_delta,0);
19
18
  assert.equal(release.r160.production_activation,'NOT_RUN');
@@ -8,9 +8,9 @@ import {fileURLToPath} from 'node:url';
8
8
  import {atomicWriteActivePointer,materializeReleaseSlot,parseActivePointer,parseManifest,renderStableLauncherPs1,verifyManifestTreeExact} from '../src/release-slot.mjs';
9
9
 
10
10
  const candidate=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
11
- const version='0.15.68';
12
- const parent=path.join(os.homedir(),'.deadbyte-mcp','releases','v0.15.61-055d1ff0aa99e60b');
13
- const parentDigest='055d1ff0aa99e60bbf9e9f136b02ebd4fffb8391712560f7607cff30af94dcdf';
11
+ const version='0.15.69';
12
+ const parent=path.join(os.homedir(),'.deadbyte-mcp','releases','v0.15.68-ff29dbb2fbb89862');
13
+ const parentDigest='ff29dbb2fbb898625592afb47835a4a62ef289429f1e0a89a83242398cd98885';
14
14
  const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
15
15
  async function generation(state,label){
16
16
  const root=path.join(state,'generations',label);
@@ -21,7 +21,7 @@ async function generation(state,label){
21
21
  await writeFile(path.join(root,'GENERATION.SHA256'),manifest,{flag:'wx'});
22
22
  return {root,manifest_sha256:sha(manifest),config_path:path.join(root,'controller-config.psd1')};
23
23
  }
24
- test('R173 isolated immutable slot activates, rolls back, and restores 0.15.68 without authority or production mutation',async()=>{
24
+ test('R179 isolated immutable slot activates, rolls back, and restores 0.15.69 without authority or production mutation',async()=>{
25
25
  const stateBase=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r163-isolated-'));
26
26
  const state=path.join(stateBase,'state');
27
27
  try{
@@ -31,7 +31,7 @@ test('R173 isolated immutable slot activates, rolls back, and restores 0.15.68 w
31
31
  const nextManifest=await readFile(path.join(candidate,'MANIFEST.SHA256'));
32
32
  assert.equal(sha(parentManifest),parentDigest);
33
33
  const nextDigest=sha(nextManifest);
34
- const parentSlot=await materializeReleaseSlot({source:parent,stateRoot:state,version:'0.15.61',manifestSha256:parentDigest});
34
+ const parentSlot=await materializeReleaseSlot({source:parent,stateRoot:state,version:'0.15.68',manifestSha256:parentDigest});
35
35
  const nextSlot=await materializeReleaseSlot({source:candidate,stateRoot:state,version,manifestSha256:nextDigest});
36
36
  const pGen=await generation(state,'r165-parent');
37
37
  const nGen=await generation(state,'r165-next');
@@ -43,7 +43,7 @@ test('R173 isolated immutable slot activates, rolls back, and restores 0.15.68 w
43
43
  const activated=await atomicWriteActivePointer(pointerPath,ptr(nextSlot,version,nGen));
44
44
  assert.equal(activated.release_id,nextSlot.release_id);
45
45
  await verifyManifestTreeExact(nextSlot.release_root,parseManifest(nextManifest.toString('utf8')));
46
- const rolled=await atomicWriteActivePointer(pointerPath,ptr(parentSlot,'0.15.61',pGen));
46
+ const rolled=await atomicWriteActivePointer(pointerPath,ptr(parentSlot,'0.15.68',pGen));
47
47
  assert.equal(rolled.release_id,parentSlot.release_id);
48
48
  assert.equal(rolled.manifest_sha256,parentDigest);
49
49
  await verifyManifestTreeExact(parentSlot.release_root,parseManifest(parentManifest.toString('utf8')));
@@ -17,7 +17,7 @@ const hashMarker=c=>c.repeat(64);
17
17
  async function fixture(t,kind){
18
18
  assert.equal(typeof envRoot,'string'); assert.ok(path.isAbsolute(envRoot));
19
19
  assert.match(sha(await readFile(path.join(envRoot,'MANIFEST.SHA256'))),/^[0-9a-f]{64}$/);
20
- assert.equal(JSON.parse(await readFile(path.join(envRoot,'package.json'),'utf8')).version,'0.15.68');
20
+ assert.equal(JSON.parse(await readFile(path.join(envRoot,'package.json'),'utf8')).version,'0.15.69');
21
21
  const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r164-skew-e2e-'));
22
22
  t.after(async()=>{await rm(dir,{recursive:true,force:true,maxRetries:10,retryDelay:150});});
23
23
  const state=path.join(dir,'state'),bin=path.join(state,'bin');
@@ -65,26 +65,26 @@ async function run(fx){
65
65
  assert.doesNotMatch(trace,/\b(?:start|restart|armprocess|armcoding|armautonomous|deploy)\b/i,'unexpected runtime operation');
66
66
  return {output:(result.stderr||'')+'\n'+(result.stdout||''),trace};
67
67
  }
68
- gate('R168 executable CLI blocks npm-cache activation on version skew before touching R159 history',async t=>{
68
+ gate('R175 executable CLI blocks foreground observation on version skew before touching history',async t=>{
69
69
  const fx=await fixture(t,'completed');
70
70
  const r=await run(fx);
71
- assert.match(r.output,/R168 version skew.*0\.15\.61.*0\.15\.68/i,r.output);
71
+ assert.match(r.output,/R175_EXACT_ACTIVE_RELEASE_REQUIRED/i,r.output);
72
72
  assert.doesNotMatch(r.output,/R159 ambiguous multiple completed/i);
73
73
  assert.equal(r.trace,'');
74
74
  assert.doesNotMatch(r.trace,/disarmdesktop/);
75
75
  });
76
76
  gate('R165 executable CLI rejects pending current reconciliation before upgrade',async t=>{
77
77
  const fx=await fixture(t,'pending'),r=await run(fx);
78
- assert.match(r.output,/R168 version skew/i,r.output);
78
+ assert.match(r.output,/R175_EXACT_ACTIVE_RELEASE_REQUIRED/i,r.output);
79
79
  assert.doesNotMatch(r.trace,/disarmhost/);
80
80
  });
81
81
  gate('R165 executable CLI rejects tampered terminal before upgrade',async t=>{
82
82
  const fx=await fixture(t,'tampered'),r=await run(fx);
83
- assert.match(r.output,/R168 version skew/i,r.output);
83
+ assert.match(r.output,/R175_EXACT_ACTIVE_RELEASE_REQUIRED/i,r.output);
84
84
  assert.doesNotMatch(r.trace,/disarmhost/);
85
85
  });
86
86
  gate('R165 executable CLI keeps ambiguous history fail-closed when predecessor is not ONLINE',async t=>{
87
87
  const fx=await fixture(t,'offline'),r=await run(fx);
88
- assert.match(r.output,/R168 version skew/i,r.output);
88
+ assert.match(r.output,/R175_EXACT_ACTIVE_RELEASE_REQUIRED/i,r.output);
89
89
  assert.doesNotMatch(r.trace,/disarmhost/);
90
90
  });