deadbyte-mcp 0.15.1 → 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 (55) hide show
  1. package/MANIFEST.SHA256 +54 -44
  2. package/PROVENANCE.json +6 -5
  3. package/README.txt +36 -20
  4. package/RELEASE.json +24 -8
  5. package/SBOM.cdx.json +3 -3
  6. package/controller/README.TXT +2 -2
  7. package/controller/deadbyte-controller.ps1 +50 -2
  8. package/controller/deadbyte-process-policy.json +63 -1
  9. package/docs/ARCHITECTURE.md +3 -4
  10. package/package.json +9 -7
  11. package/scripts/deferred-slot-operation-r46.mjs +126 -0
  12. package/scripts/final-closure-r46.mjs +98 -0
  13. package/scripts/final-closure-verify-r46.mjs +119 -0
  14. package/scripts/full-regression-r46.mjs +20 -0
  15. package/scripts/gate-windows-r46.ps1 +56 -0
  16. package/scripts/refresh-process-policy.mjs +64 -7
  17. package/scripts/release-parity-r46.mjs +187 -0
  18. package/scripts/windows-gate-evidence-r46.mjs +110 -0
  19. package/src/capability-ledger.mjs +1 -0
  20. package/src/compact-gateway-mcp-tools.mjs +7 -5
  21. package/src/deadbyte-cli.mjs +7 -3
  22. package/src/machine-fs-mcp-tools.mjs +3 -3
  23. package/src/machine-fs-output-contracts.mjs +1 -1
  24. package/src/machine-fs-read-worker.mjs +46 -0
  25. package/src/machine-fs.mjs +63 -10
  26. package/src/mcp-server.mjs +4 -0
  27. package/src/mcp-smoke-client.mjs +4 -1
  28. package/src/observation-runtime.mjs +25 -4
  29. package/src/process-mcp-tools.mjs +18 -5
  30. package/src/process-output-contracts.mjs +20 -3
  31. package/src/process-policy.mjs +3 -1
  32. package/src/process-runtime.mjs +99 -19
  33. package/src/process-smoke-client.mjs +67 -22
  34. package/src/release-generation.mjs +22 -3
  35. package/src/remote-device-r44.mjs +53 -15
  36. package/src/remote-mcp-smoke-client.mjs +1 -1
  37. package/src/stdio-console-guard.mjs +24 -0
  38. package/src/version.mjs +1 -1
  39. package/test/cli-entrypoint.test.mjs +3 -3
  40. package/test/compact-gateway-r36.test.mjs +28 -0
  41. package/test/core.test.mjs +1 -1
  42. package/test/full-max-catalog-r46.test.mjs +92 -0
  43. package/test/machine-fs-mcp-tools.test.mjs +4 -1
  44. package/test/machine-fs-runtime.test.mjs +18 -2
  45. package/test/observation-runtime.test.mjs +29 -2
  46. package/test/process-mcp-tools.test.mjs +23 -5
  47. package/test/process-policy-refresh.test.mjs +87 -10
  48. package/test/process-runtime.test.mjs +111 -1
  49. package/test/process-supervision.test.mjs +4 -2
  50. package/test/production-docs.test.mjs +3 -3
  51. package/test/r33-finalization.test.mjs +3 -3
  52. package/test/release-generation.test.mjs +25 -11
  53. package/test/release-version.test.mjs +99 -44
  54. package/test/remote-device-r44.test.mjs +75 -0
  55. package/test/remote-r24-catalog.test.mjs +2 -2
@@ -105,7 +105,7 @@ function rebindHostPolicy(raw,{developmentRoot,stateRoot}){
105
105
  return policy;
106
106
  }
107
107
 
108
- async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot}){
108
+ async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot,systemRoot}){
109
109
  if(!raw) return null;
110
110
  const policy=clone(raw);
111
111
  policy.grant_file=path.join(stateRoot,'runtime','process-grant.json');
@@ -116,6 +116,25 @@ async function rebindProcessPolicy(raw,{developmentRoot,stateRoot,releaseRoot}){
116
116
  const nativeHostStat=await lstat(nativeHostPath);
117
117
  if(!nativeHostStat.isFile()||nativeHostStat.isSymbolicLink()) throw new Error('process native host invalid in release slot');
118
118
  policy.native_host={path:nativeHostPath,sha256:sha256(await readFile(nativeHostPath))};
119
+
120
+ const systemProfiles=[
121
+ ['ping_hold',['System32','cmd.exe']],
122
+ ['powershell',['System32','WindowsPowerShell','v1.0','powershell.exe']],
123
+ ['powershell_repl',['System32','WindowsPowerShell','v1.0','powershell.exe']]
124
+ ];
125
+ const needsSystemRoot=systemProfiles.some(([id])=>policy.profiles?.[id]);
126
+ if(needsSystemRoot&&(typeof systemRoot!=='string'||systemRoot.length<1)) {
127
+ throw new Error('process system profiles require SystemRoot/WINDIR');
128
+ }
129
+ for(const [id,parts] of systemProfiles){
130
+ const profile=policy.profiles?.[id];
131
+ if(!profile) continue;
132
+ const executable=path.join(path.resolve(systemRoot),...parts);
133
+ const stat=await lstat(executable);
134
+ if(!stat.isFile()||stat.isSymbolicLink()) throw new Error(`process profile '${id}' system executable invalid`);
135
+ profile.executable=executable;
136
+ profile.executable_sha256=sha256(await readFile(executable));
137
+ }
119
138
  return policy;
120
139
  }
121
140
 
@@ -199,7 +218,7 @@ function renderControllerConfig({currentConfig,coreRoot,workspaceRoot,processPol
199
218
  '}',''
200
219
  ].join('\r\n');
201
220
  }
202
- export async function createGenerationBundle({stateRoot,releaseRoot,developmentRoot,workspaceRoot,releaseId,currentConfig,processPolicy,desktopPolicy,codingPolicy,autonomousPolicy,hostPolicy}){
221
+ export async function createGenerationBundle({stateRoot,releaseRoot,developmentRoot,workspaceRoot,releaseId,currentConfig,processPolicy,desktopPolicy,codingPolicy,autonomousPolicy,hostPolicy,systemRoot=process.env.SystemRoot??process.env.WINDIR??null}){
203
222
  const state=path.resolve(stateRoot),release=path.resolve(releaseRoot),development=path.resolve(developmentRoot),workspace=path.resolve(workspaceRoot);
204
223
  if(typeof releaseId!=='string'||!/^[A-Za-z0-9._-]{1,96}$/.test(releaseId)) throw new Error('generation release id invalid');
205
224
  if(typeof currentConfig?.Bridge?.FileSha256!=='string'||!HEX64.test(currentConfig.Bridge.FileSha256)) throw new Error('generation Bridge FileSha256 invalid');
@@ -255,7 +274,7 @@ export async function createGenerationBundle({stateRoot,releaseRoot,developmentR
255
274
  const autonomousBytes=jsonBytes(autonomous);
256
275
  const host=rebindHostPolicy(hostPolicy,{developmentRoot:development,stateRoot:state});
257
276
  const hostBytes=host?jsonBytes(host):null;
258
- const processBound=await rebindProcessPolicy(processPolicy,{developmentRoot:development,stateRoot:state,releaseRoot:release});
277
+ const processBound=await rebindProcessPolicy(processPolicy,{developmentRoot:development,stateRoot:state,releaseRoot:release,systemRoot});
259
278
  const processBytes=processBound?jsonBytes(processBound):null;
260
279
  const desktopBound=await rebindDesktopPolicy(desktopPolicy,{stateRoot:state,releaseRoot:release});
261
280
  const desktopBytes=desktopBound?jsonBytes(desktopBound):null;
@@ -57,19 +57,34 @@ function parseDeviceKey(spki){
57
57
  insist(key.asymmetricKeyType==='ed25519','remote device key must be Ed25519');
58
58
  return {deviceId:sha(der),spki};
59
59
  }
60
- function publicDevice(d){
60
+ function publicDevice(d,reachableTtlMs=Infinity){
61
+ const lastOk=typeof d.last_probe_ok_at_utc==='string'?Date.parse(d.last_probe_ok_at_utc):NaN;
62
+ const fresh=d.reachable===true&&(reachableTtlMs===Infinity||(Number.isFinite(lastOk)&&Date.now()-lastOk<=reachableTtlMs));
61
63
  return {device_id:d.device_id,display_name:d.display_name,epoch:d.epoch,revoked:d.revoked,
62
- reported_online:d.reported_online,reachable:d.reachable,connection_id:d.connection_id,updated_at_utc:d.updated_at_utc};
64
+ reported_online:d.reported_online,reachable:fresh,connection_id:d.connection_id,
65
+ last_probe_ok_at_utc:d.last_probe_ok_at_utc??null,updated_at_utc:d.updated_at_utc};
66
+ }
67
+ async function withTimeout(operation,timeoutMs,label){
68
+ let timer;
69
+ try{
70
+ return await Promise.race([
71
+ Promise.resolve().then(operation),
72
+ new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(`${label} timed out after ${timeoutMs}ms`)),timeoutMs);timer.unref?.();})
73
+ ]);
74
+ }finally{if(timer)clearTimeout(timer);}
63
75
  }
64
76
  function payloadHash({deviceId,epoch,capabilityId,payload}){
65
77
  return sha(Buffer.from(canonicalJson({device_id:deviceId,epoch,capability_id:capabilityId,payload}),'utf8'));
66
78
  }
67
79
 
68
- export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapability,onCredentialSuperseded=null}={}){
80
+ export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapability,onCredentialSuperseded=null,relayTimeoutMs=15000,probeRetries=1,reachableTtlMs=75000}={}){
69
81
  insist(typeof stateDir==='string'&&path.isAbsolute(stateDir),'remote stateDir must be absolute');
70
82
  insist(relay&&typeof relay.probe==='function'&&typeof relay.request==='function','remote relay contract invalid');
71
83
  insist(typeof authorizeCapability==='function','remote capability authorizer required');
72
84
  insist(onCredentialSuperseded===null||typeof onCredentialSuperseded==='function','remote credential supersede callback invalid');
85
+ insist(Number.isInteger(relayTimeoutMs)&&relayTimeoutMs>=100&&relayTimeoutMs<=120000,'remote relayTimeoutMs invalid');
86
+ insist(Number.isInteger(probeRetries)&&probeRetries>=0&&probeRetries<=3,'remote probeRetries invalid');
87
+ insist(Number.isInteger(reachableTtlMs)&&reachableTtlMs>=1000&&reachableTtlMs<=10*60*1000,'remote reachableTtlMs invalid');
73
88
  await mkdir(stateDir,{recursive:true});const root=await realpath(stateDir);
74
89
  const stateFile=path.join(root,'remote-devices.json'),credentialsDir=path.join(root,'credentials');
75
90
  await mkdir(credentialsDir,{recursive:true});
@@ -79,6 +94,8 @@ export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapabi
79
94
  const run=lock.then(fn,fn);lock=run.catch(()=>{});return run;
80
95
  };
81
96
  const persist=()=>{state.updated_at_utc=now();return atomicJson(stateFile,state);};
97
+ const pub=d=>publicDevice(d,reachableTtlMs);
98
+ const fresh=d=>pub(d).reachable;
82
99
  const getDevice=(deviceId,epoch=null)=>{
83
100
  insist(HEX64.test(deviceId??''),'device id invalid');
84
101
  const d=state.devices[deviceId];insist(d,'unknown remote device');
@@ -95,21 +112,21 @@ export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapabi
95
112
  if(existing){
96
113
  insist(!existing.revoked,'remote device revoked');
97
114
  insist(existing.public_key_spki_base64===spki,'remote device key drift');
98
- return publicDevice(existing);
115
+ return pub(existing);
99
116
  }
100
117
  const d={device_id:deviceId,public_key_spki_base64:spki,display_name:displayName,epoch:1,revoked:false,
101
118
  reported_online:false,reachable:false,connection_id:null,last_probe_nonce:null,updated_at_utc:now()};
102
- state.devices[deviceId]=d;await persist();return publicDevice(d);
119
+ state.devices[deviceId]=d;await persist();return pub(d);
103
120
  });
104
121
  }
105
122
  async function status({deviceId}={}){
106
- const d=getDevice(deviceId);return publicDevice(d);
123
+ const d=getDevice(deviceId);return pub(d);
107
124
  }
108
125
  async function reportOnline({deviceId,epoch,connectionId}={}){
109
126
  insist(typeof connectionId==='string'&&connectionId.length>=1&&connectionId.length<=256,'connection id invalid');
110
127
  return withLock(async()=>{
111
128
  const d=getDevice(deviceId,epoch);d.reported_online=true;d.reachable=false;d.connection_id=connectionId;
112
- d.last_probe_nonce=null;d.updated_at_utc=now();await persist();return publicDevice(d);
129
+ d.last_probe_nonce=null;d.updated_at_utc=now();await persist();return pub(d);
113
130
  });
114
131
  }
115
132
  async function probe({deviceId,epoch}={}){
@@ -119,12 +136,25 @@ export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapabi
119
136
  const d=getDevice(deviceId,epoch);insist(d.reported_online&&d.connection_id,'remote device is not reported online');
120
137
  connectionId=d.connection_id;d.reachable=false;d.last_probe_nonce=nonce;d.updated_at_utc=now();await persist();
121
138
  });
122
- const response=await relay.probe({device_id:deviceId,epoch,connection_id:connectionId,nonce});
139
+ let response=null,lastError=null;
140
+ for(let attempt=0;attempt<=probeRetries;attempt+=1){
141
+ try{
142
+ response=await withTimeout(
143
+ ()=>relay.probe({device_id:deviceId,epoch,connection_id:connectionId,nonce}),
144
+ relayTimeoutMs,'remote readiness probe'
145
+ );
146
+ lastError=null;break;
147
+ }catch(error){lastError=error;}
148
+ }
149
+ if(lastError){
150
+ await withLock(async()=>{const d=getDevice(deviceId,epoch);if(d.connection_id===connectionId){d.reachable=false;d.updated_at_utc=now();await persist();}});
151
+ throw lastError;
152
+ }
123
153
  return withLock(async()=>{
124
154
  const d=getDevice(deviceId,epoch);
125
155
  insist(response&&response.device_id===deviceId&&response.epoch===epoch&&response.nonce===nonce&&response.ready===true,'remote readiness probe acknowledgment invalid');
126
156
  insist(d.connection_id===connectionId&&d.last_probe_nonce===nonce,'remote connection changed during readiness probe');
127
- d.reachable=true;d.updated_at_utc=now();await persist();return {...publicDevice(d),probe_nonce:nonce};
157
+ d.reachable=true;d.last_probe_ok_at_utc=now();d.updated_at_utc=now();await persist();return {...pub(d),probe_nonce:nonce};
128
158
  });
129
159
  }
130
160
  async function dispatch({deviceId,epoch,capabilityId,payload={},requestId}={}){
@@ -141,7 +171,7 @@ export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapabi
141
171
  const allowed=await authorizeCapability({deviceId,epoch,capabilityId});
142
172
  insist(allowed===true,'remote capability denied by local policy');
143
173
  await withLock(async()=>{
144
- const d=getDevice(deviceId,epoch);insist(d.reported_online&&d.reachable,'remote device not reachable');
174
+ const d=getDevice(deviceId,epoch);insist(d.reported_online&&fresh(d),'remote device not reachable or readiness probe is stale');
145
175
  const again=state.requests[requestId];
146
176
  if(again){insist(again.request_sha256===requestSha,'remote request id conflict');return;}
147
177
  state.requests[requestId]={request_id:requestId,request_sha256:requestSha,device_id:deviceId,epoch,capability_id:capabilityId,
@@ -150,9 +180,17 @@ export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapabi
150
180
  });
151
181
  if(state.requests[requestId].state!=='accepted')return {...structuredClone(state.requests[requestId]),replay:true};
152
182
  let response=null;
153
- try{response=await relay.request({device_id:deviceId,epoch,capability_id:capabilityId,payload,request_id:requestId,request_sha256:requestSha});}
154
- catch(error){
155
- return withLock(async()=>{const rec=state.requests[requestId];rec.state='failed';rec.error_code='transport_error';rec.error=String(error?.message??error);rec.updated_at_utc=now();await persist();return structuredClone(rec);});
183
+ try{
184
+ response=await withTimeout(
185
+ ()=>relay.request({device_id:deviceId,epoch,capability_id:capabilityId,payload,request_id:requestId,request_sha256:requestSha}),
186
+ relayTimeoutMs,'remote dispatch'
187
+ );
188
+ }catch(error){
189
+ return withLock(async()=>{
190
+ const rec=state.requests[requestId];rec.state='failed';rec.error_code='transport_error';rec.error=String(error?.message??error);rec.updated_at_utc=now();
191
+ const d=state.devices[deviceId];if(d&&d.epoch===epoch){d.reachable=false;d.updated_at_utc=now();}
192
+ await persist();return structuredClone(rec);
193
+ });
156
194
  }
157
195
  if(!response||response.ack!=='accepted'||response.request_id!==requestId){
158
196
  return withLock(async()=>{const rec=state.requests[requestId];rec.state='failed';rec.error_code='transport_no_ack';rec.updated_at_utc=now();await persist();return structuredClone(rec);});
@@ -172,7 +210,7 @@ export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapabi
172
210
  async function revoke({deviceId,epoch}={}){
173
211
  return withLock(async()=>{
174
212
  const d=getDevice(deviceId,epoch);d.revoked=true;d.reported_online=false;d.reachable=false;d.connection_id=null;d.updated_at_utc=now();await persist();
175
- return {...publicDevice(d),revoked:true};
213
+ return {...pub(d),revoked:true};
176
214
  });
177
215
  }
178
216
  async function rotateCredential({deviceId,epoch,encryptedBundleBase64,expectedPreviousSha256}={}){
@@ -200,6 +238,6 @@ export async function createRemoteDevicePlaneR44({stateDir,relay,authorizeCapabi
200
238
  }
201
239
  return {status:'committed',device_id:deviceId,epoch,credential_sha256:credentialSha,previous_credential_sha256:previous?.credential_sha256??null};
202
240
  }
203
- async function list(){return {status:'ok',devices:Object.values(state.devices).map(publicDevice).sort((a,b)=>a.device_id.localeCompare(b.device_id)),requests:structuredClone(state.requests)};}
241
+ async function list(){return {status:'ok',devices:Object.values(state.devices).map(pub).sort((a,b)=>a.device_id.localeCompare(b.device_id)),requests:structuredClone(state.requests)};}
204
242
  return Object.freeze({pair,status,reportOnline,probe,dispatch,revoke,rotateCredential,list,state_file:stateFile});
205
243
  }
@@ -13,7 +13,7 @@ const privilegedTools = ['host_exec', 'host_file_read', 'host_file_write'];
13
13
  const machineTools = ['machine_list', 'machine_ping', 'machine_revoke', 'machine_status'];
14
14
  const processTools = [
15
15
  'process_session_action', 'process_session_list', 'process_session_read', 'process_session_start',
16
- 'process_session_status', 'process_session_terminate'
16
+ 'process_session_status', 'process_session_terminate', 'process_session_write'
17
17
  ];
18
18
  const machineFsTools = [
19
19
  'machine_fs_delete', 'machine_fs_edit_exact', 'machine_fs_list', 'machine_fs_mkdir',
@@ -0,0 +1,24 @@
1
+ import { format } from 'node:util';
2
+
3
+ export function installStdioConsoleGuard({ stderr = process.stderr } = {}) {
4
+ if (!stderr || typeof stderr.write !== 'function') throw new Error('stdio console guard requires writable stderr');
5
+ const original = {
6
+ log: console.log,
7
+ info: console.info,
8
+ debug: console.debug
9
+ };
10
+ const write = (level, args) => {
11
+ const text = format(...args);
12
+ stderr.write(`DEADBYTE MCP ${level.toUpperCase()} ${text}${text.endsWith('\n') ? '' : '\n'}`);
13
+ };
14
+ console.log = (...args) => write('log', args);
15
+ console.info = (...args) => write('info', args);
16
+ console.debug = (...args) => write('debug', args);
17
+ return Object.freeze({
18
+ restore() {
19
+ console.log = original.log;
20
+ console.info = original.info;
21
+ console.debug = original.debug;
22
+ }
23
+ });
24
+ }
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.1';
1
+ export const DEADBYTE_VERSION = '0.15.2';
2
2
  export const MCP_PROTOCOL_VERSION = '2026-07-28';
3
3
  export const RECEIPT_SCHEMA_V1 = 'deadbyte.receipt.v1';
4
4
  export const RECEIPT_SCHEMA_V2 = 'deadbyte.receipt.v2';
@@ -21,7 +21,7 @@ test('npx CLI reports the canonical package version through --version, -v and ve
21
21
  for(const arg of ['--version','-v','version']){
22
22
  const result=spawnSync(process.execPath,[cliPath,arg],{cwd:root,encoding:'utf8',windowsHide:true});
23
23
  assert.equal(result.status,0,result.stderr);
24
- assert.equal(result.stdout.trim(),'0.15.1');
24
+ assert.equal(result.stdout.trim(),'0.15.2');
25
25
  }
26
26
  });
27
27
 
@@ -50,6 +50,6 @@ test('npx CLI exposes local supervisor lifecycle without any supervisor arm comm
50
50
 
51
51
  test('README makes npx the primary operator UX',async()=>{
52
52
  const readme=await readFile(path.join(root,'README.txt'),'utf8');
53
- assert.match(readme,/npx -y deadbyte-mcp@0\.15\.1 remote/);
54
- assert.match(readme,/npx -y deadbyte-mcp@0\.15\.1 audit verify/);
53
+ assert.match(readme,/npx -y deadbyte-mcp@0\.15\.2 remote/);
54
+ assert.match(readme,/npx -y deadbyte-mcp@0\.15\.2 audit verify/);
55
55
  });
@@ -24,6 +24,8 @@ test('R36 compact gateway schemas reject generic arbitrary dispatch',()=>{
24
24
  assert.equal(codingInspectInputSchema.safeParse({op:'call',tool_name:'host_exec',args:{}}).success,false);
25
25
  assert.equal(machineFsGatewayInputSchema.safeParse({op:'exec',command:'whoami'}).success,false);
26
26
  assert.equal(processSessionGatewayInputSchema.safeParse({op:'stdin',session_id:'0'.repeat(64),input:'raw'}).success,false);
27
+ assert.equal(processSessionGatewayInputSchema.safeParse({op:'write',session_id:'0'.repeat(64),input:'raw',append_newline:true}).success,true);
28
+ assert.equal(processSessionGatewayInputSchema.safeParse({op:'write',session_id:'0'.repeat(64),input:'raw',pid:1234}).success,false);
27
29
  assert.equal(machineObserveInputSchema.safeParse({op:'revoke',reason:'no'}).success,false);
28
30
  assert.equal(desktopControlInputSchema.safeParse({op:'call',tool_name:'host_exec'}).success,false);
29
31
  });
@@ -36,6 +38,7 @@ test('R36 compact schemas preserve representative preconditions',()=>{
36
38
  op:'move',root_id:'core',source_path:'a.txt',destination_path:'b.txt'
37
39
  }).success,false);
38
40
  assert.equal(processSessionGatewayInputSchema.safeParse({op:'action',session_id:'b'.repeat(64),action_id:'ping'}).success,true);
41
+ assert.equal(processSessionGatewayInputSchema.safeParse({op:'read',session_id:'b'.repeat(64),offset:-4096,max_bytes:4096,wait_ms:5000,wait_for:'READY>'}).success,true);
39
42
  });
40
43
 
41
44
  test('R40 registers exactly six compact gateway names including desktop_control',()=>{
@@ -116,3 +119,28 @@ test('R40 desktop_control is stable when runtime is absent and routes typed read
116
119
  assert.equal(unavailable.isError,true);
117
120
  assert.equal(unavailable.structuredContent.status,'unavailable');
118
121
  });
122
+
123
+
124
+ test('R46 compact process_session routes guarded write and wait/tail read without adding a tool name',async()=>{
125
+ const calls=[];
126
+ const processRuntime={
127
+ write:async args=>{calls.push(['write',args]);return {logical_bytes:4,logical_sha256:'d'.repeat(64),append_newline:true,session:{session_id:'a'.repeat(64)}};},
128
+ read:async args=>{calls.push(['read',args]);return {session_id:'a'.repeat(64),offset:0,next_offset:0,earliest_offset:0,stream_head:0,stream_sha256:'b'.repeat(64),truncated:false,wait_satisfied:true,data_base64:'',state:'running',exit_code:null};}
129
+ };
130
+ const server=fakeServer();
131
+ registerCompactGatewayTools(server,{
132
+ profile:'compact',ledger:{schema:'deadbyte.capability-ledger.v1',entries:[],ledger_sha256:'0'.repeat(64)},
133
+ codingRuntime:null,semanticRuntime:null,machineFsRuntime:null,processRuntime,observationRuntime:null,machineRuntime:null,desktopRuntime:null,
134
+ catalogProvider:()=>[...server.tools.keys()]
135
+ });
136
+ const tool=server.tools.get('process_session');
137
+ const wrote=await tool.handler({op:'write',session_id:'a'.repeat(64),input:'x=1',append_newline:true});
138
+ assert.equal(wrote.isError,false);
139
+ const read=await tool.handler({op:'read',session_id:'a'.repeat(64),offset:-1024,max_bytes:4096,wait_ms:250,wait_for:'done'});
140
+ assert.equal(read.isError,false);
141
+ assert.deepEqual(calls,[
142
+ ['write',{sessionId:'a'.repeat(64),input:'x=1',appendNewline:true}],
143
+ ['read',{sessionId:'a'.repeat(64),offset:-1024,maxBytes:4096,waitMs:250,waitFor:'done'}]
144
+ ]);
145
+ assert.equal(server.tools.size,6);
146
+ });
@@ -105,7 +105,7 @@ test('proof execution creates deterministic output and signed verifiable receipt
105
105
  assert.equal(result.receipt.request.schema, 'deadbyte.request.v1');
106
106
  assert.match(result.receipt.request.nonce, /^[0-9a-f]{64}$/);
107
107
  assert.match(result.receipt.request.sha256, /^[0-9a-f]{64}$/);
108
- assert.equal(result.receipt.runtime.package_version, '0.15.1');
108
+ assert.equal(result.receipt.runtime.package_version, '0.15.2');
109
109
  assert.match(result.receipt.runtime.mcp_ingress_adapter, /^(?:none|tunnel-stdio-probe-compat-v1)$/);
110
110
  assert.match(result.receipt.runtime.mcp_ingress_adapter_sha256, /^[0-9a-f]{64}$/);
111
111
  assert.match(result.receipt.runtime.mcp_ingress_mode_source_sha256, /^[0-9a-f]{64}$/);
@@ -0,0 +1,92 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createHash } from 'node:crypto';
4
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { Client } from '@modelcontextprotocol/client';
8
+ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
9
+ import { createR34Fixture } from './helpers/autonomous-r34-fixture.mjs';
10
+
11
+ const here=path.dirname(fileURLToPath(import.meta.url));
12
+ const repoRoot=path.resolve(here,'..');
13
+ const serverPath=path.join(repoRoot,'src','mcp-server.mjs');
14
+ const desktopPolicyPath=path.join(repoRoot,'controller','deadbyte-desktop-policy.json');
15
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
16
+ const absent=async p=>{try{await access(p);return false;}catch(e){if(e?.code==='ENOENT')return true;throw e;}};
17
+
18
+ test('R46 real max-runtime full stdio catalog is exactly 83 with skill and four Desktop primitives while authority remains disarmed',async()=>{
19
+ const f=await createR34Fixture({authority:'disarmed'});
20
+ const processPolicyPath=path.join(f.root,'process-policy.json');
21
+ const processGrant=path.join(f.root,'process-grant.json');
22
+ const processState=path.join(f.root,'process-state');
23
+ const hostPolicyPath=path.join(f.root,'host-policy.json');
24
+ const hostArm=path.join(f.root,'host-arm.json');
25
+ const nodeSha=sha(await readFile(process.execPath));
26
+ await mkdir(processState,{recursive:true});
27
+ await writeFile(processPolicyPath,JSON.stringify({
28
+ schema:'deadbyte.process-policy.v1',enabled:true,grant_file:processGrant,state_dir:processState,
29
+ native_host:{path:process.execPath,sha256:nodeSha},
30
+ roots:{core:{path:f.project,deny_paths:[]}},
31
+ profiles:{node:{executable:process.execPath,executable_sha256:nodeSha,root_ids:['core'],
32
+ prefix_args:[],transport:'pipe',max_args:8,max_arg_bytes:1024,max_sessions:2,
33
+ max_input_bytes:65536,max_retained_output_bytes:262144,env_allow:['PATH','TEMP','TMP'],
34
+ input_actions:{ping:'PING\r\n'}}}
35
+ },null,2));
36
+ await writeFile(hostPolicyPath,JSON.stringify({
37
+ schema:'deadbyte.host-policy.v1',enabled:true,arm_file:hostArm,max_read_bytes:262144,max_write_bytes:1048576,
38
+ roots:{core:{path:f.project,read:true,write:true,exec:true}},
39
+ commands:{node_script:{kind:'script',executable:process.execPath,executable_sha256:nodeSha,
40
+ root_ids:['core'],prefix_args:[],extensions:['.mjs'],timeout_ms:5000,max_output_bytes:65536}}
41
+ },null,2));
42
+
43
+ const desktopRaw=JSON.parse(await readFile(desktopPolicyPath,'utf8'));
44
+ const desktopGrant=desktopRaw.grant_file;
45
+ const codingLease=f.codingPolicy.lease_file;
46
+ const autonomousLease=f.policy.lease_file;
47
+ for(const q of [processGrant,hostArm,desktopGrant,codingLease,autonomousLease]) assert.equal(await absent(q),true,'authority unexpectedly armed before start: '+q);
48
+
49
+ const client=new Client({name:'deadbyte-r46-full-catalog',version:'0.15.2'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
50
+ const env={
51
+ ...getDefaultEnvironment(),
52
+ DEADBYTE_CAPABILITY_PROFILE:'full',
53
+ DEADBYTE_AUDIT_RUNTIME:'0',
54
+ DEADBYTE_STATE_ROOT:path.join(f.root,'state'),
55
+ DEADBYTE_WORKSPACE_ROOT:f.root,
56
+ DEADBYTE_SIGNING_KEY:f.keys.privateKeyPath,
57
+ DEADBYTE_VERIFY_KEY:f.keys.publicKeyPath,
58
+ DEADBYTE_PRIVILEGED_POLICY:hostPolicyPath,
59
+ DEADBYTE_MACHINE_RUNTIME:'1',
60
+ DEADBYTE_AGENT_RUNTIME:'1',
61
+ DEADBYTE_PROCESS_RUNTIME:'1',
62
+ DEADBYTE_PROCESS_POLICY:processPolicyPath,
63
+ DEADBYTE_CODING_RUNTIME:'1',
64
+ DEADBYTE_CODING_POLICY:path.join(f.root,'coding-policy.json'),
65
+ DEADBYTE_MACHINE_FS_RUNTIME:'1',
66
+ DEADBYTE_OBSERVATION_RUNTIME:'1',
67
+ DEADBYTE_DESKTOP_RUNTIME:'1',
68
+ DEADBYTE_DESKTOP_POLICY:desktopPolicyPath,
69
+ DEADBYTE_AUTONOMOUS_RUNTIME:'1',
70
+ DEADBYTE_AUTONOMOUS_POLICY:path.join(f.root,'autonomous-policy.json'),
71
+ DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE:'external'
72
+ };
73
+ const transport=new StdioClientTransport({command:process.execPath,args:[serverPath],cwd:repoRoot,env,stderr:'pipe'});
74
+ try{
75
+ await client.connect(transport);
76
+ const listed=await client.listTools();
77
+ const names=listed.tools.map(t=>t.name).sort();
78
+ const {R45_EXPECTED_TOOLS}=await import('../src/remote-mcp-r45-parity-client.mjs');
79
+ const R46_EXPECTED_TOOLS=[...R45_EXPECTED_TOOLS,'process_session_write'].sort();
80
+ assert.equal(names.length,83,'expected exact full R46 catalog 83, got '+names.length+': '+names.join(','));
81
+ assert.deepEqual(names,R46_EXPECTED_TOOLS);
82
+ for(const name of ['machine_hardware_query','desktop_status','desktop_observe','desktop_target','desktop_action',
83
+ 'host_file_read','host_file_write','host_exec','skill_catalog']) assert.ok(names.includes(name),'missing '+name);
84
+ for(const tool of listed.tools) assert.equal(tool.outputSchema?.additionalProperties,false,'non-strict output '+tool.name);
85
+ const catalog_sha256=sha(Buffer.from(JSON.stringify(names),'utf8'));
86
+ console.log(JSON.stringify({tool_count:names.length,catalog_sha256,desktop_tools:4,skill:true,authority:'DISARMED'}));
87
+ } finally {
88
+ await client.close().catch(()=>{});
89
+ for(const q of [processGrant,hostArm,desktopGrant,codingLease,autonomousLease]) assert.equal(await absent(q),true,'authority file appeared during catalog probe: '+q);
90
+ await f.cleanup();
91
+ }
92
+ });
@@ -11,7 +11,7 @@ function runtime() {
11
11
  return {
12
12
  stat: async () => ({ status:'ok', root_id:'core', path:'a.txt', kind:'file', bytes:1, mtime_utc:new Date(0).toISOString(), sha256:hex('a'), identity_sha256:hex('b') }),
13
13
  list: async () => ({ status:'ok', root_id:'core', path:'.', entries:[], next_cursor:null }),
14
- read: async () => ({ status:'ok', root_id:'core', path:'a.txt', offset:0, next_offset:1, file_size:1, eof:true, data_base64:'YQ==', data_sha256:hex('a'), file_sha256:hex('a') }),
14
+ read: async () => ({ status:'ok', root_id:'core', path:'a.txt', offset:0, next_offset:1, file_size:1, eof:true, data_base64:'YQ==', data_sha256:hex('a'), file_sha256:hex('a'), timeout_ms:30000 }),
15
15
  write: async () => ({ status:'committed', root_id:'core', path:'a.txt', previous_sha256:null, sha256:hex('a'), bytes:1, created:true, receipt_id:'a'.repeat(24), receipt_sha256:hex('b'), request_sha256:hex('c') }),
16
16
  editExact: async () => ({ status:'committed', root_id:'core', path:'a.txt', previous_sha256:hex('a'), sha256:hex('b'), bytes:1, replacements:1, receipt_id:'a'.repeat(24), receipt_sha256:hex('b'), request_sha256:hex('c') }),
17
17
  mkdir: async () => ({ status:'committed', root_id:'core', path:'d', identity_sha256:hex('a'), receipt_id:'a'.repeat(24), receipt_sha256:hex('b'), request_sha256:hex('c') }),
@@ -35,6 +35,9 @@ test('R31 registers exactly eight bounded machine filesystem tools', () => {
35
35
  test('R31 machine filesystem schemas expose root-relative IDs/preconditions only', () => {
36
36
  const server = new FakeServer();
37
37
  registerMachineFsTools(server, { runtime: runtime() });
38
+ const read = server.tools.get('machine_fs_read').spec.inputSchema;
39
+ assert.equal(read.safeParse({ root_id:'core', path:'a.txt', offset:0, max_bytes:65536, hash_file:false, timeout_ms:30000 }).success, true);
40
+ assert.equal(read.safeParse({ root_id:'core', path:'a.txt', timeout_ms:99 }).success, false);
38
41
  const write = server.tools.get('machine_fs_write').spec.inputSchema;
39
42
  const move = server.tools.get('machine_fs_move').spec.inputSchema;
40
43
  const del = server.tools.get('machine_fs_delete').spec.inputSchema;
@@ -10,7 +10,7 @@ import { createMachineFsRuntime, verifyMachineFsReceiptFile } from '../src/machi
10
10
 
11
11
  const sha = bytes => createHash('sha256').update(bytes).digest('hex');
12
12
 
13
- async function fixture() {
13
+ async function fixture({ readWorkerPath = undefined } = {}) {
14
14
  const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-machine-fs-'));
15
15
  const project = path.join(root, 'project');
16
16
  await mkdir(project);
@@ -30,7 +30,7 @@ async function fixture() {
30
30
  privateKeyPath: path.join(root, 'private.pem'), publicKeyPath: path.join(root, 'public.pem')
31
31
  });
32
32
  const runtime = createMachineFsRuntime({
33
- policy, signingKeyPath: keys.privateKeyPath, verifyKeyPath: keys.publicKeyPath
33
+ policy, signingKeyPath: keys.privateKeyPath, verifyKeyPath: keys.publicKeyPath, readWorkerPath
34
34
  });
35
35
  return { root, project, policy, keys, runtime };
36
36
  }
@@ -52,6 +52,7 @@ test('R31 machine fs supports exact mkdir/write/stat/list/range-read/edit/move/d
52
52
  const read = await f.runtime.read({ rootId: 'project', relativePath: 'dir/a.txt', offset: 6, maxBytes: 4 });
53
53
  assert.equal(Buffer.from(read.data_base64, 'base64').toString('utf8'), 'beta');
54
54
  assert.equal(read.next_offset, 10);
55
+ assert.equal(read.timeout_ms, 30000);
55
56
  const edited = await f.runtime.editExact({
56
57
  rootId: 'project', relativePath: 'dir/a.txt', expectedSha256: written.sha256,
57
58
  oldString: 'beta', newString: 'BETA', expectedReplacements: 1
@@ -103,3 +104,18 @@ test('R31 machine fs fails closed on traversal, denied paths and stale source id
103
104
  assert.equal(verified.signature_valid, true);
104
105
  assert.equal(verified.operation, 'machine_fs_mkdir');
105
106
  });
107
+
108
+
109
+ test('R46 machine fs read timeout kills a hung read worker instead of abandoning live I/O', async () => {
110
+ const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-machine-fs-hang-worker-'));
111
+ const worker = path.join(root, 'hang-worker.mjs');
112
+ await writeFile(worker, "setInterval(()=>{},1000);\n");
113
+ const f = await fixture({ readWorkerPath: worker });
114
+ await writeFile(path.join(f.project, 'hang.txt'), 'never read');
115
+ const started = Date.now();
116
+ await assert.rejects(
117
+ () => f.runtime.read({ rootId:'project', relativePath:'hang.txt', offset:0, maxBytes:16, timeoutMs:100 }),
118
+ /timed out after 100ms/
119
+ );
120
+ assert.ok(Date.now() - started < 3000, 'hung worker was not terminated promptly');
121
+ });
@@ -7,7 +7,7 @@ import { generateTrustKeypair } from '../src/trust.mjs';
7
7
  import { loadCodingPolicy } from '../src/coding-plane.mjs';
8
8
  import { createObservationRuntime, verifyObservationReceiptFile } from '../src/observation-runtime.mjs';
9
9
 
10
- async function fixture() {
10
+ async function fixture(runtimeOptions={}) {
11
11
  const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-observation-'));
12
12
  const project = path.join(root, 'project');
13
13
  await mkdir(project); await mkdir(path.join(project, 'blocked'));
@@ -36,7 +36,8 @@ async function fixture() {
36
36
  const runtime = createObservationRuntime({
37
37
  policy, signingKeyPath:keys.privateKeyPath, verifyKeyPath:keys.publicKeyPath,
38
38
  processRuntime, stateRoot:root, processSnapshotProvider, systemSnapshotProvider,
39
- ownedRuntimePidProvider: async () => new Set([111])
39
+ ownedRuntimePidProvider: async () => new Set([111]),
40
+ ...runtimeOptions
40
41
  });
41
42
  return { root, project, policy, keys, runtime, getProcessCalls:() => processCalls };
42
43
  }
@@ -121,3 +122,29 @@ test('R32 system observation is timestamped and signed without mutation authorit
121
122
  assert.equal(verified.ok,true);
122
123
  assert.equal(verified.result_head_sha256,status.result_head_sha256);
123
124
  });
125
+
126
+
127
+ test('R46 observation search sessions expire after bounded terminal TTL and free capacity', async () => {
128
+ let now=1000;
129
+ const f=await fixture({searchSessionTtlMs:50,maxSearchSessions:1,clockMs:()=>now});
130
+ await writeFile(path.join(f.project,'ttl.txt'),'needle\n');
131
+ const first=await f.runtime.searchStart({rootId:'project',relativePath:'.',pattern:'needle',maxResults:10,maxFiles:10,maxBytes:65536});
132
+ const done=await waitTerminal(f.runtime,first.search_id);
133
+ assert.equal(done.state,'completed');
134
+ now+=60;
135
+ await assert.rejects(f.runtime.searchStatus({searchId:first.search_id}),/unknown or expired search/);
136
+ const second=await f.runtime.searchStart({rootId:'project',relativePath:'.',pattern:'needle',maxResults:10,maxFiles:10,maxBytes:65536});
137
+ assert.match(second.search_id,/^[0-9a-f]{64}$/);
138
+ });
139
+
140
+ test('R46 observation search hard cap fails closed while all retained sessions are active', async () => {
141
+ const f=await fixture({searchSessionTtlMs:5000,maxSearchSessions:2});
142
+ await writeFile(path.join(f.project,'cap.txt'),'needle\n');
143
+ const a=await f.runtime.searchStart({rootId:'project',relativePath:'.',pattern:'needle'});
144
+ const b=await f.runtime.searchStart({rootId:'project',relativePath:'.',pattern:'needle'});
145
+ assert.match(a.search_id,/^[0-9a-f]{64}$/);
146
+ assert.match(b.search_id,/^[0-9a-f]{64}$/);
147
+ await assert.rejects(f.runtime.searchStart({rootId:'project',relativePath:'.',pattern:'needle'}),/session limit reached/);
148
+ await waitTerminal(f.runtime,a.search_id).catch(()=>{});
149
+ await waitTerminal(f.runtime,b.search_id).catch(()=>{});
150
+ });
@@ -1,4 +1,4 @@
1
- import test from 'node:test';
1
+ import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { registerProcessTools } from '../src/process-mcp-tools.mjs';
4
4
 
@@ -20,18 +20,19 @@ function runtime() {
20
20
  return {
21
21
  start: async () => session(), read: async () => ({
22
22
  session_id: hex('a'), offset: 0, next_offset: 4, earliest_offset: 0, stream_head: 4,
23
- stream_sha256: hex('b'), truncated: false, data_base64: 'VEVTVA==', state: 'running', exit_code: null
23
+ stream_sha256: hex('b'), truncated: false, wait_satisfied: true, data_base64: 'VEVTVA==', state: 'running', exit_code: null
24
24
  }),
25
25
  status: async () => session(), list: async () => ({ sessions: [session()] }),
26
+ write: async ({ input, appendNewline }) => ({ logical_bytes: Buffer.byteLength(input + (appendNewline ? '\r\n' : '')), logical_sha256: hex('e'), append_newline: appendNewline, session: session() }),
26
27
  action: async ({ actionId }) => ({ action_id: actionId, logical_bytes: 4, logical_sha256: hex('d'), session: session() }),
27
28
  terminate: async () => session('terminated')
28
29
  };
29
30
  }
30
- test('R32 registers exactly six bounded process supervision tools', () => {
31
+ test('R46 registers exactly seven bounded process supervision tools', () => {
31
32
  const server = new FakeServer();
32
33
  registerProcessTools(server, { runtime: runtime() });
33
34
  assert.deepEqual([...server.tools.keys()].sort(), [
34
- 'process_session_action','process_session_list','process_session_read','process_session_start','process_session_status','process_session_terminate'
35
+ 'process_session_action','process_session_list','process_session_read','process_session_start','process_session_status','process_session_terminate','process_session_write'
35
36
  ]);
36
37
  for (const [name, tool] of server.tools) {
37
38
  assert.ok(tool.spec.inputSchema, `${name} input schema missing`);
@@ -41,7 +42,7 @@ test('R32 registers exactly six bounded process supervision tools', () => {
41
42
  }
42
43
  });
43
44
 
44
- test('R32 process tool inputs expose IDs and bounds but no executable or raw input field', () => {
45
+ test('R46 process tool inputs expose guarded raw input only through process_session_write', () => {
45
46
  const server = new FakeServer();
46
47
  registerProcessTools(server, { runtime: runtime() });
47
48
  const start = server.tools.get('process_session_start').spec.inputSchema;
@@ -52,6 +53,10 @@ test('R32 process tool inputs expose IDs and bounds but no executable or raw inp
52
53
  assert.equal(start.safeParse({ profile_id: 'hold', root_id: 'core', input: 'whoami' }).success, false);
53
54
  assert.equal(terminate.safeParse({ session_id: hex('a') }).success, true);
54
55
  assert.equal(terminate.safeParse({ pid: 1234 }).success, false);
56
+ const write = server.tools.get('process_session_write').spec.inputSchema;
57
+ assert.equal(write.safeParse({ session_id: hex('a'), input: 'Write-Output 42', append_newline: true }).success, true);
58
+ assert.equal(write.safeParse({ session_id: hex('a'), input: '' }).success, false);
59
+ assert.equal(write.safeParse({ session_id: hex('a'), input: 'x', pid: 1234 }).success, false);
55
60
  assert.equal(action.safeParse({ session_id: hex('a'), action_id: 'ping' }).success, true);
56
61
  for (const forbidden of ['input','command','executable','environment','pid']) {
57
62
  assert.equal(action.safeParse({ session_id: hex('a'), action_id: 'ping', [forbidden]: 'forbidden' }).success, false);
@@ -69,3 +74,16 @@ test('R32 process action handler returns only strict logical identity and update
69
74
  assert.equal(result.structuredContent.session.state, 'running');
70
75
  assert.equal(tool.spec.outputSchema.safeParse({ ...result.structuredContent, raw_input: 'forbidden' }).success, false);
71
76
  });
77
+
78
+
79
+ test('R46 process write handler returns only input identity and session state', async () => {
80
+ const server = new FakeServer();
81
+ registerProcessTools(server, { runtime: runtime() });
82
+ const tool = server.tools.get('process_session_write');
83
+ const result = await tool.handler({ session_id: hex('a'), input: 'Write-Output 42', append_newline: true });
84
+ assert.equal(result.isError, false);
85
+ assert.deepEqual(Object.keys(result.structuredContent).sort(), ['append_newline','logical_bytes','logical_sha256','session','status']);
86
+ assert.equal(result.structuredContent.append_newline, true);
87
+ assert.equal(result.structuredContent.session.state, 'running');
88
+ assert.equal(tool.spec.outputSchema.safeParse({ ...result.structuredContent, raw_input: 'forbidden' }).success, false);
89
+ });