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.
- package/MANIFEST.SHA256 +109 -64
- package/PROVENANCE.json +11 -0
- package/R45-PARENT.json +18 -0
- package/README.txt +36 -20
- package/RELEASE.json +53 -0
- package/SBOM.cdx.json +46 -0
- package/TEST-MATRIX.json +31 -0
- package/bin/deadbyte-hardware-observer-r45.exe +0 -0
- package/bin/deadbyte-hardware-observer-r45.obj +0 -0
- package/controller/README.TXT +2 -2
- package/controller/deadbyte-controller.ps1 +51 -3
- package/controller/deadbyte-process-policy.json +63 -1
- package/docs/ARCHITECTURE.md +3 -4
- package/native/deadbyte-hardware-observer.cpp +308 -0
- package/package.json +14 -7
- package/scripts/autonomous-tests.ps1 +2 -2
- package/scripts/build-hardware-r45.ps1 +71 -0
- package/scripts/deferred-slot-operation-r45.mjs +126 -0
- package/scripts/deferred-slot-operation-r46.mjs +126 -0
- package/scripts/derive-r45-parent.mjs +24 -0
- package/scripts/derive-r45-release.mjs +72 -0
- package/scripts/derive-r45-tests.mjs +20 -0
- package/scripts/final-closure-r45.mjs +121 -0
- package/scripts/final-closure-r46.mjs +98 -0
- package/scripts/final-closure-verify-r45.mjs +220 -0
- package/scripts/final-closure-verify-r46.mjs +119 -0
- package/scripts/finalize-r45.mjs +219 -0
- package/scripts/full-regression-r45.mjs +20 -0
- package/scripts/full-regression-r46.mjs +20 -0
- package/scripts/full-regression-summary-r45.mjs +14 -0
- package/scripts/gate-windows-r45.ps1 +44 -0
- package/scripts/gate-windows-r46.ps1 +56 -0
- package/scripts/init-host-policy.ps1 +39 -1
- package/scripts/inspect-active-release-r45.mjs +20 -0
- package/scripts/refresh-process-policy.mjs +64 -7
- package/scripts/release-manifest.mjs +3 -1
- package/scripts/release-parity-r45.mjs +253 -0
- package/scripts/release-parity-r46.mjs +187 -0
- package/scripts/release-parity-tests-r45.ps1 +8 -0
- package/scripts/verify-r45-parent.mjs +142 -0
- package/scripts/windows-gate-evidence-r45.mjs +60 -0
- package/scripts/windows-gate-evidence-r46.mjs +110 -0
- package/src/autonomous-runtime.mjs +12 -7
- package/src/capability-ledger.mjs +2 -0
- package/src/compact-gateway-mcp-tools.mjs +11 -7
- package/src/deadbyte-cli.mjs +24 -12
- package/src/doctor-r45.mjs +65 -0
- package/src/host-plane.mjs +133 -85
- package/src/machine-fs-mcp-tools.mjs +3 -3
- package/src/machine-fs-output-contracts.mjs +1 -1
- package/src/machine-fs-read-worker.mjs +46 -0
- package/src/machine-fs.mjs +63 -10
- package/src/mcp-server.mjs +68 -23
- package/src/mcp-smoke-client.mjs +4 -1
- package/src/native-diff-v2.mjs +9 -4
- package/src/native-diff.mjs +29 -7
- package/src/observation-mcp-tools.mjs +5 -1
- package/src/observation-output-contracts.mjs +8 -0
- package/src/observation-runtime.mjs +57 -5
- package/src/observation-smoke-client.mjs +6 -2
- package/src/output-contracts.mjs +27 -11
- package/src/process-mcp-tools.mjs +18 -5
- package/src/process-output-contracts.mjs +20 -3
- package/src/process-policy.mjs +3 -1
- package/src/process-runtime.mjs +99 -19
- package/src/process-smoke-client.mjs +67 -22
- package/src/r45-compact-disarmed-probe.mjs +74 -0
- package/src/r45-full-surface-probe.mjs +34 -0
- package/src/release-generation.mjs +23 -3
- package/src/remote-device-r44.mjs +53 -15
- package/src/remote-mcp-r45-parity-client.mjs +206 -0
- package/src/remote-mcp-smoke-client.mjs +1 -1
- package/src/runtime-update.mjs +36 -8
- package/src/stdio-console-guard.mjs +24 -0
- package/src/version.mjs +1 -1
- package/test/agent-runtime.test.mjs +1 -0
- package/test/autonomous-loop.test.mjs +24 -0
- package/test/autonomous-runtime.test.mjs +1 -0
- package/test/cli-entrypoint.test.mjs +4 -4
- package/test/coding-plane.test.mjs +1 -0
- package/test/compact-gateway-r36.test.mjs +28 -0
- package/test/compact-max-catalog-r45.test.mjs +99 -0
- package/test/contained.test.mjs +23 -11
- package/test/controller.test.mjs +5 -1
- package/test/core.test.mjs +1 -1
- package/test/full-max-catalog-r45.test.mjs +91 -0
- package/test/full-max-catalog-r46.test.mjs +92 -0
- package/test/hardware-observation-r45.test.mjs +44 -0
- package/test/host-inline-r45.test.mjs +67 -0
- package/test/machine-fs-mcp-tools.test.mjs +4 -1
- package/test/machine-fs-runtime.test.mjs +18 -2
- package/test/machine-mcp-integration.test.mjs +1 -0
- package/test/observation-mcp-tools.test.mjs +4 -3
- package/test/observation-runtime.test.mjs +29 -2
- package/test/output-schema.test.mjs +1 -0
- package/test/package-boundary.test.mjs +1 -1
- package/test/process-mcp-tools.test.mjs +23 -5
- package/test/process-policy-refresh.test.mjs +87 -10
- package/test/process-runtime.test.mjs +111 -1
- package/test/process-supervision.test.mjs +4 -2
- package/test/production-docs.test.mjs +3 -3
- package/test/r33-closeout-regression.test.mjs +20 -19
- package/test/r33-finalization.test.mjs +3 -3
- package/test/r45-autonomous-quick-test.test.mjs +10 -0
- package/test/r45-finalization.test.mjs +84 -0
- package/test/release-generation.test.mjs +26 -11
- package/test/release-version.test.mjs +112 -46
- package/test/remote-device-r44.test.mjs +75 -0
- package/test/remote-r24-catalog.test.mjs +2 -2
- 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:
|
|
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.
|
|
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
|
}
|
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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(
|
|
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;}
|
package/src/host-plane.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
+
}
|