deadbyte-mcp 0.15.25 → 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.
@@ -0,0 +1,54 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ Set-StrictMode -Version 3.0
3
+
4
+ function Invoke-GateStep {
5
+ param([Parameter(Mandatory)][string]$Name,[Parameter(Mandatory)][string[]]$NpmArgs)
6
+ Write-Host "==== $Name ===="
7
+ & npm.cmd @NpmArgs
8
+ if ($LASTEXITCODE -ne 0) { throw "DEADBYTE 0.15.26 / R49 gate failed at $Name with exit code $LASTEXITCODE" }
9
+ }
10
+
11
+ function Invoke-NodeTest {
12
+ param([Parameter(Mandatory)][string]$Name,[Parameter(Mandatory)][string[]]$TestFiles)
13
+ Write-Host "==== $Name ===="
14
+ & node.exe --test --test-concurrency=1 @TestFiles
15
+ if ($LASTEXITCODE -ne 0) { throw "DEADBYTE 0.15.26 / R49 gate failed at $Name with exit code $LASTEXITCODE" }
16
+ }
17
+
18
+ Invoke-GateStep 'containment-build' @('run','containment:build')
19
+ Invoke-GateStep 'process-policy-refresh' @('run','process:policy:refresh')
20
+ Invoke-GateStep 'desktop-policy-refresh' @('run','desktop:policy:refresh')
21
+ Invoke-GateStep 'hardware-build' @('run','hardware:build')
22
+
23
+ $hostPolicy = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\controller\deadbyte-host-policy.json'))
24
+ Remove-Item -LiteralPath $hostPolicy -Force -ErrorAction SilentlyContinue
25
+ if (Test-Path -LiteralPath $hostPolicy) { throw 'could not establish hermetic Host policy absence' }
26
+
27
+ Invoke-GateStep 'r49-release-focused' @('run','r48:release:test')
28
+ Invoke-NodeTest 'r49-donor-proof' @('test/r49-remote-control-donors.test.mjs')
29
+ Invoke-NodeTest 'r49-desktop-wait' @('test/desktop-runtime-r40.test.mjs','test/desktop-mcp-tools-r40.test.mjs','test/autonomous-desktop-r40.test.mjs')
30
+ Invoke-GateStep 'r49-catalog' @('run','r48:catalog:test')
31
+ Invoke-GateStep 'commander-hardening' @('run','r47:hardening:test')
32
+ Invoke-GateStep 'full-regression' @('run','r48:full:test')
33
+ Invoke-GateStep 'mcp-smoke' @('run','mcp:smoke')
34
+ Invoke-GateStep 'mcp-agent-smoke' @('run','mcp:agent:smoke')
35
+ Invoke-GateStep 'mcp-coding-smoke' @('run','mcp:coding:smoke')
36
+ Invoke-GateStep 'mcp-machine-fs-smoke' @('run','mcp:machine-fs:smoke')
37
+ Invoke-GateStep 'mcp-observation-smoke' @('run','mcp:observation:smoke')
38
+ Invoke-GateStep 'mcp-process-smoke' @('run','mcp:process:smoke')
39
+ Invoke-GateStep 'mcp-autonomous-smoke' @('run','mcp:autonomous:smoke')
40
+ Invoke-GateStep 'tunnel-stdio-compat-smoke' @('run','mcp:tunnel-compat:smoke')
41
+ Invoke-GateStep 'containment-smoke' @('run','containment:smoke')
42
+ Invoke-GateStep 'contained-smoke' @('run','contained:smoke')
43
+ Invoke-GateStep 'mcp-contained-smoke' @('run','mcp:contained:smoke')
44
+ Invoke-GateStep 'host-policy-refresh' @('run','host:policy:init')
45
+ Invoke-GateStep 'manifest-write' @('run','manifest:write')
46
+ Invoke-GateStep 'manifest-verify' @('run','manifest:verify')
47
+ Invoke-GateStep 'doctor' @('run','doctor')
48
+ Invoke-GateStep 'mcp-host-smoke' @('run','mcp:host:smoke')
49
+ Invoke-GateStep 'authority-smoke' @('run','authority:smoke')
50
+ Invoke-GateStep 'manifest-final-verify' @('run','manifest:verify')
51
+
52
+ & node.exe (Join-Path $PSScriptRoot 'windows-gate-evidence-r49.mjs')
53
+ if ($LASTEXITCODE -ne 0) { throw "R49 evidence writer failed with exit code $LASTEXITCODE" }
54
+ Write-Host '==== DEADBYTE 0.15.26 / R49 WINDOWS GATE: PASS ===='
@@ -0,0 +1,27 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {mkdir,open,readFile} from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {doctor} from '../src/doctor-r45.mjs';
7
+
8
+ const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
9
+ const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
10
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
11
+ const insist=(ok,message)=>{if(!ok)throw new Error(message);};
12
+ const pkg=JSON.parse(await readFile(path.join(root,'package.json'),'utf8'));
13
+ const release=JSON.parse(await readFile(path.join(root,'RELEASE.json'),'utf8'));
14
+ const active=JSON.parse(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
15
+ const manifestSha256=sha256(await readFile(path.join(root,'MANIFEST.SHA256')));
16
+ insist(pkg.version==='0.15.26'&&release.version===pkg.version&&release.train==='R49','R49 candidate identity mismatch');
17
+ insist(active.release_id===release.parent.release_id&&active.manifest_sha256===release.parent.manifest_sha256&&active.generation_manifest_sha256===release.parent.generation_manifest_sha256,'active runtime is not the exact sealed R49 parent');
18
+ const health=await doctor();
19
+ insist(health.status==='ok'&&health.package_version===pkg.version&&health.manifest?.verified===true,'R49 doctor evidence invalid');
20
+ insist(health.policy?.host_v2_ready===true,'R49 Host policy v2 is not ready');
21
+ const evidence={schema:'deadbyte.r49-windows-gate.v1',status:'passed',exit_code:0,version:pkg.version,train:'R49',completed_at_utc:new Date().toISOString(),manifest_sha256:manifestSha256,parent:release.parent,active_predecessor:{release_id:active.release_id,version:active.version,manifest_sha256:active.manifest_sha256,generation_manifest_sha256:active.generation_manifest_sha256},doctor:{verified_entries:health.manifest.verified_entries,host_v2_ready:true},gates:{full_regression:true,commander_hardening:true,donor_proof:true,desktop_wait:true,exact_catalog:true,stdio_smokes:true,manifest_final_verify:true}};
22
+ const bytes=Buffer.from(`${JSON.stringify(evidence,null,2)}\n`);
23
+ const dir=path.join(stateRoot,'evidence','release'); await mkdir(dir,{recursive:true});
24
+ const target=path.join(dir,`r49-windows-gate-${manifestSha256.slice(0,16)}.json`);
25
+ try{const handle=await open(target,'wx',0o600);try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}}
26
+ catch(error){if(error?.code!=='EEXIST')throw error;const prior=JSON.parse(await readFile(target,'utf8'));insist(prior.schema===evidence.schema&&prior.status==='passed'&&prior.version===evidence.version&&prior.manifest_sha256===manifestSha256&&prior.parent?.release_id===release.parent.release_id,'conflicting R49 gate evidence identity');}
27
+ console.log(JSON.stringify({status:'passed',path:target,sha256:sha256(bytes),manifest_sha256:manifestSha256}));
@@ -132,6 +132,7 @@ const r36TypedActionSchemas=[
132
132
  z.strictObject({kind:z.literal('observation_system_status')}),
133
133
  z.strictObject({kind:z.literal('desktop_status')}),
134
134
  z.strictObject({kind:z.literal('desktop_observe')}),
135
+ z.strictObject({kind:z.literal('desktop_wait'),baseline_frame_id:z.string().regex(HEX64),until:z.enum(['frame_changed','topology_changed','foreground_changed','uia_present','uia_absent']),uia_query:plannerDesktopQuery.nullable().default(null),timeout_ms:z.number().int().min(100).max(30000).default(5000),poll_ms:z.number().int().min(50).max(1000).default(100)}),
135
136
  z.strictObject({kind:z.literal('desktop_target'),frame_id:z.string().regex(HEX64),uia_query:plannerDesktopQuery}),
136
137
  z.strictObject({
137
138
  kind:z.literal('desktop_action'),action_kind:plannerDesktopActionKind,expected_frame_id:z.string().regex(HEX64),
@@ -134,11 +134,17 @@ function validateR36TypedAction(action) {
134
134
  if (typeof action.destination_path === 'string') pathFields.push(action.destination_path);
135
135
  for (const item of action.files ?? []) if (typeof item?.path === 'string') pathFields.push(item.path);
136
136
  for (const candidate of pathFields) validatePortableRelativePath(candidate);
137
- if(action.kind==='desktop_target'){
137
+ if(action.kind==='desktop_target'||action.kind==='desktop_wait'){
138
138
  const q=action.uia_query;
139
- if(q.kind==='root'&&q.value!=='') throw new Error('desktop root query value must be empty');
140
- if(q.kind!=='root'&&q.value.length<1) throw new Error('desktop UIA query value required');
141
- if(q.ordinal>=q.max_results) throw new Error('desktop UIA ordinal must be below max_results');
139
+ const needsQuery=action.kind==='desktop_target'||action.until==='uia_present'||action.until==='uia_absent';
140
+ if(needsQuery&&!q)throw new Error('desktop UIA query required');
141
+ if(!needsQuery&&q)throw new Error('desktop non-UIA wait cannot carry query');
142
+ if(q){
143
+ if(q.kind==='root'&&q.value!=='') throw new Error('desktop root query value must be empty');
144
+ if(q.kind!=='root'&&q.value.length<1) throw new Error('desktop UIA query value required');
145
+ if(q.ordinal>=q.max_results) throw new Error('desktop UIA ordinal must be below max_results');
146
+ }
147
+ if(action.kind==='desktop_wait'&&action.poll_ms>action.timeout_ms)throw new Error('desktop wait poll exceeds timeout');
142
148
  }
143
149
  if(action.kind==='desktop_action'){
144
150
  const pointer=['pointer_move','click_primary','click_secondary','double_click_primary','scroll'].includes(action.action_kind);
@@ -372,7 +378,7 @@ export function buildPlannerPrompt({ goal, policy, view }) {
372
378
  'patch: operations are DEADBYTE Native Diff operations (write, replace, delete) and MUST use exact SHA-256 preconditions from supplied context.',
373
379
  'run_profile: profile_id must be one of the policy profile ids in state.',
374
380
  'delegate: operation is one bounded existing-runtime action. Allowed kinds: machine_stat, machine_list, machine_read, observation_search_start, observation_search_status, observation_search_page, observation_search_cancel, observation_process_list, observation_system_status, process_start, process_read, process_status, process_list, process_action, process_terminate.',
375
- 'When Desktop capabilities are present, use typed desktop_status, desktop_observe, desktop_target with one bounded UIA query, and desktop_action. desktop_action requires separate Desktop authority; Autonomous authority never implies Desktop authority. After stale or unknown Desktop evidence, observe again and replan; never blindly repeat an action.',
381
+ 'When Desktop capabilities are present, use typed desktop_status, desktop_observe, bounded desktop_wait, desktop_target with one bounded UIA query, and desktop_action. desktop_wait is read-only and replaces fixed sleeps; it never retries an action. desktop_action requires separate Desktop authority; Autonomous authority never implies Desktop authority. After stale or unknown Desktop evidence, observe again and replan; never blindly repeat an action.',
376
382
  'Delegate paths are relative to the goal root. Process start accepts only profile_id and cwd; process_action accepts only session_id and fixed action_id. Never emit raw stdin, executable, PID, environment, shell command, or process arguments.',
377
383
  'finish only when the goal is actually satisfied. Required release profiles are enforced by the engine and cannot be bypassed.',
378
384
  'clarify only when one bounded operator choice is genuinely required. Ask one concrete question and provide 2-5 short mutually exclusive choices.',
@@ -726,7 +726,7 @@ const CAPABILITY_BY_ACTION=Object.freeze({
726
726
  machine_stat:'machine_fs.stat',machine_list:'machine_fs.list',machine_read:'machine_fs.read',
727
727
  observation_search_start:'observation.search_start',observation_search_status:'observation.search_status',observation_search_page:'observation.search_page',observation_search_cancel:'observation.search_cancel',
728
728
  observation_process_list:'observation.process_list',observation_system_status:'observation.system_status',
729
- desktop_status:'desktop.status',desktop_observe:'desktop.observe',desktop_target:'desktop.target',desktop_action:'desktop.action'
729
+ desktop_status:'desktop.status',desktop_observe:'desktop.observe',desktop_wait:'desktop.wait',desktop_target:'desktop.target',desktop_action:'desktop.action'
730
730
  });
731
731
  function capabilityIdForOperation(operation){return CAPABILITY_BY_ACTION[operation?.kind]??null;}
732
732
  function assertDelegatedIdentity(events, operation) {
@@ -745,6 +745,12 @@ function assertDelegatedIdentity(events, operation) {
745
745
  event.payload?.operation?.kind==='desktop_observe'&&event.payload?.result?.frame?.frame_id===operation.frame_id);
746
746
  if(!owned) throw new Error(`desktop frame '${operation.frame_id}' was not freshly observed by this goal`);
747
747
  }
748
+ if(operation.kind==='desktop_wait'){
749
+ const owned=events.some(event=>event.type==='delegated_action_completed'&&
750
+ ['desktop_observe','desktop_wait','desktop_target'].includes(event.payload?.operation?.kind)&&
751
+ event.payload?.result?.frame?.frame_id===operation.baseline_frame_id);
752
+ if(!owned)throw new Error(`desktop wait baseline '${operation.baseline_frame_id}' was not freshly observed by this goal`);
753
+ }
748
754
  if(operation.kind==='desktop_action'){
749
755
  const frameOwned=events.some(event=>event.type==='delegated_action_completed'&&
750
756
  ['desktop_observe','desktop_target'].includes(event.payload?.operation?.kind)&&
@@ -856,6 +862,9 @@ async function invokeDelegatedOperation(goal, operation, runtimes, desktopContex
856
862
  case 'desktop_observe':
857
863
  if(!desktopRuntime) throw new Error('desktop runtime unavailable');
858
864
  return desktopRuntime.observe({preview:false});
865
+ case 'desktop_wait':
866
+ if(!desktopRuntime) throw new Error('desktop runtime unavailable');
867
+ return desktopRuntime.wait({baselineFrameId:operation.baseline_frame_id,until:operation.until,query:operation.uia_query,timeoutMs:operation.timeout_ms,pollMs:operation.poll_ms,preview:false});
859
868
  case 'desktop_target':
860
869
  if(!desktopRuntime) throw new Error('desktop runtime unavailable');
861
870
  return desktopRuntime.ground({frameId:operation.frame_id,query:operation.uia_query});
@@ -903,6 +912,7 @@ async function verifyDelegatedEvidence(operation, result, runtimes, verifyKeyPat
903
912
  if(!runtimes.desktopRuntime?.verifyReceipt) throw new Error('desktop receipt verifier unavailable');
904
913
  const refs=[];
905
914
  if(operation.kind==='desktop_observe'&&result?.receipt_id) refs.push([result.receipt_id,result.receipt_sha256,'desktop_observation']);
915
+ if(operation.kind==='desktop_wait'&&result?.receipt_id) refs.push([result.receipt_id,result.receipt_sha256,'desktop_wait_observation']);
906
916
  if(operation.kind==='desktop_target'&&result?.grounding?.receipt_id) refs.push([result.grounding.receipt_id,result.grounding.receipt_sha256,'desktop_grounding']);
907
917
  if(operation.kind==='desktop_action'){
908
918
  refs.push(
@@ -147,6 +147,7 @@ export const R36_CAPABILITY_ENTRIES=Object.freeze([
147
147
 
148
148
  gateway('desktop.status','desktop_status','desktop_control','status','DEADBYTE_DESKTOP_RUNTIME','none',{receipt_class:'desktop'}),
149
149
  gateway('desktop.observe','desktop_observe','desktop_control','observe','DEADBYTE_DESKTOP_RUNTIME','none',{receipt_class:'desktop'}),
150
+ gateway('desktop.wait','desktop_wait','desktop_control','wait','DEADBYTE_DESKTOP_RUNTIME','none',{receipt_class:'desktop'}),
150
151
  gateway('desktop.target','desktop_target','desktop_control','target','DEADBYTE_DESKTOP_RUNTIME','none',{receipt_class:'desktop'}),
151
152
  gateway('desktop.action','desktop_action','desktop_control','action','DEADBYTE_DESKTOP_RUNTIME','desktop',{mutating:true,receipt_class:'desktop'}),
152
153
 
@@ -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
  }
@@ -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');
@@ -1,15 +1,23 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { R36_CAPABILITY_LEDGER } from './capability-ledger.mjs';
5
6
 
6
7
  const packageRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
7
8
  const catalogPath=path.join(packageRoot,'docs','R48-FULL-TOOL-CATALOG.json');
9
+ const overlayPath=path.join(packageRoot,'docs','R49-FULL-TOOL-CATALOG.json');
8
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']);
9
11
 
10
12
  export async function expectedFullCatalogForEnv(env){
11
13
  const canonical=JSON.parse(await readFile(catalogPath,'utf8'));
12
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');
13
21
  const disabled=new Set();
14
22
  for(const capability of R36_CAPABILITY_LEDGER.entries){
15
23
  const flag=capability.runtime_flag;
@@ -21,7 +29,7 @@ export async function expectedFullCatalogForEnv(env){
21
29
  if(env.DEADBYTE_CODING_RUNTIME==='1') disabled.delete('coding_symbol_rename_preview');
22
30
  if(env.DEADBYTE_AGENT_RUNTIME!=='1') for(const name of AGENT_TOOLS) disabled.add(name);
23
31
  if(env.DEADBYTE_PROCESS_RUNTIME!=='1') disabled.add('process_session_cancel');
24
- return canonical.tools.filter(name=>!disabled.has(name));
32
+ return full.filter(name=>!disabled.has(name));
25
33
  }
26
34
 
27
35
  export function assertExactCatalog(actual,expected,label='MCP'){
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.25';
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});
@@ -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()=>{
@@ -19,7 +19,7 @@ function F(at,image=H('same'),window=WIN){
19
19
  bounds:{left:0,top:0,width:1000,height:800},activeWindow:window,imageSha256:image
20
20
  });
21
21
  }
22
- async function fixture({grantActions=['click_primary','text_input'],captureFrames=null,times=null,inputProvider=null,grounderProvider=null}={}){
22
+ async function fixture({grantActions=['click_primary','text_input'],captureFrames=null,times=null,inputProvider=null,grounderProvider=null,clockMs=null,sleeper=null}={}){
23
23
  const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r40-desktop-runtime-'));
24
24
  const grant=path.join(root,'grant.json'),evidence=path.join(root,'evidence');
25
25
  const policy={
@@ -59,7 +59,7 @@ async function fixture({grantActions=['click_primary','text_input'],captureFrame
59
59
  '2026-09-19T02:00:01.000Z'
60
60
  ])];
61
61
  const now=()=>clock.length?clock.shift():'2026-09-19T02:00:01.000Z';
62
- const runtime=createDesktopRuntime({policy,observerRuntime,inputProvider:provider,grounderProvider,privateKey,keyId:'test-key',evidenceDir:evidence,now});
62
+ const runtime=createDesktopRuntime({policy,observerRuntime,inputProvider:provider,grounderProvider,privateKey,keyId:'test-key',evidenceDir:evidence,now,clockMs:clockMs??undefined,sleeper:sleeper??undefined});
63
63
  return {root,grant,evidence,policy,expected,runtime,calls,publicKey};
64
64
  }
65
65
 
@@ -242,6 +242,47 @@ test('read-only observe works independently of desktop mutation grant',async()=>
242
242
  assert.match(observed.receipt_sha256,/^[0-9a-f]{64}$/);
243
243
  });
244
244
 
245
+ test('successor desktop wait observes until a frame changes and publishes signed final evidence',async()=>{
246
+ let tick=0;
247
+ const same={frame:F('2026-09-19T02:00:00.600Z'),backend:'fake',byte_length:4};
248
+ const changed={frame:F('2026-09-19T02:00:00.900Z',H('changed')),backend:'fake',byte_length:4};
249
+ const fx=await fixture({
250
+ captureFrames:[same,changed],clockMs:()=>tick,sleeper:async ms=>{tick+=ms;},
251
+ times:['2026-09-19T02:00:01.000Z']
252
+ });
253
+ await writeFile(fx.grant,'{}');
254
+ const result=await fx.runtime.wait({baselineFrameId:fx.expected.frame_id,until:'frame_changed',timeoutMs:1000,pollMs:100});
255
+ assert.equal(result.satisfied,true);
256
+ assert.equal(result.timed_out,false);
257
+ assert.equal(result.attempts,2);
258
+ assert.equal(result.frame.frame_id,changed.frame.frame_id);
259
+ assert.match(result.receipt_sha256,/^[0-9a-f]{64}$/);
260
+ const receipt=JSON.parse(await readFile(path.join(fx.evidence,`${result.receipt_id}.json`),'utf8'));
261
+ assert.equal(receipt.kind,'desktop_wait_observation');
262
+ assert.equal(receipt.payload.condition,'frame_changed');
263
+ assert.equal(receipt.payload.satisfied,true);
264
+ assert.equal(fx.calls.length,0);
265
+ });
266
+
267
+ test('successor desktop wait uses local UIA matching without leaking query text or dispatching input',async()=>{
268
+ let tick=0,queries=0;
269
+ const first={frame:F('2026-09-19T02:00:00.600Z'),backend:'fake',byte_length:4};
270
+ const second={frame:F('2026-09-19T02:00:00.900Z'),backend:'fake',byte_length:4};
271
+ const grounderProvider=async({query})=>{
272
+ queries++;
273
+ return {status:'ok',query_sha256:H('wait-query'),query_kind:query.kind,ordinal:0,visited:4,truncated:false,
274
+ matches:queries===1?[]:[{rect:{left:1,top:2,width:3,height:4},control_type:50000,enabled:true,offscreen:false}],
275
+ provider_receipt_sha256:H('wait-provider')};
276
+ };
277
+ const fx=await fixture({captureFrames:[first,second],grounderProvider,clockMs:()=>tick,sleeper:async ms=>{tick+=ms;},times:['2026-09-19T02:00:01.000Z']});
278
+ const result=await fx.runtime.wait({baselineFrameId:fx.expected.frame_id,until:'uia_present',query:{kind:'name_exact',value:'SECRET READY',max_results:8,ordinal:0},timeoutMs:1000,pollMs:100});
279
+ assert.equal(result.satisfied,true);
280
+ assert.equal(result.match.actionable,true);
281
+ assert.equal(result.match.query_sha256,H('wait-query'));
282
+ assert.equal(JSON.stringify(result).includes('SECRET READY'),false);
283
+ assert.equal(fx.calls.length,0);
284
+ });
285
+
245
286
 
246
287
  test('desktop runtime enforces policy max_text_bytes before observation or input effect',async()=>{
247
288
  const fx=await fixture({grantActions:['text_input']});
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url';
7
7
  import { Client } from '@modelcontextprotocol/client';
8
8
  import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
9
9
  import { createR34Fixture } from './helpers/autonomous-r34-fixture.mjs';
10
+ import { expectedFullCatalogForEnv } from '../src/runtime-catalog-expectation.mjs';
10
11
 
11
12
  const here=path.dirname(fileURLToPath(import.meta.url));
12
13
  const repoRoot=path.resolve(here,'..');
@@ -15,7 +16,7 @@ const desktopPolicyPath=path.join(repoRoot,'controller','deadbyte-desktop-policy
15
16
  const catalogPath=path.join(repoRoot,'docs','R48-FULL-TOOL-CATALOG.json');
16
17
  const absent=async p=>{try{await access(p);return false;}catch(e){if(e?.code==='ENOENT')return true;throw e;}};
17
18
 
18
- test('R48 runtime full stdio catalog exactly matches canonical registration surface',async()=>{
19
+ test('R49 runtime full stdio catalog exactly matches sealed R48 base plus reviewed overlay',async()=>{
19
20
  const canonical=JSON.parse(await readFile(catalogPath,'utf8'));
20
21
  assert.equal(canonical.schema,'deadbyte.r48-full-catalog.v1');
21
22
  assert.equal(canonical.train,'R48');
@@ -61,15 +62,16 @@ test('R48 runtime full stdio catalog exactly matches canonical registration surf
61
62
  DEADBYTE_AUTONOMOUS_RUNTIME:'1',DEADBYTE_AUTONOMOUS_POLICY:path.join(f.root,'autonomous-policy.json'),
62
63
  DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE:'external'
63
64
  };
65
+ const expected=await expectedFullCatalogForEnv(env);
64
66
  const transport=new StdioClientTransport({command:process.execPath,args:[serverPath],cwd:repoRoot,env,stderr:'pipe'});
65
67
  try{
66
68
  await client.connect(transport);
67
69
  const listed=await client.listTools();
68
70
  const names=listed.tools.map(t=>t.name).sort();
69
- assert.deepEqual(names,canonical.tools);
71
+ assert.deepEqual(names,expected);
70
72
  assert.equal(new Set(names).size,names.length,'runtime duplicate tool names');
71
73
  for(const tool of listed.tools) if(tool.outputSchema!==undefined) assert.equal(tool.outputSchema?.additionalProperties,false,'non-strict output '+tool.name);
72
- for(const name of ['agent_task_submit','autonomous_goal_submit','coding_task_submit','process_session_start','machine_fs_write','machine_search_start','desktop_action','host_exec','native_diff_apply','skill_catalog']) assert.ok(names.includes(name),'missing R48 surface '+name);
74
+ for(const name of ['agent_task_submit','autonomous_goal_submit','coding_task_submit','process_session_start','machine_fs_write','machine_search_start','desktop_wait','desktop_action','host_exec','native_diff_apply','skill_catalog']) assert.ok(names.includes(name),'missing R49 surface '+name);
73
75
  } finally {
74
76
  await client.close().catch(()=>{});
75
77
  for(const q of [processGrant,hostArm,desktopGrant,codingLease,autonomousLease]) assert.equal(await absent(q),true,'authority file appeared during catalog probe: '+q);