deadbyte-mcp 0.15.14 → 0.15.16
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 +65 -27
- package/PROVENANCE.json +6 -6
- package/README.txt +6 -6
- package/RELEASE.json +58 -53
- package/docs/COMMANDER_EXHAUSTIVE_SURFACE.json +49 -0
- package/docs/COMMANDER_REFERENCE_INVENTORY.json +70 -0
- package/docs/R47-FULL-TOOL-CATALOG.json +123 -0
- package/package.json +5 -2
- package/scripts/commander-hardening-gate-r47.mjs +35 -0
- package/scripts/commander-parity-benchmark.mjs +6 -0
- package/scripts/commander-real-performance-benchmark-r47.mjs +41 -0
- package/scripts/deploy-r47.mjs +67 -8
- package/scripts/full-regression-r47.mjs +28 -0
- package/scripts/inventory-reference-dist.mjs +17 -0
- package/scripts/npm-release-r47.mjs +25 -1
- package/scripts/r47-syntax-check.mjs +6 -0
- package/scripts/r47-wire-remote-plane.mjs +23 -0
- package/src/atomic-mutation.mjs +59 -29
- package/src/commander-compat-contracts.mjs +90 -0
- package/src/commander-compat-runtime.mjs +91 -0
- package/src/commander-core-bridge.mjs +30 -0
- package/src/commander-extended-runtime.mjs +74 -0
- package/src/commander-observability.mjs +33 -0
- package/src/commander-remote-bridge-r47.mjs +41 -0
- package/src/commander-remote-relay-r47.mjs +44 -0
- package/src/machine-fs.mjs +108 -22
- package/src/mcp-server.mjs +94 -0
- package/src/observation-output-contracts.mjs +9 -8
- package/src/observation-runtime.mjs +47 -17
- package/src/operation-history.mjs +95 -0
- package/src/operation-lifecycle.mjs +63 -0
- package/src/process-output-contracts.mjs +24 -1
- package/src/process-readiness.mjs +120 -0
- package/src/process-runtime.mjs +178 -6
- package/src/release-slot.mjs +296 -293
- package/src/runtime-update.mjs +61 -7
- package/src/tool-audit.mjs +15 -2
- package/src/version.mjs +1 -2
- package/test/OBSERVED-COMMANDER-TOOLS.json +1 -0
- package/test/agent-runtime.test.mjs +1 -1
- package/test/autonomous-runtime.test.mjs +1 -1
- package/test/coding-plane.test.mjs +1 -1
- package/test/coding-search-r36.test.mjs +14 -0
- package/test/commander-compat-runtime.test.mjs +34 -0
- package/test/commander-core-bridge-r47.test.mjs +31 -0
- package/test/commander-exhaustive-surface-r47.test.mjs +32 -0
- package/test/commander-extended-runtime-r47.test.mjs +46 -0
- package/test/commander-observability-r47.test.mjs +34 -0
- package/test/commander-parity-gate-r47.test.mjs +49 -0
- package/test/commander-protocol-r47.test.mjs +30 -0
- package/test/commander-real-parity-r47.test.mjs +121 -0
- package/test/commander-remote-bridge-r47.test.mjs +39 -0
- package/test/commander-remote-plane-r47.test.mjs +25 -0
- package/test/commander-torture-r47.test.mjs +146 -0
- package/test/full-max-catalog-r46.test.mjs +1 -1
- package/test/full-max-catalog-r47.test.mjs +78 -0
- package/test/machine-fs-runtime.test.mjs +97 -0
- package/test/observation-runtime.test.mjs +28 -0
- package/test/operation-history.test.mjs +61 -0
- package/test/operation-lifecycle.test.mjs +72 -0
- package/test/output-schema.test.mjs +1 -1
- package/test/process-readiness.test.mjs +32 -0
- package/test/process-runtime.test.mjs +147 -0
- package/test/r47-release-contract.test.mjs +37 -0
- package/test/release-activation-gate.test.mjs +30 -0
- package/test/release-version.test.mjs +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { COMMANDER_COMPAT_TOOLS, DEADBYTE_NATIVE_TOOLS } from './commander-compat-contracts.mjs';
|
|
3
|
+
|
|
4
|
+
export const COMMANDER_COMPAT_RUNTIME_SCHEMA = 'deadbyte.commander-compat-runtime.v2';
|
|
5
|
+
const sha256 = value => createHash('sha256').update(JSON.stringify(value), 'utf8').digest('hex');
|
|
6
|
+
const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
7
|
+
|
|
8
|
+
const CONTRACTS = Object.freeze({
|
|
9
|
+
contained_transform:{native:'contained.transform',required:['operation','input'],mutation:false},
|
|
10
|
+
create_directory:{native:'machine_fs.mkdir',required:['root_id','path'],mutation:true},
|
|
11
|
+
edit_block:{native:'native_diff',required:['root_id','path','expected_sha256'],mutation:true},
|
|
12
|
+
evidence_verify:{native:'evidence.verify',required:['receipt'],mutation:false},
|
|
13
|
+
file_preview:{native:'commander.file_preview',required:['root_id','path'],mutation:false},
|
|
14
|
+
file_read:{native:'machine_fs.read',required:['root_id','path'],mutation:false},
|
|
15
|
+
force_terminate:{native:'process.terminate',required:['session_id'],mutation:true},
|
|
16
|
+
get_config:{native:'commander.config.get',required:[],mutation:false},
|
|
17
|
+
get_file_info:{native:'machine_fs.stat',required:['root_id','path'],mutation:false},
|
|
18
|
+
get_more_search_results:{native:'machine_search.page',required:['search_id'],mutation:false},
|
|
19
|
+
get_prompts:{native:'commander.prompts',required:[],mutation:false},
|
|
20
|
+
get_recent_tool_calls:{native:'operation_history.recent',required:[],mutation:false},
|
|
21
|
+
get_usage_stats:{native:'operation_history.stats',required:[],mutation:false},
|
|
22
|
+
give_feedback_to_desktop_commander:{native:'commander.feedback',required:['message'],mutation:true},
|
|
23
|
+
host_exec:{native:'host.exec',required:['command_id','root_id','script_path','script_sha256'],mutation:true},
|
|
24
|
+
host_file_read:{native:'host.file_read',required:['path'],mutation:false},
|
|
25
|
+
host_file_write:{native:'host.file_write',required:['path','content','expected_sha256'],mutation:true},
|
|
26
|
+
interact_with_process:{native:'process.input',required:['session_id','input'],mutation:true},
|
|
27
|
+
kill_process:{native:'process.terminate',required:['session_id'],mutation:true},
|
|
28
|
+
list_directory:{native:'machine_fs.list',required:['root_id','path'],mutation:false},
|
|
29
|
+
list_processes:{native:'process.sessions',required:[],mutation:false},
|
|
30
|
+
list_searches:{native:'machine_search.list',required:[],mutation:false},
|
|
31
|
+
list_sessions:{native:'process.sessions',required:[],mutation:false},
|
|
32
|
+
markdown_editor:{native:'native_diff',required:['root_id','path','expected_sha256'],mutation:true},
|
|
33
|
+
move_file:{native:'machine_fs.move',required:['root_id','source_path','destination_path','expected_source_identity'],mutation:true},
|
|
34
|
+
proof_execute:{native:'proof.execute',required:['operation'],mutation:false},
|
|
35
|
+
read_file:{native:'machine_fs.read',required:['root_id','path'],mutation:false},
|
|
36
|
+
read_multiple_files:{native:'machine_fs.read_many',required:['root_id','paths'],mutation:false},
|
|
37
|
+
read_process_output:{native:'process.output',required:['session_id'],mutation:false},
|
|
38
|
+
remote_device:{native:'remote_device',required:['device_id'],mutation:false},
|
|
39
|
+
remote_mcp:{native:'remote_mcp',required:['request_id'],mutation:false},
|
|
40
|
+
resources:{native:'commander.resources',required:[],mutation:false},
|
|
41
|
+
set_config_value:{native:'commander.config.set',required:['key','value','expected_sha256'],mutation:true},
|
|
42
|
+
skill_catalog:{native:'skill.catalog',required:[],mutation:false},
|
|
43
|
+
start_process:{native:'process.start',required:['profile_id','root_id'],mutation:true},
|
|
44
|
+
start_search:{native:'machine_search.start',required:['root_id','path','pattern'],mutation:false},
|
|
45
|
+
stop_search:{native:'machine_search.cancel',required:['search_id'],mutation:false},
|
|
46
|
+
workspace_open:{native:'workspace.open',required:['root_id'],mutation:false},
|
|
47
|
+
write_file:{native:'machine_fs.write',required:['root_id','path','content','expected_sha256'],mutation:true},
|
|
48
|
+
write_pdf:{native:'document.write_pdf',required:['root_id','path','content','expected_sha256'],mutation:true}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export function commanderCompatibilityContract(name) {
|
|
52
|
+
if (!COMMANDER_COMPAT_TOOLS.includes(name)) throw new Error(`unsupported Commander capability: ${name}`);
|
|
53
|
+
return CONTRACTS[name] ?? {native:DEADBYTE_NATIVE_TOOLS[name] ?? null,required:[],mutation:false};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateCommanderRequest(name,args={}) {
|
|
57
|
+
const c=commanderCompatibilityContract(name);
|
|
58
|
+
if (!args || typeof args!=='object' || Array.isArray(args)) throw new Error('request arguments must be an object');
|
|
59
|
+
const missing=c.required.filter(k=>!own(args,k)||args[k]===undefined||args[k]===null||args[k]==='');
|
|
60
|
+
if(missing.length) throw new Error(`missing required fields: ${missing.join(', ')}`);
|
|
61
|
+
if(c.mutation && !['create_directory','start_process','force_terminate','kill_process','interact_with_process','host_exec'].includes(name) && !('expected_sha256' in args) && !('expected_source_identity' in args)) throw new Error('mutation requires an explicit precondition');
|
|
62
|
+
if((name==='host_exec') && typeof args.script_sha256!=='string') throw new Error('host_exec requires hash-pinned script');
|
|
63
|
+
return Object.freeze({commander_tool:name,native_tool:c.native,mutation:c.mutation,args});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createCommanderCompatibilityRuntime({invoke,snapshot}) {
|
|
67
|
+
if(typeof invoke!=='function') throw new Error('invoke function required');
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
schema:COMMANDER_COMPAT_RUNTIME_SCHEMA,
|
|
70
|
+
async call(name,args={}) {
|
|
71
|
+
const request=validateCommanderRequest(name,args);
|
|
72
|
+
const before=typeof snapshot==='function'?await snapshot(request):null;
|
|
73
|
+
const result=await invoke(request.native_tool,request.args);
|
|
74
|
+
const after=typeof snapshot==='function'?await snapshot(request):null;
|
|
75
|
+
const resultHash=sha256(result);
|
|
76
|
+
const receipt={schema:COMMANDER_COMPAT_RUNTIME_SCHEMA,commander_tool:name,native_tool:request.native_tool,request_sha256:sha256(request),before_sha256:sha256(before),result_sha256:resultHash,after_sha256:sha256(after),verified:true};
|
|
77
|
+
return {status:'ok',result,receipt};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function assertNoCommanderDependency(sourceText) {
|
|
83
|
+
if(typeof sourceText!=='string') throw new Error('source text must be a string');
|
|
84
|
+
if(/@wonderwhy-er\/desktop-commander|desktop-commander\/dist/.test(sourceText)) throw new Error('native DEADBYTE source must not depend on Commander package');
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function commanderCompatibilityCoverage() {
|
|
89
|
+
const covered=COMMANDER_COMPAT_TOOLS.filter(name=>CONTRACTS[name]?.native);
|
|
90
|
+
return Object.freeze({schema:COMMANDER_COMPAT_RUNTIME_SCHEMA,covered:covered.length,total:COMMANDER_COMPAT_TOOLS.length,missing:COMMANDER_COMPAT_TOOLS.filter(name=>!CONTRACTS[name]?.native),coverage_sha256:sha256(covered.map(name=>[name,CONTRACTS[name].native]))});
|
|
91
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
const anyArgs=z.record(z.string(),z.unknown());
|
|
3
|
+
const sessions=new Map(); let sessionCounter=0;
|
|
4
|
+
function denied(tool,reason='required native runtime is disabled'){return {status:'denied',tool,error:reason};}
|
|
5
|
+
function register(server,name,schema,handler){server.registerTool(name,{description:'Native Commander-compatible adapter; execution remains inside DEADBYTE policy and evidence boundaries.',inputSchema:schema},handler);}
|
|
6
|
+
function fsOr(runtime,name,fn){return runtime?fn(runtime):denied(name);}
|
|
7
|
+
function processOr(runtime,name,fn){return runtime?fn(runtime):denied(name);}
|
|
8
|
+
function mapResult(item,type){return type==='files'?{file:item.path,type:'file'}:{file:item.path,line:item.line,match:item.preview,type:'content'};}
|
|
9
|
+
function queryArgs(a){return {rootId:a.root_id??'core',path:a.path,pattern:a.pattern,target:a.searchType==='content'?'content':'path',mode:a.literalSearch===true?'literal':'regex',caseSensitive:a.ignoreCase===false,maxResults:Number.isInteger(a.maxResults)?Math.min(Math.max(a.maxResults,1),1000):200,maxFileBytes:262144,maxScanBytes:64*1024*1024,pageSize:500,cursor:null};}
|
|
10
|
+
export function registerCommanderCoreBridge(server,{machineFsRuntime=null,processRuntime=null,searchRuntime=null,remoteDevicePlane=null}={}){
|
|
11
|
+
register(server,'read_file',z.strictObject({root_id:z.string().min(1).max(64),path:z.string().min(1).max(2048),offset:z.number().int().min(0).default(0),max_bytes:z.number().int().min(1).max(4194304).default(65536),hash_file:z.boolean().default(false),timeout_ms:z.number().int().min(100).max(180000).default(30000)}),a=>fsOr(machineFsRuntime,'read_file',r=>r.read({rootId:a.root_id,relativePath:a.path,offset:a.offset,maxBytes:a.max_bytes,hashFile:a.hash_file,timeoutMs:a.timeout_ms})));
|
|
12
|
+
register(server,'read_multiple_files',z.strictObject({root_id:z.string().min(1).max(64),paths:z.array(z.string().min(1).max(2048)).min(1).max(64),max_bytes:z.number().int().min(1).max(4194304).default(65536),timeout_ms:z.number().int().min(100).max(180000).default(30000)}),a=>fsOr(machineFsRuntime,'read_multiple_files',r=>r.readMany({rootId:a.root_id,relativePaths:a.paths,maxBytes:a.max_bytes,timeoutMs:a.timeout_ms})));
|
|
13
|
+
register(server,'write_file',z.strictObject({root_id:z.string().min(1).max(64),path:z.string().min(1).max(2048),content:z.string().max(12582912),encoding:z.enum(['utf8','base64']).default('utf8'),expected_sha256:z.string().regex(/^(?:absent|[0-9a-f]{64})$/)}),a=>fsOr(machineFsRuntime,'write_file',r=>r.write({rootId:a.root_id,relativePath:a.path,content:a.content,encoding:a.encoding,expectedSha256:a.expected_sha256})));
|
|
14
|
+
register(server,'create_directory',z.strictObject({root_id:z.string().min(1).max(64),path:z.string().min(1).max(2048)}),a=>fsOr(machineFsRuntime,'create_directory',r=>r.mkdir({rootId:a.root_id,relativePath:a.path})));
|
|
15
|
+
register(server,'list_directory',z.strictObject({root_id:z.string().min(1).max(64),path:z.string().min(1).max(2048).default('.'),cursor:z.string().max(4096).nullable().default(null),limit:z.number().int().min(1).max(1000).default(100),hash_files:z.boolean().default(false)}),a=>fsOr(machineFsRuntime,'list_directory',r=>r.list({rootId:a.root_id,relativePath:a.path,cursor:a.cursor,limit:a.limit,hashFiles:a.hash_files})));
|
|
16
|
+
register(server,'move_file',z.strictObject({root_id:z.string().min(1).max(64),source_path:z.string().min(1).max(2048),destination_path:z.string().min(1).max(2048),expected_source_identity:z.string().regex(/^[0-9a-f]{64}$/)}),a=>fsOr(machineFsRuntime,'move_file',r=>r.move({rootId:a.root_id,sourcePath:a.source_path,destinationPath:a.destination_path,expectedSourceIdentity:a.expected_source_identity})));
|
|
17
|
+
register(server,'get_file_info',z.strictObject({root_id:z.string().min(1).max(64),path:z.string().min(1).max(2048),hash:z.boolean().default(false)}),a=>fsOr(machineFsRuntime,'get_file_info',r=>r.stat({rootId:a.root_id,relativePath:a.path,hash:a.hash})));
|
|
18
|
+
register(server,'edit_block',z.strictObject({root_id:z.string().min(1).max(64),path:z.string().min(1).max(2048),expected_sha256:z.string().regex(/^[0-9a-f]{64}$/),old_string:z.string().min(1).max(262144),new_string:z.string().max(524288),expected_replacements:z.number().int().min(1).max(64).default(1)}),a=>fsOr(machineFsRuntime,'edit_block',r=>r.editExact({rootId:a.root_id,relativePath:a.path,expectedSha256:a.expected_sha256,oldString:a.old_string,newString:a.new_string,expectedReplacements:a.expected_replacements})));
|
|
19
|
+
register(server,'start_process',anyArgs,a=>processOr(processRuntime,'start_process',r=>r.start(a)));
|
|
20
|
+
register(server,'read_process_output',anyArgs,a=>processOr(processRuntime,'read_process_output',r=>r.read(a)));
|
|
21
|
+
register(server,'interact_with_process',anyArgs,a=>processOr(processRuntime,'interact_with_process',r=>r.action(a)));
|
|
22
|
+
register(server,'force_terminate',anyArgs,a=>processOr(processRuntime,'force_terminate',r=>r.terminate(a)));
|
|
23
|
+
register(server,'list_sessions',z.strictObject({}),()=>processOr(processRuntime,'list_sessions',r=>r.list()));
|
|
24
|
+
register(server,'list_processes',z.strictObject({}),()=>processOr(processRuntime,'list_processes',r=>r.list()));
|
|
25
|
+
register(server,'kill_process',anyArgs,a=>processOr(processRuntime,'kill_process',r=>r.cancel(a)));
|
|
26
|
+
register(server,'start_search',z.strictObject({path:z.string().min(1),pattern:z.string().min(1).max(256),searchType:z.enum(['files','content']).default('files'),filePattern:z.string().optional(),ignoreCase:z.boolean().default(true),maxResults:z.number().int().min(1).max(1000).optional(),includeHidden:z.boolean().default(false),contextLines:z.number().int().min(0).max(50).default(5),timeout_ms:z.number().int().min(100).max(120000).optional(),earlyTermination:z.boolean().optional(),literalSearch:z.boolean().default(false),origin:z.enum(['ui','llm']).optional()}),async a=>{if(!searchRuntime)return denied('start_search','dedicated search runtime is disabled');const id='search-'+(++sessionCounter)+'-'+Date.now().toString(36);const s={id,searchType:a.searchType,pattern:a.pattern,startTime:Date.now(),isComplete:false,isError:false,results:[],totalMatches:0,wasIncomplete:false};sessions.set(id,s);s.promise=(async()=>{try{const r=await searchRuntime(queryArgs(a));s.results=r.matches.map(x=>mapResult(x,a.searchType)).slice(0,a.maxResults??200);s.totalMatches=r.matches.length;s.wasIncomplete=r.truncated===true;s.isComplete=true;}catch(e){s.isError=true;s.error=String(e?.message??e).slice(0,512);s.isComplete=true;}})();return {sessionId:id,isComplete:false,isError:false,results:[],totalResults:0,runtime:0};});
|
|
27
|
+
register(server,'get_more_search_results',z.strictObject({sessionId:z.string().min(1).max(128),offset:z.number().int().default(0),length:z.number().int().min(1).max(1000).default(100)}),async({sessionId,offset,length})=>{const s=sessions.get(sessionId);if(!s)return {status:'error',error:'search session not found'};if(!s.isComplete)return {results:[],returnedCount:0,totalResults:s.results.length,totalMatches:s.totalMatches,isComplete:false,isError:false,hasMoreResults:true,runtime:Date.now()-s.startTime};const start=offset<0?Math.max(0,s.results.length+offset):offset;const end=offset<0?s.results.length:Math.min(s.results.length,start+length);const results=s.results.slice(start,end);return {results,returnedCount:results.length,totalResults:s.results.length,totalMatches:s.totalMatches,isComplete:true,isError:s.isError,error:s.error,hasMoreResults:end<s.results.length,runtime:Date.now()-s.startTime,wasIncomplete:s.wasIncomplete};});
|
|
28
|
+
register(server,'stop_search',z.strictObject({sessionId:z.string().min(1).max(128)}),async({sessionId})=>{const s=sessions.get(sessionId);if(!s)return false;s.cancelled=true;return true;});
|
|
29
|
+
register(server,'list_searches',z.strictObject({}),async()=>[...sessions.values()].map(s=>({id:s.id,searchType:s.searchType,pattern:s.pattern,isComplete:s.isComplete,isError:s.isError,runtime:Date.now()-s.startTime,totalResults:s.results.length})));
|
|
30
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
5
|
+
|
|
6
|
+
export const COMMANDER_EXTENDED_SCHEMA='deadbyte.commander-extended.v1';
|
|
7
|
+
const MAX_VALUE=16384,MAX_FEEDBACK=4096,MAX_PREVIEW=65536;
|
|
8
|
+
const sha256=v=>createHash('sha256').update(Buffer.isBuffer(v)?v:Buffer.from(typeof v==='string'?v:canonicalJson(v),'utf8')).digest('hex');
|
|
9
|
+
const keyOk=k=>typeof k==='string'&&/^[A-Za-z0-9._-]{1,128}$/.test(k);
|
|
10
|
+
|
|
11
|
+
function pdfEscape(s){return String(s).replaceAll('\\\\','\\\\\\\\').replaceAll('(','\\\\(').replaceAll(')','\\\\)');}
|
|
12
|
+
function makePdf(text){
|
|
13
|
+
const nl=String.fromCharCode(10),lines=String(text).replaceAll(String.fromCharCode(13),'').split(nl).slice(0,200);
|
|
14
|
+
const stream=['BT','/F1 11 Tf','50 790 14 TL',...lines.map(line=>'('+pdfEscape(line).slice(0,180)+') Tj 0 -14 Td'),'ET'].join(nl);
|
|
15
|
+
const objs=['<< /Type /Catalog /Pages 2 0 R >>','<< /Type /Pages /Kids [3 0 R] /Count 1 >>','<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> /Contents 5 0 R >>','<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>','<< /Length '+Buffer.byteLength(stream)+' >>'+nl+'stream'+nl+stream+nl+'endstream'];
|
|
16
|
+
let out='%PDF-1.4'+nl,offsets=[0];
|
|
17
|
+
for(let i=0;i<objs.length;i++){offsets.push(Buffer.byteLength(out));out+=(i+1)+' 0 obj'+nl+objs[i]+nl+'endobj'+nl;}
|
|
18
|
+
const xref=Buffer.byteLength(out);
|
|
19
|
+
out+='xref'+nl+'0 6'+nl+'0000000000 65535 f '+nl+offsets.slice(1).map(o=>String(o).padStart(10,'0')+' 00000 n ').join(nl)+nl+'trailer'+nl+'<< /Size 6 /Root 1 0 R >>'+nl+'startxref'+nl+xref+nl+'%%EOF'+nl;
|
|
20
|
+
return Buffer.from(out,'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createCommanderExtendedRuntime({stateRoot,history=null,prompts=[],resources=[]}={}){
|
|
24
|
+
if(typeof stateRoot!=='string'||!stateRoot)throw new Error('extended runtime stateRoot required');
|
|
25
|
+
const configPath=path.join(stateRoot,'commander-config.json'),feedbackDir=path.join(stateRoot,'feedback');
|
|
26
|
+
let configCache=null;
|
|
27
|
+
async function loadConfig(){
|
|
28
|
+
if(configCache)return configCache;
|
|
29
|
+
try{configCache=JSON.parse(await readFile(configPath,'utf8'));}
|
|
30
|
+
catch(e){if(e?.code!=='ENOENT')throw e;configCache={};}
|
|
31
|
+
return configCache;
|
|
32
|
+
}
|
|
33
|
+
async function getConfig({key=null}={}){
|
|
34
|
+
const cfg=await loadConfig();
|
|
35
|
+
if(key!==null&&!keyOk(key))throw new Error('config key invalid');
|
|
36
|
+
return {schema:COMMANDER_EXTENDED_SCHEMA,status:'ok',config:key===null?structuredClone(cfg):{[key]:cfg[key]},config_sha256:sha256(cfg)};
|
|
37
|
+
}
|
|
38
|
+
async function setConfigValue({key,value,expected_sha256}){
|
|
39
|
+
if(!keyOk(key))throw new Error('config key invalid');
|
|
40
|
+
if(!['string','number','boolean'].includes(typeof value)&&value!==null)throw new Error('config value must be scalar');
|
|
41
|
+
if(Buffer.byteLength(typeof value==='string'?value:canonicalJson(value))>MAX_VALUE)throw new Error('config value exceeds byte limit');
|
|
42
|
+
const cfg=await loadConfig(),beforeSha=sha256(cfg);
|
|
43
|
+
if(expected_sha256!==beforeSha)throw new Error('config precondition mismatch');
|
|
44
|
+
const next={...cfg,[key]:value};
|
|
45
|
+
await mkdir(stateRoot,{recursive:true});
|
|
46
|
+
const tmp=configPath+'.tmp-'+process.pid+'-'+randomUUID();
|
|
47
|
+
await writeFile(tmp,canonicalJson(next),{encoding:'utf8',flag:'wx',mode:0o600});
|
|
48
|
+
await rename(tmp,configPath); configCache=next;
|
|
49
|
+
return {schema:COMMANDER_EXTENDED_SCHEMA,status:'committed',key,previous_sha256:beforeSha,config_sha256:sha256(next)};
|
|
50
|
+
}
|
|
51
|
+
async function feedback({message,category='general'}){
|
|
52
|
+
if(typeof message!=='string'||!message.trim()||Buffer.byteLength(message)>MAX_FEEDBACK)throw new Error('feedback message invalid');
|
|
53
|
+
if(!/^[A-Za-z0-9._-]{1,64}$/.test(category))throw new Error('feedback category invalid');
|
|
54
|
+
await mkdir(feedbackDir,{recursive:true});
|
|
55
|
+
const id=randomUUID(),record={schema:COMMANDER_EXTENDED_SCHEMA,id,category,message:message.trim(),created_at_utc:new Date().toISOString()};
|
|
56
|
+
await writeFile(path.join(feedbackDir,id+'.json'),canonicalJson(record),{encoding:'utf8',flag:'wx',mode:0o600});
|
|
57
|
+
return {status:'committed',feedback_id:id,feedback_sha256:sha256(record)};
|
|
58
|
+
}
|
|
59
|
+
function getPrompts(){return {schema:COMMANDER_EXTENDED_SCHEMA,status:'ok',prompts:prompts.map(x=>({...x})).slice(0,128)};}
|
|
60
|
+
function getResources(){return {schema:COMMANDER_EXTENDED_SCHEMA,status:'ok',resources:resources.map(x=>({...x})).slice(0,128)};}
|
|
61
|
+
async function filePreview({rootId,path:relativePath,offset=0,maxBytes=MAX_PREVIEW,mimeType='text/plain'}){
|
|
62
|
+
if(!Number.isInteger(offset)||offset<0||offset>64*1024*1024)throw new Error('preview offset invalid');
|
|
63
|
+
if(!Number.isInteger(maxBytes)||maxBytes<1||maxBytes>MAX_PREVIEW)throw new Error('preview maxBytes invalid');
|
|
64
|
+
if(typeof rootId!=='string'||typeof relativePath!=='string'||path.isAbsolute(relativePath))throw new Error('preview target invalid');
|
|
65
|
+
const data=await readFile(path.resolve(rootId,relativePath)),slice=data.subarray(offset,offset+maxBytes);
|
|
66
|
+
return {schema:COMMANDER_EXTENDED_SCHEMA,status:'ok',root_id:rootId,path:relativePath,mime_type:mimeType,offset,bytes:slice.length,total_bytes:data.length,truncated:offset+slice.length<data.length,sha256:sha256(slice),text:mimeType.startsWith('text/')?slice.toString('utf8'):null};
|
|
67
|
+
}
|
|
68
|
+
function writePdf({text,metadata={}}){
|
|
69
|
+
if(typeof text!=='string'||Buffer.byteLength(text)>MAX_PREVIEW)throw new Error('PDF text exceeds byte limit');
|
|
70
|
+
const bytes=makePdf(text);
|
|
71
|
+
return {schema:COMMANDER_EXTENDED_SCHEMA,status:'ok',mime_type:'application/pdf',bytes_base64:bytes.toString('base64'),bytes_sha256:sha256(bytes),bytes:bytes.length,metadata};
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({getConfig,setConfigValue,feedback,getPrompts,getResources,filePreview,writePdf,history});
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
2
|
+
|
|
3
|
+
const MAX_LIMIT=1000;
|
|
4
|
+
const SCHEMA='deadbyte.commander-observability.v1';
|
|
5
|
+
|
|
6
|
+
function boundLimit(limit){if(!Number.isInteger(limit)||limit<1||limit>MAX_LIMIT)throw new Error('observability limit out of range');return limit;}
|
|
7
|
+
|
|
8
|
+
export function createCommanderObservability({history}={}){
|
|
9
|
+
if(!history||typeof history.read!=='function'||typeof history.verify!=='function'||typeof history.operationId!=='string'||history.operationId.length<1) throw new Error('observability requires operation history');
|
|
10
|
+
function recentToolCalls({afterSequence=0,limit=100}={}){
|
|
11
|
+
boundLimit(limit);
|
|
12
|
+
const page=history.read({afterSequence,limit});
|
|
13
|
+
const events=page.events.filter(e=>e.event_type==='tool.call'||e.event_type==='tool.result'||e.event_type==='tool.error');
|
|
14
|
+
return {schema:SCHEMA,operation_id:page.operation_id,events,next_sequence:page.next_sequence,head_event_hash:page.head_event_hash,earliest_sequence:page.earliest_sequence,overflow:page.overflow};
|
|
15
|
+
}
|
|
16
|
+
function usageStats(){
|
|
17
|
+
const page=history.read({afterSequence:0,limit:MAX_LIMIT});
|
|
18
|
+
const counts={};
|
|
19
|
+
let toolCalls=0,toolResults=0,toolErrors=0;
|
|
20
|
+
for(const event of page.events){
|
|
21
|
+
if(event.event_type==='tool.call') toolCalls++;
|
|
22
|
+
else if(event.event_type==='tool.result') toolResults++;
|
|
23
|
+
else if(event.event_type==='tool.error') toolErrors++;
|
|
24
|
+
else continue;
|
|
25
|
+
const name=event.payload?.tool;
|
|
26
|
+
if(typeof name==='string'&&name.length) counts[name]=(counts[name]??0)+1;
|
|
27
|
+
}
|
|
28
|
+
return {schema:SCHEMA,operation_id:page.operation_id,events_visible:page.events.length,tool_calls:toolCalls,tool_results:toolResults,tool_errors:toolErrors,tool_counts:Object.fromEntries(Object.entries(counts).sort(([a],[b])=>a.localeCompare(b))),head_event_hash:page.head_event_hash,earliest_sequence:page.earliest_sequence,overflow:page.overflow,integrity:history.verify()};
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({usageStats,recentToolCalls});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const COMMANDER_OBSERVABILITY_SCHEMA=SCHEMA;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
4
|
+
const CAP=/^[A-Za-z0-9._-]{1,128}$/;
|
|
5
|
+
function register(server,name,schema,handler){
|
|
6
|
+
server.registerTool(name,{description:'Native remote Commander-compatible bridge backed directly by DEADBYTE remote-device-r44. No Commander runtime is invoked.',inputSchema:schema},handler);
|
|
7
|
+
}
|
|
8
|
+
function requirePlane(plane,name){
|
|
9
|
+
if(!plane) return {status:'unavailable',tool:name,error:'remote-device-r44 plane is not configured'};
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
export function registerCommanderRemoteBridge(server,{remoteDevicePlane=null}={}){
|
|
13
|
+
register(server,'remote_device',z.strictObject({
|
|
14
|
+
op:z.enum(['pair','status','report_online','probe','dispatch','revoke','rotate_credential','list']),
|
|
15
|
+
device_id:z.string().regex(HEX64).optional(),epoch:z.number().int().min(1).optional(),
|
|
16
|
+
public_key_spki_base64:z.string().min(1).max(4096).optional(),display_name:z.string().min(1).max(128).optional(),
|
|
17
|
+
connection_id:z.string().min(1).max(256).optional(),capability_id:z.string().regex(CAP).optional(),
|
|
18
|
+
payload:z.record(z.string(),z.unknown()).optional(),request_id:z.string().regex(HEX64).optional(),
|
|
19
|
+
encrypted_bundle_base64:z.string().min(1).max(1024*1024).optional(),expected_previous_sha256:z.string().regex(HEX64).nullable().optional()
|
|
20
|
+
}),async a=>{
|
|
21
|
+
const unavailable=requirePlane(remoteDevicePlane,'remote_device');if(unavailable)return unavailable;
|
|
22
|
+
switch(a.op){
|
|
23
|
+
case 'pair': return remoteDevicePlane.pair({publicKeySpkiBase64:a.public_key_spki_base64,displayName:a.display_name});
|
|
24
|
+
case 'status': return remoteDevicePlane.status({deviceId:a.device_id});
|
|
25
|
+
case 'report_online': return remoteDevicePlane.reportOnline({deviceId:a.device_id,epoch:a.epoch,connectionId:a.connection_id});
|
|
26
|
+
case 'probe': return remoteDevicePlane.probe({deviceId:a.device_id,epoch:a.epoch});
|
|
27
|
+
case 'dispatch': return remoteDevicePlane.dispatch({deviceId:a.device_id,epoch:a.epoch,capabilityId:a.capability_id,payload:a.payload??{},requestId:a.request_id});
|
|
28
|
+
case 'revoke': return remoteDevicePlane.revoke({deviceId:a.device_id,epoch:a.epoch});
|
|
29
|
+
case 'rotate_credential': return remoteDevicePlane.rotateCredential({deviceId:a.device_id,epoch:a.epoch,encryptedBundleBase64:a.encrypted_bundle_base64,expectedPreviousSha256:a.expected_previous_sha256??null});
|
|
30
|
+
case 'list': return remoteDevicePlane.list();
|
|
31
|
+
default: throw new Error(`unsupported remote_device operation: ${String(a.op)}`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
register(server,'remote_mcp',z.strictObject({
|
|
35
|
+
device_id:z.string().regex(HEX64),epoch:z.number().int().min(1),capability_id:z.string().regex(CAP),
|
|
36
|
+
payload:z.record(z.string(),z.unknown()).default({}),request_id:z.string().regex(HEX64)
|
|
37
|
+
}),async a=>{
|
|
38
|
+
const unavailable=requirePlane(remoteDevicePlane,'remote_mcp');if(unavailable)return unavailable;
|
|
39
|
+
return remoteDevicePlane.dispatch({deviceId:a.device_id,epoch:a.epoch,capabilityId:a.capability_id,payload:a.payload,requestId:a.request_id});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
|
2
|
+
|
|
3
|
+
function insist(ok,message){if(!ok)throw new Error(message);}
|
|
4
|
+
function structured(result,label){
|
|
5
|
+
if(result?.isError) throw new Error(`${label} returned MCP error`);
|
|
6
|
+
const value=result?.structuredContent;
|
|
7
|
+
insist(value&&typeof value==='object',`${label} missing structured output`);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function createCommanderRemoteRelayR47({endpoint,protocolVersion='2026-07-28',timeoutMs=15000}={}){
|
|
12
|
+
const url=new URL(String(endpoint??''));
|
|
13
|
+
insist(url.protocol==='https:','remote relay endpoint must use HTTPS');
|
|
14
|
+
insist(Number.isInteger(timeoutMs)&&timeoutMs>=100&&timeoutMs<=120000,'remote relay timeout invalid');
|
|
15
|
+
const client=new Client({name:'deadbyte-r47-remote-relay',version:'0.15.15'},{versionNegotiation:{mode:{pin:protocolVersion}}});
|
|
16
|
+
const transport=new StreamableHTTPClientTransport(url);
|
|
17
|
+
let connected=false;
|
|
18
|
+
let connecting=null;
|
|
19
|
+
async function ensureConnected(){
|
|
20
|
+
if(connected)return;
|
|
21
|
+
if(!connecting){connecting=client.connect(transport).then(()=>{connected=true;}).finally(()=>{connecting=null;});}
|
|
22
|
+
await connecting;
|
|
23
|
+
}
|
|
24
|
+
async function call(name,arguments_,label){await ensureConnected();return structured(await client.callTool({name,arguments:arguments_}),label);}
|
|
25
|
+
async function probe({device_id,epoch,connection_id,nonce}){
|
|
26
|
+
const value=await call('remote_device',{op:'probe',device_id,epoch,connection_id,nonce},'remote readiness probe');
|
|
27
|
+
const result=value.result??value;
|
|
28
|
+
return {device_id,epoch,nonce,ready:result.ready===true||result.reachable===true};
|
|
29
|
+
}
|
|
30
|
+
async function request({device_id,epoch,capability_id,payload,request_id,request_sha256}){
|
|
31
|
+
insist(typeof request_sha256==='string'&&/^[0-9a-f]{64}$/.test(request_sha256),'remote request hash invalid');
|
|
32
|
+
const value=await call('remote_mcp',{device_id,epoch,capability_id,payload,request_id,request_sha256},'remote dispatch');
|
|
33
|
+
const result=value.result??value;
|
|
34
|
+
if(result?.request_id!==undefined) insist(result.request_id===request_id,'remote request identity mismatch');
|
|
35
|
+
if(result?.status==='completed'||result?.state==='completed'){
|
|
36
|
+
insist(typeof result.result_sha256==='string'&&/^[0-9a-f]{64}$/.test(result.result_sha256),'remote terminal result hash invalid');
|
|
37
|
+
return {ack:'accepted',request_id,terminal:{status:'completed',result_sha256:result.result_sha256}};
|
|
38
|
+
}
|
|
39
|
+
if(result?.status==='failed'||result?.state==='failed') return {ack:'accepted',request_id,terminal:{status:'failed',error_code:result.error_code??'remote_failed'}};
|
|
40
|
+
throw new Error('remote dispatch returned non-terminal state');
|
|
41
|
+
}
|
|
42
|
+
async function close(){connected=false;await client.close().catch(()=>{});}
|
|
43
|
+
return Object.freeze({probe,request,close,endpoint:url.toString()});
|
|
44
|
+
}
|
package/src/machine-fs.mjs
CHANGED
|
@@ -33,7 +33,36 @@ async function sha256File(file) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const machineFsReadWorkerPath = fileURLToPath(new URL('./machine-fs-read-worker.mjs', import.meta.url));
|
|
36
|
+
async function boundedReadInProcess({ file, offset, length, hashFile, timeoutMs }) {
|
|
37
|
+
const handle = await open(file, 'r');
|
|
38
|
+
let timer;
|
|
39
|
+
let settled = false;
|
|
40
|
+
const abort = new Promise((_, reject) => {
|
|
41
|
+
timer = setTimeout(() => {
|
|
42
|
+
settled = true;
|
|
43
|
+
void handle.close().catch(() => {});
|
|
44
|
+
reject(new Error(`machine fs read timed out after ${timeoutMs}ms`));
|
|
45
|
+
}, timeoutMs);
|
|
46
|
+
timer.unref?.();
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
const operation = (async () => {
|
|
50
|
+
const buffer = Buffer.alloc(length);
|
|
51
|
+
const result = length ? await handle.read(buffer, 0, length, offset) : { bytesRead: 0 };
|
|
52
|
+
let fileSha256 = null;
|
|
53
|
+
if (hashFile) fileSha256 = await sha256File(file);
|
|
54
|
+
return { schema: 'deadbyte.machine-fs-read-worker.v1', bytes_read: result.bytesRead,
|
|
55
|
+
data_base64: buffer.subarray(0, result.bytesRead).toString('base64'), file_sha256: fileSha256 };
|
|
56
|
+
})();
|
|
57
|
+
operation.catch(() => {});
|
|
58
|
+
return await Promise.race([operation, abort]);
|
|
59
|
+
} finally {
|
|
60
|
+
if (!settled) { clearTimeout(timer); await handle.close().catch(() => {}); }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
async function boundedReadWorker({ file, offset, length, hashFile, timeoutMs, workerPath = machineFsReadWorkerPath }) {
|
|
65
|
+
if (workerPath === machineFsReadWorkerPath) return boundedReadInProcess({ file, offset, length, hashFile, timeoutMs });
|
|
37
66
|
return await new Promise((resolve, reject) => {
|
|
38
67
|
const child = spawn(process.execPath, [workerPath, file, String(offset), String(length), hashFile ? '1' : '0'], {
|
|
39
68
|
windowsHide: true, shell: false, stdio: ['ignore','pipe','pipe']
|
|
@@ -103,20 +132,27 @@ function assertNotDenied(root, relativePath) {
|
|
|
103
132
|
}
|
|
104
133
|
async function assertNoReparse(rootPath, target, allowMissingLeaf = false) {
|
|
105
134
|
const relative = path.relative(rootPath, target);
|
|
106
|
-
if (relative === '')
|
|
135
|
+
if (relative === '') {
|
|
136
|
+
const info = await lstat(rootPath);
|
|
137
|
+
if (info.isSymbolicLink()) throw new Error('machine fs root reparse point denied');
|
|
138
|
+
return info;
|
|
139
|
+
}
|
|
107
140
|
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('machine fs path escapes allowed root');
|
|
108
141
|
const parts = relative.split(path.sep).filter(Boolean);
|
|
109
142
|
let cursor = rootPath;
|
|
143
|
+
let targetInfo = null;
|
|
110
144
|
for (let i = 0; i < parts.length; i += 1) {
|
|
111
145
|
cursor = path.join(cursor, parts[i]);
|
|
112
146
|
try {
|
|
113
147
|
const info = await lstat(cursor);
|
|
114
148
|
if (info.isSymbolicLink()) throw new Error(`machine fs reparse point denied at '${parts.slice(0, i + 1).join('/')}'`);
|
|
149
|
+
targetInfo = info;
|
|
115
150
|
} catch (error) {
|
|
116
|
-
if (allowMissingLeaf && i === parts.length - 1 && error?.code === 'ENOENT') return;
|
|
151
|
+
if (allowMissingLeaf && i === parts.length - 1 && error?.code === 'ENOENT') return null;
|
|
117
152
|
throw error;
|
|
118
153
|
}
|
|
119
154
|
}
|
|
155
|
+
return targetInfo;
|
|
120
156
|
}
|
|
121
157
|
|
|
122
158
|
async function resolveExisting(policy, grant, rootId, relativePath, permission, allowDot = false) {
|
|
@@ -126,10 +162,10 @@ async function resolveExisting(policy, grant, rootId, relativePath, permission,
|
|
|
126
162
|
if (normalized !== '.') assertNotDenied(root, normalized);
|
|
127
163
|
const lexical = normalized === '.' ? root.real_path : path.resolve(root.real_path, normalized);
|
|
128
164
|
if (!isInside(root.real_path, lexical) && lexical !== root.real_path) throw new Error('machine fs path escapes allowed root');
|
|
129
|
-
await assertNoReparse(root.real_path, lexical, false);
|
|
130
|
-
const resolved = await realpath(lexical);
|
|
165
|
+
const targetInfo = await assertNoReparse(root.real_path, lexical, false);
|
|
166
|
+
const resolved = lexical === root.real_path ? lexical : await realpath(lexical);
|
|
131
167
|
if (!isInside(root.real_path, resolved) && resolved !== root.real_path) throw new Error('machine fs path resolves outside allowed root');
|
|
132
|
-
return { root, lexical, resolved, relative: normalized };
|
|
168
|
+
return { root, lexical, resolved, relative: normalized, targetInfo };
|
|
133
169
|
}
|
|
134
170
|
async function resolveTarget(policy, grant, rootId, relativePath, permission) {
|
|
135
171
|
const normalized = normalizeRelative(relativePath, false);
|
|
@@ -208,15 +244,41 @@ async function objectIdentity(target, budget = { remaining: MAX_DIRECTORY_IDENTI
|
|
|
208
244
|
return { ...core, identity_sha256: sha256Object(core) };
|
|
209
245
|
}
|
|
210
246
|
|
|
211
|
-
|
|
247
|
+
const LIST_CURSOR_DOMAIN = Buffer.from('DEADBYTE-MACHINE-FS-LIST-CURSOR-V3\\0', 'utf8');
|
|
248
|
+
async function encodeListCursor({ path: relativePath, snapshot_sha256, after_name, signingKeyPath }) {
|
|
249
|
+
const signing = await loadPrivateSigningKey(signingKeyPath);
|
|
250
|
+
const body = { schema: 'deadbyte.machine-fs-list-cursor.v3', path: relativePath, snapshot_sha256, after_name };
|
|
251
|
+
const payload = Buffer.from(canonicalJson(body), 'utf8');
|
|
252
|
+
const signature_base64 = cryptoSign(null, Buffer.concat([LIST_CURSOR_DOMAIN, payload]), signing.privateKey).toString('base64');
|
|
253
|
+
return Buffer.from(canonicalJson({
|
|
254
|
+
...body,
|
|
255
|
+
attestation: { algorithm: 'ed25519', key_id: signing.keyId, signature_base64 }
|
|
256
|
+
}), 'utf8').toString('base64url');
|
|
257
|
+
}
|
|
258
|
+
async function decodeListCursor(cursor, verifyKeyPath) {
|
|
212
259
|
if (cursor === null || cursor === undefined) return null;
|
|
213
|
-
if (typeof cursor !== 'string' || cursor.length < 1 || cursor.length >
|
|
260
|
+
if (typeof cursor !== 'string' || cursor.length < 1 || cursor.length > 4096) throw new Error('machine fs cursor invalid');
|
|
214
261
|
let decoded;
|
|
215
|
-
try { decoded = Buffer.from(cursor, 'base64url').toString('utf8'); }
|
|
262
|
+
try { decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')); }
|
|
216
263
|
catch { throw new Error('machine fs cursor invalid'); }
|
|
217
|
-
if (!decoded ||
|
|
264
|
+
if (!decoded || decoded.schema !== 'deadbyte.machine-fs-list-cursor.v3' ||
|
|
265
|
+
typeof decoded.path !== 'string' || !SHA256_RE.test(decoded.snapshot_sha256 ?? '') ||
|
|
266
|
+
typeof decoded.after_name !== 'string' || decoded.after_name.length < 1 ||
|
|
267
|
+
decoded.attestation?.algorithm !== 'ed25519' || typeof decoded.attestation?.key_id !== 'string' ||
|
|
268
|
+
typeof decoded.attestation?.signature_base64 !== 'string') throw new Error('machine fs cursor invalid');
|
|
269
|
+
if (Buffer.from(canonicalJson(decoded), 'utf8').toString('base64url') !== cursor) throw new Error('machine fs cursor invalid');
|
|
270
|
+
const { attestation, ...body } = decoded;
|
|
271
|
+
const verifyKey = await loadPublicVerifyKey(verifyKeyPath);
|
|
272
|
+
if (attestation.key_id !== verifyKey.keyId) throw new Error('machine fs cursor key mismatch');
|
|
273
|
+
const signature = Buffer.from(attestation.signature_base64, 'base64');
|
|
274
|
+
const valid = signature.length === 64 &&
|
|
275
|
+
cryptoVerify(null, Buffer.concat([LIST_CURSOR_DOMAIN, Buffer.from(canonicalJson(body), 'utf8')]), verifyKey.publicKey, signature);
|
|
276
|
+
if (!valid) throw new Error('machine fs cursor signature mismatch');
|
|
218
277
|
return decoded;
|
|
219
278
|
}
|
|
279
|
+
function directoryListingSnapshot(children) {
|
|
280
|
+
return sha256Object(children.map(child => ({ name: child.name, kind: child.isSymbolicLink() ? 'reparse' : child.isDirectory() ? 'directory' : child.isFile() ? 'file' : 'other' })));
|
|
281
|
+
}
|
|
220
282
|
|
|
221
283
|
function encodeCursor(name) { return Buffer.from(name, 'utf8').toString('base64url'); }
|
|
222
284
|
|
|
@@ -286,7 +348,14 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath,
|
|
|
286
348
|
const info = await lstat(found.resolved);
|
|
287
349
|
const kind = kindOf(info);
|
|
288
350
|
if (kind === 'reparse') throw new Error('machine fs reparse target denied');
|
|
289
|
-
|
|
351
|
+
let identity;
|
|
352
|
+
if (kind === 'file') {
|
|
353
|
+
const contentSha256 = await sha256File(found.resolved);
|
|
354
|
+
const core = { kind: 'file', bytes: info.size, content_sha256: contentSha256 };
|
|
355
|
+
identity = { ...core, identity_sha256: sha256Object(core) };
|
|
356
|
+
} else {
|
|
357
|
+
identity = await objectIdentity(found.resolved);
|
|
358
|
+
}
|
|
290
359
|
return {
|
|
291
360
|
status: 'ok', root_id: rootId, path: found.relative, kind,
|
|
292
361
|
bytes: kind === 'file' ? info.size : 0,
|
|
@@ -300,27 +369,29 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath,
|
|
|
300
369
|
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) throw new Error(`machine fs list limit must be in [1, ${MAX_LIST_LIMIT}]`);
|
|
301
370
|
const grant = await assertCodingArmed(policy, rootId);
|
|
302
371
|
const found = await resolveExisting(policy, grant, rootId, relativePath, 'read', true);
|
|
303
|
-
const info =
|
|
304
|
-
if (!info
|
|
305
|
-
const
|
|
372
|
+
const info = found.targetInfo;
|
|
373
|
+
if (!info?.isDirectory()) throw new Error('machine fs list path must be a directory');
|
|
374
|
+
const decodedCursor = await decodeListCursor(cursor, verifyKeyPath);
|
|
375
|
+
if (decodedCursor && decodedCursor.path !== found.relative) throw new Error('machine fs cursor path mismatch');
|
|
306
376
|
const children = await readdir(found.resolved, { withFileTypes: true });
|
|
307
377
|
children.sort((a, b) => a.name.localeCompare(b.name));
|
|
378
|
+
const snapshot_sha256 = directoryListingSnapshot(children);
|
|
379
|
+
if (decodedCursor && decodedCursor.snapshot_sha256 !== snapshot_sha256) throw new Error('machine fs cursor snapshot changed');
|
|
380
|
+
const afterName = decodedCursor?.after_name ?? null;
|
|
308
381
|
const filtered = children.filter(child => afterName === null || child.name.localeCompare(afterName) > 0);
|
|
309
382
|
const selected = filtered.slice(0, limit);
|
|
310
|
-
const entries =
|
|
311
|
-
for (const child of selected) {
|
|
383
|
+
const entries = await Promise.all(selected.map(async child => {
|
|
312
384
|
const rel = found.relative === '.' ? child.name : `${found.relative}/${child.name}`;
|
|
313
|
-
if (child.isSymbolicLink()) {
|
|
314
|
-
entries.push({ path: rel, kind: 'reparse', bytes: 0, mtime_utc: null, sha256: null });
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
385
|
+
if (child.isSymbolicLink()) return { path: rel, kind: 'reparse', bytes: 0, mtime_utc: null, sha256: null };
|
|
317
386
|
const childPath = path.join(found.resolved, child.name);
|
|
318
387
|
const childInfo = await lstat(childPath);
|
|
319
388
|
const kind = kindOf(childInfo);
|
|
320
389
|
const digest = hashFiles && kind === 'file' ? await sha256File(childPath) : null;
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
const nextCursor = filtered.length > selected.length && selected.length
|
|
390
|
+
return { path: rel, kind, bytes: kind === 'file' ? childInfo.size : 0, mtime_utc: childInfo.mtime.toISOString(), sha256: digest };
|
|
391
|
+
}));
|
|
392
|
+
const nextCursor = filtered.length > selected.length && selected.length
|
|
393
|
+
? await encodeListCursor({ path: found.relative, snapshot_sha256, after_name: selected.at(-1).name, signingKeyPath })
|
|
394
|
+
: null;
|
|
324
395
|
return { status: 'ok', root_id: rootId, path: found.relative, entries, next_cursor: nextCursor };
|
|
325
396
|
}
|
|
326
397
|
async function readRange({ rootId, relativePath, offset = 0, maxBytes = 65536, hashFile = false, timeoutMs = 30000 }) {
|
|
@@ -348,6 +419,20 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath,
|
|
|
348
419
|
};
|
|
349
420
|
}
|
|
350
421
|
|
|
422
|
+
async function readMany({ rootId, paths, maxBytes = 65536, timeoutMs = 30000 }) {
|
|
423
|
+
if (!Array.isArray(paths) || paths.length < 1 || paths.length > 64) throw new Error('machine fs read_many paths must contain 1-64 entries');
|
|
424
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > policy.max_read_bytes) throw new Error(`machine fs read_many max_bytes must be in [1, ${policy.max_read_bytes}]`);
|
|
425
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 180000) throw new Error('machine fs read_many timeout_ms must be in [100, 180000]');
|
|
426
|
+
const unique = new Set();
|
|
427
|
+
for (const relativePath of paths) {
|
|
428
|
+
if (typeof relativePath !== 'string' || relativePath.length < 1 || relativePath.length > 512) throw new Error('machine fs read_many path invalid');
|
|
429
|
+
if (unique.has(relativePath)) throw new Error(`machine fs read_many duplicate path '${relativePath}'`);
|
|
430
|
+
unique.add(relativePath);
|
|
431
|
+
}
|
|
432
|
+
const results = await Promise.all(paths.map(relativePath => readRange({ rootId, relativePath, offset: 0, maxBytes, timeoutMs })));
|
|
433
|
+
return { status: 'ok', root_id: rootId, count: results.length, results };
|
|
434
|
+
}
|
|
435
|
+
|
|
351
436
|
async function writePath({ rootId, relativePath, content, encoding = 'utf8', expectedSha256, receiptId = null }) {
|
|
352
437
|
const grant = await assertCodingArmed(policy, rootId);
|
|
353
438
|
const target = await resolveTarget(policy, grant, rootId, relativePath, 'write');
|
|
@@ -551,6 +636,7 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath,
|
|
|
551
636
|
stat: statPath,
|
|
552
637
|
list: listPage,
|
|
553
638
|
read: readRange,
|
|
639
|
+
readMany,
|
|
554
640
|
write: writePath,
|
|
555
641
|
editExact,
|
|
556
642
|
move: movePath,
|