deadbyte-mcp 0.15.50 → 0.15.52

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 (48) hide show
  1. package/MANIFEST.SHA256 +47 -17
  2. package/PROVENANCE.json +29 -11
  3. package/README.txt +18 -18
  4. package/RELEASE.json +392 -368
  5. package/SBOM.cdx.json +9 -4
  6. package/TEST-MATRIX.json +32 -2
  7. package/docs/R132-MANIFEST-PORTABILITY.md +23 -0
  8. package/docs/R135-AUTONOMOUS-SCOPE.md +21 -0
  9. package/docs/R136-SCOPED-OBSERVER.md +21 -0
  10. package/docs/R137-DESKTOP-COMMANDER-FOUNDATION.md +31 -0
  11. package/docs/R138-COMMANDER-WORKFLOW.md +21 -0
  12. package/docs/R139-COMMANDER-ERGONOMICS.md +17 -0
  13. package/docs/THIRD-PARTY-DESKTOP-COMMANDER-MIT.md +27 -0
  14. package/package.json +1 -1
  15. package/scripts/npm-prepublish-contract-r74.mjs +8 -2
  16. package/scripts/release-manifest.mjs +15 -1
  17. package/src/autonomous-scope-r135.mjs +91 -0
  18. package/src/commander-config-recovery-r137.mjs +67 -0
  19. package/src/commander-extended-runtime.mjs +22 -5
  20. package/src/commander-external-process-r48.mjs +25 -3
  21. package/src/commander-large-text-r139.mjs +66 -0
  22. package/src/commander-wire-adapter-r47.mjs +63 -12
  23. package/src/release-slot.mjs +13 -3
  24. package/src/scoped-observer-launcher-r136.mjs +77 -0
  25. package/src/scoped-observer-worker-r136.mjs +65 -0
  26. package/src/version.mjs +1 -1
  27. package/test/commander-wire-contract-r47.test.mjs +8 -5
  28. package/test/r130-release-security-contract.test.mjs +7 -7
  29. package/test/r131-installed-manifest-boundary.test.mjs +65 -0
  30. package/test/r132-release-identity.test.mjs +44 -0
  31. package/test/r135-autonomous-scope.test.mjs +103 -0
  32. package/test/r136-scoped-command-rights.test.mjs +39 -0
  33. package/test/r136-scoped-observer.test.mjs +89 -0
  34. package/test/r137-commander-config-recovery.test.mjs +111 -0
  35. package/test/r137-commander-three-version-contract.test.mjs +56 -0
  36. package/test/r138-commander-process-pagination.test.mjs +42 -0
  37. package/test/r138-commander-session-list.test.mjs +24 -0
  38. package/test/r138-commander-stream-resume.test.mjs +50 -0
  39. package/test/r139-large-text-edit.test.mjs +51 -0
  40. package/test/r139-large-text-read.test.mjs +46 -0
  41. package/test/r139-process-start-preview.test.mjs +50 -0
  42. package/test/r139-search-partial.test.mjs +42 -0
  43. package/test/r140-large-text-integrity.test.mjs +68 -0
  44. package/test/r141-commander-tail-utf8.test.mjs +55 -0
  45. package/test/r141-real-windows-disk.test.mjs +71 -0
  46. package/test/r142-native-machinefs-authority.test.mjs +127 -0
  47. package/test/r59-tool-surface-compat.test.mjs +1 -1
  48. package/test/r74-prepublish-contract.test.mjs +6 -4
@@ -0,0 +1,66 @@
1
+ // Bounded-memory Commander text paging, using only the policy-authorized native filesystem.
2
+ // Never rewrites the file. Pages accumulate a SHA-256 checked against one final native hash.
3
+ import {createHash} from 'node:crypto';
4
+ const SHA=/^[a-f0-9]{64}$/;
5
+ const MAX_SCAN=64*1024*1024;
6
+ const MAX_LINE=4*1024*1024;
7
+
8
+ export async function readCommanderLargeText({runtime,rootId,relativePath,offset=0,length=1000}={}){
9
+ if(!runtime||typeof runtime.read!=='function')throw new Error('native filesystem read unavailable');
10
+ if(!Number.isInteger(offset)||offset<0||!Number.isInteger(length)||length<0||length>10000)
11
+ throw new Error('large text offset or length invalid');
12
+ const policyChunk=runtime.policy?.max_read_bytes??256*1024;
13
+ const chunkSize=Math.min(256*1024,policyChunk);
14
+ if(!Number.isSafeInteger(chunkSize)||chunkSize<1)throw new Error('large text read policy limit invalid');
15
+ const decoder=new TextDecoder('utf-8',{fatal:true,ignoreBOM:true});
16
+ let pending='',position=0,fileSha=null,fileSize=null,totalLines=0,scanned=0,done=false;
17
+ const streamedHash=createHash('sha256');
18
+ const selected=[];
19
+ const emit=line=>{
20
+ if(totalLines>=offset&&selected.length<length)selected.push(line);
21
+ totalLines++;
22
+ };
23
+ const feed=(part,final=false)=>{
24
+ const value=pending+part;
25
+ const re=/\r\n|\r|\n/g;
26
+ let match,from=0;
27
+ while((match=re.exec(value))){
28
+ if(!final&&match[0]==='\r'&&match.index+1===value.length)break;
29
+ emit(value.slice(from,match.index));
30
+ from=match.index+match[0].length;
31
+ }
32
+ pending=value.slice(from);
33
+ if(Buffer.byteLength(pending,'utf8')>MAX_LINE)throw new Error('large text line exceeds bounded read limit');
34
+ if(final)emit(pending);
35
+ };
36
+ while(!done){
37
+ const page=await runtime.read({rootId,relativePath,offset:position,maxBytes:chunkSize,hashFile:false});
38
+ if(Number.isSafeInteger(page.file_size)){
39
+ if(fileSize===null)fileSize=page.file_size;
40
+ else if(fileSize!==page.file_size)throw new Error('large text file changed during paged read: size mismatch');
41
+ if(fileSize>MAX_SCAN)throw new Error('large text file exceeds bounded 64MiB scan limit');
42
+ }
43
+ if(!Number.isSafeInteger(page.offset)||page.offset!==position)throw new Error('large text native page offset drift');
44
+ if(typeof page.data_base64!=='string')throw new Error('large text native payload missing');
45
+ const bytes=Buffer.from(page.data_base64,'base64');
46
+ if(bytes.length>chunkSize||page.next_offset!==position+bytes.length)throw new Error('large text native page cursor invalid');
47
+ if(bytes.length===0&&page.eof!==true)throw new Error('large text read stalled before EOF');
48
+ streamedHash.update(bytes);
49
+ position+=bytes.length;scanned+=bytes.length;
50
+ if(scanned>MAX_SCAN)throw new Error('large text file exceeds bounded 64MiB scan limit');
51
+ feed(decoder.decode(bytes,{stream:true}));
52
+ if(page.eof===true){feed(decoder.decode(),true);done=true;}
53
+ }
54
+ if(fileSize!==null&&fileSize!==position)throw new Error('large text file changed during paged read: size mismatch');
55
+ // One native full-file hash after all delivered bytes binds them to final file contents.
56
+ const closing=await runtime.read({rootId,relativePath,offset:position,maxBytes:1,hashFile:true});
57
+ if(!SHA.test(closing?.file_sha256??''))throw new Error('large text native file identity missing');
58
+ if(closing.offset!==position||closing.next_offset!==position||closing.eof!==true||
59
+ closing.data_base64!==''||!Number.isSafeInteger(closing.file_size)||closing.file_size!==position)
60
+ throw new Error('large text file changed during paged read: closing cursor or size mismatch');
61
+ fileSha=closing.file_sha256;
62
+ if(streamedHash.digest('hex')!==fileSha)throw new Error('large text file changed during paged read: identity mismatch');
63
+ const start=Math.min(offset,totalLines);
64
+ return {content:selected.join('\n')+(start+selected.length<totalLines?'\n':''),
65
+ lineOffset:start,lineCount:selected.length,totalLines,fileSha256:fileSha,bytesScanned:scanned,paged:true};
66
+ }
@@ -5,6 +5,7 @@ import { readCommanderUrl } from './commander-url-reader-r48.mjs';
5
5
  import { readDocxBytes, searchDocxTextBytes, createDocxBytes, editDocxBytes, readXlsxBytes, editXlsxBytes, createXlsxBytes, appendXlsxBytes } from './commander-office-reader-r48.mjs';
6
6
  import { editDeadbytePdf, parseDeadbytePdf, readPdfText } from './commander-pdf-r48.mjs';
7
7
  import { decodeUtf8Strict } from './text-policy.mjs';
8
+ import { readCommanderLargeText } from './commander-large-text-r139.mjs';
8
9
 
9
10
  const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
10
11
  const RDC_PROCESS_READ_CHUNK=256*1024;
@@ -207,12 +208,26 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
207
208
  const tail=await machineFsRuntime.read({
208
209
  rootId:target.rootId,relativePath:target.relativePath,offset:-policyMax,maxBytes:policyMax,hashFile:false
209
210
  });
210
- let decoded=Buffer.from(tail.data_base64,'base64').toString('utf8').replaceAll('\r\n','\n').replaceAll('\r','\n');
211
+ let tailBytes=Buffer.from(tail.data_base64,'base64');
211
212
  if((tail.offset??0)>0){
212
- const firstBreak=decoded.indexOf('\n');
213
- if(firstBreak<0) throw new Error('text tail window contains no complete line');
214
- decoded=decoded.slice(firstBreak+1);
213
+ const previous=await machineFsRuntime.read({
214
+ rootId:target.rootId,relativePath:target.relativePath,
215
+ offset:tail.offset-1,maxBytes:1,hashFile:false
216
+ });
217
+ const before=Buffer.from(previous.data_base64,'base64');
218
+ if(previous.offset!==tail.offset-1||previous.next_offset!==tail.offset||
219
+ previous.file_size!==tail.file_size||before.length!==1)
220
+ throw new Error('text tail window changed before boundary check');
221
+ const aligned=before[0]===0x0a||(before[0]===0x0d&&tailBytes[0]!==0x0a);
222
+ if(!aligned){
223
+ const firstBreak=tailBytes.findIndex(byte=>byte===0x0a||byte===0x0d);
224
+ if(firstBreak<0) throw new Error('text tail window contains no complete line');
225
+ const crlf=tailBytes[firstBreak]===0x0d&&tailBytes[firstBreak+1]===0x0a;
226
+ tailBytes=tailBytes.subarray(firstBreak+(crlf?2:1));
227
+ }
215
228
  }
229
+ if(tailBytes.includes(0)) throw new Error('text tail window contains NUL byte');
230
+ let decoded=decodeUtf8Strict(tailBytes).replaceAll('\r\n','\n').replaceAll('\r','\n');
216
231
  let all=decoded.split('\n');
217
232
  if(all.length&&all.at(-1)==='') all.pop();
218
233
  const wanted=Math.abs(requestedOffset);
@@ -232,6 +247,13 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
232
247
  hashFile:richDocument
233
248
  });
234
249
  if(richDocument&&raw.eof===false) throw new Error('document exceeds bounded native read limit');
250
+ if(!richDocument&&raw.eof===false&&!imageMime&&ext!=='.xls'){
251
+ const large=await readCommanderLargeText({runtime:machineFsRuntime,
252
+ rootId:target.rootId,relativePath:target.relativePath,
253
+ offset:requestedOffset,length:requestedLength});
254
+ return text(large.content,false,{path:args.path,content:large.content,
255
+ mimeType:'text/plain',isImage:false,isPdf:false,...large,...optionMeta});
256
+ }
235
257
  if(!richDocument&&raw.eof===false) throw new Error('text file exceeds bounded compatibility read limit');
236
258
  const bytes=Buffer.from(raw.data_base64,'base64');
237
259
  const fileSha256=raw.file_sha256??sha256Bytes(bytes);
@@ -477,11 +499,17 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
477
499
  });
478
500
  }
479
501
  if(args.old_string===undefined||args.new_string===undefined) throw new Error('Must provide either old_string + new_string');
480
- const raw=await machineFsRuntime.read({rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:Math.min(4*1024*1024,machineFsRuntime.policy.max_read_bytes??4*1024*1024),hashFile:false});
502
+ const editBound=16*1024*1024;
503
+ const raw=await machineFsRuntime.read({rootId:target.rootId,relativePath:target.relativePath,offset:0,
504
+ maxBytes:Math.min(editBound,machineFsRuntime.policy.max_read_bytes??editBound),hashFile:true});
481
505
  if(raw.eof===false) throw new Error('text edit target exceeds bounded compatibility edit limit');
482
506
  const before=Buffer.from(raw.data_base64,'base64');
507
+ if(before.includes(0))throw new Error('binary text edit target rejected');
508
+ let current;
509
+ try{current=decodeUtf8Strict(before);}catch{throw new Error('invalid UTF-8 in text edit target');}
510
+ if(before.length>=3&&before[0]===0xef&&before[1]===0xbb&&before[2]===0xbf)
511
+ current='\uFEFF'+current;
483
512
  const fileSha256=raw.file_sha256??sha256Bytes(before);
484
- const current=before.toString('utf8');
485
513
  let count=0,index=0;
486
514
  while((index=current.indexOf(args.old_string,index))>=0){count++;index+=Math.max(1,args.old_string.length);}
487
515
  if(count!==args.expected_replacements) throw new Error(`expected ${args.expected_replacements} replacements, found ${count}`);
@@ -652,7 +680,20 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
652
680
  }
653
681
  pidMap.set(compatPid,native.session_id);
654
682
  processCompatState.set(compatPid,{last_read_byte:0});
655
- return text(`Process started with PID ${compatPid} (DEADBYTE session ${native.session_id})\\n`,false,{pid:compatPid,session_id:native.session_id});
683
+ let initialOutput='',previewUnavailable=false,previewTruncated=false;
684
+ try{
685
+ // Optional, non-waiting preview: does not advance the independent output cursor.
686
+ const first=await processRuntime.read({sessionId:native.session_id,offset:0,maxBytes:16384,waitMs:0});
687
+ const data=Buffer.from(first.data_base64??'','base64');
688
+ if(data.includes(0))throw new Error('binary process output preview');
689
+ initialOutput=decodeUtf8Strict(data);
690
+ previewTruncated=(first.stream_head??data.length)>(first.next_offset??data.length);
691
+ }catch{previewUnavailable=true;}
692
+ const summary=`Process started with PID ${compatPid} (DEADBYTE session ${native.session_id})`;
693
+ return text(initialOutput?summary+'\n'+initialOutput:summary+'\n',false,{
694
+ pid:compatPid,session_id:native.session_id,initial_output:initialOutput,
695
+ initial_output_truncated:previewTruncated,output_preview_unavailable:previewUnavailable
696
+ });
656
697
  }
657
698
  function sessionForPid(pid){
658
699
  const sessionId=pidMap.get(pid);
@@ -693,7 +734,9 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
693
734
  state:snapshot.state,exitCode:snapshot.exit_code,truncated:wasTruncated
694
735
  }),false,{
695
736
  pid:args.pid,session_id:sessionId,read_from:readFrom,read_count:readCount,total_lines:retainedRanges.length,
696
- remaining,next_line:readFrom+readCount,stream_head:snapshot.stream_head,state:snapshot.state,exit_code:snapshot.exit_code
737
+ remaining,next_line:readFrom+readCount,stream_head:snapshot.stream_head,state:snapshot.state,exit_code:snapshot.exit_code,
738
+ earliest_offset:snapshot.earliest_offset,truncated:wasTruncated,
739
+ next_byte_offset:state.last_read_byte,more_unread:state.last_read_byte<snapshot.stream_head
697
740
  });
698
741
  }
699
742
  const totalLines=retainedRanges.length;
@@ -705,7 +748,9 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
705
748
  readFrom,readCount,totalLines,remaining,lines,state:snapshot.state,exitCode:snapshot.exit_code,truncated:snapshot.earliest_offset>0
706
749
  }),false,{
707
750
  pid:args.pid,session_id:sessionId,read_from:readFrom,read_count:readCount,total_lines:totalLines,
708
- remaining,next_line:readFrom+readCount,stream_head:snapshot.stream_head,state:snapshot.state,exit_code:snapshot.exit_code
751
+ remaining,next_line:readFrom+readCount,stream_head:snapshot.stream_head,state:snapshot.state,exit_code:snapshot.exit_code,
752
+ earliest_offset:snapshot.earliest_offset,truncated:snapshot.earliest_offset>0,
753
+ next_byte_offset:null,more_unread:remaining>0
709
754
  });
710
755
  }
711
756
  async function interactProcess(args){
@@ -745,8 +790,15 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
745
790
  return text(`Successfully terminated process ${args.pid}`,false,{pid:args.pid,session_id:sessionId,...native});
746
791
  }
747
792
  async function listSessions(){
793
+ if(!processRuntime)throw new Error('process runtime is disabled');
748
794
  const native=await processRuntime.list();
749
- return text(native.sessions.map(s=>`Session ${s.session_id}: state=${s.state}, profile=${s.profile_id}, root=${s.root_id}`).join('\\n'),false,{sessions:native.sessions});
795
+ if(!Array.isArray(native?.sessions))throw new Error('native session inventory unavailable');
796
+ const rows=native.sessions.map(s=>{
797
+ const pid=Number.isInteger(s.child_pid)&&s.child_pid>0?String(s.child_pid):'unknown';
798
+ const exit=s.state==='running'?'':`, exit=${Number.isInteger(s.exit_code)?s.exit_code:'unknown'}`;
799
+ return `Session ${s.session_id}: PID=${pid}, state=${s.state}, profile=${s.profile_id}, root=${s.root_id}${exit}`;
800
+ });
801
+ return text(rows.length?rows.join('\n'):'No managed sessions.',false,{sessions:native.sessions});
750
802
  }
751
803
  async function listProcesses(){
752
804
  if(!externalProcessRuntime) return listSessions();
@@ -882,11 +934,10 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
882
934
  async function getMoreSearchResults(args){
883
935
  const s=searchSessions.get(args.sessionId);
884
936
  if(!s) throw new Error('search session not found');
885
- if(!s.isComplete) return {results:[],returnedCount:0,totalResults:0,isComplete:false,isError:false,hasMoreResults:true};
886
937
  const start=args.offset<0?Math.max(0,s.results.length+args.offset):args.offset;
887
938
  const end=args.offset<0?s.results.length:Math.min(s.results.length,start+args.length);
888
939
  const results=s.results.slice(start,end);
889
- return {results,returnedCount:results.length,totalResults:s.results.length,isComplete:true,isError:s.isError,error:s.error,hasMoreResults:end<s.results.length,runtime:Date.now()-s.created};
940
+ return {results,returnedCount:results.length,totalResults:s.results.length,isComplete:s.isComplete,isError:s.isError,error:s.error,hasMoreResults:!s.isComplete||end<s.results.length,runtime:Date.now()-s.created};
890
941
  }
891
942
  async function stopSearch(args){
892
943
  const s=searchSessions.get(args.sessionId);
@@ -79,13 +79,23 @@ async function listManifestTreeFiles(root){
79
79
  return files.sort((a,b)=>a.localeCompare(b,'en'));
80
80
  }
81
81
 
82
- export async function verifyManifestTreeExact(root,manifest){
82
+ export async function verifyManifestTreeExact(root,manifest,{allowedExtraPaths=[]}={}){
83
+ // The only unmanifested files accepted in a protected test fixture are
84
+ // packaging inputs that npm intentionally excludes from published payloads.
85
+ // No subdirectory, wildcard, executable or mutable runtime state is exempt.
86
+ const allowedPackagingExtras=new Set(['.npmignore','package-lock.json']);
87
+ if(!Array.isArray(allowedExtraPaths)||allowedExtraPaths.some(rel=>!allowedPackagingExtras.has(rel))){
88
+ throw new Error('invalid allowed manifest extra');
89
+ }
90
+ const extras=new Set(allowedExtraPaths);
83
91
  await verifyManifestTree(root,manifest);
84
92
  const actual=await listManifestTreeFiles(root);
85
93
  const expected=[...manifest.keys()].sort((a,b)=>a.localeCompare(b,'en'));
86
- for(const rel of actual) if(!manifest.has(rel)) throw new Error(`unexpected manifest-tree file: ${rel}`);
94
+ for(const rel of actual) if(!manifest.has(rel)&&!extras.has(rel)) throw new Error(`unexpected manifest-tree file: ${rel}`);
87
95
  for(const rel of expected) if(!actual.includes(rel)) throw new Error(`missing manifest-tree file: ${rel}`);
88
- if(actual.length!==expected.length) throw new Error(`manifest-tree file count mismatch: expected=${expected.length} actual=${actual.length}`);
96
+ if(actual.length!==expected.length+actual.filter(rel=>extras.has(rel)&&!manifest.has(rel)).length){
97
+ throw new Error(`manifest-tree file count mismatch: expected=${expected.length} actual=${actual.length}`);
98
+ }
89
99
  return {files:manifest.size};
90
100
  }
91
101
  export async function copyManifestTree({source,destination,manifest}){
@@ -0,0 +1,77 @@
1
+ import {fork} from 'node:child_process';
2
+ import {fileURLToPath} from 'node:url';
3
+ import path from 'node:path';
4
+
5
+ const worker=fileURLToPath(new URL('./scoped-observer-worker-r136.mjs',import.meta.url));
6
+ const hex=/^[0-9a-f]{64}$/;
7
+ export async function startScopedObserver({stateRoot,codingPolicyFile,autonomousPolicyFile,codingSha256,autonomousSha256,ambientEnv={}}={}){
8
+ if(![stateRoot,codingPolicyFile,autonomousPolicyFile].every(p=>typeof p==='string'&&path.isAbsolute(p)))
9
+ throw new Error('observer absolute isolated paths required');
10
+ if(!hex.test(codingSha256??'')||!hex.test(autonomousSha256??''))
11
+ throw new Error('observer pinned policy SHA256 required');
12
+ // Deliberately no DEADBYTE_ authority config, inherited token, NODE_OPTIONS,
13
+ // or shell PATH. Child is a disarmed status-only IPC process, not an OS sandbox.
14
+ const env={};
15
+ for(const key of ['SystemRoot','SYSTEMROOT','windir','WINDIR','TEMP','TMP']){
16
+ if(typeof process.env[key]==='string')env[key]=process.env[key];
17
+ }
18
+ // ambientEnv is an adversarial test vector and is NEVER merged.
19
+ void ambientEnv;
20
+ const child=fork(worker,[stateRoot,codingPolicyFile,codingSha256,autonomousPolicyFile,autonomousSha256],{
21
+ execPath:process.execPath,execArgv:[],cwd:stateRoot,env,stdio:['ignore','ignore','ignore','ipc'],windowsHide:true
22
+ });
23
+ let pending=new Map(),serial=0,closed=false;
24
+ const rejectAll=reason=>{for(const {reject,timer} of pending.values()){clearTimeout(timer);reject(reason);}pending.clear();};
25
+ let bootResolve,bootReject;
26
+ const boot=new Promise((resolve,reject)=>{bootResolve=resolve;bootReject=reject});
27
+ let bootFinished=false;
28
+ const bootTimer=setTimeout(()=>{if(!bootFinished){bootFinished=true;bootReject(new Error('observer boot failed: timeout'));child.kill();}},6000);
29
+ child.on('message',msg=>{
30
+ if(msg?.kind==='ready'&&!bootFinished){bootFinished=true;clearTimeout(bootTimer);bootResolve(msg.result);return;}
31
+ if(msg?.kind==='boot-error'&&!bootFinished){bootFinished=true;clearTimeout(bootTimer);bootReject(new Error('observer boot failed: '+msg.reason));child.kill();return;}
32
+ if(msg?.kind==='reply'){
33
+ const p=pending.get(msg.id);if(!p)return;pending.delete(msg.id);clearTimeout(p.timer);p.resolve(msg.result);
34
+ }
35
+ });
36
+ child.on('error',error=>{if(!bootFinished){bootFinished=true;clearTimeout(bootTimer);bootReject(error);}rejectAll(error);});
37
+ child.on('exit',(code)=>{
38
+ closed=true;
39
+ if(!bootFinished){bootFinished=true;clearTimeout(bootTimer);bootReject(new Error('observer boot failed: exit '+code));}
40
+ rejectAll(new Error('observer exited'));
41
+ });
42
+ let ready;
43
+ try{ready=await boot;}catch(error){
44
+ if(!closed){
45
+ const exited=new Promise(resolve=>{
46
+ const timer=setTimeout(()=>{child.kill();},1500);
47
+ child.once('exit',()=>{clearTimeout(timer);resolve();});
48
+ });
49
+ child.kill();await exited;
50
+ }
51
+ throw error;
52
+ }
53
+ const request=payload=>{
54
+ if(closed) return Promise.reject(new Error('observer already exited'));
55
+ return new Promise((resolve,reject)=>{
56
+ const id=++serial;
57
+ const timer=setTimeout(()=>{pending.delete(id);reject(new Error('observer request timeout'));child.kill();},3000);
58
+ pending.set(id,{resolve,reject,timer});
59
+ try{child.send({kind:'request',id,op:payload?.op},err=>{if(err){clearTimeout(timer);pending.delete(id);reject(err);}});}
60
+ catch(error){clearTimeout(timer);pending.delete(id);reject(error);}
61
+ });
62
+ };
63
+ return Object.freeze({
64
+ ready,
65
+ request,
66
+ async close(){
67
+ if(closed)return;
68
+ const exited=new Promise(resolve=>{
69
+ if(closed)return resolve();
70
+ const timer=setTimeout(()=>{child.kill();},1500);
71
+ child.once('exit',()=>{clearTimeout(timer);resolve();});
72
+ });
73
+ try{await request({op:'shutdown'});}catch{child.kill();}
74
+ await exited;
75
+ }
76
+ });
77
+ }
@@ -0,0 +1,65 @@
1
+ import {readFile,lstat,realpath} from 'node:fs/promises';
2
+ import {createHash} from 'node:crypto';
3
+ import path from 'node:path';
4
+
5
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
6
+ const digest=/^[0-9a-f]{64}$/;
7
+ const eq=(a,b)=>process.platform==='win32'?a.toLowerCase()===b.toLowerCase():a===b;
8
+ async function fileInside(state,filename,expected){
9
+ if(!digest.test(expected??''))throw new Error('policy digest invalid');
10
+ const root=path.resolve(state),file=path.resolve(filename);
11
+ const rel=path.relative(root,file);
12
+ if(rel===''||path.isAbsolute(rel)||rel==='..'||rel.startsWith('..'+path.sep))throw new Error('policy outside isolated state');
13
+ const item=await lstat(file);
14
+ if(!item.isFile()||item.isSymbolicLink()||!eq(await realpath(file),file))throw new Error('policy not regular');
15
+ const bytes=await readFile(file);
16
+ if(sha(bytes)!==expected)throw new Error('policy digest mismatch');
17
+ return JSON.parse(bytes.toString('utf8'));
18
+ }
19
+ async function boot(){
20
+ if(!process.send)throw new Error('IPC required');
21
+ const [state,codingFile,codingDigest,autoFile,autoDigest]=process.argv.slice(2);
22
+ if(!state||!path.isAbsolute(state))throw new Error('isolated state required');
23
+ const isolated=path.resolve(state);
24
+ if(!eq(await realpath(isolated),isolated))throw new Error('isolated state redirected');
25
+ const coding=await fileInside(isolated,codingFile,codingDigest);
26
+ const auto=await fileInside(isolated,autoFile,autoDigest);
27
+ if(coding?.schema!=='deadbyte.coding-policy.v1'||coding.enabled!==true)throw new Error('coding schema invalid');
28
+ if(auto?.schema!=='deadbyte.autonomous-policy.v1'||auto.enabled!==true)throw new Error('autonomous schema invalid');
29
+ if(auto.coding_policy_sha256!==codingDigest)throw new Error('autonomous binding invalid');
30
+ const ids=Object.keys(coding.roots??{}),selected=auto.root_ids;
31
+ if(!Array.isArray(selected)||selected.length!==1||ids.length!==1||ids[0]!==selected[0])throw new Error('root scope mismatch');
32
+ const id=ids[0],root=coding.roots[id]?.path;
33
+ if(!path.isAbsolute(root??'')||eq(path.resolve(root),path.parse(root).root))throw new Error('broad Coding root forbidden');
34
+ const stat=await lstat(root);
35
+ if(!stat.isDirectory()||stat.isSymbolicLink()||!eq(await realpath(root),path.resolve(root)))throw new Error('untrusted project root');
36
+ const commands=Object.entries(coding.commands??{});
37
+ for(const [,command] of commands){
38
+ if(!Array.isArray(command.root_ids)||command.root_ids.length!==1||command.root_ids[0]!==id)
39
+ throw new Error('command root scope mismatch');
40
+ }
41
+ for(const p of Object.values(auto.profiles??{})){
42
+ if(p?.root_id!==id||!Object.hasOwn(coding.commands,p.command_id))throw new Error('autonomous profile mismatch');
43
+ }
44
+ for(const lease of [coding.lease_file,auto.lease_file]){
45
+ if(typeof lease!=='string'||!path.isAbsolute(lease)||!eq(path.dirname(lease),path.join(isolated,'runtime')))
46
+ throw new Error('lease state isolation mismatch');
47
+ try{await lstat(lease);throw new Error('inherited grant present');}
48
+ catch(error){if(error?.code!=='ENOENT')throw error;}
49
+ }
50
+ const ambient=Object.keys(process.env).filter(k=>k.toUpperCase().startsWith('DEADBYTE_'));
51
+ const ready={pid:process.pid,status:'DISARMED',root_ids:ids,command_ids:commands.map(x=>x[0]),
52
+ coding_sha256:codingDigest,autonomous_sha256:autoDigest,ambient_deadbyte_keys:ambient.length,
53
+ node_options_present:Object.hasOwn(process.env,'NODE_OPTIONS'),
54
+ user_token_present:Object.keys(process.env).some(k=>/OPENAI_API_KEY|NPM_TOKEN|GITHUB_TOKEN|GH_TOKEN|AWS_SECRET_ACCESS_KEY/i.test(k)),
55
+ grant_files_present:false};
56
+ process.send({kind:'ready',result:ready});
57
+ process.on('message',msg=>{
58
+ if(!msg||msg.kind!=='request'||!Number.isSafeInteger(msg.id))return;
59
+ if(msg.op==='shutdown'){process.send({kind:'reply',id:msg.id,result:{status:'STOPPING'}});return process.exit(0);}
60
+ const result=msg.op==='status'?{status:'DISARMED',root_ids:ids}:
61
+ {status:'DENIED_UNAUTHORIZED',op:typeof msg.op==='string'?msg.op:''};
62
+ process.send({kind:'reply',id:msg.id,result});
63
+ });
64
+ }
65
+ boot().catch(err=>{process.send?.({kind:'boot-error',reason:String(err.message??err).slice(0,140)});process.exitCode=3;});
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.50';
1
+ export const DEADBYTE_VERSION = '0.15.52';
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';
@@ -406,13 +406,14 @@ test('R48 read_file tails a large text file from the end without reading the who
406
406
  policy:{roots:{project:{real_path:'D:\\RDC-COMPAT-TEST'}},max_read_bytes:64},
407
407
  async read(args){
408
408
  calls.push({...args});
409
- if(args.offset>=0) return {
410
- data_base64:Buffer.from('HEAD-ONLY').toString('base64'),
411
- file_sha256:null,file_size:1024,eof:false,offset:0,next_offset:9
409
+ if(args.offset===959) return {
410
+ data_base64:Buffer.from('x').toString('base64'),
411
+ file_sha256:null,file_size:1024,eof:false,offset:959,next_offset:960
412
412
  };
413
413
  assert.equal(args.offset,-64);
414
+ const suffix='older\nline7\nline8\nline9\n';
414
415
  return {
415
- data_base64:Buffer.from('older\nline7\nline8\nline9\n').toString('base64'),
416
+ data_base64:Buffer.from('p'.repeat(64-Buffer.byteLength(suffix))+suffix).toString('base64'),
416
417
  file_sha256:null,file_size:1024,eof:true,offset:960,next_offset:1024
417
418
  };
418
419
  }
@@ -423,8 +424,10 @@ test('R48 read_file tails a large text file from the end without reading the who
423
424
  });
424
425
  assert.equal(result.isError,undefined,JSON.stringify(result));
425
426
  assert.equal(result.structuredContent.content,'line8\nline9');
426
- assert.equal(calls.length,1);
427
+ assert.equal(calls.length,2);
427
428
  assert.equal(calls[0].offset,-64);
429
+ assert.equal(calls[1].offset,959);
430
+ assert.equal(calls[1].maxBytes,1);
428
431
  });
429
432
 
430
433
  test('R48 edit_block range mutates XLSX through native ZIP/XML writer with hash precondition',async()=>{
@@ -13,14 +13,14 @@ test('R130 security patch has a new immutable package identity and truthful sour
13
13
  read('SBOM.cdx.json'),read('package-lock.json'),read('TEST-MATRIX.json')]);
14
14
  assert.equal(pkg.name,'deadbyte-mcp');
15
15
  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.50');
17
- assert.equal(release.train,'R130');
18
- assert.equal(matrix.train,'R130');
19
- assert.equal(release.source_predecessor.version,'0.15.49');
20
- assert.match(release.source_predecessor.source_path,/0\.15\.49/);
16
+ assert.equal(actual,'0.15.52');
17
+ assert.equal(release.train,'R143');
18
+ assert.equal(matrix.train,'R143');
19
+ assert.equal(release.source_predecessor.version,'0.15.51');
20
+ assert.match(release.source_predecessor.source_path,/0\.15\.51/);
21
21
  assert.equal(release.runtime_predecessor.version,'0.15.45');
22
- assert.equal(provenance.source_predecessor_version,'0.15.49');
23
- assert.equal(provenance.source_predecessor_manifest_file_sha256,'0c614697756731ee348f77cb36f9b1780f3037b5846e6482dc9a4c4e48cf01b6');
22
+ assert.equal(provenance.source_predecessor_version,'0.15.51');
23
+ assert.equal(provenance.source_predecessor_manifest_file_sha256,'ef27d35d0d3102b356fcb17478abefa82fae2375df27f913ce3b0ff0e5815a7e');
24
24
  assert.equal(pkg.dependencies['@modelcontextprotocol/client'],'2.3.1');
25
25
  assert.equal(pkg.dependencies['@modelcontextprotocol/server'],'2.3.1');
26
26
  for(const dep of ['@modelcontextprotocol/client','@modelcontextprotocol/server'])
@@ -0,0 +1,65 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {mkdtemp, mkdir, rm, writeFile} from 'node:fs/promises';
4
+ import {createHash} from 'node:crypto';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import {verifyManifestTreeExact} from '../src/release-slot.mjs';
8
+
9
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
10
+
11
+ async function fixture(run) {
12
+ const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r131-portable-manifest-'));
13
+ try {
14
+ await mkdir(path.join(root,'src'));
15
+ await writeFile(path.join(root,'src','payload.mjs'),'export const x=1;\n');
16
+ await writeFile(path.join(root,'.npmignore'),'# supplemental npm packaging data\n');
17
+ await writeFile(path.join(root,'package-lock.json'),'{"name":"fixture"}\n');
18
+ const manifest=new Map([['src/payload.mjs',sha(Buffer.from('export const x=1;\n'))]]);
19
+ return await run({root,manifest});
20
+ } finally {
21
+ await rm(root,{recursive:true,force:true});
22
+ }
23
+ }
24
+
25
+ test('R131 existing strict release-tree gate rejects any supplemental file', async()=>{
26
+ await fixture(async({root,manifest})=>{
27
+ await assert.rejects(verifyManifestTreeExact(root,manifest),/unexpected manifest-tree file/);
28
+ });
29
+ });
30
+
31
+ test('R131 protected-install gate verifies all manifest hashes but permits ONLY declared packaging extras',async()=>{
32
+ await fixture(async({root,manifest})=>{
33
+ const result=await verifyManifestTreeExact(root,manifest,{allowedExtraPaths:['.npmignore','package-lock.json']});
34
+ assert.deepEqual(result,{files:1});
35
+ });
36
+ });
37
+
38
+ test('R131 protected-install gate rejects unexpected executable file even with allowed packaging extras',async()=>{
39
+ await fixture(async({root,manifest})=>{
40
+ await writeFile(path.join(root,'src','rogue.mjs'),'export const privileged=true;\n');
41
+ await assert.rejects(
42
+ verifyManifestTreeExact(root,manifest,{allowedExtraPaths:['.npmignore','package-lock.json']}),
43
+ /unexpected manifest-tree file: src\/rogue\.mjs/
44
+ );
45
+ });
46
+ });
47
+
48
+ test('R131 protected-install gate rejects mutated manifested source bytes',async()=>{
49
+ await fixture(async({root,manifest})=>{
50
+ await writeFile(path.join(root,'src','payload.mjs'),'export const x=2;\n');
51
+ await assert.rejects(
52
+ verifyManifestTreeExact(root,manifest,{allowedExtraPaths:['.npmignore','package-lock.json']}),
53
+ /hash mismatch: src\/payload\.mjs/
54
+ );
55
+ });
56
+ });
57
+
58
+ test('R131 installed gate disallows broad directory exemptions',async()=>{
59
+ await fixture(async({root,manifest})=>{
60
+ await assert.rejects(
61
+ verifyManifestTreeExact(root,manifest,{allowedExtraPaths:['src/','.npmignore','package-lock.json']}),
62
+ /invalid allowed manifest extra/
63
+ );
64
+ });
65
+ });
@@ -0,0 +1,44 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {readFile} from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {DEADBYTE_VERSION} from '../src/version.mjs';
7
+
8
+ const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
9
+ const read=async rel=>JSON.parse(await readFile(path.join(root,rel),'utf8'));
10
+
11
+ test('R143 package identities, R132 historic evidence and published predecessor are consistent',async()=>{
12
+ const [pkg,lock,release,prov,bom,matrix]=await Promise.all([
13
+ read('package.json'),read('package-lock.json'),read('RELEASE.json'),
14
+ read('PROVENANCE.json'),read('SBOM.cdx.json'),read('TEST-MATRIX.json')]);
15
+ for(const value of [pkg.version,lock.version,lock.packages[''].version,release.version,prov.version,bom.metadata.component.version,matrix.version,DEADBYTE_VERSION]){
16
+ assert.equal(value,'0.15.52');
17
+ }
18
+ assert.equal(release.train,'R143');
19
+ assert.equal(matrix.train,'R143');
20
+ assert.equal(release.source_predecessor.version,'0.15.51');
21
+ assert.match(release.source_predecessor.source_path,/0\.15\.51/);
22
+ assert.equal(prov.source_predecessor_version,'0.15.51');
23
+ assert.equal(prov.source_predecessor_manifest_file_sha256,'ef27d35d0d3102b356fcb17478abefa82fae2375df27f913ce3b0ff0e5815a7e');
24
+ assert.equal(prov.parent_tarball_sha256,'69586dc07dc7a3025ec44f08e6b0eab60c1f9e75287da77c850b49e320b201ff');
25
+ assert.equal(release.runtime_predecessor.version,'0.15.45');
26
+ assert.equal(prov.runtime_predecessor.release_id,release.runtime_predecessor.release_id);
27
+ assert.equal(prov.runtime_predecessor.manifest_sha256,release.runtime_predecessor.manifest_sha256);
28
+ assert.equal(prov.runtime_predecessor.generation_manifest_sha256,release.runtime_predecessor.generation_manifest_sha256);
29
+ assert.equal(release.r132.manifest_portability,'R131_PROTECTED_FULL_GREEN');
30
+ assert.equal(release.r132.production_activation,'NOT_RUN');
31
+ assert.equal(prov.r132.source_lineage_manifest_sha256,'3e5edb9987aa7c9004dda3164a278da70a60495c0c128e2602426075a5222c10');
32
+ assert.equal(matrix.required.some(t=>t.id==='r132-protected-full'),true);
33
+ });
34
+
35
+ test('R132 shipped SBOM matches the actual audited bundled MCP SDK',async()=>{
36
+ const [pkg,bom,lock]=await Promise.all([read('package.json'),read('SBOM.cdx.json'),read('package-lock.json')]);
37
+ for(const name of ['@modelcontextprotocol/client','@modelcontextprotocol/server','@modelcontextprotocol/core']){
38
+ const entry=bom.components.find(v=>v.name===name);
39
+ assert.ok(entry,'missing SBOM component: '+name);
40
+ assert.equal(entry.version,'2.3.1',name+' SBOM stale');
41
+ assert.equal(lock.packages['node_modules/'+name].version,'2.3.1',name+' lockfile stale');
42
+ if(name!=='@modelcontextprotocol/core')assert.equal(pkg.dependencies[name],'2.3.1');
43
+ }
44
+ });