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.
Files changed (50) hide show
  1. package/MANIFEST.SHA256 +49 -40
  2. package/PROVENANCE.json +11 -10
  3. package/README.txt +7 -7
  4. package/RELEASE.json +19 -38
  5. package/TEST-MATRIX.json +32 -96
  6. package/package.json +3 -3
  7. package/proof/commander-closure-20260928/DIFFERENTIAL-CHECKPOINT.json +122 -0
  8. package/scripts/commander-hardening-gate-r47.mjs +10 -1
  9. package/scripts/commander-real-performance-benchmark-r47.mjs +12 -3
  10. package/scripts/gate-windows-r48.ps1 +45 -0
  11. package/scripts/init-host-policy.ps1 +9 -1
  12. package/scripts/npm-release-r48.mjs +2 -1
  13. package/scripts/windows-gate-evidence-r48.mjs +27 -0
  14. package/src/atomic-mutation.mjs +12 -4
  15. package/src/coding-mcp-tools.mjs +15 -1
  16. package/src/coding-output-contracts.mjs +9 -3
  17. package/src/commander-office-reader-r48.mjs +36 -3
  18. package/src/commander-pdf-r48.mjs +36 -1
  19. package/src/commander-wire-adapter-r47.mjs +72 -2
  20. package/src/host-plane.mjs +10 -31
  21. package/src/machine-fs-mcp-tools.mjs +7 -1
  22. package/src/machine-fs-output-contracts.mjs +6 -0
  23. package/src/machine-fs-smoke-client.mjs +6 -17
  24. package/src/mcp-host-smoke-client.mjs +3 -2
  25. package/src/mcp-server.mjs +14 -2
  26. package/src/mcp-smoke-client.mjs +4 -9
  27. package/src/observation-smoke-client.mjs +5 -10
  28. package/src/output-contracts.mjs +6 -4
  29. package/src/process-smoke-client.mjs +3 -3
  30. package/src/runtime-catalog-expectation.mjs +37 -0
  31. package/src/stdio-protocol-guard.mjs +71 -0
  32. package/src/tunnel-stdio-compat-smoke.mjs +5 -9
  33. package/src/version.mjs +1 -1
  34. package/test/atomic-mutation.test.mjs +61 -0
  35. package/test/coding-catalog-consistency.test.mjs +9 -1
  36. package/test/coding-mcp-unknown-effect.test.mjs +42 -0
  37. package/test/commander-biff-reference-r48.test.mjs +38 -0
  38. package/test/commander-core-bridge-r47.test.mjs +19 -0
  39. package/test/commander-office-reader-r48.test.mjs +18 -2
  40. package/test/commander-pdf-r48.test.mjs +35 -0
  41. package/test/commander-protocol-adversarial-r48.test.mjs +166 -0
  42. package/test/commander-real-parity-r47.test.mjs +41 -3
  43. package/test/commander-schema-differential-r48.test.mjs +1 -1
  44. package/test/commander-wire-contract-r47.test.mjs +32 -0
  45. package/test/contained.test.mjs +4 -6
  46. package/test/controller.test.mjs +2 -1
  47. package/test/machine-fs-mcp-tools.test.mjs +12 -0
  48. package/test/output-schema.test.mjs +20 -1
  49. package/test/r48-release-contract.test.mjs +27 -27
  50. 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.map(e=>`${(e.kind??e.type)==='directory'?'[DIR]':'[FILE]'} ${e.path}`);
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);}
@@ -1,19 +1,16 @@
1
- import { createHash, randomBytes } from 'node:crypto';
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 currentTargetState(lexical);
319
- checkExpectedState(before, expectedSha256);
320
-
321
- const temp = path.join(path.dirname(lexical), `.${path.basename(lexical)}.deadbyte-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
322
- let handle;
323
- try {
324
- await authorityCheck();
325
- handle = await open(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
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) => machineFsStructuredResult(schema, { status:'denied', error:String(error) }, true);
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 baseTools = ['contained_transform','evidence_verify','file_read','host_exec','host_file_read','host_file_write','proof_execute','skill_catalog','workspace_open'];
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
- if (JSON.stringify(names) !== JSON.stringify(expectedCatalog)) throw new Error(`machine fs smoke catalog mismatch: ${JSON.stringify(names)}`);
83
- for (const tool of listed.tools) {
84
- if (tool.outputSchema?.type !== 'object' || tool.outputSchema?.additionalProperties !== false) throw new Error(`tool ${tool.name} missing strict output schema`);
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 = ['contained_transform', 'evidence_verify', 'file_read', 'host_exec', 'host_file_read', 'host_file_write', 'proof_execute', 'skill_catalog', 'workspace_open'];
88
- if (JSON.stringify(names) !== JSON.stringify(expected)) throw new Error(`privileged catalog mismatch: ${JSON.stringify(names)}`);
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',
@@ -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
- return textResult(hostFileWriteOutputSchema, { status: 'denied', error: String(error) }, true);
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() });
@@ -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 baseTools = ['contained_transform', 'evidence_verify', 'file_read', 'proof_execute', 'skill_catalog', 'workspace_open'];
71
- const privilegedTools = ['host_exec', 'host_file_read', 'host_file_write'];
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 base=['contained_transform','evidence_verify','file_read','host_exec','host_file_read','host_file_write','proof_execute','skill_catalog','workspace_open'];
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
- if(JSON.stringify(names)!==JSON.stringify(expected)) throw new Error(`observation smoke catalog mismatch: ${JSON.stringify(names)}`);
66
- for(const tool of listed.tools) if(tool.outputSchema?.type!=='object'||tool.outputSchema?.additionalProperties!==false) throw new Error(`tool ${tool.name} missing strict output schema`);
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');
@@ -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 = ['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open',
62
- 'process_session_action','process_session_cancel','process_session_list','process_session_read','process_session_start','process_session_status','process_session_terminate','process_session_write'].sort();
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 baseTools = ['contained_transform', 'evidence_verify', 'file_read', 'proof_execute', 'skill_catalog', 'workspace_open'];
116
- const privilegedTools = ['host_exec', 'host_file_read', 'host_file_write'];
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
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.23';
1
+ export const DEADBYTE_VERSION = '0.15.25';
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';