deadbyte-mcp 0.15.0 → 0.15.2

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 (110) hide show
  1. package/MANIFEST.SHA256 +109 -64
  2. package/PROVENANCE.json +11 -0
  3. package/R45-PARENT.json +18 -0
  4. package/README.txt +36 -20
  5. package/RELEASE.json +53 -0
  6. package/SBOM.cdx.json +46 -0
  7. package/TEST-MATRIX.json +31 -0
  8. package/bin/deadbyte-hardware-observer-r45.exe +0 -0
  9. package/bin/deadbyte-hardware-observer-r45.obj +0 -0
  10. package/controller/README.TXT +2 -2
  11. package/controller/deadbyte-controller.ps1 +51 -3
  12. package/controller/deadbyte-process-policy.json +63 -1
  13. package/docs/ARCHITECTURE.md +3 -4
  14. package/native/deadbyte-hardware-observer.cpp +308 -0
  15. package/package.json +14 -7
  16. package/scripts/autonomous-tests.ps1 +2 -2
  17. package/scripts/build-hardware-r45.ps1 +71 -0
  18. package/scripts/deferred-slot-operation-r45.mjs +126 -0
  19. package/scripts/deferred-slot-operation-r46.mjs +126 -0
  20. package/scripts/derive-r45-parent.mjs +24 -0
  21. package/scripts/derive-r45-release.mjs +72 -0
  22. package/scripts/derive-r45-tests.mjs +20 -0
  23. package/scripts/final-closure-r45.mjs +121 -0
  24. package/scripts/final-closure-r46.mjs +98 -0
  25. package/scripts/final-closure-verify-r45.mjs +220 -0
  26. package/scripts/final-closure-verify-r46.mjs +119 -0
  27. package/scripts/finalize-r45.mjs +219 -0
  28. package/scripts/full-regression-r45.mjs +20 -0
  29. package/scripts/full-regression-r46.mjs +20 -0
  30. package/scripts/full-regression-summary-r45.mjs +14 -0
  31. package/scripts/gate-windows-r45.ps1 +44 -0
  32. package/scripts/gate-windows-r46.ps1 +56 -0
  33. package/scripts/init-host-policy.ps1 +39 -1
  34. package/scripts/inspect-active-release-r45.mjs +20 -0
  35. package/scripts/refresh-process-policy.mjs +64 -7
  36. package/scripts/release-manifest.mjs +3 -1
  37. package/scripts/release-parity-r45.mjs +253 -0
  38. package/scripts/release-parity-r46.mjs +187 -0
  39. package/scripts/release-parity-tests-r45.ps1 +8 -0
  40. package/scripts/verify-r45-parent.mjs +142 -0
  41. package/scripts/windows-gate-evidence-r45.mjs +60 -0
  42. package/scripts/windows-gate-evidence-r46.mjs +110 -0
  43. package/src/autonomous-runtime.mjs +12 -7
  44. package/src/capability-ledger.mjs +2 -0
  45. package/src/compact-gateway-mcp-tools.mjs +11 -7
  46. package/src/deadbyte-cli.mjs +24 -12
  47. package/src/doctor-r45.mjs +65 -0
  48. package/src/host-plane.mjs +133 -85
  49. package/src/machine-fs-mcp-tools.mjs +3 -3
  50. package/src/machine-fs-output-contracts.mjs +1 -1
  51. package/src/machine-fs-read-worker.mjs +46 -0
  52. package/src/machine-fs.mjs +63 -10
  53. package/src/mcp-server.mjs +68 -23
  54. package/src/mcp-smoke-client.mjs +4 -1
  55. package/src/native-diff-v2.mjs +9 -4
  56. package/src/native-diff.mjs +29 -7
  57. package/src/observation-mcp-tools.mjs +5 -1
  58. package/src/observation-output-contracts.mjs +8 -0
  59. package/src/observation-runtime.mjs +57 -5
  60. package/src/observation-smoke-client.mjs +6 -2
  61. package/src/output-contracts.mjs +27 -11
  62. package/src/process-mcp-tools.mjs +18 -5
  63. package/src/process-output-contracts.mjs +20 -3
  64. package/src/process-policy.mjs +3 -1
  65. package/src/process-runtime.mjs +99 -19
  66. package/src/process-smoke-client.mjs +67 -22
  67. package/src/r45-compact-disarmed-probe.mjs +74 -0
  68. package/src/r45-full-surface-probe.mjs +34 -0
  69. package/src/release-generation.mjs +23 -3
  70. package/src/remote-device-r44.mjs +53 -15
  71. package/src/remote-mcp-r45-parity-client.mjs +206 -0
  72. package/src/remote-mcp-smoke-client.mjs +1 -1
  73. package/src/runtime-update.mjs +36 -8
  74. package/src/stdio-console-guard.mjs +24 -0
  75. package/src/version.mjs +1 -1
  76. package/test/agent-runtime.test.mjs +1 -0
  77. package/test/autonomous-loop.test.mjs +24 -0
  78. package/test/autonomous-runtime.test.mjs +1 -0
  79. package/test/cli-entrypoint.test.mjs +4 -4
  80. package/test/coding-plane.test.mjs +1 -0
  81. package/test/compact-gateway-r36.test.mjs +28 -0
  82. package/test/compact-max-catalog-r45.test.mjs +99 -0
  83. package/test/contained.test.mjs +23 -11
  84. package/test/controller.test.mjs +5 -1
  85. package/test/core.test.mjs +1 -1
  86. package/test/full-max-catalog-r45.test.mjs +91 -0
  87. package/test/full-max-catalog-r46.test.mjs +92 -0
  88. package/test/hardware-observation-r45.test.mjs +44 -0
  89. package/test/host-inline-r45.test.mjs +67 -0
  90. package/test/machine-fs-mcp-tools.test.mjs +4 -1
  91. package/test/machine-fs-runtime.test.mjs +18 -2
  92. package/test/machine-mcp-integration.test.mjs +1 -0
  93. package/test/observation-mcp-tools.test.mjs +4 -3
  94. package/test/observation-runtime.test.mjs +29 -2
  95. package/test/output-schema.test.mjs +1 -0
  96. package/test/package-boundary.test.mjs +1 -1
  97. package/test/process-mcp-tools.test.mjs +23 -5
  98. package/test/process-policy-refresh.test.mjs +87 -10
  99. package/test/process-runtime.test.mjs +111 -1
  100. package/test/process-supervision.test.mjs +4 -2
  101. package/test/production-docs.test.mjs +3 -3
  102. package/test/r33-closeout-regression.test.mjs +20 -19
  103. package/test/r33-finalization.test.mjs +3 -3
  104. package/test/r45-autonomous-quick-test.test.mjs +10 -0
  105. package/test/r45-finalization.test.mjs +84 -0
  106. package/test/release-generation.test.mjs +26 -11
  107. package/test/release-version.test.mjs +112 -46
  108. package/test/remote-device-r44.test.mjs +75 -0
  109. package/test/remote-r24-catalog.test.mjs +2 -2
  110. package/test/runtime-host-bootstrap-r45.test.mjs +46 -0
@@ -26,7 +26,7 @@ export const codingInspectInputSchema=z.discriminatedUnion('op',[
26
26
  export const machineFsGatewayInputSchema=z.discriminatedUnion('op',[
27
27
  z.strictObject({op:z.literal('stat'),root_id:id,path:rel.default('.'),hash:z.boolean().default(false)}),
28
28
  z.strictObject({op:z.literal('list'),root_id:id,path:rel.default('.'),cursor,limit:z.number().int().min(1).max(1000).default(100),hash_files:z.boolean().default(false)}),
29
- z.strictObject({op:z.literal('read'),root_id:id,path:rel,offset:nonneg.default(0),max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536),hash_file:z.boolean().default(false)}),
29
+ z.strictObject({op:z.literal('read'),root_id:id,path:rel,offset:nonneg.default(0),max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536),hash_file:z.boolean().default(false),timeout_ms:z.number().int().min(100).max(180000).default(30000)}),
30
30
  z.strictObject({op:z.literal('write'),root_id:id,path:rel,content:z.string().max(12*1024*1024),encoding:z.enum(['utf8','base64']).default('utf8'),expected_sha256:z.string().regex(/^(?:absent|[0-9a-f]{64})$/)}),
31
31
  z.strictObject({op:z.literal('edit_exact'),root_id:id,path:rel,expected_sha256:hex64,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)}),
32
32
  z.strictObject({op:z.literal('mkdir'),root_id:id,path:rel}),
@@ -36,7 +36,8 @@ export const machineFsGatewayInputSchema=z.discriminatedUnion('op',[
36
36
 
37
37
  export const processSessionGatewayInputSchema=z.discriminatedUnion('op',[
38
38
  z.strictObject({op:z.literal('start'),profile_id:id,root_id:id,cwd:rel.default('.'),args:z.array(z.string().max(8192)).max(64).default([])}),
39
- z.strictObject({op:z.literal('read'),session_id:hex64,offset:nonneg.default(0),max_bytes:z.number().int().min(1).max(262144).default(65536)}),
39
+ z.strictObject({op:z.literal('read'),session_id:hex64,offset:z.number().int().min(-(16*1024*1024)).default(0),max_bytes:z.number().int().min(1).max(262144).default(65536),wait_ms:z.number().int().min(0).max(30000).default(0),wait_for:z.string().min(1).max(256).nullable().default(null)}),
40
+ z.strictObject({op:z.literal('write'),session_id:hex64,input:z.string().min(1).max(1024*1024),append_newline:z.boolean().default(true)}),
40
41
  z.strictObject({op:z.literal('status'),session_id:hex64}),
41
42
  z.strictObject({op:z.literal('list')}),
42
43
  z.strictObject({op:z.literal('action'),session_id:hex64,action_id:id}),
@@ -52,7 +53,8 @@ export const machineObserveInputSchema=z.discriminatedUnion('op',[
52
53
  z.strictObject({op:z.literal('search_status'),search_id:hex64}),
53
54
  z.strictObject({op:z.literal('search_cancel'),search_id:hex64}),
54
55
  z.strictObject({op:z.literal('process_list'),limit:z.number().int().min(1).max(256).default(100),include_external:z.boolean().default(false),cursor:z.string().max(512).nullable().default(null)}),
55
- z.strictObject({op:z.literal('system_status')})
56
+ z.strictObject({op:z.literal('system_status')}),
57
+ z.strictObject({op:z.literal('hardware_query'),query:z.enum(['cpu','gpu','memory','storage','network','pci'])})
56
58
  ]);
57
59
 
58
60
  const capabilityManifestInputSchema=z.strictObject({});
@@ -82,7 +84,7 @@ async function machineFs(args,runtime){
82
84
  const map={
83
85
  stat:r=>r.stat({rootId:args.root_id,relativePath:args.path,hash:args.hash}),
84
86
  list:r=>r.list({rootId:args.root_id,relativePath:args.path,cursor:args.cursor,limit:args.limit,hashFiles:args.hash_files}),
85
- read:r=>r.read({rootId:args.root_id,relativePath:args.path,offset:args.offset,maxBytes:args.max_bytes,hashFile:args.hash_file}),
87
+ read:r=>r.read({rootId:args.root_id,relativePath:args.path,offset:args.offset,maxBytes:args.max_bytes,hashFile:args.hash_file,timeoutMs:args.timeout_ms}),
86
88
  write:r=>r.write({rootId:args.root_id,relativePath:args.path,content:args.content,encoding:args.encoding,expectedSha256:args.expected_sha256}),
87
89
  edit_exact:r=>r.editExact({rootId:args.root_id,relativePath:args.path,expectedSha256:args.expected_sha256,oldString:args.old_string,newString:args.new_string,expectedReplacements:args.expected_replacements}),
88
90
  mkdir:r=>r.mkdir({rootId:args.root_id,relativePath:args.path}),
@@ -95,7 +97,8 @@ async function machineFs(args,runtime){
95
97
  async function processSession(args,runtime){
96
98
  const map={
97
99
  start:r=>r.start({profileId:args.profile_id,rootId:args.root_id,cwd:args.cwd,args:args.args}),
98
- read:r=>r.read({sessionId:args.session_id,offset:args.offset,maxBytes:args.max_bytes}),
100
+ read:r=>r.read({sessionId:args.session_id,offset:args.offset,maxBytes:args.max_bytes,waitMs:args.wait_ms,waitFor:args.wait_for}),
101
+ write:r=>r.write({sessionId:args.session_id,input:args.input,appendNewline:args.append_newline}),
99
102
  status:r=>r.status({sessionId:args.session_id}),
100
103
  list:r=>r.list(),
101
104
  action:r=>r.action({sessionId:args.session_id,actionId:args.action_id}),
@@ -114,7 +117,8 @@ async function machineObserve(args,machineRuntime,observationRuntime){
114
117
  search_status:r=>r.searchStatus({searchId:args.search_id}),
115
118
  search_cancel:r=>r.searchCancel({searchId:args.search_id}),
116
119
  process_list:r=>r.processList({limit:args.limit,includeExternal:args.include_external,cursor:args.cursor}),
117
- system_status:r=>r.systemStatus()
120
+ system_status:r=>r.systemStatus(),
121
+ hardware_query:r=>r.hardwareQuery({query:args.query})
118
122
  };
119
123
  return invoke(args.op,observationRuntime,'observation',map[args.op]);
120
124
  }
@@ -134,7 +138,7 @@ export function registerCompactGatewayTools(server,{profile,ledger,codingRuntime
134
138
  });
135
139
  server.registerTool('coding_inspect',{description:'Typed compact coding inspection and semantic navigation gateway. No arbitrary tool dispatch.',inputSchema:codingInspectInputSchema,outputSchema:compactGatewayOutputSchema},args=>codingInspect(args,codingRuntime,semanticRuntime));
136
140
  server.registerTool('machine_fs',{description:'Typed compact machine filesystem gateway preserving root policy and mutation preconditions.',inputSchema:machineFsGatewayInputSchema,outputSchema:compactGatewayOutputSchema},args=>machineFs(args,machineFsRuntime));
137
- server.registerTool('process_session',{description:'Typed compact DEADBYTE-owned process session gateway. No raw stdin or PID authority.',inputSchema:processSessionGatewayInputSchema,outputSchema:compactGatewayOutputSchema},args=>processSession(args,processRuntime));
141
+ server.registerTool('process_session',{description:'Typed compact DEADBYTE-owned process session gateway. Raw input is available only for locally opt-in profiles and remains grant-bounded; numeric PID authority is never exposed.',inputSchema:processSessionGatewayInputSchema,outputSchema:compactGatewayOutputSchema},args=>processSession(args,processRuntime));
138
142
  server.registerTool('machine_observe',{description:'Typed compact machine identity and read-only observation gateway. Revoke is intentionally excluded.',inputSchema:machineObserveInputSchema,outputSchema:compactGatewayOutputSchema},args=>machineObserve(args,machineRuntime,observationRuntime));
139
143
  server.registerTool('desktop_control',{description:'Typed Desktop observe, target, status and guarded action gateway. No arbitrary dispatch; mutation still requires separate Desktop authority.',inputSchema:desktopControlInputSchema,outputSchema:compactGatewayOutputSchema},args=>invokeDesktopOperation(args,desktopRuntime));
140
144
  }
@@ -10,12 +10,14 @@ import { defaultPublicKeyPath } from './trust.mjs';
10
10
  import { verifyAuditChain } from './audit-verify.mjs';
11
11
  import { openRemoteResultJournal, parseRemoteResultCursor } from './remote-result-journal.mjs';
12
12
  import { DEADBYTE_VERSION } from './version.mjs';
13
+ import { printDoctor } from './doctor-r45.mjs';
13
14
  import { createLiveTimelineTracker, formatCompactAuditEvent, formatRemoteLifecycle, formatRemoteToolCall, formatRuntimeDiagnosticLine } from './remote-live-log.mjs';
14
15
  import { prepareRemoteConsole, remoteDisplayText } from './remote-console.mjs';
15
16
 
16
17
  const REMOTE_COMMANDS=new Set([
17
18
  'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
18
19
  'armprocess','disarmprocess','processstatus','armhost','disarmhost','hoststatus','hosttest',
20
+ 'armdesktop','disarmdesktop','desktopstatus',
19
21
  'armcoding','disarmcoding','codingstatus','codingtest','armautonomous','disarmautonomous','autonomousstatus','autonomoustest'
20
22
  ]);
21
23
  const SESSION_ARM=['armprocess','armcoding','armautonomous'];
@@ -35,17 +37,19 @@ const SUPERVISOR_COMMANDS=Object.freeze({
35
37
  function stateRoot(){return path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));}
36
38
  async function requireRegular(file,label){const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink()) throw new Error(`${label} must be a regular non-symlink file`);return file;}
37
39
  function printHelp(){
40
+ const exact=`deadbyte-mcp@${DEADBYTE_VERSION}`;
38
41
  process.stdout.write([
39
42
  'DEADBYTE MCP CLI','',
40
- ' npx deadbyte-mcp@latest mcp # stdio MCP server for local clients',
41
- ' npx deadbyte-mcp@latest remote # unified foreground device session + live timeline',
42
- ' npx deadbyte-mcp@latest remote --resume <cursor> # replay missed results, then reconnect',
43
- ' npx deadbyte-mcp@latest remote status',
44
- ' npx deadbyte-mcp@latest remote restart',
45
- ' npx deadbyte-mcp@latest supervisor status',
46
- ' npx deadbyte-mcp@latest supervisor recover',
47
- ' npx deadbyte-mcp@latest audit verify',
48
- ' npx deadbyte-mcp@latest audit status',''
43
+ ` npx -y ${exact} doctor # verify current package/release truth`,
44
+ ` npx -y ${exact} mcp # stdio MCP server for local clients`,
45
+ ` npx -y ${exact} remote # unified foreground device session + live timeline`,
46
+ ` npx -y ${exact} remote --resume <cursor> # replay missed results, then reconnect`,
47
+ ` npx -y ${exact} remote status`,
48
+ ` npx -y ${exact} remote restart`,
49
+ ` npx -y ${exact} supervisor status`,
50
+ ` npx -y ${exact} supervisor recover`,
51
+ ` npx -y ${exact} audit verify`,
52
+ ` npx -y ${exact} audit status`,''
49
53
  ].join('\n'));
50
54
  }
51
55
  async function remoteContext(){
@@ -83,6 +87,9 @@ function controllerRuntimeReady(text){
83
87
  /^Local ready\s*:\s*True\s*$/m.test(value)&&
84
88
  /^Loop supervisor:\s*RUNNING\s+\[verified\]\s*$/m.test(value);
85
89
  }
90
+ function controllerRuntimeDegraded(text){
91
+ return /^DEADBYTE MCP\s*=\s*DEGRADED\s*$/m.test(String(text||''));
92
+ }
86
93
  async function listLogFiles(root){
87
94
  const files=[];
88
95
  const audit=path.join(root,'audit');
@@ -315,10 +322,11 @@ async function runRemoteSession({resumeCursor=null,verbose=false}={}){
315
322
  if(preflight.status===0&&controllerRuntimeReady(preflight.stdout)){
316
323
  process.stdout.write('[BOOT] Runtime already ONLINE; reusing verified bridge/cloud/supervisor.\n');
317
324
  }else{
318
- process.stdout.write('[BOOT] Starting bridge/runtime...\n');
319
- invokeController(ctx,'start',{discardOutput:true,timeoutMs:REMOTE_START_TIMEOUT_MS});
325
+ const recovery=preflight.status===0&&controllerRuntimeDegraded(preflight.stdout);
326
+ process.stdout.write(recovery?'[RECOVERY] Runtime DEGRADED; restarting verified owned components...\n':'[BOOT] Starting bridge/runtime...\n');
327
+ invokeController(ctx,recovery?'restart':'start',{discardOutput:true,timeoutMs:REMOTE_START_TIMEOUT_MS});
320
328
  const started=invokeController(ctx,'status',{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
321
- if(started.status!==0||!controllerRuntimeReady(started.stdout)) throw new Error('remote controller start returned without a verified ONLINE runtime');
329
+ if(started.status!==0||!controllerRuntimeReady(started.stdout)) throw new Error(`remote controller ${recovery?'restart':'start'} returned without a verified ONLINE runtime`);
322
330
  startText=started.stdout||'';
323
331
  }
324
332
  process.stdout.write(`${formatRemoteLifecycle('bridge_connected',{displayMode})}\n`);
@@ -388,6 +396,10 @@ async function main(argv){
388
396
  process.stdout.write(`${DEADBYTE_VERSION}\n`);return;
389
397
  }
390
398
  if(!command||command==='help'||command==='--help'||command==='-h'){printHelp();return;}
399
+ if(command==='doctor'){
400
+ if(rest.length) throw new Error('doctor accepts no arguments');
401
+ await printDoctor();return;
402
+ }
391
403
  if(command==='mcp'){
392
404
  if(rest.length) throw new Error('mcp accepts no arguments');
393
405
  await import('./mcp-server.mjs');
@@ -0,0 +1,65 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, readFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { DEADBYTE_VERSION, MCP_PROTOCOL_VERSION } from './version.mjs';
7
+
8
+ const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
9
+ const stateRoot=()=>path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
10
+ const sha256=b=>createHash('sha256').update(b).digest('hex');
11
+ async function jsonOrNull(file){try{return JSON.parse((await readFile(file,'utf8')).replace(/^\uFEFF/,''));}catch(e){if(e?.code==='ENOENT')return null;throw e;}}
12
+ async function fileHash(file){return sha256(await readFile(file));}
13
+ function parseManifest(text){
14
+ const rows=[];
15
+ for(const [index,line] of text.split(/\r?\n/).entries()){
16
+ if(!line) continue;
17
+ const m=/^([0-9a-f]{64}) (.+)$/.exec(line);
18
+ if(!m) throw new Error(`manifest line ${index+1} malformed`);
19
+ if(path.isAbsolute(m[2])||m[2].includes('..')||m[2].includes('\\')) throw new Error(`manifest line ${index+1} path unsafe`);
20
+ rows.push({sha256:m[1],relative:m[2]});
21
+ }
22
+ return rows;
23
+ }
24
+ export async function doctor(){
25
+ const manifestPath=path.join(root,'MANIFEST.SHA256');
26
+ const manifestBytes=await readFile(manifestPath);
27
+ const manifestSha256=sha256(manifestBytes);
28
+ const rows=parseManifest(manifestBytes.toString('utf8'));
29
+ let verified=0;
30
+ for(const row of rows){
31
+ const file=path.join(root,...row.relative.split('/'));
32
+ const st=await lstat(file);
33
+ if(!st.isFile()||st.isSymbolicLink()) throw new Error(`manifest entry not regular file: ${row.relative}`);
34
+ const actual=await fileHash(file);
35
+ if(actual!==row.sha256) throw new Error(`manifest hash mismatch: ${row.relative}`);
36
+ verified+=1;
37
+ }
38
+ const metadata={};
39
+ for(const name of ['RELEASE.json','TEST-MATRIX.json','PROVENANCE.json','SBOM.cdx.json']){
40
+ const file=path.join(root,name),body=await jsonOrNull(file);
41
+ if(!body) throw new Error(`required release metadata missing: ${name}`);
42
+ metadata[name]={sha256:await fileHash(file),schema:body.schema??body.bomFormat??null};
43
+ }
44
+ const release=await jsonOrNull(path.join(root,'RELEASE.json'));
45
+ if(release?.version!==DEADBYTE_VERSION) throw new Error('RELEASE.json version mismatch');
46
+ const hardware=path.join(root,'bin','deadbyte-hardware-observer-r45.exe');
47
+ const hardwareStat=await lstat(hardware);
48
+ if(!hardwareStat.isFile()||hardwareStat.isSymbolicLink()) throw new Error('hardware observer must be a regular file');
49
+ const hardwareSha256=await fileHash(hardware);
50
+ const active=await jsonOrNull(path.join(stateRoot(),'active-release.json'));
51
+ const hostPolicy=await jsonOrNull(path.join(root,'controller','deadbyte-host-policy.json'));
52
+ if(hostPolicy&&release?.train==='R45'&&process.platform==='win32'&&hostPolicy.schema!=='deadbyte.host-policy.v2') throw new Error('R45 doctor rejects non-v2 generated host policy');
53
+ return {
54
+ schema:'deadbyte.doctor.v1',status:'ok',package_version:DEADBYTE_VERSION,protocol_revision:MCP_PROTOCOL_VERSION,
55
+ platform:{current:process.platform,architecture:process.arch,primary:'win32',supported:process.platform==='win32'},
56
+ manifest:{sha256:manifestSha256,entries:rows.length,verified_entries:verified,verified:true},
57
+ release_metadata:metadata,
58
+ native:{hardware_observer:{path:'bin/deadbyte-hardware-observer-r45.exe',sha256:hardwareSha256,bytes:hardwareStat.size}},
59
+ policy:{host_schema:hostPolicy?.schema??null,host_v2_ready:hostPolicy?.schema==='deadbyte.host-policy.v2'},
60
+ active_release:active?{release_id:active.release_id,version:active.version,manifest_sha256:active.manifest_sha256,generation_manifest_sha256:active.generation_manifest_sha256,
61
+ candidate_active:active.version===DEADBYTE_VERSION&&active.manifest_sha256===manifestSha256}:null,
62
+ known_unsupported:process.platform==='win32'?[]:['native_hardware_observation','windows_controller','desktop_native']
63
+ };
64
+ }
65
+ export async function printDoctor(){const value=await doctor();process.stdout.write(JSON.stringify(value,null,2)+'\n');return value;}
@@ -16,6 +16,7 @@ import { decodeUtf8Strict } from './text-policy.mjs';
16
16
  import { isInside, validatePortableRelativePath } from './path-policy.mjs';
17
17
 
18
18
  export const HOST_POLICY_SCHEMA = 'deadbyte.host-policy.v1';
19
+ export const HOST_POLICY_SCHEMA_V2 = 'deadbyte.host-policy.v2';
19
20
  export const HOST_ARM_SCHEMA = 'deadbyte.host-arm.v1';
20
21
  export const HOST_GRANT_SCHEMA = 'deadbyte.host-grant.v2';
21
22
  export const HOST_DEFAULT_MAX_READ_BYTES = 256 * 1024;
@@ -45,7 +46,7 @@ function finiteInteger(value, name, min, max, fallback) {
45
46
 
46
47
  function normalizePolicyObject(raw, policyPath) {
47
48
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('host policy must be a JSON object');
48
- if (raw.schema !== HOST_POLICY_SCHEMA) throw new Error('host policy schema mismatch');
49
+ if (![HOST_POLICY_SCHEMA, HOST_POLICY_SCHEMA_V2].includes(raw.schema)) throw new Error('host policy schema mismatch');
49
50
  if (raw.enabled !== true) throw new Error('host policy is not enabled');
50
51
  if (!raw.arm_file || typeof raw.arm_file !== 'string' || !path.isAbsolute(raw.arm_file)) {
51
52
  throw new Error('host policy arm_file must be absolute');
@@ -75,7 +76,10 @@ function normalizePolicyObject(raw, policyPath) {
75
76
  for (const [id, value] of Object.entries(raw.commands)) {
76
77
  if (!COMMAND_ID_RE.test(id)) throw new Error(`invalid host command id '${id}'`);
77
78
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`host command '${id}' malformed`);
78
- if (value.kind !== 'script') throw new Error(`host command '${id}' kind must be script`);
79
+ const kind=value.kind;
80
+ if (kind!=='script' && !(raw.schema===HOST_POLICY_SCHEMA_V2 && kind==='inline')) {
81
+ throw new Error(`host command '${id}' kind must be script${raw.schema===HOST_POLICY_SCHEMA_V2?' or inline':''}`);
82
+ }
79
83
  if (typeof value.executable !== 'string' || !path.isAbsolute(value.executable)) {
80
84
  throw new Error(`host command '${id}' executable must be absolute`);
81
85
  }
@@ -86,37 +90,60 @@ function normalizePolicyObject(raw, policyPath) {
86
90
  if (rootIds.length === 0 || rootIds.some(rootId => !ROOT_ID_RE.test(rootId) || !roots[rootId])) {
87
91
  throw new Error(`host command '${id}' root_ids invalid`);
88
92
  }
89
- if (!Array.isArray(value.prefix_args) || value.prefix_args.some(x => typeof x !== 'string')) {
93
+ if (!Array.isArray(value.prefix_args) || value.prefix_args.some(x => typeof x !== 'string' || x.includes('\0'))) {
90
94
  throw new Error(`host command '${id}' prefix_args must be strings`);
91
95
  }
92
- if (!Array.isArray(value.extensions) || value.extensions.length === 0 ||
93
- value.extensions.some(x => typeof x !== 'string' || !/^\.[A-Za-z0-9._-]+$/.test(x))) {
94
- throw new Error(`host command '${id}' extensions invalid`);
95
- }
96
- commands[id] = {
97
- kind: 'script',
96
+ const base={
97
+ kind,
98
98
  executable: path.resolve(value.executable),
99
99
  executable_sha256: value.executable_sha256.toLowerCase(),
100
100
  root_ids: [...new Set(rootIds)],
101
101
  prefix_args: [...value.prefix_args],
102
- extensions: value.extensions.map(x => x.toLowerCase()),
103
102
  timeout_ms: finiteInteger(value.timeout_ms, `commands.${id}.timeout_ms`, 100, 300_000, HOST_DEFAULT_TIMEOUT_MS),
104
103
  max_output_bytes: finiteInteger(value.max_output_bytes, `commands.${id}.max_output_bytes`, 1024, 4 * 1024 * 1024, HOST_DEFAULT_MAX_OUTPUT_BYTES)
105
104
  };
105
+ if(kind==='script'){
106
+ if (!Array.isArray(value.extensions) || value.extensions.length === 0 ||
107
+ value.extensions.some(x => typeof x !== 'string' || !/^\.[A-Za-z0-9._-]+$/.test(x))) {
108
+ throw new Error(`host command '${id}' extensions invalid`);
109
+ }
110
+ base.extensions=value.extensions.map(x => x.toLowerCase());
111
+ } else {
112
+ base.max_command_chars=finiteInteger(value.max_command_chars,`commands.${id}.max_command_chars`,1,262144,32768);
113
+ }
114
+ commands[id]=base;
115
+ }
116
+
117
+ let nativeExec={enabled:false,root_ids:[],timeout_ms:HOST_DEFAULT_TIMEOUT_MS,max_output_bytes:HOST_DEFAULT_MAX_OUTPUT_BYTES,max_args:64,max_arg_chars:8192};
118
+ if(raw.schema===HOST_POLICY_SCHEMA_V2 && raw.native_exec!==undefined){
119
+ const value=raw.native_exec;
120
+ if(!value||typeof value!=='object'||Array.isArray(value)) throw new Error('host native_exec malformed');
121
+ if(value.enabled===true){
122
+ const rootIds=Array.isArray(value.root_ids)?value.root_ids:[];
123
+ if(rootIds.length===0||rootIds.some(rootId=>!ROOT_ID_RE.test(rootId)||!roots[rootId])) throw new Error('host native_exec root_ids invalid');
124
+ nativeExec={
125
+ enabled:true,
126
+ root_ids:[...new Set(rootIds)],
127
+ timeout_ms:finiteInteger(value.timeout_ms,'native_exec.timeout_ms',100,300000,HOST_DEFAULT_TIMEOUT_MS),
128
+ max_output_bytes:finiteInteger(value.max_output_bytes,'native_exec.max_output_bytes',1024,4*1024*1024,HOST_DEFAULT_MAX_OUTPUT_BYTES),
129
+ max_args:finiteInteger(value.max_args,'native_exec.max_args',0,64,64),
130
+ max_arg_chars:finiteInteger(value.max_arg_chars,'native_exec.max_arg_chars',1,32768,8192)
131
+ };
132
+ }
106
133
  }
107
134
 
108
135
  return {
109
- schema: HOST_POLICY_SCHEMA,
136
+ schema: raw.schema,
110
137
  enabled: true,
111
138
  policy_path: path.resolve(policyPath),
112
139
  arm_file: path.resolve(raw.arm_file),
113
140
  roots,
114
141
  commands,
142
+ native_exec:nativeExec,
115
143
  max_read_bytes: finiteInteger(raw.max_read_bytes, 'max_read_bytes', 1, 4 * 1024 * 1024, HOST_DEFAULT_MAX_READ_BYTES),
116
144
  max_write_bytes: finiteInteger(raw.max_write_bytes, 'max_write_bytes', 1, 8 * 1024 * 1024, HOST_DEFAULT_MAX_WRITE_BYTES)
117
145
  };
118
146
  }
119
-
120
147
  export async function loadHostPolicy(policyPath) {
121
148
  if (!policyPath) throw new Error('missing DEADBYTE privileged host policy path');
122
149
  const absolute = path.resolve(policyPath);
@@ -345,6 +372,93 @@ function collectBounded(stream, limit, state, label, onExceeded) {
345
372
  });
346
373
  }
347
374
 
375
+ function validateHostArgs(args,maxArgs=64,maxChars=2048,label='host exec args'){
376
+ if(!Array.isArray(args)||args.length>maxArgs||args.some(x=>typeof x!=='string'||x.length>maxChars||x.includes('\0'))){
377
+ throw new Error(`${label} must be <=${maxArgs} strings of <=${maxChars} characters`);
378
+ }
379
+ }
380
+ async function resolveExecCwd(policy,rootId,cwd){
381
+ const root=getRoot(policy,rootId,'exec');
382
+ let cwdResolved=root.real_path;
383
+ if(cwd!=='.'){
384
+ validatePortableRelativePath(cwd);
385
+ cwdResolved=path.resolve(root.real_path,cwd);
386
+ if(!isInside(root.real_path,cwdResolved)) throw new Error('host exec cwd escapes allowed root');
387
+ const realCwd=await realpath(cwdResolved);
388
+ if(!isInside(root.real_path,realCwd)) throw new Error('host exec cwd symlink escapes allowed root');
389
+ const cwdStat=await stat(realCwd);
390
+ if(!cwdStat.isDirectory()) throw new Error('host exec cwd must be a directory');
391
+ cwdResolved=realCwd;
392
+ }
393
+ return cwdResolved;
394
+ }
395
+ async function runHostChild(policy,armed,{executable,argv,cwd,timeoutMs,maxTimeoutMs,maxOutputBytes,authorityPollMs=100}){
396
+ const activeArm=await recheckHostAuthority(policy,armed);
397
+ const grantBound=Number.isFinite(activeArm.remaining_ms)?Math.floor(activeArm.remaining_ms):maxTimeoutMs;
398
+ if(grantBound<100) throw new Error('privileged host plane arm lease expires too soon');
399
+ const requested=timeoutMs===null||timeoutMs===undefined?maxTimeoutMs:finiteInteger(timeoutMs,'timeout_ms',100,maxTimeoutMs,maxTimeoutMs);
400
+ const effectiveTimeout=Math.min(requested,maxTimeoutMs,grantBound);
401
+ const child=spawn(executable,argv,{cwd,env:scrubHostEnvironment(),shell:false,windowsHide:true,detached:false,stdio:['ignore','pipe','pipe']});
402
+ const stdoutState={chunks:[],bytes:0,exceeded:null};
403
+ const stderrState={chunks:[],bytes:0,exceeded:null};
404
+ const kill=()=>{try{child.kill('SIGKILL');}catch{}};
405
+ collectBounded(child.stdout,maxOutputBytes,stdoutState,'stdout',kill);
406
+ collectBounded(child.stderr,maxOutputBytes,stderrState,'stderr',kill);
407
+ let timedOut=false,authorityError=null,timer=null,poller=null;
408
+ const completion=new Promise((resolve,reject)=>{
409
+ child.once('error',reject);
410
+ child.once('close',(code,signal)=>resolve({code,signal}));
411
+ timer=setTimeout(()=>{timedOut=true;kill();},effectiveTimeout); timer.unref?.();
412
+ const poll=Math.max(25,Math.min(1000,Number.isInteger(authorityPollMs)?authorityPollMs:100));
413
+ poller=setInterval(()=>{void recheckHostAuthority(policy,armed).catch(error=>{if(!authorityError){authorityError=error;kill();}});},poll); poller.unref?.();
414
+ });
415
+ let result;
416
+ try{result=await completion;}finally{if(timer)clearTimeout(timer);if(poller)clearInterval(poller);}
417
+ if(authorityError) throw authorityError;
418
+ if(stdoutState.exceeded||stderrState.exceeded) throw new Error(stdoutState.exceeded??stderrState.exceeded);
419
+ const stdoutBytes=Buffer.concat(stdoutState.chunks),stderrBytes=Buffer.concat(stderrState.chunks);
420
+ return {
421
+ exit_code:result.code,signal:result.signal,timed_out:timedOut,timeout_ms:effectiveTimeout,
422
+ stdout:decodeUtf8Strict(stdoutBytes),stderr:decodeUtf8Strict(stderrBytes),
423
+ stdout_sha256:sha256Bytes(stdoutBytes),stderr_sha256:sha256Bytes(stderrBytes),
424
+ argv_sha256:sha256Bytes(Buffer.from(JSON.stringify(argv),'utf8'))
425
+ };
426
+ }
427
+
428
+ export async function hostExecInline(policy,{commandId,rootId,command,cwd='.',timeoutMs=null,authorityPollMs=100}){
429
+ const armed=await assertHostArmed(policy);
430
+ if(!COMMAND_ID_RE.test(commandId)) throw new Error('invalid host command id');
431
+ const spec=policy.commands[commandId];
432
+ if(!spec||spec.kind!=='inline') throw new Error(`host inline command '${commandId}' is not allowed`);
433
+ if(!ROOT_ID_RE.test(rootId)||!spec.root_ids.includes(rootId)) throw new Error(`host command '${commandId}' is not allowed for root '${rootId}'`);
434
+ getRoot(policy,rootId,'exec');
435
+ if(typeof command!=='string'||command.length<1||command.length>spec.max_command_chars||command.includes('\0')) throw new Error('host inline command exceeds policy bounds');
436
+ const actualExecutableHash=await sha256File(spec.executable);
437
+ if(actualExecutableHash!==spec.executable_sha256) throw new Error(`host command '${commandId}' executable hash changed`);
438
+ const cwdResolved=await resolveExecCwd(policy,rootId,cwd);
439
+ const argv=[...spec.prefix_args,command];
440
+ const result=await runHostChild(policy,armed,{executable:spec.executable,argv,cwd:cwdResolved,timeoutMs,maxTimeoutMs:spec.timeout_ms,maxOutputBytes:spec.max_output_bytes,authorityPollMs});
441
+ return {command_id:commandId,mode:'inline',executable_sha256:actualExecutableHash,command_sha256:sha256Bytes(Buffer.from(command,'utf8')),...result};
442
+ }
443
+
444
+ export async function hostExecNative(policy,{rootId,executable,executableSha256,args=[],cwd='.',timeoutMs=null,authorityPollMs=100}){
445
+ const armed=await assertHostArmed(policy);
446
+ const spec=policy.native_exec;
447
+ if(!spec?.enabled) throw new Error('host native exec is not enabled by policy');
448
+ if(!ROOT_ID_RE.test(rootId)||!spec.root_ids.includes(rootId)) throw new Error(`host native exec is not allowed for root '${rootId}'`);
449
+ getRoot(policy,rootId,'exec');
450
+ if(typeof executable!=='string'||!path.isAbsolute(executable)) throw new Error('host native executable must be absolute');
451
+ if(typeof executableSha256!=='string'||!SHA256_RE.test(executableSha256)) throw new Error('executable_sha256 must be a lowercase SHA-256 digest');
452
+ validateHostArgs(args,spec.max_args,spec.max_arg_chars,'host native args');
453
+ const resolved=path.resolve(executable); const info=await stat(resolved);
454
+ if(!info.isFile()) throw new Error('host native executable is not a file');
455
+ const actualExecutableHash=await sha256File(resolved);
456
+ if(actualExecutableHash!==executableSha256) throw new Error(`host native executable hash mismatch: expected ${executableSha256}, found ${actualExecutableHash}`);
457
+ const cwdResolved=await resolveExecCwd(policy,rootId,cwd);
458
+ const result=await runHostChild(policy,armed,{executable:resolved,argv:[...args],cwd:cwdResolved,timeoutMs,maxTimeoutMs:spec.timeout_ms,maxOutputBytes:spec.max_output_bytes,authorityPollMs});
459
+ return {command_id:'native',mode:'native',executable_sha256:actualExecutableHash,...result};
460
+ }
461
+
348
462
  export async function hostExec(policy, {
349
463
  commandId,
350
464
  rootId,
@@ -357,14 +471,12 @@ export async function hostExec(policy, {
357
471
  const armed = await assertHostArmed(policy);
358
472
  if (!COMMAND_ID_RE.test(commandId)) throw new Error('invalid host command id');
359
473
  const command = policy.commands[commandId];
360
- if (!command) throw new Error(`host command '${commandId}' is not allowed`);
361
- if (!Array.isArray(args) || args.length > 64 || args.some(x => typeof x !== 'string' || x.length > 2048 || x.includes('\0'))) {
362
- throw new Error('host exec args must be <=64 strings of <=2048 characters');
363
- }
474
+ if (!command || command.kind!=='script') throw new Error(`host command '${commandId}' is not allowed as a script command`);
475
+ validateHostArgs(args,64,2048,'host exec args');
364
476
  if (typeof scriptSha256 !== 'string' || !SHA256_RE.test(scriptSha256)) throw new Error('script_sha256 must be a lowercase SHA-256 digest');
365
477
 
366
478
  if (!ROOT_ID_RE.test(rootId) || !command.root_ids.includes(rootId)) throw new Error(`host command '${commandId}' is not allowed for root '${rootId}'`);
367
- const root = getRoot(policy, rootId, 'exec');
479
+ getRoot(policy, rootId, 'exec');
368
480
  const script = await resolveHostExisting(policy, rootId, scriptPath, 'exec');
369
481
  const extension = path.extname(script.resolved).toLowerCase();
370
482
  if (!command.extensions.includes(extension)) throw new Error(`script extension '${extension}' is not allowed for '${commandId}'`);
@@ -373,72 +485,8 @@ export async function hostExec(policy, {
373
485
 
374
486
  const actualExecutableHash = await sha256File(command.executable);
375
487
  if (actualExecutableHash !== command.executable_sha256) throw new Error(`host command '${commandId}' executable hash changed`);
376
-
377
- let cwdResolved = root.real_path;
378
- if (cwd !== '.') {
379
- validatePortableRelativePath(cwd);
380
- cwdResolved = path.resolve(root.real_path, cwd);
381
- if (!isInside(root.real_path, cwdResolved)) throw new Error('host exec cwd escapes allowed root');
382
- const realCwd = await realpath(cwdResolved);
383
- if (!isInside(root.real_path, realCwd)) throw new Error('host exec cwd symlink escapes allowed root');
384
- const cwdStat = await stat(realCwd);
385
- if (!cwdStat.isDirectory()) throw new Error('host exec cwd must be a directory');
386
- cwdResolved = realCwd;
387
- }
388
-
389
- const requestedTimeout = timeoutMs === null ? command.timeout_ms : finiteInteger(timeoutMs, 'timeout_ms', 100, command.timeout_ms, command.timeout_ms);
390
- const activeArm = await recheckHostAuthority(policy, armed);
391
- const grantBound = Number.isFinite(activeArm.remaining_ms) ? Math.floor(activeArm.remaining_ms) : command.timeout_ms;
392
- if (grantBound < 100) throw new Error('privileged host plane arm lease expires too soon');
393
- const effectiveTimeout = Math.min(requestedTimeout, command.timeout_ms, grantBound);
394
- const argv = [...command.prefix_args, script.resolved, ...args];
395
-
396
- const child = spawn(command.executable, argv, {
397
- cwd: cwdResolved,
398
- env: scrubHostEnvironment(),
399
- shell: false,
400
- windowsHide: true,
401
- detached: false,
402
- stdio: ['ignore', 'pipe', 'pipe']
403
- });
404
-
405
- const stdoutState = { chunks: [], bytes: 0, exceeded: null };
406
- const stderrState = { chunks: [], bytes: 0, exceeded: null };
407
- collectBounded(child.stdout, command.max_output_bytes, stdoutState, 'stdout', () => child.kill('SIGKILL'));
408
- collectBounded(child.stderr, command.max_output_bytes, stderrState, 'stderr', () => child.kill('SIGKILL'));
409
-
410
- let timedOut = false;
411
- let timer;
412
- const completion = new Promise((resolve, reject) => {
413
- child.once('error', reject);
414
- child.once('close', (code, signal) => resolve({ code, signal }));
415
- timer = setTimeout(() => {
416
- timedOut = true;
417
- child.kill('SIGKILL');
418
- }, effectiveTimeout);
419
- timer.unref?.();
420
- });
421
- let result;
422
- try {
423
- result = await completion;
424
- } finally {
425
- if (timer) clearTimeout(timer);
426
- }
427
-
428
- if (stdoutState.exceeded || stderrState.exceeded) {
429
- throw new Error(stdoutState.exceeded ?? stderrState.exceeded);
430
- }
431
- const stdoutBytes = Buffer.concat(stdoutState.chunks);
432
- const stderrBytes = Buffer.concat(stderrState.chunks);
433
- return {
434
- command_id: commandId,
435
- executable_sha256: actualExecutableHash,
436
- script_sha256: actualScriptHash,
437
- exit_code: result.code,
438
- signal: result.signal,
439
- timed_out: timedOut,
440
- timeout_ms: effectiveTimeout,
441
- stdout: decodeUtf8Strict(stdoutBytes),
442
- stderr: decodeUtf8Strict(stderrBytes)
443
- };
488
+ const cwdResolved=await resolveExecCwd(policy,rootId,cwd);
489
+ const argv=[...command.prefix_args,script.resolved,...args];
490
+ const result=await runHostChild(policy,armed,{executable:command.executable,argv,cwd:cwdResolved,timeoutMs,maxTimeoutMs:command.timeout_ms,maxOutputBytes:command.max_output_bytes});
491
+ return {command_id:commandId,mode:'script',executable_sha256:actualExecutableHash,script_sha256:actualScriptHash,...result};
444
492
  }
@@ -32,10 +32,10 @@ export function registerMachineFsTools(server, { runtime }) {
32
32
  });
33
33
  server.registerTool('machine_fs_read', {
34
34
  description:'Read one bounded byte range from a root-relative regular file.',
35
- inputSchema:z.strictObject({ root_id:id, path:rel, offset:z.number().int().nonnegative().default(0), max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536), hash_file:z.boolean().default(false) }),
35
+ inputSchema:z.strictObject({ root_id:id, path:rel, offset:z.number().int().nonnegative().default(0), max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536), hash_file:z.boolean().default(false), timeout_ms:z.number().int().min(100).max(180000).default(30000) }),
36
36
  outputSchema:machineFsReadOutputSchema
37
- }, async ({root_id,path,offset,max_bytes,hash_file}) => {
38
- try { return ok(machineFsReadOutputSchema, await runtime.read({rootId:root_id,relativePath:path,offset,maxBytes:max_bytes,hashFile:hash_file})); }
37
+ }, async ({root_id,path,offset,max_bytes,hash_file,timeout_ms}) => {
38
+ try { return ok(machineFsReadOutputSchema, await runtime.read({rootId:root_id,relativePath:path,offset,maxBytes:max_bytes,hashFile:hash_file,timeoutMs:timeout_ms})); }
39
39
  catch (error) { return fail(machineFsReadOutputSchema,error); }
40
40
  });
41
41
  server.registerTool('machine_fs_write', {
@@ -33,7 +33,7 @@ export const machineFsReadOutputSchema = z.strictObject({
33
33
  offset: z.number().int().nonnegative().optional(), next_offset: z.number().int().nonnegative().optional(),
34
34
  file_size: z.number().int().nonnegative().optional(), eof: z.boolean().optional(),
35
35
  data_base64: z.string().max(12 * 1024 * 1024).optional(), data_sha256: hex64.optional(),
36
- file_sha256: hex64.nullable().optional()
36
+ file_sha256: hex64.nullable().optional(), timeout_ms: z.number().int().min(100).max(180000).optional()
37
37
  });
38
38
 
39
39
  const commitBase = {
@@ -0,0 +1,46 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream, promises as fs } from 'node:fs';
3
+
4
+ const [file, offsetText, lengthText, hashText] = process.argv.slice(2);
5
+ const offset = Number(offsetText);
6
+ const length = Number(lengthText);
7
+ if (typeof file !== 'string' || !Number.isSafeInteger(offset) || offset < 0 ||
8
+ !Number.isSafeInteger(length) || length < 0 || length > 4 * 1024 * 1024 ||
9
+ !['0','1'].includes(hashText ?? '0')) {
10
+ process.stderr.write('invalid machine-fs read worker arguments\n');
11
+ process.exit(64);
12
+ }
13
+
14
+ try {
15
+ const handle = await fs.open(file, 'r');
16
+ let bytes;
17
+ try {
18
+ const buffer = Buffer.alloc(length);
19
+ const result = length ? await handle.read(buffer, 0, length, offset) : { bytesRead: 0 };
20
+ bytes = buffer.subarray(0, result.bytesRead);
21
+ } finally {
22
+ await handle.close();
23
+ }
24
+
25
+ let fileSha256 = null;
26
+ if (hashText === '1') {
27
+ const hash = createHash('sha256');
28
+ await new Promise((resolve, reject) => {
29
+ const stream = createReadStream(file);
30
+ stream.on('data', chunk => hash.update(chunk));
31
+ stream.once('error', reject);
32
+ stream.once('end', resolve);
33
+ });
34
+ fileSha256 = hash.digest('hex');
35
+ }
36
+
37
+ process.stdout.write(JSON.stringify({
38
+ schema: 'deadbyte.machine-fs-read-worker.v1',
39
+ bytes_read: bytes.length,
40
+ data_base64: bytes.toString('base64'),
41
+ file_sha256: fileSha256
42
+ }));
43
+ } catch (error) {
44
+ process.stderr.write(String(error?.stack ?? error));
45
+ process.exit(1);
46
+ }