deadbyte-mcp 0.15.51 → 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 (43) hide show
  1. package/MANIFEST.SHA256 +42 -15
  2. package/PROVENANCE.json +19 -9
  3. package/README.txt +18 -18
  4. package/RELEASE.json +19 -6
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +17 -2
  7. package/docs/R135-AUTONOMOUS-SCOPE.md +21 -0
  8. package/docs/R136-SCOPED-OBSERVER.md +21 -0
  9. package/docs/R137-DESKTOP-COMMANDER-FOUNDATION.md +31 -0
  10. package/docs/R138-COMMANDER-WORKFLOW.md +21 -0
  11. package/docs/R139-COMMANDER-ERGONOMICS.md +17 -0
  12. package/docs/THIRD-PARTY-DESKTOP-COMMANDER-MIT.md +27 -0
  13. package/package.json +1 -1
  14. package/src/autonomous-scope-r135.mjs +91 -0
  15. package/src/commander-config-recovery-r137.mjs +67 -0
  16. package/src/commander-extended-runtime.mjs +22 -5
  17. package/src/commander-external-process-r48.mjs +25 -3
  18. package/src/commander-large-text-r139.mjs +66 -0
  19. package/src/commander-wire-adapter-r47.mjs +63 -12
  20. package/src/scoped-observer-launcher-r136.mjs +77 -0
  21. package/src/scoped-observer-worker-r136.mjs +65 -0
  22. package/src/version.mjs +1 -1
  23. package/test/commander-wire-contract-r47.test.mjs +8 -5
  24. package/test/r130-release-security-contract.test.mjs +7 -7
  25. package/test/r132-release-identity.test.mjs +9 -9
  26. package/test/r135-autonomous-scope.test.mjs +103 -0
  27. package/test/r136-scoped-command-rights.test.mjs +39 -0
  28. package/test/r136-scoped-observer.test.mjs +89 -0
  29. package/test/r137-commander-config-recovery.test.mjs +111 -0
  30. package/test/r137-commander-three-version-contract.test.mjs +56 -0
  31. package/test/r138-commander-process-pagination.test.mjs +42 -0
  32. package/test/r138-commander-session-list.test.mjs +24 -0
  33. package/test/r138-commander-stream-resume.test.mjs +50 -0
  34. package/test/r139-large-text-edit.test.mjs +51 -0
  35. package/test/r139-large-text-read.test.mjs +46 -0
  36. package/test/r139-process-start-preview.test.mjs +50 -0
  37. package/test/r139-search-partial.test.mjs +42 -0
  38. package/test/r140-large-text-integrity.test.mjs +68 -0
  39. package/test/r141-commander-tail-utf8.test.mjs +55 -0
  40. package/test/r141-real-windows-disk.test.mjs +71 -0
  41. package/test/r142-native-machinefs-authority.test.mjs +127 -0
  42. package/test/r59-tool-surface-compat.test.mjs +1 -1
  43. package/test/r74-prepublish-contract.test.mjs +3 -3
@@ -0,0 +1,91 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {existsSync,lstatSync,realpathSync} from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ // R135 is an offline POLICY PROJECTION only. It cannot mint a lease or grant,
6
+ // modify an active generation, or make a broad manual Coding grant safe to
7
+ // inherit in the same process. Execution requires a separate proven boundary.
8
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
9
+ const clone=value=>structuredClone(value);
10
+ function realProjectRoot(input){
11
+ if(typeof input!=='string'||!path.isAbsolute(input))throw new Error('real project directory required');
12
+ const candidate=path.resolve(input),disk=path.parse(candidate).root;
13
+ const eq=(a,b)=>process.platform==='win32'?a.toLowerCase()===b.toLowerCase():a===b;
14
+ if(eq(candidate,disk))throw new Error('project-scoped root required');
15
+ try{
16
+ const info=lstatSync(candidate);
17
+ if(!info.isDirectory()||info.isSymbolicLink()||!eq(realpathSync(candidate),candidate)){
18
+ throw new Error('path redirected');
19
+ }
20
+ let cursor=candidate;
21
+ for(;;){
22
+ if(lstatSync(cursor).isSymbolicLink())throw new Error('linked ancestor');
23
+ const parent=path.dirname(cursor);
24
+ if(parent===cursor)break;
25
+ cursor=parent;
26
+ }
27
+ }catch{throw new Error('real project directory required')}
28
+ return candidate;
29
+ }
30
+ export function deriveIsolatedAutonomousScope({manualCodingPolicy,autonomousPolicy,scopeRootId,isolatedStateRoot}={}){
31
+ if(manualCodingPolicy?.schema!=='deadbyte.coding-policy.v1'||manualCodingPolicy.enabled!==true)
32
+ throw new Error('manual Coding policy schema invalid');
33
+ if(autonomousPolicy?.schema!=='deadbyte.autonomous-policy.v1'||autonomousPolicy.enabled!==true)
34
+ throw new Error('Autonomous policy schema invalid');
35
+ if(!/^[A-Za-z0-9._-]{1,64}$/.test(scopeRootId??''))throw new Error('invalid project scope');
36
+ if(!path.isAbsolute(isolatedStateRoot??''))throw new Error('isolated state root must be absolute');
37
+ const scopedRoot=manualCodingPolicy.roots?.[scopeRootId];
38
+ if(!scopedRoot)throw new Error('approved project root missing from Coding policy');
39
+ if(!Array.isArray(autonomousPolicy.root_ids)||autonomousPolicy.root_ids.length!==1||
40
+ autonomousPolicy.root_ids[0]!==scopeRootId)throw new Error('Autonomous root outside approved scope');
41
+ const safeRoot=realProjectRoot(scopedRoot.path);
42
+ const isolated=realProjectRoot(isolatedStateRoot);
43
+ const eq=(a,b)=>process.platform==='win32'?a.toLowerCase()===b.toLowerCase():a===b;
44
+ const manualLease=manualCodingPolicy.lease_file,autonLease=autonomousPolicy.lease_file;
45
+ for(const lease of [manualLease,autonLease]){
46
+ if(typeof lease!=='string'||!path.isAbsolute(lease))throw new Error('manual policy lease invalid');
47
+ const rel=path.relative(isolated,path.resolve(lease));
48
+ if(rel===''||(!path.isAbsolute(rel)&&rel!=='..'&&!rel.startsWith('..'+path.sep))){
49
+ throw new Error('isolated state root overlaps manual Coding lease');
50
+ }
51
+ }
52
+ // Fail closed for any profiles referencing another root or missing command.
53
+ const selectedCommands={};
54
+ for(const [id,profile] of Object.entries(autonomousPolicy.profiles??{})){
55
+ if(profile?.root_id!==scopeRootId)throw new Error('Autonomous profile '+id+' root outside approved scope');
56
+ const command=profile.command_id;
57
+ if(typeof command!=='string'||!Object.hasOwn(manualCodingPolicy.commands??{},command)){
58
+ throw new Error('Autonomous command missing from Coding policy');
59
+ }
60
+ const selected=clone(manualCodingPolicy.commands[command]);
61
+ if(!Array.isArray(selected.root_ids)||!selected.root_ids.includes(scopeRootId)){
62
+ throw new Error('Autonomous command '+command+' outside approved scope');
63
+ }
64
+ selected.root_ids=[scopeRootId];
65
+ selectedCommands[command]=selected;
66
+ }
67
+ if(autonomousPolicy.planner?.backend==='codex_app_server'&&
68
+ autonomousPolicy.planner.root_id!==scopeRootId)throw new Error('planner root outside approved scope');
69
+ const codingPolicy=clone(manualCodingPolicy);
70
+ codingPolicy.roots={[scopeRootId]:{...clone(scopedRoot),path:safeRoot}};
71
+ codingPolicy.commands=selectedCommands;
72
+ codingPolicy.lease_file=path.join(isolated,'runtime','coding-grant.json');
73
+ codingPolicy.evidence_dir=path.join(isolated,'evidence','coding');
74
+ if(existsSync(codingPolicy.lease_file)||existsSync(path.join(isolated,'runtime','autonomous-grant.json'))){
75
+ throw new Error('isolated grant file already present');
76
+ }
77
+ const codingBytes=Buffer.from(JSON.stringify(codingPolicy,null,2)+'\n','utf8');
78
+ const nextAutonomous=clone(autonomousPolicy);
79
+ nextAutonomous.root_ids=[scopeRootId];
80
+ nextAutonomous.coding_policy_sha256=sha(codingBytes);
81
+ nextAutonomous.lease_file=path.join(isolated,'runtime','autonomous-grant.json');
82
+ nextAutonomous.evidence_dir=path.join(isolated,'evidence','autonomous');
83
+ const autonomousBytes=Buffer.from(JSON.stringify(nextAutonomous,null,2)+'\n','utf8');
84
+ return Object.freeze({
85
+ scope_root_id:scopeRootId,isolated_state_root:isolated,
86
+ codingBytes,autonomousBytes,
87
+ codingPolicy,autonomousPolicy:nextAutonomous,
88
+ coding_sha256:sha(codingBytes),autonomous_sha256:sha(autonomousBytes),
89
+ status:'OFFLINE_UNAUTHORIZED_PROJECTION'
90
+ });
91
+ }
@@ -0,0 +1,67 @@
1
+ import {createHash,randomUUID} from 'node:crypto';
2
+ import {readFile,writeFile,rename} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ // Reference design: Desktop Commander 0.3.0 MIT config-recovery.
6
+ // DEADBYTE independently restricts salvage to NON-AUTHORITY display limits;
7
+ // privileged configuration never flows into this recovery path.
8
+ const MAX_BYTES=1024*1024;
9
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
10
+ const isObject=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
11
+
12
+ export function salvageSafeCommanderLimits(raw){
13
+ if(typeof raw!=='string'||Buffer.byteLength(raw,'utf8')>MAX_BYTES)return {};
14
+ let str=raw.charCodeAt(0)===0xfeff?raw.slice(1):raw;
15
+ if(!str.trimStart().startsWith('{'))return {};
16
+ const base=str.indexOf('{'),ends=[];
17
+ let depth=0,quoted=false,escaped=false;
18
+ for(let i=base;i<str.length;i++){
19
+ const c=str[i];
20
+ if(quoted){if(escaped)escaped=false;else if(c==='\\')escaped=true;else if(c==='"')quoted=false;continue;}
21
+ if(c==='"'){quoted=true;continue;}
22
+ if(c==='{'||c==='['){depth++;continue;}
23
+ if(c==='}'||c===']'){depth--;if(depth===0){ends.push({index:i+1,complete:true});break;}if(depth<0)break;continue;}
24
+ if(c===','&&depth===1)ends.push({index:i,complete:false});
25
+ }
26
+ let obj={};
27
+ for(const item of ends.reverse()){
28
+ try{
29
+ const v=JSON.parse(str.slice(base,item.index)+(item.complete?'':'}'));
30
+ if(isObject(v)){obj=v;break;}
31
+ }catch{}
32
+ }
33
+ const safe={};
34
+ for(const [key,max] of [['fileReadLineLimit',1000],['fileWriteLineLimit',50]]){
35
+ const value=obj[key];
36
+ if(Number.isSafeInteger(value)&&value>=1&&value<=max)safe[key]=value;
37
+ }
38
+ return safe;
39
+ }
40
+
41
+ export async function recoverCorruptCommanderConfig(file,bytes,defaults){
42
+ if(!Buffer.isBuffer(bytes)||bytes.length>MAX_BYTES)throw new Error('Commander config corrupt or oversized; manual inspection required');
43
+ if(!isObject(defaults))throw new Error('recovery defaults invalid');
44
+ const safe=salvageSafeCommanderLimits(bytes.toString('utf8'));
45
+ const next={...structuredClone(defaults),...safe};
46
+ // The only trusted recovered settings are bounded display limits.
47
+ next.telemetryEnabled=false;
48
+ next.allowedDirectories=[];
49
+ next.blockedCommands=structuredClone(defaults.blockedCommands);
50
+ const current=await readFile(file);
51
+ if(sha(current)!==sha(bytes))throw new Error('Commander config changed during recovery; stop');
52
+ const backup=path.basename(file)+'.corrupt.'+Date.now()+'.'+randomUUID();
53
+ const backupPath=path.join(path.dirname(file),backup);
54
+ await writeFile(backupPath,bytes,{flag:'wx',mode:0o600});
55
+ const tmp=file+'.recover-'+randomUUID();
56
+ try{
57
+ await writeFile(tmp,JSON.stringify(next)+'\n',{flag:'wx',mode:0o600});
58
+ const verify=await readFile(file);
59
+ if(sha(verify)!==sha(bytes))throw new Error('Commander config changed during recovery; stop');
60
+ await rename(tmp,file);
61
+ }catch(error){
62
+ const {rm}=await import('node:fs/promises');
63
+ await rm(tmp,{force:true}).catch(()=>{});
64
+ throw error;
65
+ }
66
+ return {config:next,recovery:{status:'recovered',backup_filename:backup,corrupt_sha256:sha(bytes),salvaged_keys:Object.keys(safe)}};
67
+ }
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { canonicalJson } from './canonical-json.mjs';
5
5
  import { createDeadbytePdf } from './commander-pdf-r48.mjs';
6
6
  import { DEADBYTE_VERSION } from './version.mjs';
7
+ import {recoverCorruptCommanderConfig} from './commander-config-recovery-r137.mjs';
7
8
 
8
9
  export const COMMANDER_EXTENDED_SCHEMA='deadbyte.commander-extended.v1';
9
10
  const MAX_VALUE=16384,MAX_FEEDBACK=4096,MAX_PREVIEW=65536;
@@ -44,17 +45,33 @@ function makePdf(text){
44
45
  export function createCommanderExtendedRuntime({stateRoot,history=null,prompts=DEFAULT_PROMPTS,resources=[]}={}){
45
46
  if(typeof stateRoot!=='string'||!stateRoot)throw new Error('extended runtime stateRoot required');
46
47
  const configPath=path.join(stateRoot,'commander-config.json'),feedbackDir=path.join(stateRoot,'feedback');
47
- let configCache=null;
48
+ let configCache=null,recovery=null,loadPromise=null;
48
49
  async function loadConfig(){
49
50
  if(configCache)return configCache;
50
- try{configCache={...structuredClone(COMPAT_CONFIG_DEFAULTS),...JSON.parse(await readFile(configPath,'utf8'))};}
51
- catch(e){if(e?.code!=='ENOENT')throw e;configCache=structuredClone(COMPAT_CONFIG_DEFAULTS);}
52
- return configCache;
51
+ if(!loadPromise)loadPromise=(async()=>{
52
+ let bytes;
53
+ try{bytes=await readFile(configPath);}
54
+ catch(e){
55
+ if(e?.code!=='ENOENT')throw e;
56
+ configCache=structuredClone(COMPAT_CONFIG_DEFAULTS);return configCache;
57
+ }
58
+ try{
59
+ const parsed=JSON.parse(bytes.toString('utf8'));
60
+ if(!parsed||typeof parsed!=='object'||Array.isArray(parsed))throw new SyntaxError('config must be an object');
61
+ configCache={...structuredClone(COMPAT_CONFIG_DEFAULTS),...parsed};
62
+ }catch(e){
63
+ if(!(e instanceof SyntaxError))throw e;
64
+ const result=await recoverCorruptCommanderConfig(configPath,bytes,COMPAT_CONFIG_DEFAULTS);
65
+ configCache=result.config;recovery=result.recovery;
66
+ }
67
+ return configCache;
68
+ })();
69
+ try{return await loadPromise;}finally{loadPromise=null;}
53
70
  }
54
71
  async function getConfig({key=null}={}){
55
72
  const cfg=await loadConfig();
56
73
  if(key!==null&&!keyOk(key))throw new Error('config key invalid');
57
- return {schema:COMMANDER_EXTENDED_SCHEMA,status:'ok',config:key===null?structuredClone(cfg):{[key]:cfg[key]},config_sha256:sha256(cfg)};
74
+ return {schema:COMMANDER_EXTENDED_SCHEMA,status:'ok',config:key===null?structuredClone(cfg):{[key]:cfg[key]},config_sha256:sha256(cfg),...(recovery?{recovery:structuredClone(recovery)}:{})};
58
75
  }
59
76
  async function setConfigValue({key,value,expected_sha256}){
60
77
  if(!keyOk(key))throw new Error('config key invalid');
@@ -61,17 +61,39 @@ export function createCommanderExternalProcessRuntime({observationRuntime,hostPo
61
61
  let cursor=null,first=null;
62
62
  const processes=[];
63
63
  const receiptIds=[];
64
+ const cursorSet=new Set(),pidSet=new Set();
64
65
  do{
65
66
  const page=await observationRuntime.processList({limit:PAGE_SIZE,includeExternal:true,cursor});
66
67
  if(page?.status!=='ok'||!Array.isArray(page.processes)) throw new Error('external process observation failed');
68
+ if(typeof page.snapshot_id!=='string'||!/^[0-9a-f]{64}$/.test(page.snapshot_id))
69
+ throw new Error('external process observation snapshot identity invalid');
70
+ if(typeof page.observed_at_utc!=='string'||!ISO_UTC.test(page.observed_at_utc)||
71
+ !Number.isFinite(Date.parse(page.observed_at_utc)))
72
+ throw new Error('external process observation timestamp invalid');
67
73
  if(!first) first=page;
68
- else if(page.snapshot_id!==first.snapshot_id) throw new Error('external process observation snapshot changed during pagination');
74
+ else{
75
+ if(page.snapshot_id!==first.snapshot_id) throw new Error('external process observation snapshot changed during pagination');
76
+ if(page.observed_at_utc!==first.observed_at_utc)
77
+ throw new Error('external process observation time changed during pagination');
78
+ }
79
+ if(page.processes.length>PAGE_SIZE)throw new Error('external process observation page exceeds bounded limit');
69
80
  for(const item of page.processes){
70
81
  if(processes.length>=MAX_PROCESSES) throw new Error('external process observation exceeds bounded process cap');
71
- processes.push(normalizeIdentity(item));
82
+ const identity=normalizeIdentity(item);
83
+ if(pidSet.has(identity.pid))throw new Error('external process observation duplicate PID');
84
+ pidSet.add(identity.pid);
85
+ processes.push(identity);
72
86
  }
73
87
  if(typeof page.receipt_id==='string') receiptIds.push(page.receipt_id);
74
- cursor=page.next_cursor??null;
88
+ const next=page.next_cursor??null;
89
+ if(next!==null){
90
+ if(typeof next!=='string'||next.length<1||next.length>8192)throw new Error('external process observation cursor invalid');
91
+ if(cursorSet.has(next))throw new Error('external process pagination cursor repeated');
92
+ cursorSet.add(next);
93
+ if(cursorSet.size>Math.ceil(MAX_PROCESSES/PAGE_SIZE)+1)
94
+ throw new Error('external process pagination exceeded page limit');
95
+ }
96
+ cursor=next;
75
97
  }while(cursor!==null);
76
98
  return {
77
99
  status:'ok',
@@ -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);
@@ -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.51';
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()=>{