deadbyte-mcp 0.11.3 → 0.12.0

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 (93) hide show
  1. package/MANIFEST.SHA256 +92 -38
  2. package/README.txt +5 -4
  3. package/acceptance/r40/desktop-live-drift.mjs +106 -0
  4. package/acceptance/r40/desktop-live-ground.mjs +110 -0
  5. package/acceptance/r40/desktop-live-input.mjs +128 -0
  6. package/acceptance/r40/desktop-live-observe.mjs +100 -0
  7. package/acceptance/r40/desktop-live-uia.mjs +121 -0
  8. package/bin/deadbyte-desktop-input-r40.exe +0 -0
  9. package/bin/deadbyte-desktop-input-r40.obj +0 -0
  10. package/bin/deadbyte-desktop-observer-r40.exe +0 -0
  11. package/bin/deadbyte-desktop-observer-r40.obj +0 -0
  12. package/bin/deadbyte-desktop-uia-r40.exe +0 -0
  13. package/bin/deadbyte-desktop-uia-r40.obj +0 -0
  14. package/controller/README.TXT +7 -5
  15. package/controller/deadbyte-controller.config.psd1.example +3 -0
  16. package/controller/deadbyte-controller.ps1 +149 -4
  17. package/controller/deadbyte-desktop-policy.json +41 -0
  18. package/docs/ARCHITECTURE.md +6 -1
  19. package/docs/superpowers/specs/2026-09-19-r40-desktop-computer-use-design.md +214 -0
  20. package/native/deadbyte-desktop-input.cpp +455 -0
  21. package/native/deadbyte-desktop-observer.cpp +253 -0
  22. package/native/deadbyte-desktop-uia.cpp +307 -0
  23. package/package.json +10 -8
  24. package/scripts/build-desktop-r40.ps1 +106 -0
  25. package/scripts/deferred-slot-operation-r40.mjs +126 -0
  26. package/scripts/final-closure-r40.mjs +114 -0
  27. package/scripts/final-closure-verify-r40.mjs +212 -0
  28. package/scripts/gate-windows-r40.ps1 +43 -0
  29. package/scripts/init-desktop-policy-r40.ps1 +79 -0
  30. package/scripts/r40-compact-ledger-probe.mjs +4 -0
  31. package/scripts/release-parity-r40.mjs +212 -0
  32. package/scripts/release-parity-tests-r40.ps1 +9 -0
  33. package/scripts/windows-gate-evidence-r40.mjs +52 -0
  34. package/src/autonomous-context-engine.mjs +2 -1
  35. package/src/autonomous-planner-contracts.mjs +19 -1
  36. package/src/autonomous-planner.mjs +22 -0
  37. package/src/autonomous-runtime.mjs +144 -24
  38. package/src/capability-ledger.mjs +6 -1
  39. package/src/compact-gateway-mcp-tools.mjs +14 -2
  40. package/src/deadbyte-cli.mjs +72 -44
  41. package/src/desktop-contract.mjs +263 -0
  42. package/src/desktop-input-r40.mjs +148 -0
  43. package/src/desktop-mcp-tools-r40.mjs +126 -0
  44. package/src/desktop-memory-r40.mjs +272 -0
  45. package/src/desktop-observer-r40.mjs +231 -0
  46. package/src/desktop-policy-r40.mjs +110 -0
  47. package/src/desktop-runtime-r40.mjs +318 -0
  48. package/src/desktop-uia-r40.mjs +123 -0
  49. package/src/mcp-server.mjs +74 -19
  50. package/src/r40-compact-disarmed-probe.mjs +71 -0
  51. package/src/r40-full-surface-probe.mjs +34 -0
  52. package/src/release-generation.mjs +38 -5
  53. package/src/remote-console.mjs +41 -0
  54. package/src/remote-live-log.mjs +208 -45
  55. package/src/remote-mcp-desktop-r40-smoke-client.mjs +70 -0
  56. package/src/remote-mcp-r40-parity-client.mjs +200 -0
  57. package/src/remote-mcp-smoke-client.mjs +20 -6
  58. package/src/remote-result-journal.mjs +10 -2
  59. package/src/runtime-update.mjs +55 -17
  60. package/src/tool-audit.mjs +29 -2
  61. package/src/version.mjs +1 -1
  62. package/test/autonomous-capability-r36.test.mjs +43 -1
  63. package/test/autonomous-desktop-r40.test.mjs +188 -0
  64. package/test/capability-ledger-r36.test.mjs +1 -0
  65. package/test/cli-entrypoint.test.mjs +1 -1
  66. package/test/compact-catalog-r36.test.mjs +2 -2
  67. package/test/compact-gateway-r36.test.mjs +56 -4
  68. package/test/compact-max-catalog-r40.test.mjs +95 -0
  69. package/test/controller-desktop-r40.test.mjs +85 -0
  70. package/test/core.test.mjs +1 -1
  71. package/test/desktop-adversarial-r40.test.mjs +62 -0
  72. package/test/desktop-contract-r40.test.mjs +200 -0
  73. package/test/desktop-input-r40.test.mjs +63 -0
  74. package/test/desktop-mcp-tools-r40.test.mjs +61 -0
  75. package/test/desktop-memory-r40.test.mjs +141 -0
  76. package/test/desktop-observer-r40.test.mjs +119 -0
  77. package/test/desktop-policy-r40.test.mjs +111 -0
  78. package/test/desktop-runtime-r40.test.mjs +278 -0
  79. package/test/desktop-uia-r40.test.mjs +42 -0
  80. package/test/full-max-catalog-r40.test.mjs +91 -0
  81. package/test/helpers/autonomous-r34-fixture.mjs +3 -3
  82. package/test/r33-closeout-regression.test.mjs +7 -7
  83. package/test/r33-finalization.test.mjs +14 -14
  84. package/test/r37-remote-tool-call-timeline.test.mjs +6 -5
  85. package/test/r40-finalization.test.mjs +118 -0
  86. package/test/release-generation.test.mjs +41 -9
  87. package/test/release-version.test.mjs +19 -61
  88. package/test/remote-console-r40.test.mjs +38 -0
  89. package/test/remote-live-log-r37.test.mjs +89 -32
  90. package/test/remote-r24-catalog.test.mjs +12 -3
  91. package/test/remote-result-journal.test.mjs +15 -0
  92. package/test/remote-session-cli.test.mjs +69 -46
  93. package/test/runtime-self-update.test.mjs +44 -0
@@ -11,6 +11,7 @@ import { verifyAuditChain } from './audit-verify.mjs';
11
11
  import { openRemoteResultJournal, parseRemoteResultCursor } from './remote-result-journal.mjs';
12
12
  import { DEADBYTE_VERSION } from './version.mjs';
13
13
  import { createLiveTimelineTracker, formatCompactAuditEvent, formatRemoteLifecycle, formatRemoteToolCall, formatRuntimeDiagnosticLine } from './remote-live-log.mjs';
14
+ import { prepareRemoteConsole, remoteDisplayText } from './remote-console.mjs';
14
15
 
15
16
  const REMOTE_COMMANDS=new Set([
16
17
  'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
@@ -95,30 +96,38 @@ async function listLogFiles(root){
95
96
  }
96
97
  return files;
97
98
  }
98
- function formatLogLine(file,line,{activityTracker=null}={}){
99
+ function formatLogLine(file,line,{activityTracker=null,sequence=null,verbose=false,displayMode='unicode'}={}){
99
100
  if(!line) return null;
100
101
  if(/^segment-\d{8}\.jsonl$/.test(path.basename(file))){
101
102
  try{
102
103
  const event=JSON.parse(line);
103
- return activityTracker?activityTracker.observe(event):formatCompactAuditEvent(event);
104
+ return activityTracker?activityTracker.observe(event,{sequence}):formatCompactAuditEvent(event);
104
105
  }catch{return null;}
105
106
  }
106
- return formatRuntimeDiagnosticLine(file,line);
107
+ return formatRuntimeDiagnosticLine(file,line,{verbose,displayMode});
107
108
  }
108
109
  function remoteResultJournalPath(root,sessionId){return path.join(root,'runtime','remote-results',`${sessionId}.json`);}
109
110
  function auditTimelineRecord(file,line){
110
111
  if(!/^segment-\d{8}\.jsonl$/.test(path.basename(file))||!line) return null;
111
112
  let event;try{event=JSON.parse(line);}catch{return null;}
112
113
  if(typeof event?.request_id!=='string'||!event.request_id||typeof event.tool!=='string'||!event.tool) return null;
113
- const observed=Number.isFinite(Date.parse(event.created_at_utc))?new Date(Date.parse(event.created_at_utc)).toISOString():new Date().toISOString();
114
+ const stamp=[event.ts,event.created_at_utc,event.observed_at_utc].find(value=>typeof value==='string'&&Number.isFinite(Date.parse(value)));
115
+ const observed=stamp?new Date(Date.parse(stamp)).toISOString():new Date().toISOString();
114
116
  const payload_sha256=createHash('sha256').update(Buffer.from(line,'utf8')).digest('hex');
115
117
  if(event.event_type==='tool.request') return {event_type:'tool.call',request_id:event.request_id,tool:event.tool,
116
118
  observed_at_utc:observed,arguments:event.args??{},payload_sha256};
117
- if(event.event_type==='tool.result') return {event_type:'tool.result',request_id:event.request_id,tool:event.tool,
118
- status:event.outcome?.is_error===true?'error':'ok',observed_at_utc:observed,payload_sha256};
119
+ if(event.event_type==='tool.result'){
120
+ const unknown=event.outcome?.side_effect_state==='unknown';
121
+ const recoveryReason=unknown&&typeof event.outcome?.recovery?.reason==='string'?event.outcome.recovery.reason:null;
122
+ return {event_type:'tool.result',request_id:event.request_id,tool:event.tool,
123
+ status:event.outcome?.is_error===true?'error':'ok',observed_at_utc:observed,payload_sha256,
124
+ ...(Number.isFinite(Number(event.outcome?.duration_ms))?{duration_ms:Number(event.outcome.duration_ms)}:{}),
125
+ ...(unknown?{side_effect_state:'unknown'}:{}),
126
+ ...(typeof event.outcome?.reason==='string'&&event.outcome.reason?{reason:event.outcome.reason}:(recoveryReason?{reason:recoveryReason}:{}))};
127
+ }
119
128
  return null;
120
129
  }
121
- async function replayRemoteResults(root,cursor){
130
+ async function replayRemoteResults(root,cursor,{displayMode='unicode'}={}){
122
131
  const parsed=parseRemoteResultCursor(cursor);
123
132
  const file=remoteResultJournalPath(root,parsed.session_id);
124
133
  await requireRegular(file,'remote result journal');
@@ -129,12 +138,15 @@ async function replayRemoteResults(root,cursor){
129
138
  for(const event of page.events){
130
139
  if(event.event_type==='tool.call'){
131
140
  const line=formatRemoteToolCall({event_type:'tool.request',tool:event.tool,request_id:event.request_id,
132
- observed_at_utc:event.observed_at_utc,args:event.arguments});
133
- process.stdout.write(`${line} | REPLAY cursor r1:${event.session_id}:${event.seq}\n`);
141
+ observed_at_utc:event.observed_at_utc,args:event.arguments},{displayMode});
142
+ process.stdout.write(remoteDisplayText(`${line} | REPLAY #${event.seq}\n`,displayMode));
134
143
  continue;
135
144
  }
136
- const marker=event.status==='error'?'[ERR]':'[OK]';
137
- process.stdout.write(`[REPLAY] ${marker} ${event.tool} [cursor r1:${event.session_id}:${event.seq}]\n`);
145
+ const unknown=event.side_effect_state==='unknown';
146
+ const marker=unknown?'OUTCOME_UNKNOWN':event.status==='error'?(displayMode==='ascii'?'ERR':'✗'):(displayMode==='ascii'?'OK':'✓');
147
+ const duration=Number.isFinite(Number(event.duration_ms))?` ${event.duration_ms<1000?Math.round(event.duration_ms)+'ms':(event.duration_ms/1000).toFixed(2)+'s'}`:'';
148
+ const reason=(event.status==='error'||unknown)&&typeof event.reason==='string'&&event.reason?` — ${event.reason}`:'';
149
+ process.stdout.write(remoteDisplayText(`[REPLAY] #${event.seq} ${event.tool}${duration} ${marker}${reason}\n`,displayMode));
138
150
  }
139
151
  next=page.next_cursor;
140
152
  if(!page.has_more) return next;
@@ -158,8 +170,10 @@ async function snapshotRemoteLogOffsets(root){
158
170
  }
159
171
  return offsets;
160
172
  }
161
- async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTracker=null}={}){
162
- process.stdout.write('[LOG] Unified live timeline. Ctrl+C disconnects and revokes authority.\n');
173
+ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTracker=null,verbose=false,displayMode='unicode'}={}){
174
+ process.stdout.write(verbose
175
+ ? '[LOG] Verbose live timeline. Full sanitized request detail enabled; Ctrl+C disconnects and revokes authority.\n'
176
+ : '[LOG] Human live timeline. Transport IN/OUT hidden; full replay/audit detail remains on disk. Ctrl+C disconnects and revokes authority.\n');
163
177
  const activeOffsets=offsets??await snapshotRemoteLogOffsets(root);
164
178
  while(!signal.aborted){
165
179
  for(const file of await listLogFiles(root)){
@@ -173,22 +187,28 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
173
187
  ? await journal.recordCall(timeline)
174
188
  : await journal.recordTerminal(timeline);
175
189
  if(recorded?.duplicate) continue;
176
- const formatted=formatLogLine(file,line,{activityTracker});
190
+ const formatted=formatLogLine(file,line,{activityTracker,sequence:recorded?.event?.seq??null,verbose,displayMode});
177
191
  if(!formatted) continue;
178
- if(recorded) process.stdout.write(`${formatted} [cursor ${recorded.cursor}]\n`);
179
- else process.stdout.write(`${formatted}\n`);
192
+ const output=Array.isArray(formatted)?formatted:[formatted];
193
+ for(const item of output) if(item) process.stdout.write(`${item}\n`);
180
194
  }
181
195
  }
182
196
  try{await sleep(250,null,{signal});}catch(error){if(error?.name!=='AbortError') throw error;}
183
197
  }
184
198
  }
185
- async function cleanupRemoteSession(ctx){
199
+ function remoteTag(kind,displayMode){
200
+ const unicode={warn:'⚠️ [WARN]',recovery:'♻️ [RECOVERY]',auth:'🔐 [AUTH]'};
201
+ const ascii={warn:'[WARN]',recovery:'[RECOVERY]',auth:'[AUTH]'};
202
+ return (displayMode==='ascii'?ascii:unicode)[kind];
203
+ }
204
+ function writeRemote(stream,text,displayMode){stream.write(remoteDisplayText(text,displayMode));}
205
+ async function cleanupRemoteSession(ctx,{displayMode='unicode'}={}){
186
206
  for(const command of SESSION_CLEANUP){
187
207
  try{
188
208
  const result=invokeController(ctx,command,{allowFailure:true,capture:true,echoCaptured:false});
189
- if(result.status!==0) process.stderr.write(`⚠️ [WARN] cleanup ${command} exit=${result.status}\n`);
209
+ if(result.status!==0) writeRemote(process.stderr,`${remoteTag('warn',displayMode)} cleanup ${command} exit=${result.status}\n`,displayMode);
190
210
  }
191
- catch(error){process.stderr.write(`⚠️ [WARN] cleanup ${command}: ${String(error?.message||error)}\n`);}
211
+ catch(error){writeRemote(process.stderr,`${remoteTag('warn',displayMode)} cleanup ${command}: ${String(error?.message||error)}\n`,displayMode);}
192
212
  }
193
213
  }
194
214
  const GUARDIAN_SCRIPT=fileURLToPath(new URL('./remote-session-guardian.mjs',import.meta.url));
@@ -215,7 +235,7 @@ async function readRemoteSessionMarker(root){
215
235
  }
216
236
  throw new Error('remote session marker schema invalid');
217
237
  }
218
- async function recoverRemoteSession(ctx){
238
+ async function recoverRemoteSession(ctx,{displayMode='unicode'}={}){
219
239
  let prior=await readRemoteSessionMarker(ctx.root);
220
240
  if(!prior) return;
221
241
  const ownerPid=remoteOwnerPid(prior);
@@ -230,8 +250,8 @@ async function recoverRemoteSession(ctx){
230
250
  }
231
251
  throw new Error(`remote session guardian cleanup still active pid=${prior.guardian_pid}`);
232
252
  }
233
- process.stdout.write(`♻️ [RECOVERY] Stale remote session pid=${ownerPid}; revoking authority before restart.\n`);
234
- await cleanupRemoteSession(ctx);
253
+ writeRemote(process.stdout,`${remoteTag('recovery',displayMode)} Stale remote session pid=${ownerPid}; revoking authority before restart.\n`,displayMode);
254
+ await cleanupRemoteSession(ctx,{displayMode});
235
255
  await rm(remoteSessionMarker(ctx.root),{force:true});
236
256
  }
237
257
  async function claimRemoteSession(ctx){
@@ -259,20 +279,23 @@ async function claimRemoteSession(ctx){
259
279
  throw error;
260
280
  }
261
281
  }
262
- async function releaseRemoteSession(ctx,marker){
263
- let current=null;try{current=await readRemoteSessionMarker(ctx.root);}catch(error){process.stderr.write(`⚠️ [WARN] remote session marker cleanup: ${String(error?.message||error)}\n`);return;}
282
+ async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
283
+ let current=null;try{current=await readRemoteSessionMarker(ctx.root);}catch(error){writeRemote(process.stderr,`${remoteTag('warn',displayMode)} remote session marker cleanup: ${String(error?.message||error)}\n`,displayMode);return;}
264
284
  const same=current?.schema==='deadbyte.remote-session.v2'&&current.owner_pid===marker.owner_pid&&current.session_nonce===marker.session_nonce;
265
285
  if(same) await rm(remoteSessionMarker(ctx.root),{force:true});
266
286
  }
267
- async function runRemoteSession({resumeCursor=null}={}){
287
+ async function runRemoteSession({resumeCursor=null,verbose=false}={}){
288
+ const display=prepareRemoteConsole();
289
+ const displayMode=display.mode;
268
290
  const ctx=await remoteContext();
269
- if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor);
270
- await recoverRemoteSession(ctx);
291
+ if(displayMode==='ascii') writeRemote(process.stdout,`[DISPLAY] ASCII fallback: ${display.reason}\n`,displayMode);
292
+ if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor,{displayMode});
293
+ await recoverRemoteSession(ctx,{displayMode});
271
294
  const marker=await claimRemoteSession(ctx);
272
295
  const resultJournal=await openRemoteResultJournal({
273
296
  filePath:remoteResultJournalPath(ctx.root,marker.result_session_id),sessionId:marker.result_session_id,maxEntries:2048
274
297
  });
275
- const activityTracker=createLiveTimelineTracker();
298
+ const activityTracker=createLiveTimelineTracker({verbose,displayMode});
276
299
  const aborter=new AbortController();
277
300
  const abort=()=>aborter.abort();
278
301
  const watchStdin=!process.stdin.isTTY;
@@ -282,10 +305,10 @@ async function runRemoteSession({resumeCursor=null}={}){
282
305
  let logTask=null;
283
306
  try{
284
307
  process.stdout.write('DEADBYTE MCP\n');
285
- process.stdout.write(`${formatRemoteLifecycle('device_start')}\n`);
286
- process.stdout.write(`${formatRemoteLifecycle(resumeCursor!==null?'session_restored':'session_new')}\n`);
308
+ process.stdout.write(`${formatRemoteLifecycle('device_start',{displayMode})}\n`);
309
+ process.stdout.write(`${formatRemoteLifecycle(resumeCursor!==null?'session_restored':'session_new',{displayMode})}\n`);
287
310
  const logOffsets=await snapshotRemoteLogOffsets(ctx.root);
288
- logTask=tailRemoteLogs(ctx.root,aborter.signal,{journal:resultJournal,offsets:logOffsets,activityTracker});
311
+ logTask=tailRemoteLogs(ctx.root,aborter.signal,{journal:resultJournal,offsets:logOffsets,activityTracker,verbose,displayMode});
289
312
  process.stdout.write('[BOOT] Starting local + remote runtime...\n');
290
313
  const preflight=invokeController(ctx,'status',{capture:true,allowFailure:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
291
314
  let startText=preflight.stdout||'';
@@ -298,42 +321,47 @@ async function runRemoteSession({resumeCursor=null}={}){
298
321
  if(started.status!==0||!controllerRuntimeReady(started.stdout)) throw new Error('remote controller start returned without a verified ONLINE runtime');
299
322
  startText=started.stdout||'';
300
323
  }
301
- process.stdout.write(`${formatRemoteLifecycle('bridge_connected')}\n`);
302
- process.stdout.write(`${formatRemoteLifecycle('cloud_connected')}\n`);
324
+ process.stdout.write(`${formatRemoteLifecycle('bridge_connected',{displayMode})}\n`);
325
+ process.stdout.write(`${formatRemoteLifecycle('cloud_connected',{displayMode})}\n`);
303
326
  for(const command of SESSION_ARM){
304
- process.stdout.write(`🔐 [AUTH] Arming ${command.slice(3)}...\n`);
327
+ writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Arming ${command.slice(3)}...\n`,displayMode);
305
328
  invokeController(ctx,command,{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
306
329
  }
307
330
  const url=startText.match(/^URL\s*:\s*(\S+)/m)?.[1]||'';
308
331
  process.stdout.write('[OK] ONLINE\n');
309
- process.stdout.write(`${formatRemoteLifecycle('ready')}\n`);
310
- if(url) process.stdout.write(`URL: ${url}\n`);
311
- process.stdout.write('🔐 [AUTH] Verifying process, coding, autonomous; host must stay DISARMED...\n');
332
+ process.stdout.write(`${formatRemoteLifecycle('ready',{displayMode})}\n`);
333
+ if(url) writeRemote(process.stdout,`URL: ${url}\n`,displayMode);
334
+ writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Verifying process, coding, autonomous; host must stay DISARMED...\n`,displayMode);
312
335
  for(const [command,pattern] of SESSION_STATUS){
313
336
  const result=invokeController(ctx,command,{capture:true,echoCaptured:false});
314
337
  if(!pattern.test(result.stdout)) throw new Error(`remote session authority verification failed: ${command} state mismatch`);
315
338
  }
316
339
  ready=true;
317
- process.stdout.write('🔐 [AUTH] ARMED: process, coding, autonomous | host DISARMED\n');
340
+ writeRemote(process.stdout,`${remoteTag('auth',displayMode)} ARMED: process, coding, autonomous | host DISARMED\n`,displayMode);
318
341
  await logTask;
319
342
  }finally{
320
343
  const wasAborted=aborter.signal.aborted;
321
344
  aborter.abort();
322
345
  if(logTask) await logTask.catch(()=>{});
323
346
  for(const line of activityTracker.reconcileInterrupted('session_end')) process.stdout.write(`${line}\n`);
347
+ process.stdout.write(`${activityTracker.summary()}\n`);
324
348
  process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
325
349
  if(watchStdin){process.stdin.removeListener('end',abort);process.stdin.removeListener('close',abort);process.stdin.pause();}
326
- process.stdout.write(`${formatRemoteLifecycle('cleanup')}\n`);
350
+ process.stdout.write(`${formatRemoteLifecycle('cleanup',{displayMode})}\n`);
327
351
  if(ready||!wasAborted) process.stdout.write('[INFO] Disconnecting; revoking authority...\n');
328
- await cleanupRemoteSession(ctx);
329
- await releaseRemoteSession(ctx,marker);
352
+ await cleanupRemoteSession(ctx,{displayMode});
353
+ await releaseRemoteSession(ctx,marker,{displayMode});
330
354
  }
331
355
  }
332
356
  async function runRemote(args){
333
357
  if(args.length===0){await runRemoteSession();return;}
334
- if(args.length===2&&args[0]==='--resume'){await runRemoteSession({resumeCursor:args[1]});return;}
335
- if(args.length>1) throw new Error('remote accepts one controller command or --resume <cursor>');
336
- const command=args[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
358
+ const verbose=args.includes('--verbose')||args.includes('-v');
359
+ const filtered=args.filter(arg=>arg!=='--verbose'&&arg!=='-v');
360
+ if(filtered.length===0){await runRemoteSession({verbose});return;}
361
+ if(filtered.length===2&&filtered[0]==='--resume'){await runRemoteSession({resumeCursor:filtered[1],verbose});return;}
362
+ if(verbose) throw new Error('remote --verbose/-v is valid only for foreground session or --resume <cursor>');
363
+ if(filtered.length>1) throw new Error('remote accepts one controller command or --resume <cursor>');
364
+ const command=filtered[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
337
365
  const ctx=await remoteContext();
338
366
  const result=invokeController(ctx,command,{allowFailure:true});
339
367
  process.exitCode=result.status;
@@ -0,0 +1,263 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.mjs';
3
+
4
+ export const DESKTOP_FRAME_SCHEMA='deadbyte.desktop-frame.v1';
5
+ export const DESKTOP_TARGET_SCHEMA='deadbyte.desktop-target.v1';
6
+ export const DESKTOP_ACTION_SCHEMA='deadbyte.desktop-action.v1';
7
+ export const DESKTOP_GUARD_SCHEMA='deadbyte.desktop-action-guard.v1';
8
+ export const DESKTOP_POST_OBSERVATION_SCHEMA='deadbyte.desktop-post-observation.v1';
9
+ export const DESKTOP_ACTION_CHAIN_SCHEMA='deadbyte.desktop-action-chain.v1';
10
+
11
+ const HEX64=/^[0-9a-f]{64}$/;
12
+ const INTENT=/^[0-9a-f]{64}$/;
13
+ const HWND=/^0x[0-9a-f]{1,16}$/;
14
+ const ACTION_KINDS=new Set([
15
+ 'pointer_move','click_primary','click_secondary','double_click_primary',
16
+ 'scroll','key_chord','text_input'
17
+ ]);
18
+ const TARGET_SOURCES=new Set(['visual','uia','accessibility','geometry']);
19
+ const KEY=/^(?:[A-Z0-9]|F(?:[1-9]|1[0-2])|ENTER|ESC|TAB|SPACE|BACKSPACE|DELETE|INSERT|HOME|END|PAGEUP|PAGEDOWN|LEFT|RIGHT|UP|DOWN|CTRL|ALT|SHIFT|WIN)$/;
20
+
21
+ const sha256Bytes=bytes=>createHash('sha256').update(bytes).digest('hex');
22
+ const sha256Object=value=>sha256Bytes(Buffer.from(canonicalJson(value),'utf8'));
23
+ const isObject=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
24
+
25
+ function insist(condition,message){ if(!condition) throw new Error(message); }
26
+ function hash(value,label){ insist(typeof value==='string'&&HEX64.test(value),`${label} must be sha256`); return value; }
27
+ function parseUtc(value,label){
28
+ insist(typeof value==='string'&&value.length>=20&&value.length<=40,`${label} invalid`);
29
+ const ms=Date.parse(value); insist(Number.isFinite(ms),`${label} invalid`);
30
+ return ms;
31
+ }
32
+ function int(value,label,{min=Number.MIN_SAFE_INTEGER,max=Number.MAX_SAFE_INTEGER}={}){
33
+ insist(Number.isSafeInteger(value)&&value>=min&&value<=max,`${label} invalid`);
34
+ return value;
35
+ }
36
+ function number(value,label,{min=-Infinity,max=Infinity}={}){
37
+ insist(Number.isFinite(value)&&value>=min&&value<=max,`${label} invalid`);
38
+ return value;
39
+ }
40
+ function bounds(value,label='desktop bounds'){
41
+ insist(isObject(value),`${label} invalid`);
42
+ const left=int(value.left,`${label}.left`,{min:-131072,max:131072});
43
+ const top=int(value.top,`${label}.top`,{min:-131072,max:131072});
44
+ const width=int(value.width,`${label}.width`,{min:1,max:131072});
45
+ const height=int(value.height,`${label}.height`,{min:1,max:131072});
46
+ insist(left+width<=262144&&top+height<=262144,`${label} extent invalid`);
47
+ return Object.freeze({left,top,width,height});
48
+ }
49
+ function rect(value,label='target rect'){
50
+ insist(isObject(value),`${label} invalid`);
51
+ return Object.freeze({
52
+ left:int(value.left,`${label}.left`,{min:-131072,max:131072}),
53
+ top:int(value.top,`${label}.top`,{min:-131072,max:131072}),
54
+ width:int(value.width,`${label}.width`,{min:1,max:131072}),
55
+ height:int(value.height,`${label}.height`,{min:1,max:131072})
56
+ });
57
+ }
58
+ function point(value,label='point'){
59
+ insist(isObject(value),`${label} invalid`);
60
+ return Object.freeze({
61
+ x:int(value.x,`${label}.x`,{min:-131072,max:131072}),
62
+ y:int(value.y,`${label}.y`,{min:-131072,max:131072})
63
+ });
64
+ }
65
+ function insidePoint(b,p){ return p.x>=b.left&&p.y>=b.top&&p.x<b.left+b.width&&p.y<b.top+b.height; }
66
+ function insideRect(b,r){
67
+ return r.left>=b.left&&r.top>=b.top&&
68
+ r.left+r.width<=b.left+b.width&&r.top+r.height<=b.top+b.height;
69
+ }
70
+ function windowIdentity(value,{optional=false}={}){
71
+ if(value===null||value===undefined){
72
+ insist(optional,'window identity required'); return null;
73
+ }
74
+ insist(isObject(value),'window identity invalid');
75
+ const hwnd=String(value.hwnd??'').toLowerCase();
76
+ insist(HWND.test(hwnd),'window hwnd invalid');
77
+ const pid=int(value.pid,'window pid',{min:1,max:0x7fffffff});
78
+ const result={hwnd,pid};
79
+ if(value.process_image_sha256!==undefined&&value.process_image_sha256!==null) result.process_image_sha256=hash(value.process_image_sha256,'window process_image_sha256');
80
+ if(value.title_sha256!==undefined&&value.title_sha256!==null) result.title_sha256=hash(value.title_sha256,'window title_sha256');
81
+ return Object.freeze(result);
82
+ }
83
+ function sameWindow(expected,actual){
84
+ if(expected===null) return true;
85
+ if(actual===null) return false;
86
+ if(expected.hwnd!==actual.hwnd||expected.pid!==actual.pid) return false;
87
+ if(expected.process_image_sha256&&expected.process_image_sha256!==actual.process_image_sha256) return false;
88
+ if(expected.title_sha256&&expected.title_sha256!==actual.title_sha256) return false;
89
+ return true;
90
+ }
91
+ function normalizeKeyChord(value){
92
+ insist(Array.isArray(value)&&value.length>=1&&value.length<=5,'key_chord invalid');
93
+ const keys=value.map(item=>String(item).toUpperCase());
94
+ insist(keys.every(item=>KEY.test(item)),'key_chord contains unsupported key');
95
+ insist(new Set(keys).size===keys.length,'key_chord contains duplicate key');
96
+ return Object.freeze(keys);
97
+ }
98
+ function validateFrameObject(frame){
99
+ insist(isObject(frame),'desktop frame invalid');
100
+ insist(frame.schema===DESKTOP_FRAME_SCHEMA,'desktop frame schema mismatch');
101
+ hash(frame.desktop_epoch,'desktop_epoch');
102
+ hash(frame.topology_sha256,'topology_sha256');
103
+ hash(frame.image_sha256,'image_sha256');
104
+ hash(frame.frame_id,'frame_id');
105
+ parseUtc(frame.observed_at_utc,'observed_at_utc');
106
+ const b=bounds(frame.bounds);
107
+ const active=frame.active_window===null?null:windowIdentity(frame.active_window);
108
+ const body={
109
+ schema:DESKTOP_FRAME_SCHEMA,
110
+ desktop_epoch:frame.desktop_epoch,
111
+ topology_sha256:frame.topology_sha256,
112
+ observed_at_utc:frame.observed_at_utc,
113
+ bounds:b,
114
+ active_window:active,
115
+ image_sha256:frame.image_sha256
116
+ };
117
+ insist(sha256Object(body)===frame.frame_id,'desktop frame hash mismatch');
118
+ return Object.freeze({...body,frame_id:frame.frame_id});
119
+ }
120
+
121
+ export function createDesktopFrame({desktopEpoch,topologySha256,observedAtUtc,bounds:inputBounds,activeWindow=null,imageSha256}={}){
122
+ hash(desktopEpoch,'desktopEpoch'); hash(topologySha256,'topologySha256'); hash(imageSha256,'imageSha256');
123
+ parseUtc(observedAtUtc,'observedAtUtc');
124
+ const body={
125
+ schema:DESKTOP_FRAME_SCHEMA,
126
+ desktop_epoch:desktopEpoch,
127
+ topology_sha256:topologySha256,
128
+ observed_at_utc:observedAtUtc,
129
+ bounds:bounds(inputBounds),
130
+ active_window:activeWindow===null?null:windowIdentity(activeWindow),
131
+ image_sha256:imageSha256
132
+ };
133
+ return Object.freeze({...body,frame_id:sha256Object(body)});
134
+ }
135
+
136
+ export function verifyDesktopFrame(frame){
137
+ try { const normalized=validateFrameObject(frame); return {ok:true,frame:normalized}; }
138
+ catch(error){ return {ok:false,reason:String(error?.message??error)}; }
139
+ }
140
+
141
+ export function createDesktopTarget({frame,rect:inputRect,source,confidence,semanticLabelSha256=null,windowBinding=undefined}={}){
142
+ const f=validateFrameObject(frame);
143
+ insist(TARGET_SOURCES.has(source),'target source invalid');
144
+ number(confidence,'target confidence',{min:0,max:1});
145
+ const r=rect(inputRect);
146
+ insist(insideRect(f.bounds,r),'target rect outside desktop bounds');
147
+ const binding=windowBinding===undefined?f.active_window:windowIdentity(windowBinding,{optional:true});
148
+ if(binding!==null) insist(sameWindow(binding,f.active_window),'target window binding does not match frame');
149
+ if(semanticLabelSha256!==null) hash(semanticLabelSha256,'semantic_label_sha256');
150
+ const body={
151
+ schema:DESKTOP_TARGET_SCHEMA,frame_id:f.frame_id,desktop_epoch:f.desktop_epoch,
152
+ topology_sha256:f.topology_sha256,rect:r,source,confidence,
153
+ window_binding:binding,semantic_label_sha256:semanticLabelSha256
154
+ };
155
+ return Object.freeze({...body,target_id:sha256Object(body)});
156
+ }
157
+
158
+ export function createDesktopAction({
159
+ intentId,kind,frameId,desktopEpoch,topologySha256,expectedWindow=null,
160
+ point:inputPoint=null,scrollDelta=null,keyChord=null,text=null,targetId=null
161
+ }={}){
162
+ insist(typeof intentId==='string'&&INTENT.test(intentId),'intentId invalid');
163
+ insist(ACTION_KINDS.has(kind),'desktop action kind invalid');
164
+ hash(frameId,'frameId'); hash(desktopEpoch,'desktopEpoch'); hash(topologySha256,'topologySha256');
165
+ if(targetId!==null) hash(targetId,'targetId');
166
+ const expected=windowIdentity(expectedWindow,{optional:true});
167
+ let p=null,delta=null,keys=null,textValue=null,textSha256=null;
168
+ if(['pointer_move','click_primary','click_secondary','double_click_primary','scroll'].includes(kind)){
169
+ p=point(inputPoint,'action point');
170
+ } else insist(inputPoint===null,'non-pointer action cannot carry point');
171
+ if(kind==='scroll'){
172
+ delta=int(scrollDelta,'scrollDelta',{min:-12000,max:12000});
173
+ insist(delta!==0,'scrollDelta cannot be zero');
174
+ } else insist(scrollDelta===null,'non-scroll action cannot carry scrollDelta');
175
+ if(kind==='key_chord') keys=normalizeKeyChord(keyChord);
176
+ else insist(keyChord===null,'non-key action cannot carry keyChord');
177
+ if(kind==='text_input'){
178
+ insist(typeof text==='string'&&text.length>=1&&text.length<=4096&&!text.includes('\u0000'),'text input invalid');
179
+ textValue=text; textSha256=sha256Bytes(Buffer.from(text,'utf8'));
180
+ } else insist(text===null,'non-text action cannot carry text');
181
+ const body={
182
+ schema:DESKTOP_ACTION_SCHEMA,intent_id:intentId,kind,frame_id:frameId,
183
+ desktop_epoch:desktopEpoch,topology_sha256:topologySha256,
184
+ expected_window:expected,target_id:targetId,point:p,scroll_delta:delta,key_chord:keys,
185
+ text:textValue,text_sha256:textSha256
186
+ };
187
+ return Object.freeze({...body,action_sha256:sha256Object(body)});
188
+ }
189
+
190
+ export function guardDesktopAction({frame,action,nowUtc,maxFrameAgeMs=2000,maxFutureSkewMs=250}={}){
191
+ const f=validateFrameObject(frame);
192
+ insist(isObject(action)&&action.schema===DESKTOP_ACTION_SCHEMA,'desktop action invalid');
193
+ hash(action.action_sha256,'action_sha256');
194
+ const actionBody={...action}; delete actionBody.action_sha256;
195
+ insist(sha256Object(actionBody)===action.action_sha256,'desktop action hash mismatch');
196
+ insist(action.frame_id===f.frame_id,'desktop action stale frame id');
197
+ insist(action.desktop_epoch===f.desktop_epoch,'desktop action desktop epoch mismatch');
198
+ insist(action.topology_sha256===f.topology_sha256,'desktop action topology mismatch');
199
+ insist(sameWindow(action.expected_window,f.active_window),'desktop action foreground window mismatch');
200
+ int(maxFrameAgeMs,'maxFrameAgeMs',{min:1,max:60000});
201
+ int(maxFutureSkewMs,'maxFutureSkewMs',{min:0,max:5000});
202
+ const now=parseUtc(nowUtc,'nowUtc'), observed=parseUtc(f.observed_at_utc,'frame observed_at_utc');
203
+ const age=now-observed;
204
+ insist(age>=-maxFutureSkewMs,'desktop frame is from the future');
205
+ insist(age<=maxFrameAgeMs,'desktop frame is stale');
206
+ if(action.point!==null) insist(insidePoint(f.bounds,action.point),'desktop action point outside frame bounds');
207
+ const guardedAt=new Date(now).toISOString();
208
+ const body={
209
+ schema:DESKTOP_GUARD_SCHEMA,frame_id:f.frame_id,action_sha256:action.action_sha256,
210
+ intent_id:action.intent_id,desktop_epoch:f.desktop_epoch,topology_sha256:f.topology_sha256,
211
+ active_window:f.active_window,frame_age_ms:Math.max(0,age),guarded_at_utc:guardedAt
212
+ };
213
+ return Object.freeze({...body,guard_sha256:sha256Object(body)});
214
+ }
215
+
216
+ export function bindDesktopPostObservation({guard,beforeFrame,action,afterFrame}={}){
217
+ insist(isObject(guard)&&guard.schema===DESKTOP_GUARD_SCHEMA,'desktop guard invalid');
218
+ hash(guard.guard_sha256,'guard_sha256');
219
+ const guardBody={...guard}; delete guardBody.guard_sha256;
220
+ insist(sha256Object(guardBody)===guard.guard_sha256,'desktop guard hash mismatch');
221
+ const before=validateFrameObject(beforeFrame),after=validateFrameObject(afterFrame);
222
+ insist(before.frame_id===guard.frame_id,'post-observation before frame mismatch');
223
+ insist(isObject(action)&&action.action_sha256===guard.action_sha256,'post-observation action mismatch');
224
+ parseUtc(guard.guarded_at_utc,'guarded_at_utc');
225
+ insist(parseUtc(after.observed_at_utc,'after observed_at_utc')>=parseUtc(guard.guarded_at_utc,'guarded_at_utc'),'post-observation predates guarded action');
226
+ const windowChanged=!sameWindow(before.active_window,after.active_window)||!sameWindow(after.active_window,before.active_window);
227
+ const result={
228
+ schema:DESKTOP_POST_OBSERVATION_SCHEMA,
229
+ guard_sha256:guard.guard_sha256,
230
+ action_sha256:action.action_sha256,
231
+ before_frame_id:before.frame_id,
232
+ after_frame_id:after.frame_id,
233
+ change_observed:before.image_sha256!==after.image_sha256,
234
+ window_changed:windowChanged,
235
+ foreground_transition:Object.freeze({
236
+ before:before.active_window,
237
+ after:after.active_window,
238
+ changed:windowChanged
239
+ }),
240
+ topology_changed:before.topology_sha256!==after.topology_sha256,
241
+ desktop_epoch_changed:before.desktop_epoch!==after.desktop_epoch,
242
+ observed_at_utc:after.observed_at_utc,
243
+ success:null
244
+ };
245
+ return Object.freeze({...result,post_observation_sha256:sha256Object(result)});
246
+ }
247
+
248
+ export function bindDesktopActionChain({
249
+ beforeFrameId,actionSha256,afterFrameId,guardSha256,postObservationSha256,
250
+ postReceiptSha256,effectId,effectFingerprint
251
+ }={}){
252
+ hash(beforeFrameId,'beforeFrameId'); hash(actionSha256,'actionSha256'); hash(afterFrameId,'afterFrameId');
253
+ hash(guardSha256,'guardSha256'); hash(postObservationSha256,'postObservationSha256');
254
+ hash(postReceiptSha256,'postReceiptSha256'); hash(effectId,'effectId'); hash(effectFingerprint,'effectFingerprint');
255
+ const body={
256
+ schema:DESKTOP_ACTION_CHAIN_SCHEMA,before_frame_id:beforeFrameId,action_sha256:actionSha256,
257
+ after_frame_id:afterFrameId,guard_sha256:guardSha256,post_observation_sha256:postObservationSha256,
258
+ post_receipt_sha256:postReceiptSha256,effect_id:effectId,effect_fingerprint:effectFingerprint
259
+ };
260
+ return Object.freeze({...body,chain_sha256:sha256Object(body)});
261
+ }
262
+
263
+ export function desktopActionKinds(){ return Object.freeze([...ACTION_KINDS]); }