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.
- package/MANIFEST.SHA256 +92 -38
- package/README.txt +5 -4
- package/acceptance/r40/desktop-live-drift.mjs +106 -0
- package/acceptance/r40/desktop-live-ground.mjs +110 -0
- package/acceptance/r40/desktop-live-input.mjs +128 -0
- package/acceptance/r40/desktop-live-observe.mjs +100 -0
- package/acceptance/r40/desktop-live-uia.mjs +121 -0
- package/bin/deadbyte-desktop-input-r40.exe +0 -0
- package/bin/deadbyte-desktop-input-r40.obj +0 -0
- package/bin/deadbyte-desktop-observer-r40.exe +0 -0
- package/bin/deadbyte-desktop-observer-r40.obj +0 -0
- package/bin/deadbyte-desktop-uia-r40.exe +0 -0
- package/bin/deadbyte-desktop-uia-r40.obj +0 -0
- package/controller/README.TXT +7 -5
- package/controller/deadbyte-controller.config.psd1.example +3 -0
- package/controller/deadbyte-controller.ps1 +149 -4
- package/controller/deadbyte-desktop-policy.json +41 -0
- package/docs/ARCHITECTURE.md +6 -1
- package/docs/superpowers/specs/2026-09-19-r40-desktop-computer-use-design.md +214 -0
- package/native/deadbyte-desktop-input.cpp +455 -0
- package/native/deadbyte-desktop-observer.cpp +253 -0
- package/native/deadbyte-desktop-uia.cpp +307 -0
- package/package.json +10 -8
- package/scripts/build-desktop-r40.ps1 +106 -0
- package/scripts/deferred-slot-operation-r40.mjs +126 -0
- package/scripts/final-closure-r40.mjs +114 -0
- package/scripts/final-closure-verify-r40.mjs +212 -0
- package/scripts/gate-windows-r40.ps1 +43 -0
- package/scripts/init-desktop-policy-r40.ps1 +79 -0
- package/scripts/r40-compact-ledger-probe.mjs +4 -0
- package/scripts/release-parity-r40.mjs +212 -0
- package/scripts/release-parity-tests-r40.ps1 +9 -0
- package/scripts/windows-gate-evidence-r40.mjs +52 -0
- package/src/autonomous-context-engine.mjs +2 -1
- package/src/autonomous-planner-contracts.mjs +19 -1
- package/src/autonomous-planner.mjs +22 -0
- package/src/autonomous-runtime.mjs +144 -24
- package/src/capability-ledger.mjs +6 -1
- package/src/compact-gateway-mcp-tools.mjs +14 -2
- package/src/deadbyte-cli.mjs +72 -44
- package/src/desktop-contract.mjs +263 -0
- package/src/desktop-input-r40.mjs +148 -0
- package/src/desktop-mcp-tools-r40.mjs +126 -0
- package/src/desktop-memory-r40.mjs +272 -0
- package/src/desktop-observer-r40.mjs +231 -0
- package/src/desktop-policy-r40.mjs +110 -0
- package/src/desktop-runtime-r40.mjs +318 -0
- package/src/desktop-uia-r40.mjs +123 -0
- package/src/mcp-server.mjs +74 -19
- package/src/r40-compact-disarmed-probe.mjs +71 -0
- package/src/r40-full-surface-probe.mjs +34 -0
- package/src/release-generation.mjs +38 -5
- package/src/remote-console.mjs +41 -0
- package/src/remote-live-log.mjs +208 -45
- package/src/remote-mcp-desktop-r40-smoke-client.mjs +70 -0
- package/src/remote-mcp-r40-parity-client.mjs +200 -0
- package/src/remote-mcp-smoke-client.mjs +20 -6
- package/src/remote-result-journal.mjs +10 -2
- package/src/runtime-update.mjs +55 -17
- package/src/tool-audit.mjs +29 -2
- package/src/version.mjs +1 -1
- package/test/autonomous-capability-r36.test.mjs +43 -1
- package/test/autonomous-desktop-r40.test.mjs +188 -0
- package/test/capability-ledger-r36.test.mjs +1 -0
- package/test/cli-entrypoint.test.mjs +1 -1
- package/test/compact-catalog-r36.test.mjs +2 -2
- package/test/compact-gateway-r36.test.mjs +56 -4
- package/test/compact-max-catalog-r40.test.mjs +95 -0
- package/test/controller-desktop-r40.test.mjs +85 -0
- package/test/core.test.mjs +1 -1
- package/test/desktop-adversarial-r40.test.mjs +62 -0
- package/test/desktop-contract-r40.test.mjs +200 -0
- package/test/desktop-input-r40.test.mjs +63 -0
- package/test/desktop-mcp-tools-r40.test.mjs +61 -0
- package/test/desktop-memory-r40.test.mjs +141 -0
- package/test/desktop-observer-r40.test.mjs +119 -0
- package/test/desktop-policy-r40.test.mjs +111 -0
- package/test/desktop-runtime-r40.test.mjs +278 -0
- package/test/desktop-uia-r40.test.mjs +42 -0
- package/test/full-max-catalog-r40.test.mjs +91 -0
- package/test/helpers/autonomous-r34-fixture.mjs +3 -3
- package/test/r33-closeout-regression.test.mjs +7 -7
- package/test/r33-finalization.test.mjs +14 -14
- package/test/r37-remote-tool-call-timeline.test.mjs +6 -5
- package/test/r40-finalization.test.mjs +118 -0
- package/test/release-generation.test.mjs +41 -9
- package/test/release-version.test.mjs +19 -61
- package/test/remote-console-r40.test.mjs +38 -0
- package/test/remote-live-log-r37.test.mjs +89 -32
- package/test/remote-r24-catalog.test.mjs +12 -3
- package/test/remote-result-journal.test.mjs +15 -0
- package/test/remote-session-cli.test.mjs +69 -46
- package/test/runtime-self-update.test.mjs +44 -0
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -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
|
|
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')
|
|
118
|
-
|
|
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
|
|
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
|
|
137
|
-
|
|
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(
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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'&¤t.owner_pid===marker.owner_pid&¤t.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(
|
|
270
|
-
await
|
|
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
|
|
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
|
|
311
|
-
process.stdout
|
|
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
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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]); }
|