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.
- package/MANIFEST.SHA256 +47 -17
- package/PROVENANCE.json +29 -11
- package/README.txt +18 -18
- package/RELEASE.json +392 -368
- package/SBOM.cdx.json +9 -4
- package/TEST-MATRIX.json +32 -2
- package/docs/R132-MANIFEST-PORTABILITY.md +23 -0
- package/docs/R135-AUTONOMOUS-SCOPE.md +21 -0
- package/docs/R136-SCOPED-OBSERVER.md +21 -0
- package/docs/R137-DESKTOP-COMMANDER-FOUNDATION.md +31 -0
- package/docs/R138-COMMANDER-WORKFLOW.md +21 -0
- package/docs/R139-COMMANDER-ERGONOMICS.md +17 -0
- package/docs/THIRD-PARTY-DESKTOP-COMMANDER-MIT.md +27 -0
- package/package.json +1 -1
- package/scripts/npm-prepublish-contract-r74.mjs +8 -2
- package/scripts/release-manifest.mjs +15 -1
- package/src/autonomous-scope-r135.mjs +91 -0
- package/src/commander-config-recovery-r137.mjs +67 -0
- package/src/commander-extended-runtime.mjs +22 -5
- package/src/commander-external-process-r48.mjs +25 -3
- package/src/commander-large-text-r139.mjs +66 -0
- package/src/commander-wire-adapter-r47.mjs +63 -12
- package/src/release-slot.mjs +13 -3
- package/src/scoped-observer-launcher-r136.mjs +77 -0
- package/src/scoped-observer-worker-r136.mjs +65 -0
- package/src/version.mjs +1 -1
- package/test/commander-wire-contract-r47.test.mjs +8 -5
- package/test/r130-release-security-contract.test.mjs +7 -7
- package/test/r131-installed-manifest-boundary.test.mjs +65 -0
- package/test/r132-release-identity.test.mjs +44 -0
- package/test/r135-autonomous-scope.test.mjs +103 -0
- package/test/r136-scoped-command-rights.test.mjs +39 -0
- package/test/r136-scoped-observer.test.mjs +89 -0
- package/test/r137-commander-config-recovery.test.mjs +111 -0
- package/test/r137-commander-three-version-contract.test.mjs +56 -0
- package/test/r138-commander-process-pagination.test.mjs +42 -0
- package/test/r138-commander-session-list.test.mjs +24 -0
- package/test/r138-commander-stream-resume.test.mjs +50 -0
- package/test/r139-large-text-edit.test.mjs +51 -0
- package/test/r139-large-text-read.test.mjs +46 -0
- package/test/r139-process-start-preview.test.mjs +50 -0
- package/test/r139-search-partial.test.mjs +42 -0
- package/test/r140-large-text-integrity.test.mjs +68 -0
- package/test/r141-commander-tail-utf8.test.mjs +55 -0
- package/test/r141-real-windows-disk.test.mjs +71 -0
- package/test/r142-native-machinefs-authority.test.mjs +127 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- 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
|
|
211
|
+
let tailBytes=Buffer.from(tail.data_base64,'base64');
|
|
211
212
|
if((tail.offset??0)>0){
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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);
|
package/src/release-slot.mjs
CHANGED
|
@@ -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)
|
|
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
|
@@ -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
|
|
410
|
-
data_base64:Buffer.from('
|
|
411
|
-
file_sha256:null,file_size:1024,eof:false,offset:
|
|
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('
|
|
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,
|
|
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.
|
|
17
|
-
assert.equal(release.train,'
|
|
18
|
-
assert.equal(matrix.train,'
|
|
19
|
-
assert.equal(release.source_predecessor.version,'0.15.
|
|
20
|
-
assert.match(release.source_predecessor.source_path,/0\.15\.
|
|
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.
|
|
23
|
-
assert.equal(provenance.source_predecessor_manifest_file_sha256,'
|
|
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
|
+
});
|