deadbyte-mcp 0.15.23 → 0.15.25
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 +49 -40
- package/PROVENANCE.json +11 -10
- package/README.txt +7 -7
- package/RELEASE.json +19 -38
- package/TEST-MATRIX.json +32 -96
- package/package.json +3 -3
- package/proof/commander-closure-20260928/DIFFERENTIAL-CHECKPOINT.json +122 -0
- package/scripts/commander-hardening-gate-r47.mjs +10 -1
- package/scripts/commander-real-performance-benchmark-r47.mjs +12 -3
- package/scripts/gate-windows-r48.ps1 +45 -0
- package/scripts/init-host-policy.ps1 +9 -1
- package/scripts/npm-release-r48.mjs +2 -1
- package/scripts/windows-gate-evidence-r48.mjs +27 -0
- package/src/atomic-mutation.mjs +12 -4
- package/src/coding-mcp-tools.mjs +15 -1
- package/src/coding-output-contracts.mjs +9 -3
- package/src/commander-office-reader-r48.mjs +36 -3
- package/src/commander-pdf-r48.mjs +36 -1
- package/src/commander-wire-adapter-r47.mjs +72 -2
- package/src/host-plane.mjs +10 -31
- package/src/machine-fs-mcp-tools.mjs +7 -1
- package/src/machine-fs-output-contracts.mjs +6 -0
- package/src/machine-fs-smoke-client.mjs +6 -17
- package/src/mcp-host-smoke-client.mjs +3 -2
- package/src/mcp-server.mjs +14 -2
- package/src/mcp-smoke-client.mjs +4 -9
- package/src/observation-smoke-client.mjs +5 -10
- package/src/output-contracts.mjs +6 -4
- package/src/process-smoke-client.mjs +3 -3
- package/src/runtime-catalog-expectation.mjs +37 -0
- package/src/stdio-protocol-guard.mjs +71 -0
- package/src/tunnel-stdio-compat-smoke.mjs +5 -9
- package/src/version.mjs +1 -1
- package/test/atomic-mutation.test.mjs +61 -0
- package/test/coding-catalog-consistency.test.mjs +9 -1
- package/test/coding-mcp-unknown-effect.test.mjs +42 -0
- package/test/commander-biff-reference-r48.test.mjs +38 -0
- package/test/commander-core-bridge-r47.test.mjs +19 -0
- package/test/commander-office-reader-r48.test.mjs +18 -2
- package/test/commander-pdf-r48.test.mjs +35 -0
- package/test/commander-protocol-adversarial-r48.test.mjs +166 -0
- package/test/commander-real-parity-r47.test.mjs +41 -3
- package/test/commander-schema-differential-r48.test.mjs +1 -1
- package/test/commander-wire-contract-r47.test.mjs +32 -0
- package/test/contained.test.mjs +4 -6
- package/test/controller.test.mjs +2 -1
- package/test/machine-fs-mcp-tools.test.mjs +12 -0
- package/test/output-schema.test.mjs +20 -1
- package/test/r48-release-contract.test.mjs +27 -27
- package/test/tunnel-stdio-compat.test.mjs +7 -7
|
@@ -2,8 +2,9 @@ import * as z from 'zod/v4';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { readCommanderUrl } from './commander-url-reader-r48.mjs';
|
|
5
|
-
import { readDocxBytes, createDocxBytes, editDocxBytes, readXlsxBytes, editXlsxBytes, createXlsxBytes, appendXlsxBytes } from './commander-office-reader-r48.mjs';
|
|
5
|
+
import { readDocxBytes, searchDocxTextBytes, createDocxBytes, editDocxBytes, readXlsxBytes, editXlsxBytes, createXlsxBytes, appendXlsxBytes } from './commander-office-reader-r48.mjs';
|
|
6
6
|
import { editDeadbytePdf, parseDeadbytePdf, readPdfText } from './commander-pdf-r48.mjs';
|
|
7
|
+
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
7
8
|
|
|
8
9
|
const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
9
10
|
const RDC_PROCESS_READ_CHUNK=256*1024;
|
|
@@ -122,6 +123,19 @@ function absoluteFor(runtime,rootId,relativePath){
|
|
|
122
123
|
function linesFrom(bytes){
|
|
123
124
|
return Buffer.from(bytes,'base64').toString('utf8').replaceAll('\\r\\n','\\n').replaceAll('\\r','\\n').split('\\n');
|
|
124
125
|
}
|
|
126
|
+
function isUnknownBinary(bytes){
|
|
127
|
+
if(bytes.includes(0)) return true;
|
|
128
|
+
try{decodeUtf8Strict(bytes);return false;}catch{return true;}
|
|
129
|
+
}
|
|
130
|
+
function directoryRows(entries,baseRelative){
|
|
131
|
+
const base=String(baseRelative??'.').replaceAll('\\','/').replace(/^\.\//,'').replace(/\/$/,'');
|
|
132
|
+
return entries.map(entry=>{
|
|
133
|
+
const raw=String(entry.path??'').replaceAll('\\','/').replace(/^\.\//,'');
|
|
134
|
+
const relative=base&&base!=='.'&&raw.startsWith(`${base}/`)?raw.slice(base.length+1):raw;
|
|
135
|
+
const display=process.platform==='win32'?relative.replaceAll('/','\\'):relative;
|
|
136
|
+
return `${(entry.kind??entry.type)==='directory'?'[DIR]':'[FILE]'} ${display}`;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
125
139
|
function rdcFileResult(pathname,data){
|
|
126
140
|
return {
|
|
127
141
|
path:pathname,content:data.content,mimeType:data.mime_type,
|
|
@@ -171,6 +185,17 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
|
|
|
171
185
|
}
|
|
172
186
|
if(!machineFsRuntime) throw new Error('machine filesystem runtime is disabled');
|
|
173
187
|
const target=resolveRdcPath(machineFsRuntime,args.path);
|
|
188
|
+
if(typeof machineFsRuntime.stat==='function'){
|
|
189
|
+
const info=await machineFsRuntime.stat({rootId:target.rootId,relativePath:target.relativePath,hash:false});
|
|
190
|
+
if(info?.kind==='directory'){
|
|
191
|
+
const listing=await listDirectory({path:args.path,depth:2});
|
|
192
|
+
const entries=listing.structuredContent?.entries??[];
|
|
193
|
+
const rows=directoryRows(entries,target.relativePath);
|
|
194
|
+
const content=`This is a directory, not a file. Use the list_directory tool instead of read_file for directories.\n\n${rows.join('\n')}`;
|
|
195
|
+
return text(content,false,{path:args.path,content,mimeType:'text/plain',isImage:false,isPdf:false,isDirectory:true,
|
|
196
|
+
entries,truncated:listing.structuredContent?.truncated===true,entryCount:entries.length});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
174
199
|
const ext=path.extname(args.path).toLowerCase();
|
|
175
200
|
const richDocument=ext==='.docx'||ext==='.xlsx'||ext==='.xlsm'||ext==='.pdf';
|
|
176
201
|
const imageMime={'.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.gif':'image/gif','.webp':'image/webp'}[ext];
|
|
@@ -238,6 +263,10 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
|
|
|
238
263
|
if(ext==='.xls') {
|
|
239
264
|
throw new Error('format .xls requires a native BIFF adapter; request rejected rather than returned as plain text');
|
|
240
265
|
}
|
|
266
|
+
if(isUnknownBinary(bytes)){
|
|
267
|
+
const content=`Cannot read binary file as text: ${path.basename(args.path)}\n\nUse start_process + interact_with_process to analyze binary files with appropriate tools (Node.js or Python libraries, command-line utilities, etc.).\n\nThe read_file tool only handles text files, images, and Excel files.`;
|
|
268
|
+
return text(content,false,{path:args.path,content,mimeType:'text/plain',isImage:false,isPdf:false,isBinary:true,fileSha256,...optionMeta});
|
|
269
|
+
}
|
|
241
270
|
const all=linesFrom(raw.data_base64);
|
|
242
271
|
const start=requestedOffset<0?Math.max(0,all.length+requestedOffset):Math.min(requestedOffset,all.length);
|
|
243
272
|
const end=requestedOffset<0?all.length:Math.min(all.length,start+requestedLength);
|
|
@@ -381,7 +410,7 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
|
|
|
381
410
|
}
|
|
382
411
|
frontier=next;
|
|
383
412
|
}
|
|
384
|
-
const rows=all.
|
|
413
|
+
const rows=directoryRows(all,target.relativePath);
|
|
385
414
|
return text(rows.join('\n'),false,{path:args.path,depth,entries:all,truncated,entryCount:all.length});
|
|
386
415
|
}
|
|
387
416
|
|
|
@@ -552,6 +581,19 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
|
|
|
552
581
|
}
|
|
553
582
|
return null;
|
|
554
583
|
}
|
|
584
|
+
async function waitForStreamQuiescence(sessionId,observed,timeoutMs,settleMs=100){
|
|
585
|
+
const deadline=Date.now()+timeoutMs;
|
|
586
|
+
let current=observed;
|
|
587
|
+
while(current.state==='running'&&Date.now()<deadline){
|
|
588
|
+
const baseline=current.stream_head;
|
|
589
|
+
const waitMs=Math.min(settleMs,Math.max(1,deadline-Date.now()));
|
|
590
|
+
const next=await processRuntime.read({sessionId,offset:baseline,maxBytes:1,waitMs});
|
|
591
|
+
if(next.state!=='running') return next;
|
|
592
|
+
if(next.stream_head>baseline){ current=next; continue; }
|
|
593
|
+
return next;
|
|
594
|
+
}
|
|
595
|
+
return current;
|
|
596
|
+
}
|
|
555
597
|
async function readProcessRange(sessionId,start,end){
|
|
556
598
|
if(!Number.isInteger(start)||!Number.isInteger(end)||start<0||end<start) throw new Error('native process range invalid');
|
|
557
599
|
if(end-start>RDC_PROCESS_SNAPSHOT_MAX) throw new Error(`RDC process response exceeds ${RDC_PROCESS_SNAPSHOT_MAX} byte compatibility bound`);
|
|
@@ -681,6 +723,9 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
|
|
|
681
723
|
if((observed.stream_head??before.stream_head)<=before.stream_head&&observed.state==='running'&&waitMs>0){
|
|
682
724
|
observed=await waitForStreamAdvance(sessionId,before.stream_head,waitMs)??observed;
|
|
683
725
|
}
|
|
726
|
+
if((observed.stream_head??before.stream_head)>before.stream_head&&observed.state==='running'&&waitMs>0){
|
|
727
|
+
observed=await waitForStreamQuiescence(sessionId,observed,waitMs);
|
|
728
|
+
}
|
|
684
729
|
const streamHead=Number.isInteger(observed.stream_head)?observed.stream_head:before.stream_head;
|
|
685
730
|
const earliest=Number.isInteger(observed.earliest_offset)?observed.earliest_offset:observed.offset;
|
|
686
731
|
const start=Math.max(before.stream_head,earliest);
|
|
@@ -803,6 +848,31 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
|
|
|
803
848
|
seenCursors.add(query.cursor);
|
|
804
849
|
}
|
|
805
850
|
}while(query.cursor!==null&&state.results.length<query.maxResults);
|
|
851
|
+
const docxSearchEnabled=String(args.filePattern??'').toLowerCase().includes('.docx')||String(args.path).toLowerCase().endsWith('.docx');
|
|
852
|
+
if(args.searchType==='content'&&machineFsRuntime&&docxSearchEnabled&&state.results.length<query.maxResults){
|
|
853
|
+
let docxEntries=[];
|
|
854
|
+
if(typeof machineFsRuntime.stat==='function'){
|
|
855
|
+
const info=await machineFsRuntime.stat({rootId:target.rootId,relativePath:target.relativePath,hash:false});
|
|
856
|
+
if(info?.kind==='file'&&target.relativePath.toLowerCase().endsWith('.docx'))docxEntries=[{path:target.relativePath,kind:'file'}];
|
|
857
|
+
}
|
|
858
|
+
if(docxEntries.length===0){
|
|
859
|
+
const listing=await listDirectory({path:args.path,depth:10});
|
|
860
|
+
docxEntries=(listing.structuredContent?.entries??[]).filter(entry=>(entry.kind??entry.type)==='file'&&String(entry.path).toLowerCase().endsWith('.docx'));
|
|
861
|
+
}
|
|
862
|
+
for(const entry of docxEntries){
|
|
863
|
+
if(state.cancelled||state.timedOut||state.results.length>=query.maxResults)break;
|
|
864
|
+
const raw=await machineFsRuntime.read({rootId:target.rootId,relativePath:entry.path,offset:0,
|
|
865
|
+
maxBytes:Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??64*1024*1024),hashFile:true});
|
|
866
|
+
if(raw.eof===false)continue;
|
|
867
|
+
const matches=searchDocxTextBytes(Buffer.from(raw.data_base64,'base64'),{
|
|
868
|
+
pattern:args.pattern,ignoreCase:args.ignoreCase!==false,maxResults:query.maxResults-state.results.length
|
|
869
|
+
});
|
|
870
|
+
for(const match of matches)state.results.push({
|
|
871
|
+
file:absoluteFor(machineFsRuntime,target.rootId,entry.path)+(match.part==='word/document.xml'?'':`:${match.part.slice('word/'.length)}`),
|
|
872
|
+
line:match.line,match:match.match,type:'content'
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
806
876
|
if(!state.cancelled&&!state.timedOut) state.isComplete=true;
|
|
807
877
|
}catch(error){if(!state.cancelled&&!state.timedOut){state.isError=true;state.error=String(error?.message??error);}state.isComplete=true;}
|
|
808
878
|
finally{if(timeoutHandle!==null) clearTimeout(timeoutHandle);}
|
package/src/host-plane.mjs
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import { createHash
|
|
2
|
-
import { constants as fsConstants } from 'node:fs';
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
3
2
|
import {
|
|
4
3
|
lstat,
|
|
5
4
|
mkdir,
|
|
6
|
-
open,
|
|
7
5
|
readFile,
|
|
8
6
|
realpath,
|
|
9
|
-
rename,
|
|
10
|
-
rm,
|
|
11
7
|
stat
|
|
12
8
|
} from 'node:fs/promises';
|
|
13
9
|
import path from 'node:path';
|
|
14
10
|
import { spawn } from 'node:child_process';
|
|
15
11
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
16
12
|
import { isInside, validatePortableRelativePath } from './path-policy.mjs';
|
|
13
|
+
import { atomicMutation } from './atomic-mutation.mjs';
|
|
17
14
|
|
|
18
15
|
export const HOST_POLICY_SCHEMA = 'deadbyte.host-policy.v1';
|
|
19
16
|
export const HOST_POLICY_SCHEMA_V2 = 'deadbyte.host-policy.v2';
|
|
@@ -315,32 +312,14 @@ export async function hostFileWrite(policy, { rootId, relativePath, content, enc
|
|
|
315
312
|
const bytes = decodeWriteContent(content, encoding);
|
|
316
313
|
if (bytes.length > policy.max_write_bytes) throw new Error(`host write exceeds ${policy.max_write_bytes} byte limit`);
|
|
317
314
|
|
|
318
|
-
const before = await
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
await handle.writeFile(bytes);
|
|
327
|
-
await handle.sync();
|
|
328
|
-
await handle.close();
|
|
329
|
-
handle = null;
|
|
330
|
-
|
|
331
|
-
const raceCheck = await currentTargetState(lexical);
|
|
332
|
-
checkExpectedState(raceCheck, expectedSha256);
|
|
333
|
-
await authorityCheck();
|
|
334
|
-
await rename(temp, lexical);
|
|
335
|
-
} catch (error) {
|
|
336
|
-
if (handle) await handle.close().catch(() => {});
|
|
337
|
-
await rm(temp, { force: true }).catch(() => {});
|
|
338
|
-
throw error;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const after = await currentTargetState(lexical);
|
|
342
|
-
const expectedAfter = sha256Bytes(bytes);
|
|
343
|
-
if (!after.exists || after.sha256 !== expectedAfter) throw new Error('host write post-commit hash mismatch');
|
|
315
|
+
const { before, after } = await atomicMutation({
|
|
316
|
+
target: lexical,
|
|
317
|
+
bytes,
|
|
318
|
+
expected: expectedSha256,
|
|
319
|
+
readState: currentTargetState,
|
|
320
|
+
assertExpected: checkExpectedState,
|
|
321
|
+
authorityCheck
|
|
322
|
+
});
|
|
344
323
|
return {
|
|
345
324
|
previous_sha256: before.sha256,
|
|
346
325
|
sha256: after.sha256,
|
|
@@ -10,7 +10,13 @@ const rel = z.string().min(1).max(2048);
|
|
|
10
10
|
const hex64 = z.string().regex(/^[0-9a-f]{64}$/);
|
|
11
11
|
const expectedFile = z.string().regex(/^(?:absent|[0-9a-f]{64})$/);
|
|
12
12
|
const ok = (schema, value) => machineFsStructuredResult(schema, value, false);
|
|
13
|
-
const fail = (schema, error) =>
|
|
13
|
+
const fail = (schema, error) => {
|
|
14
|
+
const unknown=error?.side_effect_state==='unknown'&&error?.outcome==='FAILED_AFTER_UNKNOWN_EFFECT';
|
|
15
|
+
return machineFsStructuredResult(schema, {
|
|
16
|
+
status:unknown?'failed':'denied',error:String(error),
|
|
17
|
+
...(unknown?{outcome:error.outcome,side_effect_state:'unknown',expected_after_sha256:error.expected_after_sha256}: {})
|
|
18
|
+
}, true);
|
|
19
|
+
};
|
|
14
20
|
|
|
15
21
|
export function registerMachineFsTools(server, { runtime }) {
|
|
16
22
|
if (!server || !runtime) throw new Error('machine fs tool registration requires server/runtime');
|
|
@@ -7,6 +7,11 @@ const pathText = z.string().min(1).max(2048);
|
|
|
7
7
|
const statusOk = z.enum(['ok','denied','failed']);
|
|
8
8
|
const statusCommit = z.enum(['committed','denied','failed']);
|
|
9
9
|
const error = z.string().min(1).max(4096).optional();
|
|
10
|
+
const unknownEffect = {
|
|
11
|
+
outcome: z.literal('FAILED_AFTER_UNKNOWN_EFFECT').optional(),
|
|
12
|
+
side_effect_state: z.literal('unknown').optional(),
|
|
13
|
+
expected_after_sha256: hex64.optional()
|
|
14
|
+
};
|
|
10
15
|
|
|
11
16
|
export const machineFsStatOutputSchema = z.strictObject({
|
|
12
17
|
status: statusOk, error,
|
|
@@ -38,6 +43,7 @@ export const machineFsReadOutputSchema = z.strictObject({
|
|
|
38
43
|
|
|
39
44
|
const commitBase = {
|
|
40
45
|
status: statusCommit, error,
|
|
46
|
+
...unknownEffect,
|
|
41
47
|
root_id: z.string().min(1).max(64).optional(),
|
|
42
48
|
receipt_id: receiptId.optional(), receipt_sha256: hex64.optional(), request_sha256: hex64.optional()
|
|
43
49
|
};
|
|
@@ -8,6 +8,7 @@ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotoc
|
|
|
8
8
|
import { CODING_GRANT_SCHEMA, loadCodingPolicy } from './coding-plane.mjs';
|
|
9
9
|
import { verifyMachineFsReceiptFile } from './machine-fs.mjs';
|
|
10
10
|
import { generateTrustKeypair } from './trust.mjs';
|
|
11
|
+
import { assertExactCatalog, expectedFullCatalogForEnv } from './runtime-catalog-expectation.mjs';
|
|
11
12
|
|
|
12
13
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const repoRoot = path.resolve(here, '..');
|
|
@@ -51,24 +52,13 @@ const client = new Client({ name: 'deadbyte-machine-fs-smoke', version: '0.8.0'
|
|
|
51
52
|
const transport = new StdioClientTransport({
|
|
52
53
|
command: process.execPath, args: [serverPath], cwd: repoRoot,
|
|
53
54
|
env: { ...getDefaultEnvironment(), DEADBYTE_CODING_RUNTIME: '1', DEADBYTE_MACHINE_FS_RUNTIME: '1',
|
|
55
|
+
DEADBYTE_PRIVILEGED_POLICY: '',
|
|
54
56
|
DEADBYTE_CODING_POLICY: policyPath, DEADBYTE_SIGNING_KEY: keys.privateKeyPath,
|
|
55
57
|
DEADBYTE_VERIFY_KEY: keys.publicKeyPath }, stderr: 'pipe'
|
|
56
58
|
});
|
|
57
59
|
const stderrChunks = [];
|
|
58
60
|
transport.stderr?.on('data', chunk => stderrChunks.push(Buffer.from(chunk)));
|
|
59
|
-
const
|
|
60
|
-
const codingTools = [
|
|
61
|
-
'coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read',
|
|
62
|
-
'coding_receipt_verify','coding_search','coding_session_status','coding_stat',
|
|
63
|
-
'coding_symbol_definition','coding_symbol_diagnostics','coding_symbol_implementations','coding_symbol_outline','coding_symbol_references','coding_symbol_rename_preview','coding_symbol_replace',
|
|
64
|
-
'coding_task_approve','coding_task_cancel','coding_task_events','coding_task_run',
|
|
65
|
-
'coding_task_status','coding_task_submit','coding_task_verify','coding_text_replace','coding_tree'
|
|
66
|
-
];
|
|
67
|
-
const machineFsTools = [
|
|
68
|
-
'machine_fs_delete','machine_fs_edit_exact','machine_fs_list','machine_fs_mkdir',
|
|
69
|
-
'machine_fs_move','machine_fs_read','machine_fs_stat','machine_fs_write'
|
|
70
|
-
];
|
|
71
|
-
const expectedCatalog = [...baseTools, ...codingTools, ...machineFsTools].sort();
|
|
61
|
+
const expectedCatalog = await expectedFullCatalogForEnv({DEADBYTE_CODING_RUNTIME:'1',DEADBYTE_MACHINE_FS_RUNTIME:'1'});
|
|
72
62
|
const receipts = [];
|
|
73
63
|
const remember = (operation, result) => {
|
|
74
64
|
if (result.status !== 'committed' || !/^[0-9a-f]{24}$/.test(result.receipt_id ?? '')) throw new Error(`${operation} missing committed receipt`);
|
|
@@ -79,10 +69,9 @@ try {
|
|
|
79
69
|
await client.connect(transport);
|
|
80
70
|
const listed = await client.listTools();
|
|
81
71
|
const names = listed.tools.map(tool => tool.name).sort();
|
|
82
|
-
|
|
83
|
-
for (const tool of listed.tools)
|
|
84
|
-
|
|
85
|
-
}
|
|
72
|
+
assertExactCatalog(names,expectedCatalog,'machine fs smoke');
|
|
73
|
+
for (const tool of listed.tools) if (tool.outputSchema !== undefined &&
|
|
74
|
+
(tool.outputSchema?.type !== 'object' || tool.outputSchema?.additionalProperties !== false)) throw new Error(`tool ${tool.name} has non-strict output schema`);
|
|
86
75
|
const rootStat = structured(await client.callTool({ name:'machine_fs_stat', arguments:{ root_id:'core', path:'.', hash:false } }), 'machine_fs_stat(root)');
|
|
87
76
|
if (rootStat.status !== 'ok' || rootStat.kind !== 'directory') throw new Error('machine fs root stat malformed');
|
|
88
77
|
const made = remember('machine_fs_mkdir', structured(await client.callTool({
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { Client } from '@modelcontextprotocol/client';
|
|
7
7
|
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
|
|
8
8
|
import { generateTrustKeypair } from './trust.mjs';
|
|
9
|
+
import { assertExactCatalog, expectedFullCatalogForEnv } from './runtime-catalog-expectation.mjs';
|
|
9
10
|
|
|
10
11
|
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
11
12
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -84,8 +85,8 @@ function structured(result, label) {
|
|
|
84
85
|
try {
|
|
85
86
|
await client.connect(transport);
|
|
86
87
|
const names = (await client.listTools()).tools.map(t => t.name).sort();
|
|
87
|
-
const expected =
|
|
88
|
-
|
|
88
|
+
const expected = await expectedFullCatalogForEnv({DEADBYTE_PRIVILEGED_POLICY:policyPath});
|
|
89
|
+
assertExactCatalog(names,expected,'privileged smoke');
|
|
89
90
|
|
|
90
91
|
const writeText = structured(await client.callTool({
|
|
91
92
|
name: 'host_file_write',
|
package/src/mcp-server.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
7
7
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
8
|
+
import { DeadbyteStdioServerTransport } from './stdio-protocol-guard.mjs';
|
|
8
9
|
import * as z from 'zod/v4';
|
|
9
10
|
import { executeProof } from './executor.mjs';
|
|
10
11
|
import { CONTAINED_TRANSFORM_OPERATIONS, executeContainedTransform } from './contained-executor.mjs';
|
|
@@ -529,7 +530,18 @@ serveStdio(() => {
|
|
|
529
530
|
});
|
|
530
531
|
return textResult(hostFileWriteOutputSchema, { status: 'committed', root_id, path: relativePath, ...result });
|
|
531
532
|
} catch (error) {
|
|
532
|
-
|
|
533
|
+
const unknown = error?.outcome === 'FAILED_AFTER_UNKNOWN_EFFECT'
|
|
534
|
+
&& error?.side_effect_state === 'unknown'
|
|
535
|
+
&& /^[0-9a-f]{64}$/.test(error?.expected_after_sha256 ?? '');
|
|
536
|
+
return textResult(hostFileWriteOutputSchema, {
|
|
537
|
+
status: unknown ? 'failed' : 'denied',
|
|
538
|
+
error: String(error),
|
|
539
|
+
...(unknown ? {
|
|
540
|
+
outcome: error.outcome,
|
|
541
|
+
side_effect_state: error.side_effect_state,
|
|
542
|
+
expected_after_sha256: error.expected_after_sha256
|
|
543
|
+
} : {})
|
|
544
|
+
}, true);
|
|
533
545
|
}
|
|
534
546
|
}
|
|
535
547
|
);
|
|
@@ -618,4 +630,4 @@ serveStdio(() => {
|
|
|
618
630
|
}
|
|
619
631
|
|
|
620
632
|
return server;
|
|
621
|
-
}, { legacy: 'reject' });
|
|
633
|
+
}, { legacy: 'reject', transport: new DeadbyteStdioServerTransport() });
|
package/src/mcp-smoke-client.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { Client } from '@modelcontextprotocol/client';
|
|
7
7
|
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
|
|
8
8
|
import { generateTrustKeypair } from './trust.mjs';
|
|
9
|
+
import { assertExactCatalog, expectedFullCatalogForEnv } from './runtime-catalog-expectation.mjs';
|
|
9
10
|
|
|
10
11
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const repoRoot = path.resolve(here, '..');
|
|
@@ -39,6 +40,7 @@ const transport = new StdioClientTransport({
|
|
|
39
40
|
DEADBYTE_WORKSPACE_ROOT: workspaceRoot,
|
|
40
41
|
DEADBYTE_SIGNING_KEY: keys.privateKeyPath,
|
|
41
42
|
DEADBYTE_VERIFY_KEY: keys.publicKeyPath,
|
|
43
|
+
DEADBYTE_PRIVILEGED_POLICY: '',
|
|
42
44
|
DEADBYTE_STDIO_CONSOLE_GUARD_PROBE: '1'
|
|
43
45
|
},
|
|
44
46
|
stderr: 'pipe'
|
|
@@ -67,15 +69,8 @@ try {
|
|
|
67
69
|
const names = listed.tools.map(tool => tool.name).sort();
|
|
68
70
|
const guardStderr = Buffer.concat(serverStderr).toString('utf8');
|
|
69
71
|
if (!guardStderr.includes('DEADBYTE MCP LOG DEADBYTE_STDIO_CONSOLE_GUARD_PROBE')) throw new Error('stdio console guard probe did not reach stderr');
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const allowedCatalogs = [
|
|
73
|
-
baseTools,
|
|
74
|
-
[...baseTools, ...privilegedTools]
|
|
75
|
-
].map(items => [...items].sort());
|
|
76
|
-
if (!allowedCatalogs.some(expected => JSON.stringify(names) === JSON.stringify(expected))) {
|
|
77
|
-
throw new Error(`tool catalog mismatch: ${JSON.stringify(names)}`);
|
|
78
|
-
}
|
|
72
|
+
const expected=await expectedFullCatalogForEnv({});
|
|
73
|
+
assertExactCatalog(names,expected,'MCP smoke');
|
|
79
74
|
|
|
80
75
|
const opened = structured(await client.callTool({
|
|
81
76
|
name: 'workspace_open',
|
|
@@ -8,6 +8,7 @@ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotoc
|
|
|
8
8
|
import { loadCodingPolicy } from './coding-plane.mjs';
|
|
9
9
|
import { verifyObservationReceiptFile } from './observation-runtime.mjs';
|
|
10
10
|
import { generateTrustKeypair } from './trust.mjs';
|
|
11
|
+
import { assertExactCatalog, expectedFullCatalogForEnv } from './runtime-catalog-expectation.mjs';
|
|
11
12
|
|
|
12
13
|
const here=path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const repoRoot=path.resolve(here,'..');
|
|
@@ -49,21 +50,15 @@ const client=new Client({name:'deadbyte-observation-smoke',version:'0.8.0'},
|
|
|
49
50
|
{versionNegotiation:{mode:{pin:'2026-07-28'}}});
|
|
50
51
|
const transport=new StdioClientTransport({command:process.execPath,args:[serverPath],cwd:repoRoot,
|
|
51
52
|
env:{...getDefaultEnvironment(),DEADBYTE_MACHINE_RUNTIME:'1',DEADBYTE_CODING_RUNTIME:'1',DEADBYTE_OBSERVATION_RUNTIME:'1',
|
|
53
|
+
DEADBYTE_PRIVILEGED_POLICY:'',
|
|
52
54
|
DEADBYTE_CODING_POLICY:policyPath,DEADBYTE_STATE_ROOT:state,DEADBYTE_SIGNING_KEY:keys.privateKeyPath,DEADBYTE_VERIFY_KEY:keys.publicKeyPath},stderr:'pipe'});
|
|
53
55
|
const stderr=[]; transport.stderr?.on('data',chunk=>stderr.push(Buffer.from(chunk)));
|
|
54
|
-
const
|
|
55
|
-
const machine=['machine_list','machine_ping','machine_revoke','machine_status'];
|
|
56
|
-
const coding=['coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read','coding_receipt_verify','coding_search',
|
|
57
|
-
'coding_session_status','coding_stat','coding_symbol_definition','coding_symbol_diagnostics','coding_symbol_implementations','coding_symbol_outline','coding_symbol_references','coding_symbol_rename_preview','coding_symbol_replace',
|
|
58
|
-
'coding_task_approve','coding_task_cancel','coding_task_events','coding_task_run','coding_task_status',
|
|
59
|
-
'coding_task_submit','coding_task_verify','coding_text_replace','coding_tree'];
|
|
60
|
-
const observation=['machine_hardware_query','machine_process_list','machine_search_cancel','machine_search_page','machine_search_start','machine_search_status','machine_system_status'];
|
|
61
|
-
const expected=[...base,...machine,...coding,...observation].sort();
|
|
56
|
+
const expected=await expectedFullCatalogForEnv({DEADBYTE_MACHINE_RUNTIME:'1',DEADBYTE_CODING_RUNTIME:'1',DEADBYTE_OBSERVATION_RUNTIME:'1'});
|
|
62
57
|
try{
|
|
63
58
|
await client.connect(transport);
|
|
64
59
|
const listed=await client.listTools(); const names=listed.tools.map(tool=>tool.name).sort();
|
|
65
|
-
|
|
66
|
-
for(const tool of listed.tools) if(tool.outputSchema?.type!=='object'||tool.outputSchema?.additionalProperties!==false) throw new Error(`tool ${tool.name}
|
|
60
|
+
assertExactCatalog(names,expected,'observation smoke');
|
|
61
|
+
for(const tool of listed.tools) if(tool.outputSchema!==undefined&&(tool.outputSchema?.type!=='object'||tool.outputSchema?.additionalProperties!==false)) throw new Error(`tool ${tool.name} has non-strict output schema`);
|
|
67
62
|
const codingStatus=structured(await client.callTool({name:'coding_session_status',arguments:{}}),'coding_session_status');
|
|
68
63
|
if(codingStatus.status!=='ok'||codingStatus.armed!==false) throw new Error('observation smoke requires coding mutation authority disarmed');
|
|
69
64
|
const system=structured(await client.callTool({name:'machine_system_status',arguments:{}}),'machine_system_status');
|
package/src/output-contracts.mjs
CHANGED
|
@@ -217,13 +217,15 @@ export const hostFileReadOutputSchema = statusContract({
|
|
|
217
217
|
});
|
|
218
218
|
|
|
219
219
|
export const hostFileWriteOutputSchema = statusContract({
|
|
220
|
-
statuses: ['committed', 'denied'],
|
|
220
|
+
statuses: ['committed', 'denied', 'failed'],
|
|
221
221
|
shape: {
|
|
222
222
|
root_id: z.string().min(1).max(64).optional(), path: z.string().min(1).max(512).optional(),
|
|
223
223
|
previous_sha256: hex64.nullable().optional(), sha256: hex64.optional(),
|
|
224
|
-
bytes: z.number().int().min(0).optional(), created: z.boolean().optional()
|
|
224
|
+
bytes: z.number().int().min(0).optional(), created: z.boolean().optional(),
|
|
225
|
+
outcome: z.literal('FAILED_AFTER_UNKNOWN_EFFECT').optional(),
|
|
226
|
+
side_effect_state: z.literal('unknown').optional(), expected_after_sha256: hex64.optional()
|
|
225
227
|
},
|
|
226
|
-
successStatuses: ['committed'], successRequired: ['root_id', 'path', 'previous_sha256', 'sha256', 'bytes', 'created'], errorStatuses: ['denied']
|
|
228
|
+
successStatuses: ['committed'], successRequired: ['root_id', 'path', 'previous_sha256', 'sha256', 'bytes', 'created'], errorStatuses: ['denied', 'failed']
|
|
227
229
|
});
|
|
228
230
|
|
|
229
231
|
export const hostExecOutputSchema = z.strictObject({
|
|
@@ -265,4 +267,4 @@ export function structuredToolResult(schema, object, isError = false) {
|
|
|
265
267
|
structuredContent: parsed.data,
|
|
266
268
|
content: [{ type: 'text', text: JSON.stringify(parsed.data) }]
|
|
267
269
|
};
|
|
268
|
-
}
|
|
270
|
+
}
|
|
@@ -8,6 +8,7 @@ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotoc
|
|
|
8
8
|
import { canonicalJson } from './canonical-json.mjs';
|
|
9
9
|
import { loadProcessPolicy, PROCESS_GRANT_SCHEMA } from './process-policy.mjs';
|
|
10
10
|
import { generateTrustKeypair, loadPublicVerifyKey } from './trust.mjs';
|
|
11
|
+
import { assertExactCatalog, expectedFullCatalogForEnv } from './runtime-catalog-expectation.mjs';
|
|
11
12
|
|
|
12
13
|
const EVENT_DOMAIN = Buffer.from('DEADBYTE-PROCESS-EVENT-V1\0', 'utf8');
|
|
13
14
|
const ZERO_SHA256 = '0'.repeat(64);
|
|
@@ -58,9 +59,8 @@ try {
|
|
|
58
59
|
await client.connect(transport);
|
|
59
60
|
const listed = await client.listTools();
|
|
60
61
|
const names = listed.tools.map(tool => tool.name).sort();
|
|
61
|
-
const expected =
|
|
62
|
-
|
|
63
|
-
if (JSON.stringify(names) !== JSON.stringify(expected)) throw new Error(`process smoke catalog mismatch: ${JSON.stringify(names)}`);
|
|
62
|
+
const expected = await expectedFullCatalogForEnv({DEADBYTE_PROCESS_RUNTIME:'1'});
|
|
63
|
+
assertExactCatalog(names,expected,'process smoke');
|
|
64
64
|
const started = structured(await client.callTool({ name: 'process_session_start', arguments: {
|
|
65
65
|
profile_id: 'ping_hold', root_id: 'core', cwd: '.', args: []
|
|
66
66
|
} }), 'process_session_start');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { R36_CAPABILITY_LEDGER } from './capability-ledger.mjs';
|
|
5
|
+
|
|
6
|
+
const packageRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
7
|
+
const catalogPath=path.join(packageRoot,'docs','R48-FULL-TOOL-CATALOG.json');
|
|
8
|
+
const AGENT_TOOLS=new Set(['agent_task_approve','agent_task_cancel','agent_task_events','agent_task_run','agent_task_status','agent_task_submit','agent_task_verify']);
|
|
9
|
+
|
|
10
|
+
export async function expectedFullCatalogForEnv(env){
|
|
11
|
+
const canonical=JSON.parse(await readFile(catalogPath,'utf8'));
|
|
12
|
+
if(canonical.schema!=='deadbyte.r48-full-catalog.v1'||canonical.tool_count!==canonical.tools?.length) throw new Error('canonical R48 full catalog is invalid');
|
|
13
|
+
const disabled=new Set();
|
|
14
|
+
for(const capability of R36_CAPABILITY_LEDGER.entries){
|
|
15
|
+
const flag=capability.runtime_flag;
|
|
16
|
+
const enabled=flag===null||(flag==='DEADBYTE_PRIVILEGED_POLICY'?Boolean(env[flag]):env[flag]==='1');
|
|
17
|
+
if(!enabled) disabled.add(capability.full_tool);
|
|
18
|
+
}
|
|
19
|
+
// The rename preview tool is registered by the coding surface even when its
|
|
20
|
+
// optional Native Diff backend is absent; only the apply path needs Autonomous.
|
|
21
|
+
if(env.DEADBYTE_CODING_RUNTIME==='1') disabled.delete('coding_symbol_rename_preview');
|
|
22
|
+
if(env.DEADBYTE_AGENT_RUNTIME!=='1') for(const name of AGENT_TOOLS) disabled.add(name);
|
|
23
|
+
if(env.DEADBYTE_PROCESS_RUNTIME!=='1') disabled.add('process_session_cancel');
|
|
24
|
+
return canonical.tools.filter(name=>!disabled.has(name));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function assertExactCatalog(actual,expected,label='MCP'){
|
|
28
|
+
const names=[...actual].sort((a,b)=>a.localeCompare(b,'en'));
|
|
29
|
+
const wanted=[...expected].sort((a,b)=>a.localeCompare(b,'en'));
|
|
30
|
+
if(JSON.stringify(names)!==JSON.stringify(wanted)){
|
|
31
|
+
const actualSet=new Set(names); const expectedSet=new Set(wanted);
|
|
32
|
+
const missing=wanted.filter(name=>!actualSet.has(name));
|
|
33
|
+
const unexpected=names.filter(name=>!expectedSet.has(name));
|
|
34
|
+
throw new Error(`${label} catalog mismatch: missing=${JSON.stringify(missing)} unexpected=${JSON.stringify(unexpected)}`);
|
|
35
|
+
}
|
|
36
|
+
return names;
|
|
37
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { deserializeMessage, STDIO_DEFAULT_MAX_BUFFER_SIZE } from '@modelcontextprotocol/server';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
3
|
+
|
|
4
|
+
const JSON_RPC_PARSE_ERROR=-32700;
|
|
5
|
+
const JSON_RPC_INVALID_REQUEST=-32600;
|
|
6
|
+
|
|
7
|
+
class StrictStdioReadBuffer {
|
|
8
|
+
constructor({maxBufferSize=STDIO_DEFAULT_MAX_BUFFER_SIZE}={}){
|
|
9
|
+
this._buffer=undefined;
|
|
10
|
+
this._maxBufferSize=maxBufferSize;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
append(chunk){
|
|
14
|
+
if((this._buffer?.length??0)+chunk.length>this._maxBufferSize){
|
|
15
|
+
this.clear();
|
|
16
|
+
throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
|
|
17
|
+
}
|
|
18
|
+
this._buffer=this._buffer?Buffer.concat([this._buffer,chunk]):chunk;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
readMessage(){
|
|
22
|
+
if(!this._buffer) return null;
|
|
23
|
+
const index=this._buffer.indexOf('\n');
|
|
24
|
+
if(index===-1) return null;
|
|
25
|
+
const line=this._buffer.toString('utf8',0,index).replace(/\r$/,'');
|
|
26
|
+
this._buffer=this._buffer.subarray(index+1);
|
|
27
|
+
return deserializeMessage(line);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
clear(){
|
|
31
|
+
this._buffer=undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function decodeErrorReply(error){
|
|
36
|
+
if(error instanceof SyntaxError){
|
|
37
|
+
return {code:JSON_RPC_PARSE_ERROR,message:'Parse error'};
|
|
38
|
+
}
|
|
39
|
+
if(error?.name==='ZodError'){
|
|
40
|
+
return {code:JSON_RPC_INVALID_REQUEST,message:'Invalid Request'};
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class DeadbyteStdioServerTransport extends StdioServerTransport {
|
|
46
|
+
constructor(stdin,stdout,options){
|
|
47
|
+
super(stdin,stdout,options);
|
|
48
|
+
this._readBuffer=new StrictStdioReadBuffer({maxBufferSize:options?.maxBufferSize});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
processReadBuffer(){
|
|
52
|
+
while(true){
|
|
53
|
+
try{
|
|
54
|
+
const message=this._readBuffer.readMessage();
|
|
55
|
+
if(message===null) break;
|
|
56
|
+
this.onmessage?.(message);
|
|
57
|
+
}catch(error){
|
|
58
|
+
const reply=decodeErrorReply(error);
|
|
59
|
+
if(reply){
|
|
60
|
+
void this.send({
|
|
61
|
+
jsonrpc:'2.0',
|
|
62
|
+
id:null,
|
|
63
|
+
error:reply
|
|
64
|
+
}).catch(sendError=>this.onerror?.(sendError));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
this.onerror?.(error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { Client } from '@modelcontextprotocol/client';
|
|
5
5
|
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
|
|
6
|
+
import { assertExactCatalog, expectedFullCatalogForEnv } from './runtime-catalog-expectation.mjs';
|
|
6
7
|
|
|
7
8
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const repoRoot = path.resolve(here, '..');
|
|
@@ -11,6 +12,7 @@ const workspaceRoot = path.join(repoRoot, 'workspaces');
|
|
|
11
12
|
const baseEnv = {
|
|
12
13
|
...process.env,
|
|
13
14
|
DEADBYTE_WORKSPACE_ROOT: workspaceRoot,
|
|
15
|
+
DEADBYTE_PRIVILEGED_POLICY: '',
|
|
14
16
|
DEADBYTE_TUNNEL_STDIO_PROBE_COMPAT: '1'
|
|
15
17
|
};
|
|
16
18
|
|
|
@@ -100,6 +102,7 @@ async function modernPassThrough() {
|
|
|
100
102
|
env: {
|
|
101
103
|
...getDefaultEnvironment(),
|
|
102
104
|
DEADBYTE_WORKSPACE_ROOT: workspaceRoot,
|
|
105
|
+
DEADBYTE_PRIVILEGED_POLICY: '',
|
|
103
106
|
DEADBYTE_TUNNEL_STDIO_PROBE_COMPAT: '1'
|
|
104
107
|
},
|
|
105
108
|
stderr: 'pipe'
|
|
@@ -112,15 +115,8 @@ async function modernPassThrough() {
|
|
|
112
115
|
if (era !== 'modern') throw new Error(`expected modern era through compat prefilter, got ${String(era)}`);
|
|
113
116
|
const listed = await client.listTools();
|
|
114
117
|
const names = listed.tools.map(tool => tool.name).sort();
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const allowedCatalogs = [
|
|
118
|
-
baseTools,
|
|
119
|
-
[...baseTools, ...privilegedTools]
|
|
120
|
-
].map(items => [...items].sort());
|
|
121
|
-
if (!allowedCatalogs.some(expected => JSON.stringify(names) === JSON.stringify(expected))) {
|
|
122
|
-
throw new Error(`modern catalog changed through compat prefilter: ${JSON.stringify(names)}`);
|
|
123
|
-
}
|
|
118
|
+
const expected=await expectedFullCatalogForEnv({});
|
|
119
|
+
assertExactCatalog(names,expected,'modern tunnel compatibility');
|
|
124
120
|
return { protocol_revision: '2026-07-28', protocol_era: era, tools: names };
|
|
125
121
|
} catch (error) {
|
|
126
122
|
const stderr = Buffer.concat(serverStderr).toString('utf8').trim();
|
package/src/version.mjs
CHANGED