deadbyte-mcp 0.15.45 → 0.15.46

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 (35) hide show
  1. package/MANIFEST.SHA256 +34 -24
  2. package/PROVENANCE.json +260 -257
  3. package/README.txt +22 -21
  4. package/RELEASE.json +367 -346
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +501 -503
  7. package/bridge/package.json +1 -1
  8. package/controller/deadbyte-controller.ps1 +20 -6
  9. package/controller/deadbyte-process-policy.json +4 -4
  10. package/package.json +2 -2
  11. package/scripts/gate-windows-r72.ps1 +76 -0
  12. package/scripts/npm-release-r48.mjs +2 -0
  13. package/scripts/npm-release-readiness-r72.mjs +76 -0
  14. package/src/deadbyte-cli.mjs +722 -721
  15. package/src/launcher-isolation.mjs +58 -0
  16. package/src/remote-authority.mjs +75 -75
  17. package/src/remote-mcp-machine-smoke-client.mjs +84 -84
  18. package/src/runtime-update.mjs +2056 -2008
  19. package/src/staging-input-isolation.mjs +79 -0
  20. package/src/version.mjs +1 -1
  21. package/test/deferred-slot-operation-behavior.test.mjs +2 -2
  22. package/test/r58-cleanup-retention.test.mjs +3 -3
  23. package/test/r58-concurrent-recovery.test.mjs +3 -3
  24. package/test/r58-stale-owner.test.mjs +1 -1
  25. package/test/r59-tool-surface-compat.test.mjs +2 -2
  26. package/test/r60-authority-coherence.test.mjs +10 -2
  27. package/test/r71-isolated-slot-rollback-restore.test.mjs +11 -11
  28. package/test/r71-release-identity.test.mjs +12 -10
  29. package/test/r72-launcher-isolation.test.mjs +99 -0
  30. package/test/r72-publication-fence.test.mjs +56 -0
  31. package/test/r72-publication-output-integrity.test.mjs +110 -0
  32. package/test/r72-remote-safe-contract.test.mjs +101 -0
  33. package/test/r72-staging-input-isolation.test.mjs +87 -0
  34. package/test/r72-staging-workspace-isolation.test.mjs +114 -0
  35. package/test/remote-session-cli.test.mjs +110 -64
@@ -0,0 +1,79 @@
1
+ import {lstatSync} from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ const canonical = value => process.platform === 'win32' ? value.toLowerCase() : value;
6
+ function within(parent,child){
7
+ const relative=path.relative(canonical(parent),canonical(child));
8
+ return relative===''||(!path.isAbsolute(relative)&&relative!=='..'&&!relative.startsWith('..'+path.sep));
9
+ }
10
+ function productionRoot(){return path.resolve(os.homedir(),'.deadbyte-mcp');}
11
+ function isStage(stateRoot){
12
+ const state=path.resolve(stateRoot),production=productionRoot();
13
+ if(state===production||canonical(state)===canonical(production))return false;
14
+ if(within(production,state)||within(state,production))throw new Error('STAGING_STATE_OVERLAPS_PRODUCTION');
15
+ return true;
16
+ }
17
+ function noReparseComponents(file,label){
18
+ let current=file;
19
+ for(;;){
20
+ try {if(lstatSync(current).isSymbolicLink())throw new Error('STAGING_REPARSE_INPUT: '+label);}
21
+ catch(error){if(error?.code!=='ENOENT')throw error;}
22
+ const parent=path.dirname(current);
23
+ if(parent===current)break;
24
+ current=parent;
25
+ }
26
+ }
27
+ /** A stage never reads policy, token, executable or config bytes from a production state. */
28
+ export function assertStageOwnedPath(file,stateRoot,label){
29
+ if(!isStage(stateRoot))return;
30
+ if(typeof file!=='string'||!path.isAbsolute(file)||file.includes('\0'))throw new Error('STAGING_ABSOLUTE_PATH_REQUIRED: '+label);
31
+ const state=path.resolve(stateRoot),target=path.resolve(file);
32
+ if(!within(state,target)||canonical(state)===canonical(target))throw new Error('STAGING_EXTERNAL_INPUT: '+label);
33
+ noReparseComponents(target,label);
34
+ }
35
+ export function assertStageNoProductionPath(file,stateRoot,label){
36
+ if(!isStage(stateRoot))return;
37
+ if(typeof file!=='string'||!path.isAbsolute(file)||file.includes('\0'))throw new Error('STAGING_ABSOLUTE_PATH_REQUIRED: '+label);
38
+ const target=path.resolve(file),production=productionRoot();
39
+ if(within(production,target))throw new Error('STAGING_PRODUCTION_INPUT: '+label);
40
+ noReparseComponents(target,label);
41
+ }
42
+ export function assertStagingBootstrapInputs({stateRoot,sourceRoot=null,bridgeBootstrap=null,cloudTransport=null,
43
+ processPolicyBootstrap=null,hostPolicyBootstrap=null,codingPolicyBootstrap=null,autonomousPolicyBootstrap=null,
44
+ ownerApprovalBootstrap=null,desktopPolicyBootstrap=null,semanticLspBootstrap=null}={}){
45
+ if(!isStage(stateRoot))return;
46
+ noReparseComponents(path.resolve(stateRoot),'state root');
47
+ if(sourceRoot!==null)assertStageOwnedPath(sourceRoot,stateRoot,'legacy source root');
48
+ if(bridgeBootstrap)assertStageOwnedPath(bridgeBootstrap.root,stateRoot,'bridge bootstrap root');
49
+ for(const [label,item] of Object.entries({processPolicyBootstrap,hostPolicyBootstrap,codingPolicyBootstrap,
50
+ autonomousPolicyBootstrap,ownerApprovalBootstrap,desktopPolicyBootstrap,semanticLspBootstrap})){
51
+ if(item)assertStageOwnedPath(item.path,stateRoot,label);
52
+ }
53
+ if(cloudTransport){
54
+ assertStageOwnedPath(cloudTransport.tokenFile?.path,stateRoot,'named tunnel token file');
55
+ assertStageOwnedPath(cloudTransport.childWorkingDirectory,stateRoot,'named tunnel child directory');
56
+ assertStageNoProductionPath(cloudTransport.cloudflared?.path,stateRoot,'named tunnel executable');
57
+ }
58
+ }
59
+ export function assertStagingControlConfig(config,stateRoot,{configPath}={}){
60
+ if(!isStage(stateRoot))return;
61
+ assertStageOwnedPath(configPath,stateRoot,'controller config file');
62
+ const owned=['CoreRoot','WorkspaceRoot','CodingPolicyFile','AutonomousPolicyFile','HostPolicyFile',
63
+ 'ProcessPolicyFile','DesktopPolicyFile','SemanticLspConfigFile','McpPathFile','McpUrlFile'];
64
+ for(const key of owned)if(config[key]!==undefined&&config[key]!==null&&config[key]!=='')
65
+ assertStageOwnedPath(config[key],stateRoot,key);
66
+ if(!config.CodingPolicyFile||!config.AutonomousPolicyFile)throw new Error('STAGING_REQUIRED_POLICIES_MISSING');
67
+ for(const [name,component] of [['Bridge',config.Bridge],['Cloud',config.Cloud]]){
68
+ if(!component||typeof component!=='object')throw new Error('STAGING_COMPONENT_MISSING: '+name);
69
+ // A hash-pinned executable (e.g. system Node) may be shared. Its script,
70
+ // cwd, policy inputs and secrets may not be shared with production.
71
+ if(component.FilePath)assertStageNoProductionPath(component.FilePath,stateRoot,name+'.FilePath');
72
+ for(const key of ['WorkingDirectory','ScriptPath'])if(component[key])
73
+ assertStageOwnedPath(component[key],stateRoot,name+'.'+key);
74
+ if(Array.isArray(component.Dependencies))for(const dep of component.Dependencies){
75
+ if(dep?.Name==='cloudflared'&&name==='Cloud')assertStageNoProductionPath(dep.Path,stateRoot,'Cloud.cloudflared');
76
+ else assertStageOwnedPath(dep?.Path,stateRoot,name+'.dependency.'+String(dep?.Name??''));
77
+ }
78
+ }
79
+ }
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.45';
1
+ export const DEADBYTE_VERSION = '0.15.46';
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';
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
 
10
10
  const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
11
11
  test('R33 deferred worker survives scheduler exit and stays bound to explicit state root',
12
- { skip: process.platform !== 'win32', timeout:15000 }, async()=>{
12
+ { skip: process.platform !== 'win32', timeout:30000 }, async()=>{
13
13
  const state=await mkdtemp(path.join(os.tmpdir(),'deadbyte-deferred-'));
14
14
  try {
15
15
  const child=spawn(process.execPath,['scripts/deferred-slot-operation.mjs','activate','0'.repeat(64)],{
@@ -22,7 +22,7 @@ test('R33 deferred worker survives scheduler exit and stays bound to explicit st
22
22
  assert.equal(scheduled.status,'scheduled');
23
23
  assert.ok(path.resolve(scheduled.state_path).startsWith(path.resolve(state)+path.sep));
24
24
  let record;
25
- for(let i=0;i<30;i++){
25
+ for(let i=0;i<60;i++){
26
26
  await sleep(300);
27
27
  record=JSON.parse(await readFile(scheduled.state_path,'utf8'));
28
28
  if(record.state==='failed'||record.state==='passed') break;
@@ -6,7 +6,7 @@ import path from 'node:path';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { setTimeout as delay } from 'node:timers/promises';
8
8
 
9
- test('R58 foreground cleanup failure retains ownership evidence for recovery', {timeout:30000}, async () => {
9
+ test('R58 foreground cleanup failure retains ownership evidence for recovery', {timeout:60000}, async () => {
10
10
  const base='D:/Projects/deadbyte-mcp/testing/0.15.35/fixtures';
11
11
  await mkdir(base,{recursive:true});
12
12
  const root=await mkdtemp(path.join(base,'cleanup-'));
@@ -29,12 +29,12 @@ switch ($Command) {
29
29
  }
30
30
  exit 0
31
31
  `);
32
- const child=spawn(process.execPath,[fileURLToPath(new URL('../src/deadbyte-cli.mjs',import.meta.url)),'remote'],{env:{...process.env,DEADBYTE_STATE_ROOT:root},windowsHide:true,stdio:['pipe','pipe','pipe']});
32
+ const child=spawn(process.execPath,[fileURLToPath(new URL('../src/deadbyte-cli.mjs',import.meta.url)),'remote','--operator'],{env:{...process.env,DEADBYTE_STATE_ROOT:root},windowsHide:true,stdio:['pipe','pipe','pipe']});
33
33
  let stdout='',stderr='';
34
34
  child.stdout.on('data',b=>{stdout+=b;});child.stderr.on('data',b=>{stderr+=b;});
35
35
  const closed=new Promise(resolve=>child.once('close',code=>resolve(code)));
36
36
  try {
37
- const deadline=Date.now()+12000;
37
+ const deadline=Date.now()+50000;
38
38
  while(!stdout.includes('[READY]')&&child.exitCode===null&&Date.now()<deadline) await delay(50);
39
39
  assert.match(stdout,/\[READY\]/,stderr);
40
40
  assert.match(stdout,/\[AUTH\] process ARMED \| coding ARMED \| autonomous ARMED \| desktop DISARMED \| host DISARMED/,stderr);
@@ -6,7 +6,7 @@ import {fileURLToPath} from 'node:url';
6
6
  import {spawn} from 'node:child_process';
7
7
  import {setTimeout as delay} from 'node:timers/promises';
8
8
 
9
- test('R58 simultaneous stale recovery cannot both revoke the shared authority', {timeout:25000},async()=>{
9
+ test('R58 simultaneous stale recovery cannot both revoke the shared authority', {timeout:90000},async()=>{
10
10
  const base='D:/Projects/deadbyte-mcp/testing/0.15.35/fixtures';await mkdir(base,{recursive:true});
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');
@@ -47,14 +47,14 @@ exit 0
47
47
  const c=spawn(process.execPath,[fileURLToPath(new URL('../src/deadbyte-cli.mjs',import.meta.url)),'remote'],{env:{...process.env,DEADBYTE_STATE_ROOT:root},stdio:['pipe','pipe','pipe'],windowsHide:true});
48
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);
49
49
  }
50
- const until=Date.now()+12000;let commands=[];
50
+ const until=Date.now()+45000;let commands=[];
51
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);}
52
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'});
53
53
  assert.ok(outputs.some(o=>o.stdout.includes('[READY]')),'a recovered foreground must become ready');
54
54
  for(const cmd of ['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'])assert.equal(commands.filter(x=>x===cmd).length,1,`stale recovery must serialize ${cmd}`);
55
55
  }finally{
56
56
  for(const c of children)if(c.exitCode===null)c.stdin.end();
57
- await Promise.race([Promise.all(outputs.map(o=>o.exit)),delay(7000)]);
57
+ await Promise.race([Promise.all(outputs.map(o=>o.exit)),delay(30000)]);
58
58
  for(const c of children)if(c.exitCode===null&&c.signalCode===null)c.kill();
59
59
  }
60
60
  });
@@ -46,7 +46,7 @@ test('R58 live unrelated PID is not accepted as a remote foreground owner', {tim
46
46
  assert.notEqual(code,0);
47
47
  });
48
48
 
49
- test('R58 bound stale birth identity reconciles on the first invocation without killing a reused PID', {timeout:20000}, async()=>{
49
+ test('R58 bound stale birth identity reconciles on the first invocation without killing a reused PID', {timeout:60000}, async()=>{
50
50
  const base='D:/Projects/deadbyte-mcp/testing/0.15.35/fixtures';
51
51
  await mkdir(base,{recursive:true});const root=await mkdtemp(path.join(base,'stale-birth-'));
52
52
  await mkdir(path.join(root,'runtime'));await mkdir(path.join(root,'bin'));
@@ -71,7 +71,7 @@ async function captureCurrentSurface(){
71
71
  }
72
72
  }
73
73
 
74
- test('R71 compact baseline permits only R64 read offset and authorized coding-only preview output',async()=>{
74
+ test('R72 compact baseline permits only R64 read offset and authorized coding-only preview output',async()=>{
75
75
  const raw=JSON.parse(await readFile(fixturePath,'utf8'));
76
76
  assert.equal(raw.schema,'deadbyte.tool-surface-baseline.v1');
77
77
  assert.equal(raw.release_id,'v0.15.35-1b5715b276facbc0');
@@ -83,7 +83,7 @@ test('R71 compact baseline permits only R64 read offset and authorized coding-on
83
83
  assert.deepEqual(diff.added,[]);
84
84
  assert.deepEqual(diff.removed,[]);
85
85
  const packageVersion=JSON.parse(await readFile(path.join(repoRoot,'package.json'),'utf8')).version;
86
- if(['0.15.41','0.15.42','0.15.43','0.15.44','0.15.45'].includes(packageVersion)){
86
+ if(['0.15.41','0.15.42','0.15.43','0.15.44','0.15.45','0.15.46'].includes(packageVersion)){
87
87
  assert.deepEqual([...diff.schema_changed].sort(),['machine_fs','native_diff_preview']);
88
88
  assert.equal(diff.unchanged.length,41);
89
89
  const prior=baseline.tools.find(tool=>tool.name==='machine_fs');
@@ -10,7 +10,8 @@ const observed = (armed={}) => ({
10
10
  });
11
11
 
12
12
  test('remote modes have exact non-widening authority vectors',()=>{
13
- assert.deepEqual(authorityVector('default'),{process:true,coding:true,autonomous:true,desktop:false,host:false});
13
+ assert.deepEqual(authorityVector('default'),{process:false,coding:false,autonomous:false,desktop:false,host:false});
14
+ assert.deepEqual(authorityVector('operator'),{process:true,coding:true,autonomous:true,desktop:false,host:false});
14
15
  assert.deepEqual(authorityVector('desktop'),{process:true,coding:true,autonomous:true,desktop:true,host:false});
15
16
  assert.deepEqual(authorityVector('host'),{process:true,coding:true,autonomous:true,desktop:false,host:true});
16
17
  assert.deepEqual(authorityVector('full'),{process:true,coding:true,autonomous:true,desktop:true,host:true});
@@ -18,7 +19,7 @@ test('remote modes have exact non-widening authority vectors',()=>{
18
19
 
19
20
  test('observed standing Host grant rejects bounded READY',()=>{
20
21
  const snapshot=parseAuthorityObservation(JSON.stringify(observed({process:true,coding:true,autonomous:true,host:true})));
21
- assert.throws(()=>assertAuthorityVector(snapshot,authorityVector('default')),/host.*expected DISARMED/);
22
+ assert.throws(()=>assertAuthorityVector(snapshot,authorityVector('operator')),/host.*expected DISARMED/);
22
23
  });
23
24
 
24
25
  test('malformed or incomplete controller observation fails closed',()=>{
@@ -34,3 +35,10 @@ test('readiness requires verified Bridge Core Cloud and local MCP, not just ONLI
34
35
  assert.throws(()=>assertRuntimeReadyObservation({...value,core:{alive:false,reason:'wrong-core-generation'}}),/core/);
35
36
  assert.throws(()=>assertRuntimeReadyObservation({...value,core_root:'D:\\wrong'} ,{coreRoot:'D:\\exact-release'}),/core root/);
36
37
  });
38
+
39
+ test('safe default rejects a pre-existing grant in any plane, not only Host',()=>{
40
+ for(const plane of ['process','coding','autonomous','desktop','host']){
41
+ const snapshot=parseAuthorityObservation(JSON.stringify(observed({[plane]:true})));
42
+ assert.throws(()=>assertAuthorityVector(snapshot,authorityVector('default')),new RegExp(`${plane}.*expected DISARMED`));
43
+ }
44
+ });
@@ -8,8 +8,8 @@ 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 parent=path.join(os.homedir(),'.deadbyte-mcp','releases','v0.15.44-9f500af433df3e77');
12
- const parentManifestSha='9f500af433df3e7778d4f11acf418526d5889f478114c955c8c72c0077a0636e';
11
+ const parent=path.join(os.homedir(),'.deadbyte-mcp','releases','v0.15.45-34aa8d1241cfb8bf');
12
+ const parentManifestSha='34aa8d1241cfb8bf0b5ba303bf3c06e0e9e1bf01f64457bca659d6dd4d634fdc';
13
13
  const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
14
14
 
15
15
  async function generation(state,label){
@@ -22,34 +22,34 @@ async function generation(state,label){
22
22
  return {root,manifest_sha256:sha(manifest),config_path:path.join(root,'controller-config.psd1')};
23
23
  }
24
24
 
25
- test('R71 isolated immutable slot activates, rolls back exact sealed R70, and restores same R71 bytes with no authority',async()=>{
26
- const base=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r71-slot-parity-'));
25
+ test('R72 isolated immutable slot activates, rolls back exact active R71, and restores same R72 bytes with no authority',async()=>{
26
+ const base=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r72-slot-parity-'));
27
27
  const state=path.join(base,'state');
28
28
  try{
29
29
  const parentManifest=await readFile(path.join(parent,'MANIFEST.SHA256'));
30
30
  const candidateManifest=await readFile(path.join(candidate,'MANIFEST.SHA256'));
31
31
  assert.equal(sha(parentManifest),parentManifestSha);
32
32
  const candidateManifestSha=sha(candidateManifest);
33
- const parentSlot=await materializeReleaseSlot({source:parent,stateRoot:state,version:'0.15.44',manifestSha256:parentManifestSha});
34
- const candidateSlot=await materializeReleaseSlot({source:candidate,stateRoot:state,version:'0.15.45',manifestSha256:candidateManifestSha});
35
- const parentGeneration=await generation(state,'r70-parent');
36
- const candidateGeneration=await generation(state,'r71-candidate');
33
+ const parentSlot=await materializeReleaseSlot({source:parent,stateRoot:state,version:'0.15.45',manifestSha256:parentManifestSha});
34
+ const candidateSlot=await materializeReleaseSlot({source:candidate,stateRoot:state,version:'0.15.46',manifestSha256:candidateManifestSha});
35
+ const parentGeneration=await generation(state,'r71-parent');
36
+ const candidateGeneration=await generation(state,'r72-candidate');
37
37
  const launcherSha=sha(Buffer.from(renderStableLauncherPs1(),'utf8'));
38
38
  const pointerPath=path.join(state,'active-release.json');
39
39
  const pointer=(slot,version,g)=>({schema:'deadbyte.active-release.v1',release_id:slot.release_id,version,
40
40
  manifest_sha256:slot.manifest_sha256,release_root:slot.release_root,generation_root:g.root,
41
41
  generation_manifest_sha256:g.manifest_sha256,launcher_sha256:launcherSha,config_path:g.config_path});
42
42
 
43
- const activated=await atomicWriteActivePointer(pointerPath,pointer(candidateSlot,'0.15.45',candidateGeneration));
43
+ const activated=await atomicWriteActivePointer(pointerPath,pointer(candidateSlot,'0.15.46',candidateGeneration));
44
44
  assert.equal(activated.release_id,candidateSlot.release_id);
45
45
  await verifyManifestTreeExact(candidateSlot.release_root,parseManifest(candidateManifest.toString('utf8')));
46
46
 
47
- const rolledBack=await atomicWriteActivePointer(pointerPath,pointer(parentSlot,'0.15.44',parentGeneration));
47
+ const rolledBack=await atomicWriteActivePointer(pointerPath,pointer(parentSlot,'0.15.45',parentGeneration));
48
48
  assert.equal(rolledBack.release_id,parentSlot.release_id);
49
49
  assert.equal(rolledBack.manifest_sha256,parentManifestSha);
50
50
  await verifyManifestTreeExact(parentSlot.release_root,parseManifest(parentManifest.toString('utf8')));
51
51
 
52
- const restored=await atomicWriteActivePointer(pointerPath,pointer(candidateSlot,'0.15.45',candidateGeneration));
52
+ const restored=await atomicWriteActivePointer(pointerPath,pointer(candidateSlot,'0.15.46',candidateGeneration));
53
53
  assert.deepEqual(restored,activated);
54
54
  assert.deepEqual(parseActivePointer(await readFile(pointerPath,'utf8')),activated);
55
55
  await verifyManifestTreeExact(candidateSlot.release_root,parseManifest(candidateManifest.toString('utf8')));
@@ -3,23 +3,23 @@ import assert from 'node:assert/strict';
3
3
  import {readFile} from 'node:fs/promises';
4
4
 
5
5
  const json=async rel=>JSON.parse(await readFile(new URL('../'+rel,import.meta.url),'utf8'));
6
- const parent={release_id:'v0.15.44-9f500af433df3e77',
7
- manifest_sha256:'9f500af433df3e7778d4f11acf418526d5889f478114c955c8c72c0077a0636e',
8
- generation_manifest_sha256:'c35f6ccca58083acd53484a983ef401ee18f28b11185cd02c7b1eb3e29028c71'};
6
+ const parent={version:'0.15.45',release_id:'v0.15.45-34aa8d1241cfb8bf',
7
+ manifest_sha256:'34aa8d1241cfb8bf0b5ba303bf3c06e0e9e1bf01f64457bca659d6dd4d634fdc',
8
+ generation_manifest_sha256:'ff2b73410062688651abd7bc2dd7b2b8c87810a28e3808bfe1f4352a25b0e6e4'};
9
9
 
10
- test('R71 current source, package, bridge, provenance and Windows gate bind exact predecessor',async()=>{
10
+ test('R72 current source, package, bridge, provenance and Windows gate bind exact predecessor',async()=>{
11
11
  const [pkg,lock,bridge,release,provenance,matrix,sbom,version]=await Promise.all([
12
12
  json('package.json'),json('package-lock.json'),json('bridge/package.json'),json('RELEASE.json'),
13
13
  json('PROVENANCE.json'),json('TEST-MATRIX.json'),json('SBOM.cdx.json'),
14
14
  readFile(new URL('../src/version.mjs',import.meta.url),'utf8')]);
15
15
  for(const observed of [pkg.version,lock.version,lock.packages[''].version,bridge.version,
16
16
  release.version,provenance.version,matrix.version,sbom.metadata.component.version]){
17
- assert.equal(observed,'0.15.45');
17
+ assert.equal(observed,'0.15.46');
18
18
  }
19
- assert.match(version,/DEADBYTE_VERSION = '0\.15\.45'/);
20
- assert.equal(release.train,'R71');
21
- assert.equal(matrix.train,'R71');
22
- assert.equal(sbom.serialNumber,'urn:cdx:deadbyte-mcp:r71:0.15.45');
19
+ assert.match(version,/DEADBYTE_VERSION = '0\.15\.46'/);
20
+ assert.equal(release.train,'R72');
21
+ assert.equal(matrix.train,'R72');
22
+ assert.equal(sbom.serialNumber,'urn:cdx:deadbyte-mcp:r72:0.15.46');
23
23
  assert.deepEqual(release.runtime_predecessor,parent);
24
24
  assert.equal(release.parent.release_id,parent.release_id);
25
25
  assert.equal(release.parent.manifest_sha256,parent.manifest_sha256);
@@ -27,5 +27,7 @@ test('R71 current source, package, bridge, provenance and Windows gate bind exac
27
27
  assert.equal(provenance.parent_release_id,parent.release_id);
28
28
  assert.equal(provenance.parent_manifest_sha256,parent.manifest_sha256);
29
29
  assert.equal(provenance.parent_generation_manifest_sha256,parent.generation_manifest_sha256);
30
- assert.match(pkg.scripts['gate:windows'],/gate-windows-r71\.ps1/);
30
+ assert.match(pkg.scripts['gate:windows'],/gate-windows-r72\.ps1/);
31
+ assert.equal(provenance.source_predecessor_version,'0.15.45');
32
+ assert.ok(['CANDIDATE_UNSEALED','CANDIDATE_SEALED'].includes(release.r72_hotfix.status), 'R72 release status must be explicit');
31
33
  });
@@ -0,0 +1,99 @@
1
+ import {test} from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {mkdtemp, mkdir, symlink, rm} from 'node:fs/promises';
4
+ import {tmpdir, homedir} from 'node:os';
5
+ import path from 'node:path';
6
+ import {createHash} from 'node:crypto';
7
+ import {
8
+ canonicalLauncherPaths, assertCanonicalLaunchers, renderStateBoundLauncherCmd
9
+ } from '../src/launcher-isolation.mjs';
10
+ import {rollbackLauncherPlanFromHandoff} from '../src/runtime-update.mjs';
11
+
12
+ const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
13
+ const ptr=(version,manifest,launcherSha)=>({
14
+ schema:'deadbyte.active-release.v1',version,release_id:`v${version}-${manifest.slice(0,16)}`,
15
+ manifest_sha256:manifest,generation_manifest_sha256:'c'.repeat(64),launcher_sha256:launcherSha,
16
+ release_root:'/fixture/releases/'+version,generation_root:'/fixture/generations/'+version,config_path:'/fixture/config/'+version
17
+ });
18
+ function handoff(state,prior,active,top){
19
+ const newBytes=Buffer.from('synthetic new PowerShell launcher');
20
+ const priorBytes=Buffer.from('synthetic old PowerShell launcher');
21
+ assert.equal(prior.launcher_sha256,hash(priorBytes));
22
+ assert.equal(active.launcher_sha256,hash(newBytes));
23
+ const p=canonicalLauncherPaths(state);
24
+ return {
25
+ prior_pointer:prior,new_pointer:active,
26
+ launchers:{ps1:p.ps1,cmd:p.cmd,top:top??p.top,
27
+ launcher_sha256:active.launcher_sha256,
28
+ ps1_bytes:newBytes.toString('base64'),cmd_bytes:Buffer.from('cmd').toString('base64'),
29
+ snapshots:{
30
+ ps1:{exists:true,sha256:hash(priorBytes),bytes_base64:priorBytes.toString('base64')},
31
+ cmd:{exists:false,sha256:null,bytes_base64:null},top:{exists:false,sha256:null,bytes_base64:null}
32
+ }
33
+ }
34
+ };
35
+ }
36
+
37
+ test('production compatibility retains historical Downloads launcher',()=>{
38
+ const prod=path.join(homedir(),'.deadbyte-mcp');
39
+ const plan=canonicalLauncherPaths(prod);
40
+ assert.equal(plan.production,true);
41
+ assert.equal(plan.top,path.join(homedir(),'Downloads','DEADBYTE-MCP.cmd'));
42
+ assert.equal(plan.ps1,path.join(prod,'bin','DEADBYTE-MCP.ps1'));
43
+ });
44
+
45
+ test('staging launcher routes into its own state, not Downloads',async()=>{
46
+ const base=await mkdtemp(path.join(tmpdir(),'r72-launcher-stage-'));
47
+ try{
48
+ const stage=path.join(base,'state');
49
+ const plan=canonicalLauncherPaths(stage);
50
+ assert.equal(plan.production,false);
51
+ assert.equal(plan.top,path.join(stage,'launchers','DEADBYTE-MCP.cmd'));
52
+ assert.equal(plan.cmd,path.join(stage,'bin','DEADBYTE-MCP.cmd'));
53
+ assert.doesNotMatch(plan.top,/Downloads[\\/]DEADBYTE-MCP\.cmd/);
54
+ const text=renderStateBoundLauncherCmd();
55
+ assert.match(text,/set "DEADBYTE_STATE_ROOT=%~dp0\.\."/);
56
+ assert.match(text,/%DEADBYTE_STATE_ROOT%\\bin\\DEADBYTE-MCP\.ps1/);
57
+ assert.doesNotMatch(text,/USERPROFILE/);
58
+ assert.doesNotMatch(text,/\\\.deadbyte-mcp/);
59
+ }finally{await rm(base,{recursive:true,force:true});}
60
+ });
61
+
62
+ test('staging cannot reuse production top launcher even through signed handoff',async()=>{
63
+ const base=await mkdtemp(path.join(tmpdir(),'r72-launcher-signed-'));
64
+ try{
65
+ const stage=path.join(base,'state');
66
+ const prior=ptr('0.15.45','a'.repeat(64),hash(Buffer.from('synthetic old PowerShell launcher')));
67
+ const active=ptr('0.15.46','b'.repeat(64),hash(Buffer.from('synthetic new PowerShell launcher')));
68
+ const proper=handoff(stage,prior,active);
69
+ const plan=rollbackLauncherPlanFromHandoff(proper,prior,active,stage);
70
+ assert.equal(plan.top,canonicalLauncherPaths(stage).top);
71
+ assert.equal(plan.stateRoot,path.resolve(stage));
72
+ const foreign=handoff(stage,prior,active,path.join(homedir(),'Downloads','DEADBYTE-MCP.cmd'));
73
+ assert.throws(()=>rollbackLauncherPlanFromHandoff(foreign,prior,active,stage),/not canonical/);
74
+ const missing=handoff(stage,prior,active); delete missing.launchers.top;
75
+ assert.throws(()=>rollbackLauncherPlanFromHandoff(missing,prior,active,stage),/not canonical/);
76
+ const wrongBin=handoff(stage,prior,active);wrongBin.launchers.cmd=path.join(base,'escape.cmd');
77
+ assert.throws(()=>rollbackLauncherPlanFromHandoff(wrongBin,prior,active,stage),/not canonical/);
78
+ }finally{await rm(base,{recursive:true,force:true});}
79
+ });
80
+
81
+ test('staging nested in production state is forbidden',()=>{
82
+ assert.throws(()=>canonicalLauncherPaths(path.join(homedir(),'.deadbyte-mcp','staging')),/overlaps production/);
83
+ });
84
+
85
+ test('staging path through existing symlink or junction is rejected',async t=>{
86
+ const base=await mkdtemp(path.join(tmpdir(),'r72-launcher-alias-'));
87
+ try{
88
+ const real=path.join(base,'real'); await mkdir(real);
89
+ const alias=path.join(base,'alias');
90
+ try{await symlink(real,alias,process.platform==='win32'?'junction':'dir');}
91
+ catch(error){if(['EPERM','EACCES','ENOSYS'].includes(error.code)){t.diagnostic('OS disallows symlink/junction fixture');return;}throw error;}
92
+ assert.throws(()=>canonicalLauncherPaths(path.join(alias,'staging')),/reparse\/symlink/);
93
+ const root=path.join(base,'state');await mkdir(root);
94
+ const dir=path.join(root,'launchers');
95
+ try{await symlink(real,dir,process.platform==='win32'?'junction':'dir');}
96
+ catch(error){if(['EPERM','EACCES','ENOSYS'].includes(error.code)){t.diagnostic('OS disallows symlink/junction fixture');return;}throw error;}
97
+ assert.throws(()=>canonicalLauncherPaths(root),/reparse\/symlink/);
98
+ }finally{await rm(base,{recursive:true,force:true});}
99
+ });
@@ -0,0 +1,56 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {createHash} from 'node:crypto';
4
+ import {mkdtemp,mkdir,readFile,rm,writeFile} from 'node:fs/promises';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import {assertR72NpmPublishReady} from '../scripts/npm-release-readiness-r72.mjs';
8
+ const sha=x=>createHash('sha256').update(x).digest('hex');
9
+ const parent={version:'0.15.45',release_id:'v0.15.45-34aa8d1241cfb8bf',
10
+ manifest_sha256:'34aa8d1241cfb8bf0b5ba303bf3c06e0e9e1bf01f64457bca659d6dd4d634fdc',
11
+ generation_manifest_sha256:'ff2b73410062688651abd7bc2dd7b2b8c87810a28e3808bfe1f4352a25b0e6e4'};
12
+ const readyRelease=()=>({name:'deadbyte-mcp',version:'0.15.46',train:'R72',parent,
13
+ runtime_predecessor:parent,r72_hotfix:{status:'CANDIDATE_SEALED',windows_regression:'PASS',
14
+ package_acceptance:'PASS',manifest:'PASS',remote_lifecycle:'PASS',security_gate:'PASS',independent_judge:'PASS'}});
15
+ const gates=['full_regression','windows_lifecycle','package_acceptance','independent_judge'];
16
+ async function fixture(){
17
+ const root=await mkdtemp(path.join(os.tmpdir(),'r72-fence-'));
18
+ const dir=path.join(root,'testing','r72-release-evidence');await mkdir(path.join(dir,'outputs'),{recursive:true});
19
+ const manifest=Buffer.from('fixture-manifest\n');await writeFile(path.join(root,'MANIFEST.SHA256'),manifest);
20
+ const manifestSha=sha(manifest),hashes={};
21
+ for(const gate of gates){
22
+ // Synthetic bytes are fixture data only, never real release evidence.
23
+ const output=Buffer.from(`SYNTHETIC-R72-FIXTURE gate=${gate}\n`);
24
+ await writeFile(path.join(dir,'outputs',`${gate}.log`),output);
25
+ const record={schema:'deadbyte.r72-gate-result.v1',gate,version:'0.15.46',manifest_sha256:manifestSha,
26
+ status:'PASS',exit_code:0,output_file:`outputs/${gate}.log`,output_sha256:sha(output),
27
+ ...(gate==='full_regression'?{pass_count:1500,fail_count:0}:{}),
28
+ ...(gate==='windows_lifecycle'?{safe_default_arm_count:0,operator_owner_consent_enforced:true,
29
+ stale_owner_recovery_verified:true,signed_epoch_fail_closed:true}:{}),
30
+ ...(gate==='independent_judge'?{independent_verification:'PASS'}:{})};
31
+ const bytes=Buffer.from(JSON.stringify(record));await writeFile(path.join(dir,`${gate}.json`),bytes);hashes[gate]=sha(bytes);
32
+ }
33
+ await writeFile(path.join(dir,'ready.json'),JSON.stringify({schema:'deadbyte.r72-release-evidence-index.v1',
34
+ version:'0.15.46',manifest_sha256:manifestSha,parent,gate_sha256:hashes}));
35
+ return {root,dir};
36
+ }
37
+ test('R72 publish path checks successor evidence before packing',async()=>{
38
+ const source=await readFile(new URL('../scripts/npm-release-r48.mjs',import.meta.url),'utf8');
39
+ const guard=source.indexOf('await assertR72NpmPublishReady(release,{root})');
40
+ assert.ok(guard>=0&&guard<source.indexOf("'pack', '--json'")&&guard<source.indexOf("'publish',publishTarball"));
41
+ });
42
+ test('R72 pending candidate cannot publish even when a local manifest exists',async()=>{
43
+ const release=readyRelease();release.r72_hotfix.status='CANDIDATE_UNSEALED';
44
+ await assert.rejects(assertR72NpmPublishReady(release,{root:path.resolve('.')}),/PUBLISH_BLOCKED R72/);
45
+ });
46
+ test('R72 readiness is bound to sealed predecessor, bytes, Windows authority gates and independent judge',async()=>{
47
+ const f=await fixture();try{
48
+ const candidate=readyRelease();
49
+ assert.equal((await assertR72NpmPublishReady(candidate,{root:f.root})).evidence_count,4);
50
+ const drift=structuredClone(candidate);drift.parent.release_id='v0.15.44-tampered';
51
+ await assert.rejects(assertR72NpmPublishReady(drift,{root:f.root}),/PUBLISH_BLOCKED/);
52
+ const lifecycle=path.join(f.dir,'windows_lifecycle.json');
53
+ await writeFile(lifecycle,'{"status":"PASS","forge":true}');
54
+ await assert.rejects(assertR72NpmPublishReady(candidate,{root:f.root}),/PUBLISH_BLOCKED.*windows_lifecycle/);
55
+ }finally{await rm(f.root,{recursive:true,force:true});}
56
+ });
@@ -0,0 +1,110 @@
1
+ import assert from 'node:assert/strict';
2
+ import {test} from 'node:test';
3
+ import {createHash} from 'node:crypto';
4
+ import {mkdtemp,mkdir,writeFile,readFile,rm,symlink,rename} from 'node:fs/promises';
5
+ import {tmpdir} from 'node:os';
6
+ import path from 'node:path';
7
+ import {assertR72NpmPublishReady} from '../scripts/npm-release-readiness-r72.mjs';
8
+
9
+ const sha = bytes=>createHash('sha256').update(bytes).digest('hex');
10
+ const gates = ['full_regression','windows_lifecycle','package_acceptance','independent_judge'];
11
+ const parent = {
12
+ version:'0.15.45',release_id:'v0.15.45-34aa8d1241cfb8bf',
13
+ manifest_sha256:'34aa8d1241cfb8bf0b5ba303bf3c06e0e9e1bf01f64457bca659d6dd4d634fdc',
14
+ generation_manifest_sha256:'ff2b73410062688651abd7bc2dd7b2b8c87810a28e3808bfe1f4352a25b0e6e4'
15
+ };
16
+ const release={version:'0.15.46',name:'deadbyte-mcp',train:'R72',parent,runtime_predecessor:parent,r72_hotfix:{
17
+ status:'CANDIDATE_SEALED',windows_regression:'PASS',package_acceptance:'PASS',manifest:'PASS',
18
+ remote_lifecycle:'PASS',security_gate:'PASS',independent_judge:'PASS'
19
+ }};
20
+
21
+ async function fixture(){
22
+ const root=await mkdtemp(path.join(tmpdir(),'r72-fence-integrity-'));
23
+ const evidence=path.join(root,'testing','r72-release-evidence');
24
+ const outputs=path.join(evidence,'outputs');
25
+ await mkdir(outputs,{recursive:true});
26
+ const manifest=Buffer.from('synthetic test-only manifest, not a real release\n');
27
+ await writeFile(path.join(root,'MANIFEST.SHA256'),manifest);
28
+ const index={schema:'deadbyte.r72-release-evidence-index.v1',version:'0.15.46',parent,
29
+ manifest_sha256:sha(manifest),gate_sha256:{}};
30
+ const outputPaths={};
31
+ for(const gate of gates){
32
+ const output=Buffer.from(`SYNTHETIC-TEST-ONLY\nGATE=${gate}\nEXIT=0\n`);
33
+ const outputPath=path.join(outputs,`${gate}.log`);
34
+ outputPaths[gate]=outputPath;
35
+ await writeFile(outputPath,output);
36
+ const receipt={schema:'deadbyte.r72-gate-result.v1',gate,version:'0.15.46',
37
+ manifest_sha256:index.manifest_sha256,status:'PASS',exit_code:0,
38
+ output_file:`outputs/${gate}.log`,output_sha256:sha(output),
39
+ ...(gate==='full_regression'?{pass_count:1,fail_count:0}:{}),
40
+ ...(gate==='windows_lifecycle'?{safe_default_arm_count:0,operator_owner_consent_enforced:true,
41
+ stale_owner_recovery_verified:true,signed_epoch_fail_closed:true}:{}),
42
+ ...(gate==='independent_judge'?{independent_verification:'PASS'}:{})};
43
+ const bytes=Buffer.from(JSON.stringify(receipt)+'\n');
44
+ index.gate_sha256[gate]=sha(bytes);
45
+ await writeFile(path.join(evidence,`${gate}.json`),bytes);
46
+ }
47
+ await writeFile(path.join(evidence,'ready.json'),JSON.stringify(index)+'\n');
48
+ return {root,evidence,outputPaths,dispose:()=>rm(root,{recursive:true,force:true})};
49
+ }
50
+
51
+ test('synthetic complete fixture validates receipt/output bytes linkage (not real release evidence)',async()=>{
52
+ const f=await fixture();try{
53
+ const result=await assertR72NpmPublishReady(release,{root:f.root});
54
+ assert.equal(result.status,'R72_RELEASE_READY_EVIDENCE_BOUND');
55
+ } finally {await f.dispose();}
56
+ });
57
+
58
+ test('mutating full regression output, without changing receipt, rejects',async()=>{
59
+ const f=await fixture();try{
60
+ await writeFile(f.outputPaths.full_regression,'tampered output\n');
61
+ await assert.rejects(assertR72NpmPublishReady(release,{root:f.root}),/output.*(mismatch|tamper|hash)/i);
62
+ } finally {await f.dispose();}
63
+ });
64
+
65
+ test('missing Windows lifecycle output rejects even with intact receipt',async()=>{
66
+ const f=await fixture();try{
67
+ await rm(f.outputPaths.windows_lifecycle);
68
+ await assert.rejects(assertR72NpmPublishReady(release,{root:f.root}),/output.*(missing|invalid|unavailable)/i);
69
+ } finally {await f.dispose();}
70
+ });
71
+
72
+ test('receipt pointing to other output file rejects even with recomputed index SHA',async()=>{
73
+ const f=await fixture();try{
74
+ const gate='package_acceptance',receiptPath=path.join(f.evidence,`${gate}.json`);
75
+ const receipt=JSON.parse(await readFile(receiptPath,'utf8'));
76
+ receipt.output_file='outputs/full_regression.log';
77
+ const content=Buffer.from(JSON.stringify(receipt)+'\n');
78
+ await writeFile(receiptPath,content);
79
+ const indexPath=path.join(f.evidence,'ready.json');
80
+ const index=JSON.parse(await readFile(indexPath,'utf8'));
81
+ index.gate_sha256[gate]=sha(content);
82
+ await writeFile(indexPath,JSON.stringify(index)+'\n');
83
+ await assert.rejects(assertR72NpmPublishReady(release,{root:f.root}),/output.*path/i);
84
+ } finally {await f.dispose();}
85
+ });
86
+
87
+ test('symlinked evidence output or junction directory must not be trusted',async()=>{
88
+ const f=await fixture();try{
89
+ const target=f.outputPaths.independent_judge,other=path.join(f.root,'external.log');
90
+ await writeFile(other,await readFile(target));
91
+ await rm(target);
92
+ try {await symlink(other,target,'file');}
93
+ catch(e){
94
+ if(!['EPERM','EACCES','ENOTSUP'].includes(e.code))throw e;
95
+ // Windows can deny file symlinks while permitting unprivileged directory junctions.
96
+ const outputs=path.join(f.evidence,'outputs');
97
+ const externalOutputs=path.join(f.root,'external-outputs');
98
+ await rename(outputs,externalOutputs);
99
+ await symlink(externalOutputs,outputs,'junction');
100
+ }
101
+ await assert.rejects(assertR72NpmPublishReady(release,{root:f.root}),/(output.*(regular|symlink)|evidence\/output directory is not a real directory)/i);
102
+ } finally {await f.dispose();}
103
+ });
104
+
105
+ test('missing status seal cannot be bypassed by synthetic receipts',async()=>{
106
+ const f=await fixture();try{
107
+ const uns={...release,r72_hotfix:{...release.r72_hotfix,status:'CANDIDATE_UNSEALED'}};
108
+ await assert.rejects(assertR72NpmPublishReady(uns,{root:f.root}),/CANDIDATE_SEALED/);
109
+ } finally {await f.dispose();}
110
+ });