deadbyte-mcp 0.15.36 → 0.15.37
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 +55 -27
- package/PROVENANCE.json +114 -87
- package/README.txt +12 -12
- package/RELEASE.json +214 -187
- package/SBOM.cdx.json +10 -25
- package/TEST-MATRIX.json +297 -258
- package/bridge/package.json +1 -1
- package/bridge/src/bridge-health-r59.mjs +4 -3
- package/bridge/src/bridge.mjs +71 -14
- package/bridge/src/canonical-json-r60.mjs +13 -0
- package/bridge/src/reconnect-delay-r60.mjs +31 -0
- package/bridge/src/remote-call-claims-r60.mjs +157 -0
- package/bridge/src/remote-dispatch-r60.mjs +75 -0
- package/controller/deadbyte-controller.ps1 +6 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R59-PROVENANCE.json +87 -0
- package/docs/R59-RELEASE.json +187 -0
- package/docs/R59-SBOM.cdx.json +68 -0
- package/docs/R59-TEST-MATRIX.json +258 -0
- package/docs/R59-bridge-package.json +13 -0
- package/docs/R59-package-lock.json +184 -0
- package/docs/R59-package.json +106 -0
- package/docs/R59-version.mjs +6 -0
- package/docs/R60-TASK-CONTRACT.md +22 -0
- package/package.json +5 -3
- package/scripts/full-regression-r60.mjs +32 -0
- package/scripts/gate-windows-r60-prepublish.ps1 +74 -0
- package/src/capability-snapshot.mjs +6 -2
- package/src/deadbyte-cli.mjs +80 -34
- package/src/remote-authority.mjs +56 -0
- package/src/remote-enforcement-probe-r60.mjs +17 -0
- package/src/remote-live-log.mjs +19 -2
- package/src/remote-log-decode-r60.mjs +32 -0
- package/src/remote-mcp-machine-smoke-client.mjs +19 -1
- package/src/remote-session-guardian.mjs +20 -3
- package/src/tool-audit.mjs +5 -0
- package/src/version.mjs +1 -1
- package/test/r48-release-contract.test.mjs +1 -1
- package/test/r51-runtime-coherence.test.mjs +2 -2
- package/test/r58-cleanup-retention.test.mjs +9 -3
- package/test/r58-concurrent-recovery.test.mjs +9 -3
- package/test/r58-controller-live-local.test.mjs +22 -3
- package/test/r58-stale-owner.test.mjs +2 -2
- package/test/r59-release-contract.test.mjs +6 -6
- package/test/r60-authority-coherence.test.mjs +36 -0
- package/test/r60-authority-enforcement.test.mjs +28 -0
- package/test/r60-bridge-dispatch.test.mjs +119 -0
- package/test/r60-clock-discipline.test.mjs +15 -0
- package/test/r60-live-timeline.test.mjs +29 -0
- package/test/r60-reconnect-errors.test.mjs +21 -0
- package/test/r60-reconnect-jitter.test.mjs +18 -0
- package/test/r60-release-contract.test.mjs +81 -0
- package/test/r60-remote-call-recovery.test.mjs +170 -0
- package/test/r60-windows-log-decode.test.mjs +25 -0
- package/test/remote-live-log-r37.test.mjs +1 -1
- package/test/remote-session-cli.test.mjs +200 -43
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -20,6 +20,8 @@ import { windowsPowerShellEnvironment } from './runtime-update.mjs';
|
|
|
20
20
|
import { resolveStateRoot } from './state-root.mjs';
|
|
21
21
|
import {foregroundIdentityMatches,queryForegroundIdentity,validForegroundIdentity} from './foreground-process-identity.mjs';
|
|
22
22
|
import {acquireForegroundSessionLock} from './foreground-session-lock.mjs';
|
|
23
|
+
import { AUTHORITY_PLANES, authorityVector, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation, authorityBanner } from './remote-authority.mjs';
|
|
24
|
+
import { decodeRemoteLogBytes, boundedRemoteDiagnostic, safeRemoteLogPrefixLength } from './remote-log-decode-r60.mjs';
|
|
23
25
|
|
|
24
26
|
const REMOTE_COMMANDS=new Set([
|
|
25
27
|
'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
|
|
@@ -27,17 +29,9 @@ const REMOTE_COMMANDS=new Set([
|
|
|
27
29
|
'armdesktop','disarmdesktop','desktopstatus',
|
|
28
30
|
'armcoding','disarmcoding','codingstatus','codingtest','armautonomous','disarmautonomous','autonomousstatus','autonomoustest'
|
|
29
31
|
]);
|
|
30
|
-
const SESSION_ARM=['armprocess','armcoding','armautonomous'];
|
|
31
32
|
const REMOTE_STATUS_TIMEOUT_MS=30000;
|
|
32
33
|
const REMOTE_START_TIMEOUT_MS=150000;
|
|
33
|
-
const
|
|
34
|
-
['processstatus',/^Process plane\s*:\s*ARMED\b/m],
|
|
35
|
-
['codingstatus',/^Coding plane\s*:\s*ARMED\b/m],
|
|
36
|
-
['autonomousstatus',/^Autonomous\s*:\s*ARMED\b/m],
|
|
37
|
-
['desktopstatus',/^Desktop plane\s*:\s*DISARMED\b/m],
|
|
38
|
-
['hoststatus',/^Host plane\s*:\s*DISARMED\b/m]
|
|
39
|
-
];
|
|
40
|
-
const SESSION_CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','park'];
|
|
34
|
+
const SESSION_CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'];
|
|
41
35
|
const SUPERVISOR_COMMANDS=Object.freeze({
|
|
42
36
|
start:'supervisorstart',stop:'supervisorstop',status:'supervisorstatus',recover:'supervisorrecover'
|
|
43
37
|
});
|
|
@@ -55,6 +49,9 @@ function printHelp(){
|
|
|
55
49
|
` npx -y ${exact} doctor # verify current package/release truth`,
|
|
56
50
|
` npx -y ${exact} mcp # stdio MCP server for local clients`,
|
|
57
51
|
` npx -y ${exact} remote # unified foreground device session + live timeline`,
|
|
52
|
+
` npx -y ${exact} remote --desktop # session with Desktop authority; Host remains disarmed`,
|
|
53
|
+
` npx -y ${exact} remote --host # session with Host authority; Desktop remains disarmed`,
|
|
54
|
+
` npx -y ${exact} remote --full # explicit five-plane foreground session`,
|
|
58
55
|
` npx -y ${exact} remote --resume <cursor> # replay missed results, then reconnect`,
|
|
59
56
|
` npx -y ${exact} remote --trace # show trace/span identity without payload detail`,
|
|
60
57
|
` npx -y ${exact} remote --filter <expr> # presentation filter: tool/category/status/trace`,
|
|
@@ -75,8 +72,7 @@ async function remoteContext(){
|
|
|
75
72
|
return {root,launcher,powershell};
|
|
76
73
|
}
|
|
77
74
|
function boundedSingleLine(value,maxChars=2048){
|
|
78
|
-
|
|
79
|
-
return line.length<=maxChars?line:`${line.slice(0,maxChars)}...`;
|
|
75
|
+
return boundedRemoteDiagnostic(value,maxChars);
|
|
80
76
|
}
|
|
81
77
|
async function readActiveRuntimeBinding(root){
|
|
82
78
|
const file=path.join(root,'active-release.json');
|
|
@@ -158,11 +154,16 @@ async function readTailBounded(file,maxBytes=4096){
|
|
|
158
154
|
if(length===0) return '';
|
|
159
155
|
const buffer=Buffer.alloc(length);
|
|
160
156
|
const handle=await open(file,'r');
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
let encoding=null;
|
|
158
|
+
try{
|
|
159
|
+
const header=Buffer.alloc(2);await handle.read(header,0,2,0);
|
|
160
|
+
if(header[0]===0xff&&header[1]===0xfe)encoding='utf16le';
|
|
161
|
+
await handle.read(buffer,0,length,info.size-length);
|
|
162
|
+
}finally{await handle.close();}
|
|
163
|
+
return boundedSingleLine(decodeRemoteLogBytes(file,buffer,{encoding}),maxBytes);
|
|
163
164
|
}catch(error){if(error?.code==='ENOENT') return '';throw error;}
|
|
164
165
|
}
|
|
165
|
-
function invokeController(ctx,command,{capture=false,allowFailure=false,echoCaptured=true,timeoutMs=null,discardOutput=false,redirectOutput=null}={}){
|
|
166
|
+
function invokeController(ctx,command,{capture=false,allowFailure=false,echoCaptured=true,timeoutMs=null,discardOutput=false,redirectOutput=null,json=false,probeMode=null}={}){
|
|
166
167
|
if(capture&&discardOutput) throw new Error('invokeController cannot capture and discard output together');
|
|
167
168
|
if(capture&&redirectOutput) throw new Error('invokeController cannot capture and redirect output together');
|
|
168
169
|
if(discardOutput&&redirectOutput) throw new Error('invokeController cannot discard and redirect output together');
|
|
@@ -175,9 +176,10 @@ function invokeController(ctx,command,{capture=false,allowFailure=false,echoCapt
|
|
|
175
176
|
stderrFd=openSync(redirectOutput.stderr_path,'w');
|
|
176
177
|
stdio=['ignore',stdoutFd,stderrFd];
|
|
177
178
|
}else stdio=capture?['ignore','pipe','pipe']:'inherit';
|
|
178
|
-
const options={stdio,encoding:capture?'utf8':undefined,windowsHide:false,shell:false,
|
|
179
|
+
const options={stdio,encoding:capture?'utf8':undefined,windowsHide:false,shell:false,
|
|
180
|
+
env:windowsPowerShellEnvironment(probeMode?{...process.env,DEADBYTE_REMOTE_PROBE_MODE:probeMode}:process.env)};
|
|
179
181
|
if(Number.isInteger(timeoutMs)&&timeoutMs>0) options.timeout=timeoutMs;
|
|
180
|
-
result=spawnSync(ctx.powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',ctx.launcher,command],options);
|
|
182
|
+
result=spawnSync(ctx.powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',ctx.launcher,command,...(json?['-Json']:[])],options);
|
|
181
183
|
}finally{
|
|
182
184
|
if(stdoutFd!==null) closeSync(stdoutFd);
|
|
183
185
|
if(stderrFd!==null) closeSync(stderrFd);
|
|
@@ -193,6 +195,10 @@ function invokeController(ctx,command,{capture=false,allowFailure=false,echoCapt
|
|
|
193
195
|
if(status!==0&&!allowFailure) throw new Error(`remote controller '${command}' failed exit=${status}`);
|
|
194
196
|
return {status,stdout,stderr};
|
|
195
197
|
}
|
|
198
|
+
function observedAuthority(ctx){
|
|
199
|
+
const result=invokeController(ctx,'status',{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS,json:true});
|
|
200
|
+
return parseAuthorityObservation(result.stdout);
|
|
201
|
+
}
|
|
196
202
|
function controllerRuntimeReady(text){
|
|
197
203
|
const value=String(text||'');
|
|
198
204
|
return /^DEADBYTE MCP\s*=\s*ONLINE\s*$/m.test(value)&&
|
|
@@ -278,13 +284,23 @@ async function replayRemoteResults(root,cursor,{displayMode='unicode'}={}){
|
|
|
278
284
|
async function readAppend(file,offset){
|
|
279
285
|
let info;try{info=await stat(file);}catch(error){if(error?.code==='ENOENT') return {offset:0,text:''};throw error;}
|
|
280
286
|
let start=offset;
|
|
281
|
-
|
|
287
|
+
const reset=info.size<start;
|
|
288
|
+
if(reset) start=0;
|
|
282
289
|
if(info.size===start) return {offset:start,text:''};
|
|
283
|
-
const length=info.size-start;
|
|
284
|
-
const buffer=Buffer.alloc(length);
|
|
285
290
|
const handle=await open(file,'r');
|
|
286
|
-
|
|
287
|
-
|
|
291
|
+
let encoding=null,buffer,bytesRead=0;
|
|
292
|
+
try{
|
|
293
|
+
const header=Buffer.alloc(2);await handle.read(header,0,2,0);
|
|
294
|
+
if(header[0]===0xff&&header[1]===0xfe&&/\.log$/i.test(file)){
|
|
295
|
+
encoding='utf16le';if(start%2!==0)start=Math.max(0,start-1);
|
|
296
|
+
}
|
|
297
|
+
const length=Math.min(1024*1024,info.size-start);
|
|
298
|
+
if(length===0)return {offset:start,text:''};
|
|
299
|
+
buffer=Buffer.alloc(length);
|
|
300
|
+
({bytesRead}=await handle.read(buffer,0,length,start));
|
|
301
|
+
}finally{await handle.close();}
|
|
302
|
+
const complete=safeRemoteLogPrefixLength(buffer.subarray(0,bytesRead),encoding??'utf8');
|
|
303
|
+
return {offset:start+complete,text:decodeRemoteLogBytes(file,buffer.subarray(0,complete),{encoding}),reset};
|
|
288
304
|
}
|
|
289
305
|
async function snapshotRemoteLogOffsets(root){
|
|
290
306
|
const offsets=new Map();
|
|
@@ -314,6 +330,7 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
|
|
|
314
330
|
? '[LOG] Verbose live timeline. Full sanitized request detail enabled; Ctrl+C disconnects and revokes authority.\n'
|
|
315
331
|
: '[LOG] Human live timeline. Transport IN/OUT hidden; full replay/audit detail remains on disk. Ctrl+C disconnects and revokes authority.\n');
|
|
316
332
|
const activeOffsets=offsets??await snapshotRemoteLogOffsets(root);
|
|
333
|
+
const partialLines=new Map();
|
|
317
334
|
let liveFiles=[...activeOffsets.keys()];
|
|
318
335
|
let nextDiscoveryAt=0;
|
|
319
336
|
while(!signal.aborted){
|
|
@@ -326,7 +343,13 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
|
|
|
326
343
|
const prior=activeOffsets.has(file)?activeOffsets.get(file):0;
|
|
327
344
|
const next=await readAppend(file,prior);
|
|
328
345
|
activeOffsets.set(file,next.offset);
|
|
329
|
-
|
|
346
|
+
if(next.reset)partialLines.delete(file);
|
|
347
|
+
if(!next.text)continue;
|
|
348
|
+
const lines=((partialLines.get(file)??'')+next.text).split(/\r?\n/);
|
|
349
|
+
partialLines.set(file,lines.pop()??'');
|
|
350
|
+
if(Buffer.byteLength(partialLines.get(file),'utf8')>1024*1024) throw new Error(`remote log line exceeds bounded decode limit: ${path.basename(file)}`);
|
|
351
|
+
for(const line of lines){
|
|
352
|
+
if(Buffer.byteLength(line,'utf8')>1024*1024) throw new Error(`remote log line exceeds bounded decode limit: ${path.basename(file)}`);
|
|
330
353
|
const auditEvent=parseAuditLogLine(file,line);
|
|
331
354
|
const timeline=auditTimelineRecord(file,line,auditEvent);
|
|
332
355
|
let recorded=null;
|
|
@@ -363,6 +386,15 @@ async function cleanupRemoteSession(ctx,{displayMode='unicode'}={}){
|
|
|
363
386
|
catch(error){failed.push(command);writeRemote(process.stderr,`${remoteTag('warn',displayMode)} cleanup ${command}: ${String(error?.message||error)}\n`,displayMode);}
|
|
364
387
|
}
|
|
365
388
|
if(failed.length) throw new Error(`remote session cleanup failed; ownership marker retained: ${failed.join(',')}`);
|
|
389
|
+
let snapshot;
|
|
390
|
+
try{snapshot=assertAuthorityVector(observedAuthority(ctx),Object.fromEntries(AUTHORITY_PLANES.map(plane=>[plane,false])));}
|
|
391
|
+
catch(error){throw new Error(`remote session cleanup verification failed; ownership marker retained: ${String(error?.message||error)}`);}
|
|
392
|
+
if(snapshot.state==='ONLINE'){
|
|
393
|
+
try{invokeController(ctx,'machinetest',{capture:true,echoCaptured:false,timeoutMs:REMOTE_START_TIMEOUT_MS,probeMode:'revoked'});}
|
|
394
|
+
catch(error){throw new Error(`remote session enforcement cleanup verification failed; ownership marker retained: ${String(error?.message||error)}`);}
|
|
395
|
+
}
|
|
396
|
+
const parked=invokeController(ctx,'park',{allowFailure:true,capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
|
|
397
|
+
if(parked.status!==0) throw new Error(`remote session park failed exit=${parked.status}; ownership marker retained`);
|
|
366
398
|
}
|
|
367
399
|
const GUARDIAN_SCRIPT=fileURLToPath(new URL('./remote-session-guardian.mjs',import.meta.url));
|
|
368
400
|
function remoteSessionMarker(root){return path.join(root,'runtime','remote-session.json');}
|
|
@@ -454,7 +486,7 @@ async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
|
|
|
454
486
|
const same=current?.schema==='deadbyte.remote-session.v2'&¤t.owner_pid===marker.owner_pid&¤t.session_nonce===marker.session_nonce;
|
|
455
487
|
if(same) await rm(remoteSessionMarker(ctx.root),{force:true});
|
|
456
488
|
}
|
|
457
|
-
async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression=''}={}){
|
|
489
|
+
async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default'}={}){
|
|
458
490
|
const display=prepareRemoteConsole();
|
|
459
491
|
const displayMode=display.mode;
|
|
460
492
|
const ctx=await remoteContext();
|
|
@@ -524,21 +556,31 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
524
556
|
}
|
|
525
557
|
process.stdout.write(`${formatRemoteLifecycle('bridge_connected',{displayMode})}\n`);
|
|
526
558
|
process.stdout.write(`${formatRemoteLifecycle('cloud_connected',{displayMode})}\n`);
|
|
527
|
-
|
|
559
|
+
const desired=authorityVector(mode);
|
|
560
|
+
const priorAuthority=observedAuthority(ctx);
|
|
561
|
+
for(const plane of AUTHORITY_PLANES.filter(name=>!desired[name]&&priorAuthority.authority[name].armed)){
|
|
562
|
+
const command=`disarm${plane}`;
|
|
563
|
+
writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Revoking unwanted ${plane} authority...\n`,displayMode);
|
|
564
|
+
invokeController(ctx,command,{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
|
|
565
|
+
}
|
|
566
|
+
for(const plane of AUTHORITY_PLANES.filter(name=>desired[name])){
|
|
567
|
+
const command=`arm${plane}`;
|
|
528
568
|
writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Arming ${command.slice(3)}...\n`,displayMode);
|
|
529
569
|
invokeController(ctx,command,{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
|
|
530
570
|
}
|
|
531
571
|
const url=startText.match(/^URL\s*:\s*(\S+)/m)?.[1]||'';
|
|
572
|
+
writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Verifying exact ${mode} authority vector...\n`,displayMode);
|
|
573
|
+
const current=await readActiveRuntimeBinding(ctx.root);
|
|
574
|
+
const authority=assertAuthorityVector(assertRuntimeReadyObservation(observedAuthority(ctx),{
|
|
575
|
+
coreRoot:current?.release_root??null
|
|
576
|
+
}),desired);
|
|
577
|
+
process.stdout.write('[BOOT] Verifying public signed MCP execution path...\n');
|
|
578
|
+
invokeController(ctx,'machinetest',{capture:true,echoCaptured:false,timeoutMs:REMOTE_START_TIMEOUT_MS,probeMode:mode});
|
|
579
|
+
ready=true;
|
|
580
|
+
writeRemote(process.stdout,`${remoteTag('auth',displayMode)} ${authorityBanner(authority)}\n`,displayMode);
|
|
532
581
|
process.stdout.write('[OK] ONLINE\n');
|
|
533
582
|
process.stdout.write(`${formatRemoteLifecycle('ready',{displayMode})}\n`);
|
|
534
583
|
if(url) writeRemote(process.stdout,`URL: ${url}\n`,displayMode);
|
|
535
|
-
writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Verifying process, coding, autonomous; desktop + host must stay DISARMED...\n`,displayMode);
|
|
536
|
-
for(const [command,pattern] of SESSION_STATUS){
|
|
537
|
-
const result=invokeController(ctx,command,{capture:true,echoCaptured:false});
|
|
538
|
-
if(!pattern.test(result.stdout)) throw new Error(`remote session authority verification failed: ${command} state mismatch`);
|
|
539
|
-
}
|
|
540
|
-
ready=true;
|
|
541
|
-
writeRemote(process.stdout,`${remoteTag('auth',displayMode)} ARMED: process, coding, autonomous | desktop, host DISARMED\n`,displayMode);
|
|
542
584
|
await logTask;
|
|
543
585
|
}finally{
|
|
544
586
|
const wasAborted=aborter.signal.aborted;
|
|
@@ -556,12 +598,16 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
556
598
|
}
|
|
557
599
|
}
|
|
558
600
|
async function runRemote(args){
|
|
559
|
-
let verbose=false,trace=false,filterExpression='',resumeCursor=null;
|
|
601
|
+
let verbose=false,trace=false,filterExpression='',resumeCursor=null,mode='default';
|
|
560
602
|
const positional=[];
|
|
561
603
|
for(let index=0;index<args.length;index++){
|
|
562
604
|
const arg=args[index];
|
|
563
605
|
if(arg==='--verbose'||arg==='-v'){verbose=true;continue;}
|
|
564
606
|
if(arg==='--trace'){trace=true;continue;}
|
|
607
|
+
if(['--desktop','--host','--full'].includes(arg)){
|
|
608
|
+
if(mode!=='default') throw new Error('remote authority modes are mutually exclusive');
|
|
609
|
+
mode=arg.slice(2);continue;
|
|
610
|
+
}
|
|
565
611
|
if(arg==='--filter'){
|
|
566
612
|
if(filterExpression) throw new Error('remote --filter may be specified once');
|
|
567
613
|
if(index+1>=args.length) throw new Error('remote --filter requires an expression');
|
|
@@ -574,8 +620,8 @@ async function runRemote(args){
|
|
|
574
620
|
}
|
|
575
621
|
positional.push(arg);
|
|
576
622
|
}
|
|
577
|
-
const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null;
|
|
578
|
-
if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression});return;}
|
|
623
|
+
const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null||mode!=='default';
|
|
624
|
+
if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression,mode});return;}
|
|
579
625
|
if(presentation) throw new Error('remote presentation flags are valid only for foreground session');
|
|
580
626
|
if(positional.length>1) throw new Error('remote accepts one controller command');
|
|
581
627
|
const command=positional[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const AUTHORITY_PLANES=Object.freeze(['process','coding','autonomous','desktop','host']);
|
|
4
|
+
|
|
5
|
+
export function authorityVector(mode='default'){
|
|
6
|
+
if(!['default','desktop','host','full'].includes(mode)) throw new Error('remote authority mode invalid');
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
process:true,coding:true,autonomous:true,
|
|
9
|
+
desktop:mode==='desktop'||mode==='full',
|
|
10
|
+
host:mode==='host'||mode==='full'
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseAuthorityObservation(raw){
|
|
15
|
+
let value;
|
|
16
|
+
try{value=typeof raw==='string'?JSON.parse(raw.replace(/^\uFEFF/,'')):raw;}
|
|
17
|
+
catch{throw new Error('controller observation JSON invalid');}
|
|
18
|
+
if(value?.schema!=='deadbyte.controller-observation.v1'||!value.authority||typeof value.authority!=='object'||Array.isArray(value.authority))
|
|
19
|
+
throw new Error('controller observation schema invalid');
|
|
20
|
+
const authority={};
|
|
21
|
+
for(const plane of AUTHORITY_PLANES){
|
|
22
|
+
const entry=value.authority[plane];
|
|
23
|
+
if(!entry||typeof entry!=='object'||typeof entry.armed!=='boolean'||typeof entry.configured!=='boolean'||typeof entry.reason!=='string')
|
|
24
|
+
throw new Error(`controller authority ${plane} observation invalid`);
|
|
25
|
+
authority[plane]=Object.freeze({configured:entry.configured,armed:entry.armed,reason:entry.reason.slice(0,128)});
|
|
26
|
+
}
|
|
27
|
+
return Object.freeze({...value,authority:Object.freeze(authority)});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function assertAuthorityVector(snapshot,expected){
|
|
31
|
+
for(const plane of AUTHORITY_PLANES){
|
|
32
|
+
const entry=snapshot?.authority?.[plane];
|
|
33
|
+
if(!entry||typeof expected?.[plane]!=='boolean') throw new Error(`authority ${plane} observation or expectation invalid`);
|
|
34
|
+
if(entry.armed!==expected[plane]) throw new Error(`${plane} authority expected ${expected[plane]?'ARMED':'DISARMED'}; observed ${entry.armed?'ARMED':'DISARMED'} (${entry.reason})`);
|
|
35
|
+
if(expected[plane]&&!entry.configured) throw new Error(`${plane} authority unconfigured`);
|
|
36
|
+
}
|
|
37
|
+
return snapshot;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function authorityBanner(snapshot){
|
|
41
|
+
return AUTHORITY_PLANES.map(plane=>`${plane} ${snapshot.authority[plane].armed?'ARMED':'DISARMED'}`).join(' | ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function assertRuntimeReadyObservation(snapshot,{coreRoot=null}={}){
|
|
45
|
+
if(snapshot?.state!=='ONLINE') throw new Error(`runtime state not ONLINE: ${String(snapshot?.state)}`);
|
|
46
|
+
for(const name of ['bridge','core','cloud']){
|
|
47
|
+
if(snapshot[name]?.alive!==true||snapshot[name]?.reason!=='verified') throw new Error(`${name} runtime identity unverified`);
|
|
48
|
+
}
|
|
49
|
+
if(snapshot.local_ready!==true) throw new Error('local MCP readiness unverified');
|
|
50
|
+
if(snapshot.supervisor?.running!==true) throw new Error('autonomous supervisor not running');
|
|
51
|
+
if(coreRoot!==null){
|
|
52
|
+
if(typeof snapshot.core_root!=='string'||path.win32.resolve(snapshot.core_root).toLowerCase()!==path.win32.resolve(coreRoot).toLowerCase())
|
|
53
|
+
throw new Error('core root does not match active release');
|
|
54
|
+
}
|
|
55
|
+
return snapshot;
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { authorityVector } from './remote-authority.mjs';
|
|
2
|
+
|
|
3
|
+
export async function probeRemoteEnforcement({client,mode='default'}={}){
|
|
4
|
+
if(!client||typeof client.callTool!=='function')throw new Error('remote enforcement client invalid');
|
|
5
|
+
const expected=mode==='revoked'?{desktop:false,host:false}:authorityVector(mode);
|
|
6
|
+
const desktop=await client.callTool({name:'desktop_control',arguments:{op:'status'}});
|
|
7
|
+
const desktopValue=desktop?.structuredContent;
|
|
8
|
+
const desktopArmed=desktop?.isError===false&&desktopValue?.status==='ok'&&desktopValue?.result?.armed===true;
|
|
9
|
+
const desktopUnavailable=desktop?.isError===true&&desktopValue?.status==='unavailable';
|
|
10
|
+
if(expected.desktop?!desktopArmed:desktopArmed||(!desktopUnavailable&&desktopValue?.result?.armed!==false))
|
|
11
|
+
throw new Error('Desktop enforcement does not match requested authority');
|
|
12
|
+
const host=await client.callTool({name:'host_file_read',arguments:{root_id:'core',path:'package.json',encoding:'utf8'}});
|
|
13
|
+
const hostArmed=host?.isError===false&&host?.structuredContent?.status==='ok'&&/^[0-9a-f]{64}$/.test(String(host.structuredContent.sha256??''));
|
|
14
|
+
const hostDenied=host?.isError===true&&host?.structuredContent?.status==='denied';
|
|
15
|
+
if(expected.host?!hostArmed:!hostDenied) throw new Error('Host enforcement does not match requested authority');
|
|
16
|
+
return Object.freeze({desktop_armed:desktopArmed,host_armed:hostArmed});
|
|
17
|
+
}
|
package/src/remote-live-log.mjs
CHANGED
|
@@ -319,19 +319,21 @@ export function createLiveTimelineWriter({stream=process.stdout,displayMode='uni
|
|
|
319
319
|
return Object.freeze({write,flush,pending(){return active?{...active}:null;}});
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000}={}){
|
|
322
|
+
export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,foldHeartbeats=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000}={}){
|
|
323
323
|
if(typeof clock!=='function') throw new Error('live timeline clock must be a function');
|
|
324
324
|
if(!REMOTE_DISPLAY_MODES.includes(displayMode)) throw new Error('remote display mode invalid');
|
|
325
325
|
if(!Number.isInteger(heartbeatBurstLimit)||heartbeatBurstLimit<2||heartbeatBurstLimit>100) throw new Error('heartbeatBurstLimit invalid');
|
|
326
326
|
if(!Number.isInteger(heartbeatWindowMs)||heartbeatWindowMs<250||heartbeatWindowMs>60000) throw new Error('heartbeatWindowMs invalid');
|
|
327
327
|
if(!rootPaths||typeof rootPaths!=='object'||Array.isArray(rootPaths)) throw new Error('live timeline rootPaths invalid');
|
|
328
328
|
if(typeof trace!=='boolean') throw new Error('live timeline trace invalid');
|
|
329
|
+
if(typeof foldHeartbeats!=='boolean') throw new Error('live timeline foldHeartbeats invalid');
|
|
329
330
|
if(!Number.isInteger(slowCallMs)||slowCallMs<100||slowCallMs>600000) throw new Error('live timeline slowCallMs invalid');
|
|
330
331
|
const pending=new Map();
|
|
331
332
|
const completed=new Set();
|
|
332
333
|
const toolCounts=new Map();
|
|
333
334
|
const durationSamples=[];
|
|
334
335
|
let resultCount=0,okCount=0,errorCount=0,unknownCount=0,mutationCount=0,totalDurationMs=0,slowest=null,burst=null,foldedHeartbeatLines=0,peakInFlight=0;
|
|
336
|
+
let lastCapability=null,suppressedCapability=0,suppressedCapabilityReported=false;
|
|
335
337
|
|
|
336
338
|
function eventPrefix(event,sequence,{displayTool=null,icon=null}={}){
|
|
337
339
|
const seq=Number.isSafeInteger(sequence)&&sequence>0?'#'+sequence+' ':'';
|
|
@@ -419,7 +421,18 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,he
|
|
|
419
421
|
addStats(event,span);
|
|
420
422
|
const error=event.outcome?.is_error===true;
|
|
421
423
|
const nowMs=eventMs(event,clock);
|
|
422
|
-
if(
|
|
424
|
+
if(tool==='capability_manifest'&&!error){
|
|
425
|
+
const snapshot=event.outcome?.snapshot_id,epoch=event.outcome?.state_epoch;
|
|
426
|
+
if(/^[0-9a-f]{64}$/.test(String(snapshot??''))&&Number.isInteger(epoch)&&epoch>0){
|
|
427
|
+
const identity=`${snapshot}:${epoch}`;
|
|
428
|
+
if(identity===lastCapability){suppressedCapability++;return null;}
|
|
429
|
+
lastCapability=identity;
|
|
430
|
+
const prior=flushBurst();
|
|
431
|
+
const line=resultLine(event,span,sequence)+` snapshot=${snapshot.slice(0,8)} epoch=${epoch}`;
|
|
432
|
+
return prior?[prior,line]:line;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if(foldHeartbeats&&HEARTBEAT_TOOLS.has(tool)&&!error){
|
|
423
436
|
const duration=Number(event.outcome?.duration_ms);
|
|
424
437
|
if(burst&&burst.tool===tool&&nowMs-burst.last_ms<=heartbeatWindowMs){
|
|
425
438
|
burst.count++;
|
|
@@ -449,6 +462,10 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,he
|
|
|
449
462
|
const lines=[];
|
|
450
463
|
const folded=flushBurst();
|
|
451
464
|
if(folded) lines.push(folded);
|
|
465
|
+
if(suppressedCapability>0&&!suppressedCapabilityReported){
|
|
466
|
+
lines.push(remoteDisplayText(`capability_manifest ×${suppressedCapability} unchanged suppressed`,displayMode));
|
|
467
|
+
suppressedCapabilityReported=true;
|
|
468
|
+
}
|
|
452
469
|
for(const item of pending.values()) lines.push(remoteDisplayText((displayMode==='ascii'?'[WARN]':'⚠')+' ['+shortSpanId(item.request_id)+'] OUTCOME_UNKNOWN '+item.tool+' — '+suffix+'; inspect state before retry',displayMode));
|
|
453
470
|
pending.clear();
|
|
454
471
|
return lines;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function safeRemoteLogPrefixLength(bytes,encoding='utf8'){
|
|
4
|
+
if(!Buffer.isBuffer(bytes))throw new Error('remote log prefix bytes invalid');
|
|
5
|
+
if(encoding==='utf16le'){
|
|
6
|
+
let size=bytes.length-(bytes.length%2);
|
|
7
|
+
if(size>=2){const last=bytes.readUInt16LE(size-2);if(last>=0xd800&&last<=0xdbff)size-=2;}
|
|
8
|
+
return size;
|
|
9
|
+
}
|
|
10
|
+
if(encoding!=='utf8')throw new Error('remote log prefix encoding invalid');
|
|
11
|
+
let index=bytes.length-1;
|
|
12
|
+
while(index>=0&&(bytes[index]&0xc0)===0x80)index--;
|
|
13
|
+
if(index<0)return 0;
|
|
14
|
+
const lead=bytes[index];
|
|
15
|
+
const needed=lead<0x80?1:lead>=0xc2&&lead<=0xdf?2:lead>=0xe0&&lead<=0xef?3:lead>=0xf0&&lead<=0xf4?4:1;
|
|
16
|
+
return bytes.length-index<needed?index:bytes.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function decodeRemoteLogBytes(file,bytes,{encoding=null}={}){
|
|
20
|
+
if(typeof file!=='string'||!Buffer.isBuffer(bytes))throw new Error('remote log decoder input invalid');
|
|
21
|
+
const knownLog=/\.log$/i.test(path.basename(file));
|
|
22
|
+
if(!knownLog)return bytes.toString('utf8');
|
|
23
|
+
const utf16=encoding==='utf16le'||(bytes.length>=2&&bytes[0]===0xff&&bytes[1]===0xfe);
|
|
24
|
+
if(!utf16)return bytes.toString('utf8').replace(/^\uFEFF/,'');
|
|
25
|
+
return bytes.toString('utf16le').replace(/^\uFEFF/,'');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function boundedRemoteDiagnostic(value,maxChars=2048){
|
|
29
|
+
if(!Number.isInteger(maxChars)||maxChars<32||maxChars>8192)throw new Error('remote diagnostic bound invalid');
|
|
30
|
+
const line=String(value??'').replace(/\x1b\[[0-9;]*[A-Za-z]/g,'').replace(/[\x00-\x1f\x7f]+/g,' ').replace(/\s{2,}/g,' ').trim();
|
|
31
|
+
return line.length<=maxChars?line:`${line.slice(0,maxChars)}...`;
|
|
32
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
|
3
5
|
import { verifyMachinePingPublic } from './machine-identity.mjs';
|
|
6
|
+
import { parseActivePointer } from './release-slot.mjs';
|
|
7
|
+
import { probeRemoteEnforcement } from './remote-enforcement-probe-r60.mjs';
|
|
4
8
|
|
|
5
9
|
const endpointText = process.argv[2];
|
|
6
10
|
if (!endpointText) throw new Error('usage: node src/remote-mcp-machine-smoke-client.mjs https://host/mcp/path');
|
|
7
11
|
const endpoint = new URL(endpointText);
|
|
8
12
|
if (endpoint.protocol !== 'https:') throw new Error('machine live gate requires HTTPS');
|
|
13
|
+
const enforcementMode=process.argv[3]??null;
|
|
14
|
+
if(enforcementMode!==null&&!['default','desktop','host','full','revoked'].includes(enforcementMode))throw new Error('remote enforcement probe mode invalid');
|
|
9
15
|
|
|
10
16
|
const machineTools = ['machine_list', 'machine_ping', 'machine_revoke', 'machine_status'];
|
|
11
17
|
const client = new Client(
|
|
@@ -49,6 +55,17 @@ try {
|
|
|
49
55
|
}
|
|
50
56
|
const signatureVerified = await verifyMachinePingPublic({ ping, publicKeySpkiBase64: status.public_key_spki_base64 });
|
|
51
57
|
if (!signatureVerified) throw new Error('machine ping public Ed25519 verification failed');
|
|
58
|
+
if(enforcementMode!==null){
|
|
59
|
+
const stateRoot=process.env.DEADBYTE_STATE_ROOT;
|
|
60
|
+
if(!stateRoot||!path.isAbsolute(stateRoot))throw new Error('remote enforcement probe state root missing');
|
|
61
|
+
const pointer=parseActivePointer(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
|
|
62
|
+
for(const [actual,expected] of [
|
|
63
|
+
[status.release_id,pointer.release_id],
|
|
64
|
+
[status.release_manifest_sha256,pointer.manifest_sha256],
|
|
65
|
+
[status.generation_manifest_sha256,pointer.generation_manifest_sha256]
|
|
66
|
+
])if(actual!==expected)throw new Error('signed machine status does not bind active release/generation');
|
|
67
|
+
await probeRemoteEnforcement({client,mode:enforcementMode});
|
|
68
|
+
}
|
|
52
69
|
|
|
53
70
|
console.log(JSON.stringify({
|
|
54
71
|
status: 'passed',
|
|
@@ -59,7 +76,8 @@ try {
|
|
|
59
76
|
release_id: status.release_id,
|
|
60
77
|
release_manifest_sha256: status.release_manifest_sha256,
|
|
61
78
|
generation_manifest_sha256: status.generation_manifest_sha256,
|
|
62
|
-
signature_verified: true
|
|
79
|
+
signature_verified: true,
|
|
80
|
+
...(enforcementMode?{enforcement_mode:enforcementMode,enforcement_verified:true}:{})
|
|
63
81
|
}));
|
|
64
82
|
} finally {
|
|
65
83
|
await client.close().catch(() => {});
|
|
@@ -6,8 +6,9 @@ import path from 'node:path';
|
|
|
6
6
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
7
|
import { windowsPowerShellEnvironment } from './runtime-update.mjs';
|
|
8
8
|
import { resolveStateRoot } from './state-root.mjs';
|
|
9
|
+
import { AUTHORITY_PLANES, parseAuthorityObservation, assertAuthorityVector } from './remote-authority.mjs';
|
|
9
10
|
|
|
10
|
-
const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'
|
|
11
|
+
const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'];
|
|
11
12
|
// This inherited IPC channel is an OS-owned parent lifetime handle, not a PID
|
|
12
13
|
// poll. A reused PID cannot keep a new foreground guardian alive.
|
|
13
14
|
let ownerDisconnected=false;
|
|
@@ -42,12 +43,23 @@ function markerMatches(marker,{ownerPid,nonce}){
|
|
|
42
43
|
marker?.owner_pid===ownerPid&&
|
|
43
44
|
marker?.session_nonce===nonce;
|
|
44
45
|
}
|
|
45
|
-
function invokeCleanup(powershell,launcher,command,stateRoot){
|
|
46
|
+
function invokeCleanup(powershell,launcher,command,stateRoot,probeMode=null){
|
|
46
47
|
const result=spawnSync(powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',launcher,command],{
|
|
47
|
-
stdio:'ignore',windowsHide:true,shell:false,env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:stateRoot
|
|
48
|
+
stdio:'ignore',windowsHide:true,shell:false,env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:stateRoot,
|
|
49
|
+
...(probeMode?{DEADBYTE_REMOTE_PROBE_MODE:probeMode}:{})}),timeout:probeMode?150000:30000
|
|
48
50
|
});
|
|
49
51
|
return !result.error&&result.status===0;
|
|
50
52
|
}
|
|
53
|
+
function verifyRevoked(powershell,launcher,stateRoot){
|
|
54
|
+
const result=spawnSync(powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',launcher,'status','-Json'],{
|
|
55
|
+
stdio:['ignore','pipe','pipe'],encoding:'utf8',windowsHide:true,shell:false,
|
|
56
|
+
env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:stateRoot}),timeout:30000
|
|
57
|
+
});
|
|
58
|
+
if(result.error||result.status!==0) return null;
|
|
59
|
+
try{
|
|
60
|
+
return assertAuthorityVector(parseAuthorityObservation(result.stdout),Object.fromEntries(AUTHORITY_PLANES.map(plane=>[plane,false])));
|
|
61
|
+
}catch{return null;}
|
|
62
|
+
}
|
|
51
63
|
|
|
52
64
|
async function main(){
|
|
53
65
|
if(process.platform!=='win32') return;
|
|
@@ -82,6 +94,11 @@ async function main(){
|
|
|
82
94
|
if(!invokeCleanup(powershell,launcher,command,root)) failed.push(command);
|
|
83
95
|
}
|
|
84
96
|
if(failed.length) throw new Error('guardian cleanup failed; ownership marker retained: '+failed.join(','));
|
|
97
|
+
const observed=verifyRevoked(powershell,launcher,root);
|
|
98
|
+
if(!observed) throw new Error('guardian authority cleanup unverified; ownership marker retained');
|
|
99
|
+
if(observed.state==='ONLINE'&&!invokeCleanup(powershell,launcher,'machinetest',root,'revoked'))
|
|
100
|
+
throw new Error('guardian runtime enforcement cleanup unverified; ownership marker retained');
|
|
101
|
+
if(!invokeCleanup(powershell,launcher,'park',root)) throw new Error('guardian park failed; ownership marker retained');
|
|
85
102
|
const current=await readMarker(markerPath);
|
|
86
103
|
if(current&&markerMatches(current,{ownerPid,nonce})) await rm(markerPath,{force:true});
|
|
87
104
|
return;
|
package/src/tool-audit.mjs
CHANGED
|
@@ -116,7 +116,12 @@ function summarizeOutcome(result,error,durationMs){
|
|
|
116
116
|
const structured=result?.structuredContent;
|
|
117
117
|
const status=typeof structured?.status==='string'?structured.status:(result?.isError===true?'error':'ok');
|
|
118
118
|
const isError=result?.isError===true;
|
|
119
|
+
const capability=structured?.result;
|
|
120
|
+
const snapshot=capability?.schema==='deadbyte.capability-manifest.v3'&&HEX64.test(String(capability.snapshot_id??''))&&
|
|
121
|
+
Number.isInteger(capability.state_epoch)&&capability.state_epoch>0
|
|
122
|
+
?{snapshot_id:capability.snapshot_id,state_epoch:capability.state_epoch}:{};
|
|
119
123
|
return {status:String(status).slice(0,128),is_error:isError,duration_ms:duration,receipt_sha256s:outcomeReceiptHashes(result),
|
|
124
|
+
...snapshot,
|
|
120
125
|
...(isError?{reason:resultOutcomeReason(result)}:{})};
|
|
121
126
|
}
|
|
122
127
|
|
package/src/version.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { DEADBYTE_VERSION } from '../src/version.mjs';
|
|
|
5
5
|
|
|
6
6
|
const root = new URL('../', import.meta.url);
|
|
7
7
|
const read = rel => readFile(new URL(rel, root), 'utf8');
|
|
8
|
-
const json = rel => read(rel).then(JSON.parse);
|
|
8
|
+
const json = rel => read(rel).then(raw => JSON.parse(raw.replace(/^\uFEFF/,'')));
|
|
9
9
|
|
|
10
10
|
test('R48 compatibility release contract remains coherent in the current successor', async () => {
|
|
11
11
|
const [pkg, lock, release, provenance, matrix] = await Promise.all([
|
|
@@ -53,7 +53,7 @@ test('R51 bare remote wires coherence before live generation binding and deploy
|
|
|
53
53
|
test('R51 foreground authority contract explicitly verifies and cleans desktop plus host',async()=>{
|
|
54
54
|
const {readFile}=await import('node:fs/promises');
|
|
55
55
|
const cli=await readFile(new URL('../src/deadbyte-cli.mjs',import.meta.url),'utf8');
|
|
56
|
-
assert.match(cli
|
|
56
|
+
assert.match(cli,/assertAuthorityVector\(assertRuntimeReadyObservation\(observedAuthority/);
|
|
57
57
|
assert.match(cli,/SESSION_CLEANUP=.*disarmdesktop.*disarmhost/s);
|
|
58
|
-
assert.match(cli,/
|
|
58
|
+
assert.match(cli,/authorityBanner\(authority\)/);
|
|
59
59
|
});
|
|
@@ -11,7 +11,12 @@ test('R58 foreground cleanup failure retains ownership evidence for recovery', {
|
|
|
11
11
|
await mkdir(base,{recursive:true});
|
|
12
12
|
const root=await mkdtemp(path.join(base,'cleanup-'));
|
|
13
13
|
await mkdir(path.join(root,'bin'));
|
|
14
|
-
await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command)
|
|
14
|
+
await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command,[switch]$Json)
|
|
15
|
+
if ($Command -eq 'status' -and $Json) {
|
|
16
|
+
$on=@{configured=$true;armed=$true;reason='standing-grant'}; $off=@{configured=$true;armed=$false;reason='disarmed'}
|
|
17
|
+
@{schema='deadbyte.controller-observation.v1';state='ONLINE';core_root='fixture';local_ready=$true;bridge=@{alive=$true;reason='verified'};core=@{alive=$true;reason='verified'};cloud=@{alive=$true;reason='verified'};supervisor=@{running=$true};authority=@{process=$on;coding=$on;autonomous=$on;desktop=$off;host=$off}} | ConvertTo-Json -Depth 5 -Compress
|
|
18
|
+
exit 0
|
|
19
|
+
}
|
|
15
20
|
switch ($Command) {
|
|
16
21
|
'status' { Write-Output 'DEADBYTE MCP = ONLINE'; Write-Output 'Bridge PID : 1001 [verified]'; Write-Output 'Cloud PID : 1002 [verified]'; Write-Output 'Local ready : True'; Write-Output 'Loop supervisor: RUNNING [verified]' }
|
|
17
22
|
'processstatus' { Write-Output 'Process plane : ARMED [standing-grant]' }
|
|
@@ -30,8 +35,9 @@ exit 0
|
|
|
30
35
|
const closed=new Promise(resolve=>child.once('close',code=>resolve(code)));
|
|
31
36
|
try {
|
|
32
37
|
const deadline=Date.now()+12000;
|
|
33
|
-
while(!stdout.includes('
|
|
34
|
-
assert.match(stdout
|
|
38
|
+
while(!stdout.includes('[READY]')&&child.exitCode===null&&Date.now()<deadline) await delay(50);
|
|
39
|
+
assert.match(stdout,/\[READY\]/,stderr);
|
|
40
|
+
assert.match(stdout,/\[AUTH\] process ARMED \| coding ARMED \| autonomous ARMED \| desktop DISARMED \| host DISARMED/,stderr);
|
|
35
41
|
child.stdin.end();
|
|
36
42
|
assert.notEqual(await closed,0,'cleanup failure must not report a successful foreground exit');
|
|
37
43
|
assert.match(stderr,/cleanup disarmprocess exit=9/);
|
|
@@ -11,9 +11,15 @@ test('R58 simultaneous stale recovery cannot both revoke the shared authority',
|
|
|
11
11
|
const root=await mkdtemp(path.join(base,'concurrent-recovery-'));await mkdir(path.join(root,'bin'));await mkdir(path.join(root,'runtime'));
|
|
12
12
|
const trace=path.join(root,'trace.txt'),markerPath=path.join(root,'runtime','remote-session.json');
|
|
13
13
|
await writeFile(markerPath,JSON.stringify({schema:'deadbyte.remote-session.v1',pid:2147483000,started_at_utc:'2000-01-01T00:00:00.000Z'}));
|
|
14
|
-
await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command)
|
|
14
|
+
await writeFile(path.join(root,'bin','DEADBYTE-MCP.ps1'),`param([string]$Command,[switch]$Json)
|
|
15
15
|
$ErrorActionPreference='Stop'
|
|
16
16
|
for($i=0;$i -lt 40;$i++){try{[IO.File]::AppendAllText('${trace.replaceAll("'","''")}',$Command+[Environment]::NewLine);break}catch{if($i -eq 39){throw};Start-Sleep -Milliseconds 10}}
|
|
17
|
+
if ($Command -match '^(arm|disarm)(process|coding|autonomous|desktop|host)$') { $file=Join-Path $env:DEADBYTE_STATE_ROOT ('runtime/' + $Matches[2] + '.armed'); if ($Matches[1] -eq 'arm') { [IO.File]::WriteAllText($file,'1') } else { Remove-Item -LiteralPath $file -Force -ErrorAction SilentlyContinue } }
|
|
18
|
+
if ($Command -eq 'status' -and $Json) {
|
|
19
|
+
$authority=@{};foreach($name in @('process','coding','autonomous','desktop','host')) { $armed=Test-Path -LiteralPath (Join-Path $env:DEADBYTE_STATE_ROOT ('runtime/' + $name + '.armed'));$authority[$name]=@{configured=$true;armed=[bool]$armed;reason=$(if($armed){'standing-grant'}else{'disarmed'})} }
|
|
20
|
+
@{schema='deadbyte.controller-observation.v1';state='ONLINE';core_root='fixture';local_ready=$true;bridge=@{alive=$true;reason='verified'};core=@{alive=$true;reason='verified'};cloud=@{alive=$true;reason='verified'};supervisor=@{running=$true};authority=$authority} | ConvertTo-Json -Depth 5 -Compress
|
|
21
|
+
exit 0
|
|
22
|
+
}
|
|
17
23
|
switch($Command){
|
|
18
24
|
'status' {Write-Output 'DEADBYTE MCP = ONLINE';Write-Output 'Bridge PID : 1001 [verified]';Write-Output 'Cloud PID : 1002 [verified]';Write-Output 'Local ready : True';Write-Output 'Loop supervisor: RUNNING [verified]'}
|
|
19
25
|
'processstatus' {Write-Output 'Process plane : ARMED [standing-grant]'}
|
|
@@ -42,9 +48,9 @@ exit 0
|
|
|
42
48
|
const o={stdout:'',stderr:''};c.stdout.on('data',b=>{o.stdout+=b;});c.stderr.on('data',b=>{o.stderr+=b;});o.exit=new Promise(resolve=>c.once('exit',resolve));children.push(c);outputs.push(o);
|
|
43
49
|
}
|
|
44
50
|
const until=Date.now()+12000;let commands=[];
|
|
45
|
-
while(Date.now()<until){try{commands=(await readFile(trace,'utf8')).trim().split(/\r?\n/);}catch{}if(
|
|
51
|
+
while(Date.now()<until){try{commands=(await readFile(trace,'utf8')).trim().split(/\r?\n/);}catch{}if(outputs.some(o=>o.stdout.includes('[READY]')))break;await delay(50);}
|
|
46
52
|
await writeFile(path.join(root,'observation.json'),JSON.stringify({commands,outputs:outputs.map(o=>({stdout:o.stdout,stderr:o.stderr})),simulation:'concurrent stale marker recovery with delayed second park'}),{flag:'wx'});
|
|
47
|
-
assert.ok(
|
|
53
|
+
assert.ok(outputs.some(o=>o.stdout.includes('[READY]')),'a recovered foreground must become ready');
|
|
48
54
|
for(const cmd of ['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'])assert.equal(commands.filter(x=>x===cmd).length,1,`stale recovery must serialize ${cmd}`);
|
|
49
55
|
}finally{
|
|
50
56
|
for(const c of children)if(c.exitCode===null)c.stdin.end();
|