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.
- package/MANIFEST.SHA256 +54 -44
- package/PROVENANCE.json +6 -5
- package/README.txt +36 -20
- package/RELEASE.json +24 -8
- package/SBOM.cdx.json +3 -3
- package/controller/README.TXT +2 -2
- package/controller/deadbyte-controller.ps1 +50 -2
- package/controller/deadbyte-process-policy.json +63 -1
- package/docs/ARCHITECTURE.md +3 -4
- package/package.json +9 -7
- package/scripts/deferred-slot-operation-r46.mjs +126 -0
- package/scripts/final-closure-r46.mjs +98 -0
- package/scripts/final-closure-verify-r46.mjs +119 -0
- package/scripts/full-regression-r46.mjs +20 -0
- package/scripts/gate-windows-r46.ps1 +56 -0
- package/scripts/refresh-process-policy.mjs +64 -7
- package/scripts/release-parity-r46.mjs +187 -0
- package/scripts/windows-gate-evidence-r46.mjs +110 -0
- package/src/capability-ledger.mjs +1 -0
- package/src/compact-gateway-mcp-tools.mjs +7 -5
- package/src/deadbyte-cli.mjs +7 -3
- 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 +4 -0
- package/src/mcp-smoke-client.mjs +4 -1
- package/src/observation-runtime.mjs +25 -4
- 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/release-generation.mjs +22 -3
- package/src/remote-device-r44.mjs +53 -15
- package/src/remote-mcp-smoke-client.mjs +1 -1
- package/src/stdio-console-guard.mjs +24 -0
- package/src/version.mjs +1 -1
- package/test/cli-entrypoint.test.mjs +3 -3
- package/test/compact-gateway-r36.test.mjs +28 -0
- package/test/core.test.mjs +1 -1
- package/test/full-max-catalog-r46.test.mjs +92 -0
- package/test/machine-fs-mcp-tools.test.mjs +4 -1
- package/test/machine-fs-runtime.test.mjs +18 -2
- package/test/observation-runtime.test.mjs +29 -2
- 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-finalization.test.mjs +3 -3
- package/test/release-generation.test.mjs +25 -11
- package/test/release-version.test.mjs +99 -44
- package/test/remote-device-r44.test.mjs +75 -0
- 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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {...
|
|
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
|
|
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{
|
|
154
|
-
|
|
155
|
-
|
|
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 {...
|
|
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(
|
|
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
|
@@ -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.
|
|
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\.
|
|
54
|
-
assert.match(readme,/npx -y deadbyte-mcp@0\.15\.
|
|
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
|
+
});
|
package/test/core.test.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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
|
+
});
|