deadbyte-mcp 0.15.34 → 0.15.35
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 +67 -22
- package/PROVENANCE.json +57 -30
- package/README.txt +18 -14
- package/RELEASE.json +40 -31
- package/SBOM.cdx.json +25 -3
- package/TEST-MATRIX.json +129 -48
- package/bridge/package.json +11 -0
- package/bridge/src/bridge.mjs +412 -0
- package/bridge/src/node-http-adapter.mjs +56 -0
- package/bridge/src/public-tool-definitions.mjs +14 -0
- package/bridge/src/r40-governor.mjs +160 -0
- package/controller/deadbyte-controller.ps1 +84 -8
- package/controller/deadbyte-process-policy.json +4 -4
- package/package.json +5 -3
- package/scripts/bridge-benchmark-r58.mjs +67 -0
- package/scripts/deploy-r48.mjs +2 -3
- package/scripts/gate-windows-r58.ps1 +33 -0
- package/scripts/owner-probe-r58.mjs +31 -0
- package/scripts/performance-r58.mjs +57 -0
- package/scripts/public-baseline-r58.mjs +72 -0
- package/scripts/public-surface-r58.mjs +84 -0
- package/scripts/r58-cross-plane.ps1 +6 -0
- package/scripts/r58-focused.ps1 +6 -0
- package/scripts/windows-gate-evidence-r58.mjs +93 -0
- package/src/deadbyte-cli.mjs +40 -12
- package/src/doctor-foreground-r58.mjs +44 -0
- package/src/doctor-installation-r58.mjs +63 -0
- package/src/doctor-r45.mjs +21 -4
- package/src/doctor-runtime-r58.mjs +82 -0
- package/src/doctor-transactions-r58.mjs +40 -0
- package/src/foreground-process-identity.mjs +27 -0
- package/src/foreground-session-lock.mjs +20 -0
- package/src/foreground-session-lock.ps1 +15 -0
- package/src/release-generation.mjs +1 -0
- package/src/release-slot.mjs +2 -1
- package/src/remote-session-guardian.mjs +8 -1
- package/src/runtime-update.mjs +85 -2
- package/src/version.mjs +1 -1
- package/test/controller-r211.test.mjs +4 -1
- package/test/controller.test.mjs +2 -1
- package/test/helpers/r58-doctor-fixture.mjs +26 -0
- package/test/r49-windows-gate.test.mjs +2 -2
- package/test/r57-release-contract.test.mjs +5 -3
- package/test/r58-bridge-dependency-binding.test.mjs +62 -0
- package/test/r58-bundled-bridge.test.mjs +22 -0
- package/test/r58-cleanup-retention.test.mjs +45 -0
- package/test/r58-completed-update-receipt.test.mjs +76 -0
- package/test/r58-concurrent-recovery.test.mjs +54 -0
- package/test/r58-controller-core-observation.test.mjs +36 -0
- package/test/r58-controller-json.test.mjs +48 -0
- package/test/r58-controller-live-local.test.mjs +85 -0
- package/test/r58-controller-readonly.test.mjs +23 -0
- package/test/r58-controller-state-coherence.test.mjs +45 -0
- package/test/r58-doctor-cli.test.mjs +20 -0
- package/test/r58-doctor-foreground.test.mjs +50 -0
- package/test/r58-doctor-installation.test.mjs +55 -0
- package/test/r58-doctor-pointer.test.mjs +41 -0
- package/test/r58-doctor-runtime.test.mjs +45 -0
- package/test/r58-doctor-transactions.test.mjs +21 -0
- package/test/r58-guardian-channel.test.mjs +26 -0
- package/test/r58-launcher-shell.test.mjs +28 -0
- package/test/r58-owner-identity.test.mjs +20 -0
- package/test/r58-release-contract.test.mjs +49 -0
- package/test/r58-session-lock.test.mjs +26 -0
- package/test/r58-stale-owner.test.mjs +71 -0
- package/test/r58-windows-gate.test.mjs +31 -0
- package/test/release-generation.test.mjs +4 -1
- package/test/remote-session-cli.test.mjs +55 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {mkdir,open,readFile} from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {doctor} from '../src/doctor-r45.mjs';
|
|
7
|
+
|
|
8
|
+
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
9
|
+
const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
|
|
10
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
const insist=(ok,message)=>{if(!ok)throw new Error(message);};
|
|
12
|
+
const [pkg,release,provenance,matrix,active]=await Promise.all([
|
|
13
|
+
readFile(path.join(root,'package.json'),'utf8').then(JSON.parse),
|
|
14
|
+
readFile(path.join(root,'RELEASE.json'),'utf8').then(JSON.parse),
|
|
15
|
+
readFile(path.join(root,'PROVENANCE.json'),'utf8').then(JSON.parse),
|
|
16
|
+
readFile(path.join(root,'TEST-MATRIX.json'),'utf8').then(JSON.parse),
|
|
17
|
+
readFile(path.join(stateRoot,'active-release.json'),'utf8').then(JSON.parse)
|
|
18
|
+
]);
|
|
19
|
+
const manifestSha256=sha256(await readFile(path.join(root,'MANIFEST.SHA256')));
|
|
20
|
+
|
|
21
|
+
insist(pkg.version==='0.15.35'&&release.version===pkg.version&&release.train==='R58','R58 candidate identity mismatch');
|
|
22
|
+
insist(provenance.version===pkg.version&&matrix.version===pkg.version&&matrix.train==='R58','R58 release metadata identity mismatch');
|
|
23
|
+
insist(release.parent?.version==='0.15.34','R58 parent version mismatch');
|
|
24
|
+
insist(release.parent?.release_id==='v0.15.34-2199bb59f0e0fc20','R58 parent release mismatch');
|
|
25
|
+
insist(release.parent?.manifest_sha256==='2199bb59f0e0fc205d470d11f990d3ce3df444be5eba2a7d8dbc60d5f5b790bb','R58 parent manifest mismatch');
|
|
26
|
+
insist(release.parent?.generation_manifest_sha256==='1a20a62aaeb324a9b5c03d158952f35092d58fedf2ac7725820ba2b094a4a88f','R58 parent generation mismatch');
|
|
27
|
+
insist(release.parent?.tarball_sha256==='a6f7b21faf34793ca17dfb6d0e9ec03320f1468d011e75d8349098df82fc2639','R58 parent tarball mismatch');
|
|
28
|
+
insist(
|
|
29
|
+
active.release_id===release.runtime_predecessor.release_id&&
|
|
30
|
+
active.manifest_sha256===release.runtime_predecessor.manifest_sha256&&
|
|
31
|
+
active.generation_manifest_sha256===release.runtime_predecessor.generation_manifest_sha256,
|
|
32
|
+
'active runtime is not exact sealed 0.15.34 predecessor'
|
|
33
|
+
);
|
|
34
|
+
insist(release.consolidation?.bridge_bundled_inside_release===true,'R58 bundled bridge contract missing');
|
|
35
|
+
insist(release.consolidation?.bridge_recursive_local_dependency_binding===true,'R58 recursive bridge dependency contract missing');
|
|
36
|
+
insist(release.consolidation?.performance_acceptance==='PASS_BOUNDED_PREPUBLISH','R58 performance acceptance metadata not green');
|
|
37
|
+
insist(release.consolidation?.cross_plane_acceptance==='PASS','R58 cross-plane acceptance metadata not green');
|
|
38
|
+
insist(release.consolidation?.public_surface_compatibility==='PASS_43_TOOLS_NO_SCHEMA_DRIFT','R58 public surface contract not green');
|
|
39
|
+
insist(matrix.observed_pre_release_evidence?.full_regression?.status==='PASS','R58 fresh full regression evidence not green');
|
|
40
|
+
insist(matrix.observed_pre_release_evidence?.performance?.status==='PASS_BOUNDED_PREPUBLISH','R58 performance evidence not green');
|
|
41
|
+
insist(matrix.observed_pre_release_evidence?.cross_plane?.status==='PASS','R58 cross-plane evidence not green');
|
|
42
|
+
insist(matrix.observed_pre_release_evidence?.public_surface?.status==='PASS','R58 public surface evidence not green');
|
|
43
|
+
insist(provenance.operator_override?.four_hour_soak==='NOT_COMPLETED','R58 must preserve incomplete soak truth');
|
|
44
|
+
insist(provenance.operator_override?.clean_machine==='NOT_COMPLETED','R58 must preserve incomplete clean-machine truth');
|
|
45
|
+
insist(provenance.operator_override?.stable_final_claim===false,'R58 must not claim stable/final before external evidence');
|
|
46
|
+
|
|
47
|
+
const health=await doctor();
|
|
48
|
+
insist(health.assessment!=='FAIL'&&['ok','warn'].includes(health.status)&&health.package_version===pkg.version&&health.manifest?.verified===true,'R58 doctor evidence invalid');
|
|
49
|
+
|
|
50
|
+
const evidenceBody={
|
|
51
|
+
schema:'deadbyte.r58-windows-gate.v1',
|
|
52
|
+
status:'passed',
|
|
53
|
+
exit_code:0,
|
|
54
|
+
version:pkg.version,
|
|
55
|
+
train:'R58',
|
|
56
|
+
completed_at_utc:new Date().toISOString(),
|
|
57
|
+
manifest_sha256:manifestSha256,
|
|
58
|
+
parent:release.parent,
|
|
59
|
+
runtime_predecessor:release.runtime_predecessor,
|
|
60
|
+
consolidation:release.consolidation,
|
|
61
|
+
operator_override:provenance.operator_override,
|
|
62
|
+
active_predecessor:{
|
|
63
|
+
release_id:active.release_id,
|
|
64
|
+
version:active.version,
|
|
65
|
+
manifest_sha256:active.manifest_sha256,
|
|
66
|
+
generation_manifest_sha256:active.generation_manifest_sha256
|
|
67
|
+
},
|
|
68
|
+
doctor:{status:health.status,assessment:health.assessment,verified_entries:health.manifest.verified_entries,warnings:health.findings.filter(item=>item.level==='WARN').length},
|
|
69
|
+
gates:{
|
|
70
|
+
r58_focused:true,
|
|
71
|
+
package_boundary:true,
|
|
72
|
+
process_policy:true,
|
|
73
|
+
performance_recorded:true,
|
|
74
|
+
cross_plane_recorded:true,
|
|
75
|
+
public_surface_recorded:true,
|
|
76
|
+
full_regression_recorded:true,
|
|
77
|
+
manifest_final_verify:true,
|
|
78
|
+
four_hour_soak_completed:false,
|
|
79
|
+
clean_machine_completed:false
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const bytes=Buffer.from(JSON.stringify(evidenceBody,null,2)+'\n');
|
|
83
|
+
const dir=path.join(stateRoot,'evidence','release');await mkdir(dir,{recursive:true});
|
|
84
|
+
const target=path.join(dir,`r58-windows-gate-${manifestSha256.slice(0,16)}.json`);
|
|
85
|
+
try{
|
|
86
|
+
const handle=await open(target,'wx',0o600);
|
|
87
|
+
try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
|
|
88
|
+
}catch(error){
|
|
89
|
+
if(error?.code!=='EEXIST')throw error;
|
|
90
|
+
const prior=JSON.parse(await readFile(target,'utf8'));
|
|
91
|
+
insist(prior.schema===evidenceBody.schema&&prior.status==='passed'&&prior.version===evidenceBody.version&&prior.manifest_sha256===manifestSha256,'conflicting R58 gate evidence identity');
|
|
92
|
+
}
|
|
93
|
+
console.log(JSON.stringify({status:'passed',path:target,sha256:sha256(bytes),manifest_sha256:manifestSha256}));
|
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -18,6 +18,8 @@ import { prepareRemoteConsole, remoteDisplayText } from './remote-console.mjs';
|
|
|
18
18
|
import { ensureRuntimeCoherence } from './runtime-coherence.mjs';
|
|
19
19
|
import { windowsPowerShellEnvironment } from './runtime-update.mjs';
|
|
20
20
|
import { resolveStateRoot } from './state-root.mjs';
|
|
21
|
+
import {foregroundIdentityMatches,queryForegroundIdentity,validForegroundIdentity} from './foreground-process-identity.mjs';
|
|
22
|
+
import {acquireForegroundSessionLock} from './foreground-session-lock.mjs';
|
|
21
23
|
|
|
22
24
|
const REMOTE_COMMANDS=new Set([
|
|
23
25
|
'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
|
|
@@ -200,7 +202,7 @@ function controllerRuntimeReady(text){
|
|
|
200
202
|
/^Loop supervisor:\s*RUNNING\s+\[verified\]\s*$/m.test(value);
|
|
201
203
|
}
|
|
202
204
|
function controllerRuntimeDegraded(text){
|
|
203
|
-
return /^DEADBYTE MCP\s*=\s*DEGRADED\s*$/m.test(String(text||''));
|
|
205
|
+
return /^DEADBYTE MCP\s*=\s*(?:DEGRADED|FAILED)\s*$/m.test(String(text||''));
|
|
204
206
|
}
|
|
205
207
|
async function listLogFiles(root){
|
|
206
208
|
const auditDir=path.join(root,'audit');
|
|
@@ -352,17 +354,20 @@ function remoteTag(kind,displayMode){
|
|
|
352
354
|
}
|
|
353
355
|
function writeRemote(stream,text,displayMode){stream.write(remoteDisplayText(text,displayMode));}
|
|
354
356
|
async function cleanupRemoteSession(ctx,{displayMode='unicode'}={}){
|
|
357
|
+
const failed=[];
|
|
355
358
|
for(const command of SESSION_CLEANUP){
|
|
356
359
|
try{
|
|
357
|
-
const result=invokeController(ctx,command,{allowFailure:true,capture:true,echoCaptured:false});
|
|
358
|
-
if(result.status!==0)
|
|
360
|
+
const result=invokeController(ctx,command,{allowFailure:true,capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
|
|
361
|
+
if(result.status!==0){failed.push(command);writeRemote(process.stderr,`${remoteTag('warn',displayMode)} cleanup ${command} exit=${result.status}\n`,displayMode);}
|
|
359
362
|
}
|
|
360
|
-
catch(error){writeRemote(process.stderr,`${remoteTag('warn',displayMode)} cleanup ${command}: ${String(error?.message||error)}\n`,displayMode);}
|
|
363
|
+
catch(error){failed.push(command);writeRemote(process.stderr,`${remoteTag('warn',displayMode)} cleanup ${command}: ${String(error?.message||error)}\n`,displayMode);}
|
|
361
364
|
}
|
|
365
|
+
if(failed.length) throw new Error(`remote session cleanup failed; ownership marker retained: ${failed.join(',')}`);
|
|
362
366
|
}
|
|
363
367
|
const GUARDIAN_SCRIPT=fileURLToPath(new URL('./remote-session-guardian.mjs',import.meta.url));
|
|
364
368
|
function remoteSessionMarker(root){return path.join(root,'runtime','remote-session.json');}
|
|
365
369
|
function pidAlive(pid){
|
|
370
|
+
if(!Number.isInteger(pid)||pid<1)return false;
|
|
366
371
|
try{process.kill(pid,0);return true;}
|
|
367
372
|
catch(error){return error?.code!=='ESRCH';}
|
|
368
373
|
}
|
|
@@ -376,9 +381,10 @@ async function readRemoteSessionMarker(root){
|
|
|
376
381
|
return value;
|
|
377
382
|
}
|
|
378
383
|
if(value?.schema==='deadbyte.remote-session.v2'){
|
|
384
|
+
for(const key of ['owner_identity','guardian_identity'])if(value[key]!==undefined&&!validForegroundIdentity(value[key]))throw new Error('remote session ownership identity invalid');
|
|
379
385
|
if(!Number.isInteger(value?.owner_pid)||value.owner_pid<1||
|
|
380
386
|
!/^[0-9a-f]{64}$/.test(String(value?.session_nonce||''))||
|
|
381
|
-
!Number.isInteger(value?.guardian_pid)||value.guardian_pid<
|
|
387
|
+
!Number.isInteger(value?.guardian_pid)||value.guardian_pid<0||
|
|
382
388
|
(value.result_session_id!==undefined&&!/^[0-9a-f]{24}$/.test(String(value.result_session_id)))) throw new Error('remote session marker schema invalid');
|
|
383
389
|
return value;
|
|
384
390
|
}
|
|
@@ -388,8 +394,18 @@ async function recoverRemoteSession(ctx,{displayMode='unicode'}={}){
|
|
|
388
394
|
let prior=await readRemoteSessionMarker(ctx.root);
|
|
389
395
|
if(!prior) return;
|
|
390
396
|
const ownerPid=remoteOwnerPid(prior);
|
|
391
|
-
if(pidAlive(ownerPid))
|
|
392
|
-
|
|
397
|
+
if(pidAlive(ownerPid)){
|
|
398
|
+
if(!prior.owner_identity)throw new Error(`remote session ownership unverified for legacy pid=${ownerPid}; marker retained`);
|
|
399
|
+
if(prior.owner_identity.pid!==ownerPid)throw new Error('remote session owner identity PID binding mismatch');
|
|
400
|
+
if(foregroundIdentityMatches(prior.owner_identity,queryForegroundIdentity(ownerPid)))throw new Error(`remote session already active pid=${ownerPid}`);
|
|
401
|
+
}
|
|
402
|
+
let guardianAlive=prior.schema==='deadbyte.remote-session.v2'&&pidAlive(prior.guardian_pid);
|
|
403
|
+
if(guardianAlive){
|
|
404
|
+
if(!prior.guardian_identity)throw new Error('remote guardian ownership unverified; marker retained');
|
|
405
|
+
if(prior.guardian_identity.pid!==prior.guardian_pid)throw new Error('remote guardian identity PID binding mismatch');
|
|
406
|
+
guardianAlive=foregroundIdentityMatches(prior.guardian_identity,queryForegroundIdentity(prior.guardian_pid));
|
|
407
|
+
}
|
|
408
|
+
if(guardianAlive){
|
|
393
409
|
const deadline=Date.now()+3000;
|
|
394
410
|
while(Date.now()<deadline){
|
|
395
411
|
await sleep(100);
|
|
@@ -409,18 +425,23 @@ async function claimRemoteSession(ctx){
|
|
|
409
425
|
const resultSessionId=randomBytes(12).toString('hex');
|
|
410
426
|
const marker={schema:'deadbyte.remote-session.v2',owner_pid:process.pid,guardian_pid:0,session_nonce:nonce,
|
|
411
427
|
result_session_id:resultSessionId,started_at_utc:new Date().toISOString()};
|
|
428
|
+
marker.owner_identity=queryForegroundIdentity(process.pid);
|
|
429
|
+
if(!marker.owner_identity)throw new Error('remote foreground identity unavailable');
|
|
412
430
|
const markerPath=remoteSessionMarker(ctx.root);
|
|
413
431
|
try{await writeFile(markerPath,`${JSON.stringify(marker)}\n`,{encoding:'utf8',flag:'wx'});}
|
|
414
432
|
catch(error){if(error?.code==='EEXIST') throw new Error('remote session already claimed');throw error;}
|
|
415
433
|
let guardian=null;
|
|
416
434
|
try{
|
|
417
|
-
guardian=spawn(process.execPath,[GUARDIAN_SCRIPT,'--state-root',ctx.root,'--owner-pid',String(process.pid),'--session-nonce',nonce],{
|
|
418
|
-
detached:true,stdio:'ignore',windowsHide:true,shell:false,env:process.env
|
|
435
|
+
guardian=spawn(process.execPath,[GUARDIAN_SCRIPT,'--state-root',ctx.root,'--owner-pid',String(process.pid),'--session-nonce',nonce,'--owner-channel','ipc'],{
|
|
436
|
+
detached:true,stdio:['ignore','ignore','ignore','ipc'],windowsHide:true,shell:false,env:process.env
|
|
419
437
|
});
|
|
420
438
|
if(!Number.isInteger(guardian.pid)||guardian.pid<1) throw new Error('remote session guardian did not start');
|
|
421
439
|
marker.guardian_pid=guardian.pid;
|
|
440
|
+
marker.guardian_identity=queryForegroundIdentity(guardian.pid);
|
|
441
|
+
if(!marker.guardian_identity||marker.guardian_identity.parent_pid!==process.pid)throw new Error('remote guardian parent identity mismatch');
|
|
422
442
|
await writeFile(markerPath,`${JSON.stringify(marker)}\n`,'utf8');
|
|
423
443
|
guardian.unref();
|
|
444
|
+
guardian.channel?.unref();
|
|
424
445
|
return marker;
|
|
425
446
|
}catch(error){
|
|
426
447
|
try{guardian?.kill();}catch{}
|
|
@@ -439,8 +460,15 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
439
460
|
const ctx=await remoteContext();
|
|
440
461
|
if(displayMode==='ascii') writeRemote(process.stdout,`[DISPLAY] ASCII fallback: ${display.reason}\n`,displayMode);
|
|
441
462
|
if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor,{displayMode});
|
|
442
|
-
await
|
|
443
|
-
|
|
463
|
+
const startupLock=await acquireForegroundSessionLock({root:ctx.root,powershell:ctx.powershell});
|
|
464
|
+
let marker;
|
|
465
|
+
try{
|
|
466
|
+
startupLock.assertHeld();
|
|
467
|
+
await recoverRemoteSession(ctx,{displayMode});
|
|
468
|
+
startupLock.assertHeld();
|
|
469
|
+
marker=await claimRemoteSession(ctx);
|
|
470
|
+
startupLock.assertHeld();
|
|
471
|
+
}finally{await startupLock.release();}
|
|
444
472
|
try{
|
|
445
473
|
await synchronizeForegroundRuntime(ctx,{displayMode});
|
|
446
474
|
}catch(error){
|
|
@@ -475,7 +503,7 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
475
503
|
process.stdout.write('[BOOT] Runtime already ONLINE; reusing verified bridge/cloud/supervisor.\n');
|
|
476
504
|
}else{
|
|
477
505
|
const recovery=preflight.status===0&&controllerRuntimeDegraded(preflight.stdout);
|
|
478
|
-
process.stdout.write(recovery?'[RECOVERY] Runtime
|
|
506
|
+
process.stdout.write(recovery?'[RECOVERY] Runtime failed readiness; restarting verified owned components...\n':'[BOOT] Starting bridge/runtime...\n');
|
|
479
507
|
const startCommand=recovery?'restart':'start';
|
|
480
508
|
const controllerLogDir=path.join(ctx.root,'runtime','logs');
|
|
481
509
|
await mkdir(controllerLogDir,{recursive:true});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {lstat,readFile} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {resolveStateRoot} from './state-root.mjs';
|
|
4
|
+
import {validForegroundIdentity,foregroundIdentityMatches,queryForegroundIdentity} from './foreground-process-identity.mjs';
|
|
5
|
+
|
|
6
|
+
export async function inspectDoctorForeground(explicitRoot,{queryIdentity=queryForegroundIdentity}={}){
|
|
7
|
+
const state=resolveStateRoot(explicitRoot),runtime=path.join(state,'runtime');
|
|
8
|
+
const markerPath=path.join(runtime,'remote-session.json');
|
|
9
|
+
const findings=[];
|
|
10
|
+
const add=(id,level,what,evidence,next_action=null)=>findings.push({id,level,what,evidence,next_action});
|
|
11
|
+
const inspect='Retain ownership evidence; inspect the foreground session and use supported remote lifecycle commands. Do not kill or re-arm based on PID alone.';
|
|
12
|
+
let marker;
|
|
13
|
+
try{
|
|
14
|
+
for(const dir of [state,runtime]){const info=await lstat(dir);if(!info.isDirectory()||info.isSymbolicLink())throw new Error('unsafe marker parent');}
|
|
15
|
+
const info=await lstat(markerPath);
|
|
16
|
+
if(!info.isFile()||info.isSymbolicLink()||info.size>16384)throw new Error('invalid marker file');
|
|
17
|
+
marker=JSON.parse(await readFile(markerPath,'utf8'));
|
|
18
|
+
}catch(error){
|
|
19
|
+
if(error?.code==='ENOENT')add('foreground_marker','PASS','No foreground session marker in this state root.',{present:false});
|
|
20
|
+
else add('foreground_marker','FAIL','Foreground marker cannot be safely read or parsed.',{check:'regular_bounded_json',error_code:error?.code??'VALIDATION_FAILED'},inspect);
|
|
21
|
+
return {status:findings[0].level,findings};
|
|
22
|
+
}
|
|
23
|
+
if(marker?.schema==='deadbyte.remote-session.v1'&&Number.isSafeInteger(marker.pid)&&marker.pid>0){
|
|
24
|
+
add('foreground_marker','WARN','Legacy foreground marker has no verified owner/guardian binding.',{schema:marker.schema,pid:marker.pid},inspect);
|
|
25
|
+
return {status:'WARN',findings};
|
|
26
|
+
}
|
|
27
|
+
if(marker?.schema!=='deadbyte.remote-session.v2'||!Number.isSafeInteger(marker.owner_pid)||marker.owner_pid<1||!Number.isSafeInteger(marker.guardian_pid)||marker.guardian_pid<0||typeof marker.session_nonce!=='string'||!/^[0-9a-f]{64}$/.test(marker.session_nonce)||(marker.result_session_id!==undefined&&!/^[0-9a-f]{24}$/.test(marker.result_session_id))){
|
|
28
|
+
add('foreground_marker','FAIL','Foreground marker schema, nonce or PID fields are invalid.',{check:'foreground_marker_schema'},inspect);
|
|
29
|
+
return {status:'FAIL',findings};
|
|
30
|
+
}
|
|
31
|
+
add('foreground_marker','PASS','Foreground marker fields are valid.',{schema:marker.schema,owner_pid:marker.owner_pid,guardian_pid:marker.guardian_pid});
|
|
32
|
+
for(const [role,pid,identity]of [['owner',marker.owner_pid,marker.owner_identity],['guardian',marker.guardian_pid,marker.guardian_identity]]){
|
|
33
|
+
const id='foreground_'+role;
|
|
34
|
+
if(identity===undefined){add(id,'WARN',`${role} identity is unverified; PID alone is insufficient.`,{pid,binding_present:false},inspect);continue;}
|
|
35
|
+
if(!validForegroundIdentity(identity)||identity.pid!==pid){add(id,'FAIL',`${role} identity is malformed or disagrees with the marker PID.`,{pid,check:'identity_pid_binding'},inspect);continue;}
|
|
36
|
+
if(role==='guardian'&&identity.parent_pid!==marker.owner_pid){add(id,'FAIL','Guardian identity does not bind to the declared owner.',{pid,check:'guardian_parent_binding'},inspect);continue;}
|
|
37
|
+
try{
|
|
38
|
+
const observed=await queryIdentity(pid);
|
|
39
|
+
if(foregroundIdentityMatches(identity,observed))add(id,'PASS',`${role} process matches the recorded birth/executable/command identity.`,{pid,creation_utc:identity.creation_utc,identity_verified:true});
|
|
40
|
+
else add(id,'WARN',`${role} binding is stale; process is absent or its identity changed.`,{pid,observed_present:observed!==null,identity_verified:false},inspect);
|
|
41
|
+
}catch(error){add(id,'FAIL',`${role} process identity observation failed; liveness is unknown.`,{pid,check:'process_identity',error_code:error?.code??'OBSERVATION_FAILED'},inspect);}
|
|
42
|
+
}
|
|
43
|
+
return {status:findings.some(f=>f.level==='FAIL')?'FAIL':findings.some(f=>f.level==='WARN')?'WARN':'PASS',findings};
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseActivePointer, parseManifest, sha256Bytes, verifyManifestTree } from './release-slot.mjs';
|
|
4
|
+
import { verifyGenerationBundle } from './release-generation.mjs';
|
|
5
|
+
import { resolveStateRoot } from './state-root.mjs';
|
|
6
|
+
|
|
7
|
+
async function regularPath(state, target, directory=false) {
|
|
8
|
+
const relative=path.relative(state,target);
|
|
9
|
+
if(!relative||relative.startsWith('..'+path.sep)||relative==='..'||path.isAbsolute(relative))throw new Error('containment');
|
|
10
|
+
let cursor=state;
|
|
11
|
+
const parts=relative.split(path.sep);
|
|
12
|
+
for(let i=-1;i<parts.length;i++){
|
|
13
|
+
if(i>=0)cursor=path.join(cursor,parts[i]);
|
|
14
|
+
const info=await lstat(cursor);
|
|
15
|
+
if(info.isSymbolicLink()||((i<parts.length-1||directory)?!info.isDirectory():!info.isFile()))throw new Error('regular path');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function inspectDoctorInstallation(rawPointer, explicitRoot){
|
|
20
|
+
const state=resolveStateRoot(explicitRoot);
|
|
21
|
+
const pointer=parseActivePointer(JSON.stringify(rawPointer));
|
|
22
|
+
const findings=[];
|
|
23
|
+
const check=async(id,what,nextAction,operation)=>{
|
|
24
|
+
try{const evidence=await operation();findings.push({id,level:'PASS',what,evidence,next_action:null});}
|
|
25
|
+
catch(error){findings.push({id,level:'FAIL',what,evidence:{check:id,error_code:error?.code??'VALIDATION_FAILED'},next_action:nextAction});}
|
|
26
|
+
};
|
|
27
|
+
const next='Do not activate or arm authority; inspect the signed update receipt and use supported recovery for this state root.';
|
|
28
|
+
const expectedRelease=path.join(state,'releases',pointer.release_id);
|
|
29
|
+
const generation=path.resolve(pointer.generation_root);
|
|
30
|
+
const expectedGenerationParent=path.join(state,'generations');
|
|
31
|
+
await check('active_paths','Active release/generation/config paths match the state store.',next,async()=>{
|
|
32
|
+
if(!path.isAbsolute(pointer.release_root)||path.resolve(pointer.release_root)!==expectedRelease)throw new Error('release path');
|
|
33
|
+
if(!path.isAbsolute(pointer.generation_root)||path.dirname(generation)!==expectedGenerationParent)throw new Error('generation path');
|
|
34
|
+
if(!path.isAbsolute(pointer.config_path)||path.resolve(pointer.config_path)!==path.join(generation,'controller-config.psd1'))throw new Error('config path');
|
|
35
|
+
await regularPath(state,expectedRelease,true);await regularPath(state,generation,true);
|
|
36
|
+
return {release_id:pointer.release_id,generation_id:path.basename(generation)};
|
|
37
|
+
});
|
|
38
|
+
// Never follow unverified pointer paths, even just for diagnostics.
|
|
39
|
+
if(findings[0].level==='FAIL')return {status:'FAIL',findings};
|
|
40
|
+
await check('active_release','Installed release bytes match the pointer manifest and package identity.',next,async()=>{
|
|
41
|
+
const file=path.join(expectedRelease,'MANIFEST.SHA256');await regularPath(state,file);
|
|
42
|
+
const bytes=await readFile(file);if(sha256Bytes(bytes)!==pointer.manifest_sha256)throw new Error('manifest digest');
|
|
43
|
+
const manifest=parseManifest(bytes.toString('utf8'));if(!manifest.size||!manifest.has('package.json'))throw new Error('manifest package missing');
|
|
44
|
+
await verifyManifestTree(expectedRelease,manifest);
|
|
45
|
+
const pkg=JSON.parse(await readFile(path.join(expectedRelease,'package.json'),'utf8'));
|
|
46
|
+
if(pkg.name!=='deadbyte-mcp'||pkg.version!==pointer.version)throw new Error('package identity');
|
|
47
|
+
return {manifest_sha256:pointer.manifest_sha256,verified_files:manifest.size,version:pkg.version};
|
|
48
|
+
});
|
|
49
|
+
await check('active_generation','Generation bytes and config are bound to the active pointer.',next,async()=>{
|
|
50
|
+
await regularPath(state,path.join(generation,'GENERATION.SHA256'));await regularPath(state,pointer.config_path);
|
|
51
|
+
const bytes=await readFile(path.join(generation,'GENERATION.SHA256'));
|
|
52
|
+
const entries=parseManifest(bytes.toString('utf8'));
|
|
53
|
+
if(!entries.has('controller-config.psd1'))throw new Error('unbound config');
|
|
54
|
+
for(const relative of entries.keys())await regularPath(state,path.resolve(generation,...relative.split('/')));
|
|
55
|
+
return await verifyGenerationBundle(generation,pointer.generation_manifest_sha256);
|
|
56
|
+
});
|
|
57
|
+
await check('active_launcher','Stable launcher bytes match the active pointer.',next,async()=>{
|
|
58
|
+
const file=path.join(state,'bin','DEADBYTE-MCP.ps1');await regularPath(state,file);
|
|
59
|
+
const actual=sha256Bytes(await readFile(file));if(actual!==pointer.launcher_sha256)throw new Error('launcher digest');
|
|
60
|
+
return {sha256:actual};
|
|
61
|
+
});
|
|
62
|
+
return {status:findings.some(item=>item.level==='FAIL')?'FAIL':'PASS',findings};
|
|
63
|
+
}
|
package/src/doctor-r45.mjs
CHANGED
|
@@ -5,12 +5,22 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { DEADBYTE_VERSION, MCP_PROTOCOL_VERSION } from './version.mjs';
|
|
7
7
|
import { resolveStateRoot } from './state-root.mjs';
|
|
8
|
+
import { parseActivePointer } from './release-slot.mjs';
|
|
9
|
+
import { inspectDoctorRuntime } from './doctor-runtime-r58.mjs';
|
|
10
|
+
import { inspectDoctorForeground } from './doctor-foreground-r58.mjs';
|
|
11
|
+
import { inspectDoctorTransactions } from './doctor-transactions-r58.mjs';
|
|
8
12
|
|
|
9
13
|
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
10
|
-
const stateRoot=()=>resolveStateRoot();
|
|
11
14
|
const sha256=b=>createHash('sha256').update(b).digest('hex');
|
|
12
15
|
async function jsonOrNull(file){try{return JSON.parse((await readFile(file,'utf8')).replace(/^\uFEFF/,''));}catch(e){if(e?.code==='ENOENT')return null;throw e;}}
|
|
13
16
|
async function fileHash(file){return sha256(await readFile(file));}
|
|
17
|
+
export async function readDoctorActivePointer(explicitRoot){
|
|
18
|
+
const pointerPath=path.join(resolveStateRoot(explicitRoot),'active-release.json');
|
|
19
|
+
let text;
|
|
20
|
+
try {text=await readFile(pointerPath,'utf8');}
|
|
21
|
+
catch(error){if(error?.code==='ENOENT')return null;throw error;}
|
|
22
|
+
return parseActivePointer(text.replace(/^\uFEFF/,''));
|
|
23
|
+
}
|
|
14
24
|
function parseManifest(text){
|
|
15
25
|
const rows=[];
|
|
16
26
|
for(const [index,line] of text.split(/\r?\n/).entries()){
|
|
@@ -48,11 +58,18 @@ export async function doctor(){
|
|
|
48
58
|
const hardwareStat=await lstat(hardware);
|
|
49
59
|
if(!hardwareStat.isFile()||hardwareStat.isSymbolicLink()) throw new Error('hardware observer must be a regular file');
|
|
50
60
|
const hardwareSha256=await fileHash(hardware);
|
|
51
|
-
const active=await
|
|
61
|
+
const active=await readDoctorActivePointer();
|
|
62
|
+
const installation=active?await inspectDoctorRuntime(active):{status:'PASS',findings:[{id:'active_pointer',level:'PASS',what:'No active installation in this state root.',evidence:{pointer_present:false},next_action:null}]};
|
|
63
|
+
const foreground=await inspectDoctorForeground();
|
|
64
|
+
const transactions=await inspectDoctorTransactions();
|
|
65
|
+
const findings=[...installation.findings,...foreground.findings,...transactions.findings];
|
|
66
|
+
const assessment=findings.some(item=>item.level==='FAIL')?'FAIL':findings.some(item=>item.level==='WARN')?'WARN':'PASS';
|
|
52
67
|
const hostPolicy=await jsonOrNull(path.join(root,'controller','deadbyte-host-policy.json'));
|
|
53
68
|
if(hostPolicy&&release?.train==='R45'&&process.platform==='win32'&&hostPolicy.schema!=='deadbyte.host-policy.v2') throw new Error('R45 doctor rejects non-v2 generated host policy');
|
|
54
69
|
return {
|
|
55
|
-
schema:'deadbyte.doctor.v1',status:'ok',package_version:DEADBYTE_VERSION,protocol_revision:MCP_PROTOCOL_VERSION,
|
|
70
|
+
schema:'deadbyte.doctor.v1',status:assessment==='FAIL'?'fail':assessment==='WARN'?'warn':'ok',package_version:DEADBYTE_VERSION,protocol_revision:MCP_PROTOCOL_VERSION,
|
|
71
|
+
assessment,findings,
|
|
72
|
+
runtime_availability:installation.availability??null,
|
|
56
73
|
platform:{current:process.platform,architecture:process.arch,primary:'win32',supported:process.platform==='win32'},
|
|
57
74
|
manifest:{sha256:manifestSha256,entries:rows.length,verified_entries:verified,verified:true},
|
|
58
75
|
release_metadata:metadata,
|
|
@@ -63,4 +80,4 @@ export async function doctor(){
|
|
|
63
80
|
known_unsupported:process.platform==='win32'?[]:['native_hardware_observation','windows_controller','desktop_native']
|
|
64
81
|
};
|
|
65
82
|
}
|
|
66
|
-
export async function printDoctor(){const value=await doctor();process.stdout.write(JSON.stringify(value,null,2)+'\n');return value;}
|
|
83
|
+
export async function printDoctor(){const value=await doctor();process.stdout.write(JSON.stringify(value,null,2)+'\n');if(value.assessment==='FAIL')process.exitCode=1;return value;}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import {spawnSync} from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {resolveStateRoot} from './state-root.mjs';
|
|
5
|
+
import {inspectDoctorInstallation} from './doctor-installation-r58.mjs';
|
|
6
|
+
import {parseActivePointer,parseManifest,sha256Bytes} from './release-slot.mjs';
|
|
7
|
+
import {windowsPowerShellEnvironment} from './runtime-update.mjs';
|
|
8
|
+
|
|
9
|
+
function object(value,keys){
|
|
10
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).length!==keys.length||keys.some(k=>!Object.hasOwn(value,k)))throw new Error('controller observation fields invalid');
|
|
11
|
+
}
|
|
12
|
+
const reason=value=>typeof value==='string'&&/^[a-z][a-z0-9-]{0,63}$/.test(value);
|
|
13
|
+
const date=value=>typeof value==='string'&&/^\d{4}-\d\d-\d\dT.*Z$/.test(value)&&Number.isFinite(Date.parse(value));
|
|
14
|
+
const pid=value=>Number.isSafeInteger(value)&&value>0;
|
|
15
|
+
export function validateDoctorControllerObservation(value,expectedCoreRoot){
|
|
16
|
+
object(value,['schema','observed_at_utc','state','core_root','local_ready','bridge','core','cloud','supervisor','authority']);
|
|
17
|
+
if(value.schema!=='deadbyte.controller-observation.v1'||!date(value.observed_at_utc)||!['OFFLINE','PARKED','ONLINE','STARTING','FAILED','DEGRADED'].includes(value.state)||typeof value.local_ready!=='boolean'||typeof value.core_root!=='string'||!path.isAbsolute(value.core_root)||path.resolve(value.core_root)!==path.resolve(expectedCoreRoot))throw new Error('controller observation identity invalid');
|
|
18
|
+
for(const key of ['bridge','cloud']){
|
|
19
|
+
const item=value[key];object(item,['alive','reason','pid','creation_time_utc']);
|
|
20
|
+
if(typeof item.alive!=='boolean'||!reason(item.reason)||(item.alive?(!pid(item.pid)||!date(item.creation_time_utc)||item.reason!=='verified'):(item.pid!==null||item.creation_time_utc!==null)))throw new Error('controller component observation invalid');
|
|
21
|
+
}
|
|
22
|
+
const core=value.core;object(core,['alive','reason','pid','parent_pid','creation_time_utc','identity_basis']);
|
|
23
|
+
if(typeof core.alive!=='boolean'||!reason(core.reason))throw new Error('core observation invalid');
|
|
24
|
+
if(core.alive){
|
|
25
|
+
if(!value.bridge.alive||!pid(core.pid)||core.parent_pid!==value.bridge.pid||!date(core.creation_time_utc)||core.reason!=='verified'||!['command-line','verified-bridge-single-opaque-node-child'].includes(core.identity_basis))throw new Error('core topology binding invalid');
|
|
26
|
+
}else if(core.pid!==null||core.parent_pid!==null||core.creation_time_utc!==null||core.identity_basis!==null)throw new Error('failed core observation carries live identity');
|
|
27
|
+
object(value.supervisor,['configured','running','reason']);
|
|
28
|
+
if(typeof value.supervisor.configured!=='boolean'||typeof value.supervisor.running!=='boolean'||!reason(value.supervisor.reason)||(!value.supervisor.configured&&value.supervisor.running))throw new Error('supervisor observation invalid');
|
|
29
|
+
object(value.authority,['process','coding','autonomous','desktop','host']);
|
|
30
|
+
for(const item of Object.values(value.authority)){
|
|
31
|
+
object(item,['configured','armed','reason']);
|
|
32
|
+
if(typeof item.configured!=='boolean'||typeof item.armed!=='boolean'||!reason(item.reason)||(!item.configured&&item.armed)||(item.armed&&item.reason!=='standing-grant'))throw new Error('authority observation invalid');
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function classifyDoctorControllerObservation(raw,expectedCoreRoot){
|
|
38
|
+
const value=validateDoctorControllerObservation(raw,expectedCoreRoot),findings=[];
|
|
39
|
+
const add=(id,level,what,evidence,next_action=null)=>findings.push({id,level,what,evidence,next_action});
|
|
40
|
+
const inspect='Retain evidence and inspect exact controller/config/runtime bindings; use supported lifecycle recovery only after reconciling ownership.';
|
|
41
|
+
for(const name of ['bridge','cloud']){
|
|
42
|
+
const component=value[name];
|
|
43
|
+
const level=component.alive||component.reason==='no-ledger'?'PASS':component.reason==='process-missing'?'WARN':'FAIL';
|
|
44
|
+
add('runtime_'+name,level,`${name} ownership observation: ${component.reason}.`,{alive:component.alive,pid:component.pid,reason:component.reason},level==='PASS'?null:inspect);
|
|
45
|
+
}
|
|
46
|
+
const core=value.core;
|
|
47
|
+
const coreLevel=core.alive||(!value.bridge.alive&&core.reason==='bridge-unavailable')?'PASS':core.reason==='startup-zero-child'?'WARN':'FAIL';
|
|
48
|
+
add('runtime_core',coreLevel,`Downstream core observation: ${core.reason}.`,{alive:core.alive,pid:core.pid,parent_pid:core.parent_pid,identity_basis:core.identity_basis},coreLevel==='PASS'?null:inspect);
|
|
49
|
+
const localHealthy=value.bridge.alive&&core.alive&&value.local_ready;
|
|
50
|
+
const state=localHealthy&&value.cloud.alive?'ONLINE':!value.bridge.alive&&!value.cloud.alive?'OFFLINE':!value.bridge.alive&&value.cloud.alive?'PARKED':core.reason==='startup-zero-child'?'STARTING':'FAILED';
|
|
51
|
+
const contradiction=value.state==='ONLINE'&&state!=='ONLINE';
|
|
52
|
+
const availabilityLevel=contradiction?(state==='STARTING'?'WARN':'FAIL'):state==='ONLINE'||state==='OFFLINE'?'PASS':'WARN';
|
|
53
|
+
add('runtime_availability',availabilityLevel,`Observed availability is ${state}; local and remote availability are separate.`,{state,controller_state:value.state,local_healthy:localHealthy,remote_connected:value.cloud.alive,local_ready:value.local_ready},availabilityLevel==='PASS'?null:inspect);
|
|
54
|
+
if(value.supervisor.configured&&!value.supervisor.running)add('runtime_supervisor','WARN','Configured supervisor is not verified running.',{reason:value.supervisor.reason},inspect);
|
|
55
|
+
else add('runtime_supervisor','PASS','Supervisor observation is consistent.',{configured:value.supervisor.configured,running:value.supervisor.running});
|
|
56
|
+
for(const [plane,authority]of Object.entries(value.authority)){
|
|
57
|
+
const valid=authority.armed||['no-policy','disabled','disarmed','coding-disarmed'].includes(authority.reason);
|
|
58
|
+
const level=!valid?'FAIL':authority.armed&&(['host','desktop'].includes(plane)||state==='OFFLINE')?'WARN':'PASS';
|
|
59
|
+
add('authority_'+plane,level,`${plane} authority observation: ${authority.armed?'ARMED':'DISARMED'} (${authority.reason}).`,{configured:authority.configured,armed:authority.armed,reason:authority.reason},level==='PASS'?null:'Inspect the pinned policy/grant and revoke through the supported launcher if unintended; doctor never changes authority.');
|
|
60
|
+
}
|
|
61
|
+
if(value.authority.autonomous.armed&&!value.authority.coding.armed)add('authority_dependency','FAIL','Autonomous is armed while coding is not.',{autonomous_armed:true,coding_armed:false},inspect);
|
|
62
|
+
return {status:findings.some(f=>f.level==='FAIL')?'FAIL':findings.some(f=>f.level==='WARN')?'WARN':'PASS',findings,availability:{state,local_healthy:localHealthy,remote_connected:value.cloud.alive}};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function inspectDoctorRuntime(rawPointer,explicitRoot){
|
|
66
|
+
const pointer=parseActivePointer(JSON.stringify(rawPointer)),state=resolveStateRoot(explicitRoot);
|
|
67
|
+
const installation=await inspectDoctorInstallation(pointer,state);
|
|
68
|
+
if(installation.status==='FAIL')return installation;
|
|
69
|
+
const controller=path.join(pointer.release_root,'controller','deadbyte-controller.ps1');
|
|
70
|
+
const sourceBytes=await readFile(controller),manifestBytes=await readFile(path.join(pointer.release_root,'MANIFEST.SHA256'));
|
|
71
|
+
if(sha256Bytes(manifestBytes)!==pointer.manifest_sha256||parseManifest(manifestBytes.toString('utf8')).get('controller/deadbyte-controller.ps1')!==sha256Bytes(sourceBytes))throw new Error('doctor controller is not manifest-bound');
|
|
72
|
+
const source=sourceBytes.toString('utf8');
|
|
73
|
+
if(!source.includes('[switch]$Json'))return {status:'WARN',findings:[...installation.findings,{id:'runtime_observation',level:'WARN',what:'Installed predecessor does not support read-only structured observation.',evidence:{release_id:pointer.release_id,structured_observation_supported:false},next_action:'Use the accepted predecessor evidence and supported status command; do not bypass controller root binding or rewrite the sealed parent.'}]};
|
|
74
|
+
const nativeRoot=process.env.SystemRoot||process.env.WINDIR;
|
|
75
|
+
const result=spawnSync(path.join(nativeRoot,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoLogo','-NoProfile','-File',controller,'status','-Json'],{cwd:pointer.release_root,encoding:'utf8',windowsHide:true,timeout:20000,maxBuffer:262144,env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:state,DEADBYTE_CONTROLLER_CONFIG:pointer.config_path,DEADBYTE_CONTROLLER_RUNTIME_ROOT:path.join(state,'runtime')})});
|
|
76
|
+
let runtime;
|
|
77
|
+
try{
|
|
78
|
+
if(result.error||result.status!==0)throw new Error('controller observation failed');
|
|
79
|
+
runtime=classifyDoctorControllerObservation(JSON.parse(result.stdout),pointer.release_root);
|
|
80
|
+
}catch{runtime={status:'FAIL',findings:[{id:'runtime_observation',level:'FAIL',what:'Read-only controller observation failed or its structured contract/binding is invalid.',evidence:{exit_code:result.status??null,error_code:result.error?.code??'OBSERVATION_FAILED'},next_action:'Inspect the exact release/generation and supported controller status; do not auto-repair or retry effects.'}]};}
|
|
81
|
+
return {...runtime,findings:[...installation.findings,...runtime.findings]};
|
|
82
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {lstat,opendir,readFile} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {resolveStateRoot} from './state-root.mjs';
|
|
5
|
+
|
|
6
|
+
export async function inspectDoctorTransactions(explicitRoot,{maxEntries=256}={}){
|
|
7
|
+
if(!Number.isSafeInteger(maxEntries)||maxEntries<1||maxEntries>256)throw new Error('doctor transaction budget invalid');
|
|
8
|
+
const state=resolveStateRoot(explicitRoot),root=path.join(state,'evidence','updates'),findings=[];
|
|
9
|
+
const next='Retain transaction evidence; inspect the exact signed receipt/handoff and current pointer before any supported recovery. Doctor never launches recovery.';
|
|
10
|
+
const add=(level,what,evidence)=>findings.push({id:'transaction_residue',level,what,evidence,next_action:level==='PASS'?null:next});
|
|
11
|
+
let directory;
|
|
12
|
+
try{
|
|
13
|
+
for(const dir of [state,path.join(state,'evidence'),root]){const st=await lstat(dir);if(!st.isDirectory()||st.isSymbolicLink())throw new Error('unsafe evidence parent');}
|
|
14
|
+
directory=await opendir(root);
|
|
15
|
+
}catch(error){
|
|
16
|
+
if(error?.code==='ENOENT')add('PASS','No update evidence directory in this state root.',{present:false,states_checked:0});
|
|
17
|
+
else add('FAIL','Update evidence directory cannot be safely inspected.',{error_code:error?.code??'VALIDATION_FAILED'});
|
|
18
|
+
return {status:findings[0].level,findings};
|
|
19
|
+
}
|
|
20
|
+
let entries=0,checked=0,terminal=0;
|
|
21
|
+
try{
|
|
22
|
+
for await(const entry of directory){
|
|
23
|
+
if(++entries>maxEntries){add('WARN','Transaction inventory budget reached; unobserved entries remain.',{max_entries:maxEntries,states_checked:checked,inventory_complete:false});break;}
|
|
24
|
+
if(entry.isSymbolicLink()){add('FAIL','Transaction evidence contains a link; it was not followed.',{entry_name_sha256:createHash('sha256').update(entry.name).digest('hex')});continue;}
|
|
25
|
+
if(!entry.isDirectory())continue;
|
|
26
|
+
const file=path.join(root,entry.name,'state.json');let bytes;
|
|
27
|
+
try{
|
|
28
|
+
const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink()||info.size>1048576)throw new Error('unsafe transaction state');
|
|
29
|
+
bytes=await readFile(file);if(bytes.length>1048576)throw new Error('transaction state budget');
|
|
30
|
+
const body=JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/,''));checked++;
|
|
31
|
+
const digest=createHash('sha256').update(bytes).digest('hex');
|
|
32
|
+
if(!['deadbyte.r27-slot-update-state.v1','deadbyte.r31-slot-rollback-state.v1'].includes(body?.schema)||typeof body.phase!=='string'||!/^[a-z_]{1,64}$/.test(body.phase)){add('FAIL','Transaction state schema or phase is invalid.',{state_sha256:digest,check:'transaction_schema'});continue;}
|
|
33
|
+
if(['completed','superseded','failed_receipted'].includes(body.phase)){terminal++;continue;}
|
|
34
|
+
add('WARN','Nonterminal or unresolved transaction record remains; liveness/outcome is not inferred from its age or PID.',{state_sha256:digest,schema:body.schema,recorded_phase:body.phase});
|
|
35
|
+
}catch(error){if(error?.code==='ENOENT')continue;add('FAIL','Transaction state cannot be safely parsed or inspected.',{error_code:error?.code??'VALIDATION_FAILED'});}
|
|
36
|
+
}
|
|
37
|
+
}catch(error){add('FAIL','Transaction inventory observation failed.',{error_code:error?.code??'OBSERVATION_FAILED'});}
|
|
38
|
+
add('PASS','Recorded terminal transaction phases counted; signatures and desired outcomes are not established by this inventory.',{states_checked:checked,recorded_terminal_states:terminal,receipt_verification_performed:false});
|
|
39
|
+
return {status:findings.some(f=>f.level==='FAIL')?'FAIL':findings.some(f=>f.level==='WARN')?'WARN':'PASS',findings};
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {spawnSync} from 'node:child_process';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {windowsPowerShellEnvironment} from './runtime-update.mjs';
|
|
5
|
+
|
|
6
|
+
export function validForegroundIdentity(value){
|
|
7
|
+
return value?.schema==='deadbyte.foreground-process.v1'&&Number.isSafeInteger(value.pid)&&value.pid>0&&Number.isSafeInteger(value.parent_pid)&&value.parent_pid>=0&&typeof value.creation_utc==='string'&&/^\d{4}-\d\d-\d\dT.*Z$/.test(value.creation_utc)&&Number.isFinite(Date.parse(value.creation_utc))&&typeof value.executable_path==='string'&&path.isAbsolute(value.executable_path)&&/^[0-9a-f]{64}$/.test(value.command_line_sha256??'');
|
|
8
|
+
}
|
|
9
|
+
export function foregroundIdentityMatches(expected,observed){
|
|
10
|
+
if(!validForegroundIdentity(expected))throw new Error('foreground ownership identity unverified');
|
|
11
|
+
if(observed===null)return false;
|
|
12
|
+
if(!validForegroundIdentity(observed))throw new Error('observed foreground identity incomplete');
|
|
13
|
+
return expected.pid===observed.pid&&expected.parent_pid===observed.parent_pid&&expected.creation_utc===observed.creation_utc&&path.resolve(expected.executable_path).toLowerCase()===path.resolve(observed.executable_path).toLowerCase()&&expected.command_line_sha256===observed.command_line_sha256;
|
|
14
|
+
}
|
|
15
|
+
export function queryForegroundIdentity(pid){
|
|
16
|
+
if(process.platform!=='win32'||!Number.isSafeInteger(pid)||pid<1)throw new Error('foreground identity requires a positive Windows PID');
|
|
17
|
+
const root=process.env.SystemRoot||process.env.WINDIR;if(!root)throw new Error('Windows system root missing');
|
|
18
|
+
const command=`$p=Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}'; if ($null -eq $p) {Write-Output 'null';exit 0}; [pscustomobject]@{pid=$p.ProcessId;parent_pid=$p.ParentProcessId;creation_utc=$p.CreationDate.ToUniversalTime().ToString('o');executable_path=$p.ExecutablePath;command_line=$p.CommandLine} | ConvertTo-Json -Compress`;
|
|
19
|
+
const result=spawnSync(path.join(root,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoLogo','-NoProfile','-Command',command],{encoding:'utf8',windowsHide:true,env:windowsPowerShellEnvironment(process.env),timeout:15000,maxBuffer:1024*1024});
|
|
20
|
+
if(result.error||result.status!==0)throw new Error('foreground process identity query failed');
|
|
21
|
+
let raw;try{raw=JSON.parse(result.stdout);}catch{throw new Error('foreground process identity query malformed');}
|
|
22
|
+
if(raw===null)return null;
|
|
23
|
+
if(typeof raw.command_line!=='string'||!raw.command_line)throw new Error('foreground process command identity unavailable');
|
|
24
|
+
const identity={schema:'deadbyte.foreground-process.v1',pid:raw.pid,parent_pid:raw.parent_pid,creation_utc:raw.creation_utc,executable_path:raw.executable_path,command_line_sha256:createHash('sha256').update(raw.command_line).digest('hex')};
|
|
25
|
+
if(!validForegroundIdentity(identity)||identity.pid!==pid)throw new Error('foreground process identity incomplete');
|
|
26
|
+
return identity;
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {windowsPowerShellEnvironment} from './runtime-update.mjs';
|
|
6
|
+
|
|
7
|
+
export async function acquireForegroundSessionLock({root,powershell}){
|
|
8
|
+
const hash=createHash('sha256').update(path.resolve(root).toLowerCase()).digest('hex');
|
|
9
|
+
const child=spawn(powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',fileURLToPath(new URL('./foreground-session-lock.ps1',import.meta.url)),'-LockName',`Local\\DEADBYTE-foreground-${hash}`],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:windowsPowerShellEnvironment(process.env)});
|
|
10
|
+
let text='',errorText='';child.stderr.on('data',b=>{errorText=(errorText+b).slice(-2048);});
|
|
11
|
+
let settled=false;
|
|
12
|
+
const exited=new Promise(resolve=>{child.once('exit',(code,signal)=>resolve({code,signal}));child.once('error',error=>resolve({error}));});
|
|
13
|
+
const ready=new Promise((resolve,reject)=>{
|
|
14
|
+
const timer=setTimeout(()=>{if(!settled){settled=true;child.kill();reject(new Error('foreground session lock acquisition timed out'));}},15000);
|
|
15
|
+
child.stdout.on('data',b=>{text+=b.toString();if(!settled&&text.includes('DEADBYTE_FOREGROUND_LOCK_READY')){settled=true;clearTimeout(timer);resolve();}});
|
|
16
|
+
exited.then(result=>{if(!settled){settled=true;clearTimeout(timer);reject(new Error(result.code===73?'remote session already claimed (startup/recovery lock)':`foreground session lock failed: ${errorText||result.error?.message||result.code}`));}});
|
|
17
|
+
});
|
|
18
|
+
try{await ready;}catch(error){child.stdin.end();await exited;throw error;}
|
|
19
|
+
return {assertHeld(){if(child.exitCode!==null||child.signalCode!==null)throw new Error('foreground session lock ownership lost');},async release(){child.stdin.end();const result=await exited;if(result.code!==0)throw new Error('foreground session lock release failed');}};
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
param([Parameter(Mandatory=$true)][ValidatePattern('^Local\\DEADBYTE-foreground-[0-9a-f]{64}$')][string]$LockName)
|
|
2
|
+
$ErrorActionPreference='Stop'
|
|
3
|
+
$mutex=New-Object System.Threading.Mutex($false,$LockName)
|
|
4
|
+
$owned=$false
|
|
5
|
+
try {
|
|
6
|
+
try { $owned=$mutex.WaitOne(0) } catch [System.Threading.AbandonedMutexException] { $owned=$true }
|
|
7
|
+
if(-not $owned){exit 73}
|
|
8
|
+
[Console]::Out.WriteLine('DEADBYTE_FOREGROUND_LOCK_READY')
|
|
9
|
+
[Console]::Out.Flush()
|
|
10
|
+
# Owner death closes the inherited stdin pipe. No persisted PID lockfile.
|
|
11
|
+
[void][Console]::In.ReadLine()
|
|
12
|
+
} finally {
|
|
13
|
+
if($owned){$mutex.ReleaseMutex()}
|
|
14
|
+
$mutex.Dispose()
|
|
15
|
+
}
|
|
@@ -200,6 +200,7 @@ function renderControllerConfig({currentConfig,coreRoot,workspaceRoot,processPol
|
|
|
200
200
|
` WorkingDirectory = ${psQuote(bridge.WorkingDirectory)}`,
|
|
201
201
|
` ScriptPath = ${psQuote(bridge.ScriptPath)}`,
|
|
202
202
|
` ScriptSha256 = ${psQuote(bridge.ScriptSha256)}`,
|
|
203
|
+
...renderPsDependencies(bridge.Dependencies),
|
|
203
204
|
' }',' Cloud = @{',
|
|
204
205
|
` FilePath = ${psQuote(cloud.FilePath)}`,
|
|
205
206
|
` FileSha256 = ${psQuote(cloud.FileSha256)}`,
|
package/src/release-slot.mjs
CHANGED
|
@@ -289,7 +289,8 @@ export function renderStableLauncherPs1(){
|
|
|
289
289
|
'$env:DEADBYTE_STATE_ROOT = $StateRoot',
|
|
290
290
|
'$env:DEADBYTE_CONTROLLER_CONFIG = $configPath',
|
|
291
291
|
"$env:DEADBYTE_CONTROLLER_RUNTIME_ROOT = Join-Path $StateRoot 'runtime'",
|
|
292
|
-
"$powershell = Join-Path
|
|
292
|
+
"$powershell = Join-Path ([Environment]::GetFolderPath('Windows')) 'System32\\WindowsPowerShell\\v1.0\\powershell.exe'",
|
|
293
|
+
"if (-not (Test-Path -LiteralPath $powershell -PathType Leaf)) { Fail 'native Windows PowerShell missing' }",
|
|
293
294
|
'& $powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File $controller @ControllerArgs',
|
|
294
295
|
'$exitCode = (Get-Variable -Name LASTEXITCODE -ValueOnly -ErrorAction SilentlyContinue)',
|
|
295
296
|
'if ($null -eq $exitCode) { $exitCode = 0 }',
|