deadbyte-mcp 0.15.24 → 0.15.26

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 (48) hide show
  1. package/MANIFEST.SHA256 +47 -37
  2. package/PROVENANCE.json +11 -11
  3. package/R49-PARENT.json +11 -0
  4. package/README.txt +16 -7
  5. package/RELEASE.json +15 -12
  6. package/SBOM.cdx.json +3 -3
  7. package/TEST-MATRIX.json +15 -12
  8. package/docs/R49-FULL-TOOL-CATALOG.json +13 -0
  9. package/docs/R49-REMOTE-CONTROL-DONORS.json +75 -0
  10. package/package.json +2 -2
  11. package/proof/commander-closure-20260928/DIFFERENTIAL-CHECKPOINT.json +17 -6
  12. package/scripts/commander-hardening-gate-r47.mjs +10 -1
  13. package/scripts/deferred-slot-operation-r46.mjs +44 -2
  14. package/scripts/gate-windows-r48.ps1 +45 -0
  15. package/scripts/gate-windows-r49.ps1 +54 -0
  16. package/scripts/init-host-policy.ps1 +9 -1
  17. package/scripts/npm-release-r48.mjs +2 -1
  18. package/scripts/windows-gate-evidence-r48.mjs +27 -0
  19. package/scripts/windows-gate-evidence-r49.mjs +27 -0
  20. package/src/autonomous-planner-contracts.mjs +1 -0
  21. package/src/autonomous-planner.mjs +11 -5
  22. package/src/autonomous-runtime.mjs +11 -1
  23. package/src/capability-ledger.mjs +1 -0
  24. package/src/commander-wire-adapter-r47.mjs +16 -0
  25. package/src/desktop-mcp-tools-r40.mjs +23 -1
  26. package/src/desktop-runtime-r40.mjs +46 -2
  27. package/src/machine-fs-smoke-client.mjs +6 -17
  28. package/src/mcp-host-smoke-client.mjs +3 -2
  29. package/src/mcp-smoke-client.mjs +4 -9
  30. package/src/observation-smoke-client.mjs +5 -10
  31. package/src/process-smoke-client.mjs +3 -3
  32. package/src/remote-mcp-smoke-client.mjs +7 -1
  33. package/src/runtime-catalog-expectation.mjs +45 -0
  34. package/src/tunnel-stdio-compat-smoke.mjs +5 -9
  35. package/src/version.mjs +1 -1
  36. package/test/autonomous-desktop-r40.test.mjs +27 -1
  37. package/test/coding-catalog-consistency.test.mjs +9 -1
  38. package/test/commander-real-parity-r47.test.mjs +8 -4
  39. package/test/contained.test.mjs +4 -6
  40. package/test/controller.test.mjs +2 -1
  41. package/test/desktop-mcp-tools-r40.test.mjs +19 -4
  42. package/test/desktop-runtime-r40.test.mjs +43 -2
  43. package/test/full-max-catalog-r48.test.mjs +5 -3
  44. package/test/r48-release-contract.test.mjs +14 -20
  45. package/test/r49-remote-control-donors.test.mjs +24 -0
  46. package/test/r49-windows-gate.test.mjs +33 -0
  47. package/test/remote-r24-catalog.test.mjs +9 -0
  48. package/test/tunnel-stdio-compat.test.mjs +4 -6
@@ -33,6 +33,19 @@ const explicitTargetFields={
33
33
  confidence:z.number().min(0).max(1),semantic_label_sha256:hex64.nullable().default(null)
34
34
  };
35
35
  const uiaTargetFields={frame_id:hex64,uia_query:uiaQuery};
36
+ const waitFields={
37
+ baseline_frame_id:hex64,
38
+ until:z.enum(['frame_changed','topology_changed','foreground_changed','uia_present','uia_absent']),
39
+ uia_query:uiaQuery.nullable().default(null),timeout_ms:z.number().int().min(100).max(30000).default(5000),
40
+ poll_ms:z.number().int().min(50).max(1000).default(100),preview:z.boolean().default(false)
41
+ };
42
+ function validateWait(value,ctx){
43
+ const needsQuery=value.until==='uia_present'||value.until==='uia_absent';
44
+ if(needsQuery&&value.uia_query===null)ctx.addIssue({code:'custom',path:['uia_query'],message:'UIA wait requires uia_query'});
45
+ if(!needsQuery&&value.uia_query!==null)ctx.addIssue({code:'custom',path:['uia_query'],message:'non-UIA wait cannot carry uia_query'});
46
+ if(value.poll_ms>value.timeout_ms)ctx.addIssue({code:'custom',path:['poll_ms'],message:'poll_ms cannot exceed timeout_ms'});
47
+ }
48
+ export const desktopWaitInputSchema=z.strictObject(waitFields).superRefine(validateWait);
36
49
  export const desktopTargetInputSchema=z.union([
37
50
  z.strictObject(explicitTargetFields),z.strictObject(uiaTargetFields)
38
51
  ]);
@@ -61,12 +74,13 @@ export const desktopActionInputSchema=z.strictObject(actionFields).superRefine(v
61
74
 
62
75
  const statusOp=z.strictObject({op:z.literal('status')});
63
76
  const observeOp=z.strictObject({op:z.literal('observe'),preview:z.boolean().default(false)});
77
+ const waitOp=z.strictObject({op:z.literal('wait'),...waitFields}).superRefine(validateWait);
64
78
  const targetOp=z.union([
65
79
  z.strictObject({op:z.literal('target'),...explicitTargetFields}),
66
80
  z.strictObject({op:z.literal('target'),...uiaTargetFields})
67
81
  ]);
68
82
  const actionOp=z.strictObject({op:z.literal('action'),...actionFields}).superRefine(validateAction);
69
- export const desktopControlInputSchema=z.union([statusOp,observeOp,targetOp,actionOp]);
83
+ export const desktopControlInputSchema=z.union([statusOp,observeOp,waitOp,targetOp,actionOp]);
70
84
 
71
85
  const sha256=value=>createHash('sha256').update(value).digest('hex');
72
86
  function publicResult(value){
@@ -89,6 +103,10 @@ export async function invokeDesktopOperation(args,runtime){
89
103
  let value;
90
104
  if(op==='status') value=await runtime.status();
91
105
  else if(op==='observe') value=await runtime.observe({preview:args.preview??false});
106
+ else if(op==='wait') value=await runtime.wait({
107
+ baselineFrameId:args.baseline_frame_id,until:args.until,query:args.uia_query,
108
+ timeoutMs:args.timeout_ms,pollMs:args.poll_ms,preview:args.preview
109
+ });
92
110
  else if(op==='target'&&args.uia_query) value=await runtime.ground({frameId:args.frame_id,query:args.uia_query});
93
111
  else if(op==='target') value=await runtime.target({
94
112
  frameId:args.frame_id,rect:args.rect,source:args.source,confidence:args.confidence,
@@ -115,6 +133,10 @@ export function registerDesktopTools(server,{runtime}={}){
115
133
  description:'Capture one bounded Windows desktop frame and signed observation evidence. Observation does not require Desktop mutation authority.',
116
134
  inputSchema:desktopObserveInputSchema,outputSchema:compactGatewayOutputSchema
117
135
  },args=>invokeDesktopOperation({op:'observe',...args},runtime));
136
+ server.registerTool('desktop_wait',{
137
+ description:'Wait for one bounded desktop or local UIA condition using fresh observations. This is read-only, returns signed final evidence, and never dispatches or retries an action.',
138
+ inputSchema:desktopWaitInputSchema,outputSchema:compactGatewayOutputSchema
139
+ },args=>invokeDesktopOperation({op:'wait',...args},runtime));
118
140
  server.registerTool('desktop_target',{
119
141
  description:'Bind one explicit rectangle or one local Windows UIA query to fresh desktop evidence. UIA query text is matched locally and is not returned.',
120
142
  inputSchema:desktopTargetInputSchema,outputSchema:compactGatewayOutputSchema
@@ -71,13 +71,14 @@ export function verifyDesktopReceiptObject(receipt,{policySha256,publicKey,keyId
71
71
 
72
72
  export function createDesktopRuntime({
73
73
  policy,observerRuntime,inputProvider,grounderProvider=null,privateKey,publicKey=null,keyId,evidenceDir=policy?.evidence_dir,
74
- now=()=>new Date().toISOString(),maxTargets=64
74
+ now=()=>new Date().toISOString(),clockMs=()=>Date.now(),sleeper=ms=>new Promise(resolve=>setTimeout(resolve,ms)),maxTargets=64
75
75
  }={}){
76
76
  insist(policy?.schema===DESKTOP_POLICY_SCHEMA,'desktop runtime policy invalid');
77
77
  insist(observerRuntime?.capture&&observerRuntime?.frameStatus,'desktop runtime observer invalid');
78
78
  insist(typeof inputProvider==='function','desktop runtime inputProvider required');
79
79
  insist(privateKey,'desktop runtime privateKey required');
80
80
  insist(typeof keyId==='string'&&keyId.length>=1,'desktop runtime keyId invalid');
81
+ insist(typeof clockMs==='function'&&typeof sleeper==='function','desktop wait clock invalid');
81
82
  insist(Number.isInteger(maxTargets)&&maxTargets>=1&&maxTargets<=1024,'maxTargets invalid');
82
83
  const receiptPublicKey=publicKey??createPublicKey(privateKey);
83
84
  const targets=new Map();
@@ -118,6 +119,49 @@ export function createDesktopRuntime({
118
119
  },observed.frame.observed_at_utc);
119
120
  return Object.freeze({...observed,...(previewValue?{preview:previewValue}:{}),receipt_id:evidence.receipt_id,receipt_sha256:evidence.receipt_sha256});
120
121
  }
122
+ async function wait({baselineFrameId,until,query=null,timeoutMs=5000,pollMs=100,preview=false}={}){
123
+ insist(HEX64.test(baselineFrameId??''),'desktop wait baseline frame invalid');
124
+ insist(['frame_changed','topology_changed','foreground_changed','uia_present','uia_absent'].includes(until),'desktop wait condition invalid');
125
+ insist(Number.isInteger(timeoutMs)&&timeoutMs>=100&&timeoutMs<=30000,'desktop wait timeout invalid');
126
+ insist(Number.isInteger(pollMs)&&pollMs>=50&&pollMs<=1000&&pollMs<=timeoutMs,'desktop wait poll invalid');
127
+ insist(typeof preview==='boolean','desktop wait preview invalid');
128
+ const needsQuery=until==='uia_present'||until==='uia_absent';
129
+ insist(needsQuery?query&&typeof grounderProvider==='function':query===null,'desktop wait query invalid');
130
+ const baseline=observerRuntime.frameStatus(baselineFrameId).frame;
131
+ const started=clockMs();
132
+ insist(Number.isFinite(started),'desktop wait clock result invalid');
133
+ let attempts=0,observed=null,match=null,satisfied=false;
134
+ while(true){
135
+ observed=await observerRuntime.capture();attempts++;
136
+ if(until==='frame_changed')satisfied=observed.frame.image_sha256!==baseline.image_sha256;
137
+ else if(until==='topology_changed')satisfied=observed.frame.topology_sha256!==baseline.topology_sha256;
138
+ else if(until==='foreground_changed')satisfied=!sameWindow(observed.frame.active_window,baseline.active_window);
139
+ else{
140
+ const grounded=await grounderProvider({frame:observed.frame,query});
141
+ insist(grounded&&grounded.status==='ok'&&HEX64.test(grounded.query_sha256??''),'desktop wait UIA result invalid');
142
+ const candidate=grounded.matches?.[grounded.ordinal??0]??null;
143
+ const actionable=Boolean(candidate?.enabled===true&&!candidate?.offscreen);
144
+ match=Object.freeze({query_sha256:grounded.query_sha256,query_kind:grounded.query_kind,
145
+ match_count:Array.isArray(grounded.matches)?grounded.matches.length:0,actionable,
146
+ visited:grounded.visited,truncated:grounded.truncated,provider_receipt_sha256:grounded.provider_receipt_sha256});
147
+ satisfied=until==='uia_present'?actionable:!actionable;
148
+ }
149
+ const elapsed=Math.max(0,clockMs()-started);
150
+ if(satisfied||elapsed>=timeoutMs)break;
151
+ await sleeper(Math.min(pollMs,timeoutMs-elapsed));
152
+ }
153
+ const elapsedMs=Math.max(0,clockMs()-started);
154
+ const previewValue=preview?observerRuntime.framePreview?.(observed.frame.frame_id,{maxBytes:policy.limits.max_preview_bytes}):null;
155
+ if(preview)insist(previewValue?.schema==='deadbyte.desktop-preview.v1','desktop preview unavailable');
156
+ const payload={condition:until,satisfied,attempts,elapsed_ms:elapsedMs,baseline_frame_id:baseline.frame_id,
157
+ frame_id:observed.frame.frame_id,desktop_epoch:observed.frame.desktop_epoch,topology_sha256:observed.frame.topology_sha256,
158
+ image_sha256:observed.frame.image_sha256,observed_at_utc:observed.frame.observed_at_utc,active_window:observed.frame.active_window,
159
+ match,preview:previewValue?{sha256:previewValue.sha256,byte_length:previewValue.byte_length,width:previewValue.width,height:previewValue.height,encoding:previewValue.encoding}:null};
160
+ const evidence=await receipt('desktop_wait_observation',payload,observed.frame.observed_at_utc);
161
+ return Object.freeze({status:'ok',condition:until,satisfied,timed_out:!satisfied,attempts,elapsed_ms:elapsedMs,
162
+ baseline_frame_id:baseline.frame_id,frame:observed.frame,match,...(previewValue?{preview:previewValue}:{}),
163
+ receipt_id:evidence.receipt_id,receipt_sha256:evidence.receipt_sha256});
164
+ }
121
165
  function targetFor(targetId){
122
166
  insist(HEX64.test(targetId??''),'targetId invalid');
123
167
  const found=targets.get(targetId);if(!found)throw new Error('desktop target expired or unknown');return found;
@@ -314,5 +358,5 @@ export function createDesktopRuntime({
314
358
  throw wrapped;
315
359
  }
316
360
  }
317
- return Object.freeze({status,observe,target,ground,action,verifyReceipt});
361
+ return Object.freeze({status,observe,wait,target,ground,action,verifyReceipt});
318
362
  }
@@ -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',
@@ -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');
@@ -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');
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
3
+ import { verifyMachinePingPublic } from './machine-identity.mjs';
3
4
 
4
5
  const endpointText = process.argv[2];
5
6
  const mode = process.argv[3] ?? 'smoke';
@@ -151,6 +152,11 @@ try {
151
152
  ping.release_id !== machine.release_id || ping.release_manifest_sha256 !== machine.release_manifest_sha256) {
152
153
  throw new Error('compact machine ping binding mismatch');
153
154
  }
155
+ const pingSignatureValid = await verifyMachinePingPublic({
156
+ ping,
157
+ publicKeySpkiBase64: machine.public_key_spki_base64
158
+ });
159
+ if (!pingSignatureValid) throw new Error('machine ping public Ed25519 verification failed');
154
160
  compact = {
155
161
  profile: 'compact',
156
162
  ledger_sha256: manifest.ledger_sha256,
@@ -160,7 +166,7 @@ try {
160
166
  machine_id: machine.machine_id,
161
167
  release_id: machine.release_id,
162
168
  release_manifest_sha256: machine.release_manifest_sha256,
163
- ping_signature_valid: ping.signature_valid === true
169
+ ping_signature_valid: true
164
170
  };
165
171
  } else {
166
172
  const opened = structured(await client.callTool({ name: 'workspace_open', arguments: { workspace_id: 'demo' } }), 'workspace_open');
@@ -0,0 +1,45 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { R36_CAPABILITY_LEDGER } from './capability-ledger.mjs';
6
+
7
+ const packageRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
8
+ const catalogPath=path.join(packageRoot,'docs','R48-FULL-TOOL-CATALOG.json');
9
+ const overlayPath=path.join(packageRoot,'docs','R49-FULL-TOOL-CATALOG.json');
10
+ 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']);
11
+
12
+ export async function expectedFullCatalogForEnv(env){
13
+ const canonical=JSON.parse(await readFile(catalogPath,'utf8'));
14
+ if(canonical.schema!=='deadbyte.r48-full-catalog.v1'||canonical.tool_count!==canonical.tools?.length) throw new Error('canonical R48 full catalog is invalid');
15
+ const overlay=JSON.parse(await readFile(overlayPath,'utf8'));
16
+ const baseSha=createHash('sha256').update(await readFile(catalogPath)).digest('hex');
17
+ if(overlay.schema!=='deadbyte.r49-full-catalog-overlay.v1'||overlay.base?.sha256!==baseSha||overlay.base?.tool_count!==canonical.tool_count||
18
+ overlay.tool_count!==canonical.tool_count+overlay.additions.length-overlay.removals.length) throw new Error('canonical R49 full catalog overlay is invalid');
19
+ const full=[...new Set([...canonical.tools.filter(name=>!overlay.removals.includes(name)),...overlay.additions])].sort((a,b)=>a.localeCompare(b,'en'));
20
+ if(full.length!==overlay.tool_count) throw new Error('canonical R49 full catalog count mismatch');
21
+ const disabled=new Set();
22
+ for(const capability of R36_CAPABILITY_LEDGER.entries){
23
+ const flag=capability.runtime_flag;
24
+ const enabled=flag===null||(flag==='DEADBYTE_PRIVILEGED_POLICY'?Boolean(env[flag]):env[flag]==='1');
25
+ if(!enabled) disabled.add(capability.full_tool);
26
+ }
27
+ // The rename preview tool is registered by the coding surface even when its
28
+ // optional Native Diff backend is absent; only the apply path needs Autonomous.
29
+ if(env.DEADBYTE_CODING_RUNTIME==='1') disabled.delete('coding_symbol_rename_preview');
30
+ if(env.DEADBYTE_AGENT_RUNTIME!=='1') for(const name of AGENT_TOOLS) disabled.add(name);
31
+ if(env.DEADBYTE_PROCESS_RUNTIME!=='1') disabled.add('process_session_cancel');
32
+ return full.filter(name=>!disabled.has(name));
33
+ }
34
+
35
+ export function assertExactCatalog(actual,expected,label='MCP'){
36
+ const names=[...actual].sort((a,b)=>a.localeCompare(b,'en'));
37
+ const wanted=[...expected].sort((a,b)=>a.localeCompare(b,'en'));
38
+ if(JSON.stringify(names)!==JSON.stringify(wanted)){
39
+ const actualSet=new Set(names); const expectedSet=new Set(wanted);
40
+ const missing=wanted.filter(name=>!actualSet.has(name));
41
+ const unexpected=names.filter(name=>!expectedSet.has(name));
42
+ throw new Error(`${label} catalog mismatch: missing=${JSON.stringify(missing)} unexpected=${JSON.stringify(unexpected)}`);
43
+ }
44
+ return names;
45
+ }
@@ -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.24';
1
+ export const DEADBYTE_VERSION = '0.15.26';
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';
@@ -11,7 +11,7 @@ const POLICY=H('desktop-policy');
11
11
  const WIN={hwnd:'0x1234',pid:77,process_image_sha256:H('desktop-app'),title_sha256:H('desktop-title')};
12
12
 
13
13
  function fakeDesktopRuntime({armed=false,unknown=false}={}){
14
- const calls={status:0,observe:0,ground:0,action:0,verify:0};
14
+ const calls={status:0,observe:0,wait:0,ground:0,action:0,verify:0};
15
15
  const receipts=new Map();
16
16
  let seq=0;
17
17
  const issue=kind=>{
@@ -28,6 +28,9 @@ function fakeDesktopRuntime({armed=false,unknown=false}={}){
28
28
  async status(){calls.status++;return {status:'ok',configured:true,armed,reason:armed?'standing-grant':'disarmed',
29
29
  action_kinds:armed?['click_primary']:[],policy_sha256:POLICY,observer_backend:'fake'};},
30
30
  async observe(){calls.observe++;const r=issue('desktop_observation');return {status:'ok',frame:frame(FRAME),backend:'fake',byte_length:4,...r};},
31
+ async wait({baselineFrameId,until}){calls.wait++;assert.equal(baselineFrameId,FRAME);assert.equal(until,'frame_changed');
32
+ const r=issue('desktop_wait_observation');return {status:'ok',condition:until,satisfied:true,timed_out:false,attempts:2,elapsed_ms:100,
33
+ baseline_frame_id:FRAME,frame:frame(GROUNDED_FRAME),match:null,...r};},
31
34
  async ground({frameId,query}){calls.ground++;assert.equal(frameId,FRAME);assert.equal(query.kind,'root');
32
35
  const r=issue('desktop_grounding');return {status:'ok',frame:frame(GROUNDED_FRAME),
33
36
  target:{target_id:TARGET,frame_id:GROUNDED_FRAME,rect:{left:100,top:100,width:200,height:80},source:'uia'},
@@ -99,6 +102,29 @@ test('T7 read-only Desktop observe works while Desktop mutation authority is DIS
99
102
  }finally{await f.cleanup();}
100
103
  });
101
104
 
105
+ test('successor autonomous Desktop wait replaces fixed sleep and verifies signed observation evidence',async()=>{
106
+ const d=fakeDesktopRuntime({armed:false});
107
+ const f=await createR34Fixture({desktopRuntime:d});
108
+ try{
109
+ const goal=await f.runtime.submit({submissionId:'desktop-bounded-wait',rootId:'project',title:'desktop wait',goal:'wait for visual change',acceptance:'signed bounded wait'});
110
+ let result=await f.runtime.run({goalId:goal.goal_id,maxCycles:2});
111
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,plan(['observe','wait']));
112
+ result=await toAct(f,goal,result);
113
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{kind:'desktop_observe'}});
114
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,complete('observe','wait-observe-proof'));
115
+ result=await toAct(f,goal,result);
116
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{
117
+ kind:'desktop_wait',baseline_frame_id:FRAME,until:'frame_changed',uia_query:null,timeout_ms:1000,poll_ms:100
118
+ }});
119
+ assert.equal(result.loop.phase,'VERIFYING');
120
+ assert.equal(d.calls.wait,1);assert.equal(d.calls.action,0);assert.equal(d.calls.verify,2);
121
+ const journal=await f.runtime.events({goalId:goal.goal_id,fromSeq:1,limit:200});
122
+ const completed=journal.events.find(e=>e.type==='delegated_action_completed'&&e.payload?.operation?.kind==='desktop_wait');
123
+ assert.equal(completed.payload.capability_id,'desktop.wait');
124
+ assert.equal(completed.payload.evidence.receipts[0].kind,'desktop_wait_observation');
125
+ }finally{await f.cleanup();}
126
+ });
127
+
102
128
  test('T7 Autonomous authority cannot substitute for missing Desktop mutation grant',async()=>{
103
129
  const d=fakeDesktopRuntime({armed:false});
104
130
  const f=await createR34Fixture({desktopRuntime:d});
@@ -16,9 +16,17 @@ function extract(source){
16
16
  assert.ok(m,'coding catalog array missing');
17
17
  return [...m[1].matchAll(/'([^']+)'/g)].map(x=>x[1]).sort();
18
18
  }
19
- for(const rel of ['src/machine-fs-smoke-client.mjs','src/observation-smoke-client.mjs','src/remote-mcp-coding-smoke-client.mjs','src/remote-mcp-smoke-client.mjs']){
19
+ for(const rel of ['src/remote-mcp-coding-smoke-client.mjs','src/remote-mcp-smoke-client.mjs']){
20
20
  test(`${rel} uses the exact R36 25-tool coding catalog`,async()=>{
21
21
  const source=await readFile(path.join(root,...rel.split('/')),'utf8');
22
22
  assert.deepEqual(extract(source),expected);
23
23
  });
24
24
  }
25
+ for(const rel of ['src/machine-fs-smoke-client.mjs','src/observation-smoke-client.mjs']){
26
+ test(`${rel} derives its exact R48 catalog from the canonical runtime catalog`,async()=>{
27
+ const source=await readFile(path.join(root,...rel.split('/')),'utf8');
28
+ assert.match(source,/expectedFullCatalogForEnv/);
29
+ assert.match(source,/assertExactCatalog/);
30
+ assert.doesNotMatch(source,/const (?:codingTools|coding)\s*=\s*\[/);
31
+ });
32
+ }
@@ -73,13 +73,14 @@ test('real Commander-parity filesystem lifecycle is transactional and signed',as
73
73
 
74
74
  test('real Commander adapter preserves PowerShell command bytes through the bounded REPL', {skip:process.platform!=='win32'}, async()=>{
75
75
  const f=await processFixture();
76
+ let adapter=null,pid=null;
76
77
  try{
77
- const adapter=createCommanderWireAdapter({processRuntime:f.runtime});
78
+ adapter=createCommanderWireAdapter({processRuntime:f.runtime});
78
79
  const command=`[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); $x='a b'; "DB:$x"; 'pipe' | ForEach-Object { "P:$_" }; "U:สวัสดี"`;
79
80
  const started=await adapter.handlers.start_process({command,timeout_ms:10000});
80
- const pid=started.structuredContent.pid;
81
+ pid=started.structuredContent.pid;
81
82
  let output='';
82
- for(let i=0;i<50&&!output.includes('P:pipe');i++){
83
+ for(let i=0;i<50&&!(output.includes('DB:a b')&&output.includes('P:pipe')&&output.includes('U:สวัสดี'));i++){
83
84
  const page=await adapter.handlers.read_process_output({pid,offset:0,length:100,timeout_ms:100});
84
85
  output+=page.content[0].text;
85
86
  }
@@ -94,7 +95,10 @@ test('real Commander adapter preserves PowerShell command bytes through the boun
94
95
  assert.match(largeText,/END/);
95
96
  const ended=await adapter.handlers.force_terminate({pid});
96
97
  assert.equal(ended.structuredContent.termination.verified,true);
97
- }finally{await rm(f.root,{recursive:true,force:true});}
98
+ }finally{
99
+ if(adapter&&pid!==null) await adapter.handlers.force_terminate({pid}).catch(()=>{});
100
+ await rm(f.root,{recursive:true,force:true});
101
+ }
98
102
  });
99
103
 
100
104
  test('real stale mutation is rejected before commit and leaves source intact',async()=>{
@@ -456,13 +456,11 @@ test('receipt and verifier bind contained worker, launcher and independent trans
456
456
  assert.match(verifier, /contained transform semantic mismatch/);
457
457
  });
458
458
 
459
- test('MCP protocol smoke accepts only the base or base-plus-host catalog and excludes agent tools', async () => {
459
+ test('MCP protocol smoke derives the exact full catalog and explicitly disables Host', async () => {
460
460
  const smoke = await readFile(fileURLToPath(new URL('../src/mcp-smoke-client.mjs', import.meta.url)), 'utf8');
461
- for (const name of ['contained_transform', 'evidence_verify', 'file_read', 'proof_execute', 'workspace_open']) assert.match(smoke, new RegExp(name));
462
- for (const name of ['host_exec', 'host_file_read', 'host_file_write']) assert.match(smoke, new RegExp(name));
463
- assert.match(smoke, /allowedCatalogs/);
464
- assert.match(smoke, /baseTools/);
465
- assert.match(smoke, /privilegedTools/);
461
+ assert.match(smoke, /expectedFullCatalogForEnv/);
462
+ assert.match(smoke, /assertExactCatalog/);
463
+ assert.match(smoke, /DEADBYTE_PRIVILEGED_POLICY: ''/);
466
464
  assert.doesNotMatch(smoke, /agent_task_/);
467
465
  });
468
466
 
@@ -83,7 +83,8 @@ test('controller keeps privileged policy optional and propagates it only to the
83
83
  test('Windows host policy bootstrap pins executables and creates only explicit local roots', async () => {
84
84
  const init = await readFile(fileURLToPath(new URL('../scripts/init-host-policy.ps1', import.meta.url)), 'utf8');
85
85
  assert.match(init, /deadbyte\.host-policy\.v2/);
86
- assert.match(init, /Get-FileHash/);
86
+ assert.match(init, /System\.Security\.Cryptography\.SHA256/);
87
+ assert.match(init, /System\.IO\.File.*OpenRead/);
87
88
  assert.match(init, /powershell_script/);
88
89
  assert.match(init, /cmd_script/);
89
90
  assert.match(init, /node_script/);
@@ -1,7 +1,7 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import {
4
- desktopActionInputSchema,desktopControlInputSchema,desktopTargetInputSchema,registerDesktopTools
4
+ desktopActionInputSchema,desktopControlInputSchema,desktopTargetInputSchema,desktopWaitInputSchema,registerDesktopTools
5
5
  } from '../src/desktop-mcp-tools-r40.mjs';
6
6
 
7
7
  const H=c=>c.repeat(64);
@@ -30,11 +30,26 @@ test('R40 T5 target schema supports local UIA query without arbitrary dispatch',
30
30
  assert.equal(desktopControlInputSchema.safeParse({op:'target',frame_id:frame,uia_query:{kind:'control_type',value:'button',max_results:4,ordinal:0}}).success,true);
31
31
  });
32
32
 
33
- test('R40 full desktop surface registers four narrow typed tools',()=>{
33
+ test('successor desktop wait schema is bounded, semantic, and rejects action-shaped fields',()=>{
34
+ assert.equal(desktopWaitInputSchema.safeParse({
35
+ baseline_frame_id:frame,until:'frame_changed',timeout_ms:5000,poll_ms:100,preview:false
36
+ }).success,true);
37
+ assert.equal(desktopWaitInputSchema.safeParse({
38
+ baseline_frame_id:frame,until:'uia_present',uia_query:{kind:'name_exact',value:'Ready',max_results:8,ordinal:0},timeout_ms:5000,poll_ms:100,preview:false
39
+ }).success,true);
40
+ assert.equal(desktopWaitInputSchema.safeParse({
41
+ baseline_frame_id:frame,until:'uia_present',timeout_ms:5000,poll_ms:100,preview:false
42
+ }).success,false);
43
+ assert.equal(desktopControlInputSchema.safeParse({
44
+ op:'wait',baseline_frame_id:frame,until:'frame_changed',timeout_ms:5000,poll_ms:100,preview:false,point:{x:1,y:2}
45
+ }).success,false);
46
+ });
47
+
48
+ test('successor full desktop surface registers bounded semantic wait without mutation authority',()=>{
34
49
  const server=fakeServer();
35
- const runtime={status(){},observe(){},target(){},action(){}};
50
+ const runtime={status(){},observe(){},wait(){},target(){},action(){}};
36
51
  registerDesktopTools(server,{runtime});
37
- assert.deepEqual([...server.tools.keys()].sort(),['desktop_action','desktop_observe','desktop_status','desktop_target']);
52
+ assert.deepEqual([...server.tools.keys()].sort(),['desktop_action','desktop_observe','desktop_status','desktop_target','desktop_wait']);
38
53
  });
39
54
 
40
55
  test('R40 public desktop action output removes raw text while preserving content and action hashes',async()=>{