deadbyte-mcp 0.15.33 → 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 +71 -22
- package/PROVENANCE.json +57 -28
- package/README.txt +22 -14
- package/RELEASE.json +41 -29
- package/SBOM.cdx.json +25 -3
- package/TEST-MATRIX.json +135 -43
- 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 +6 -3
- package/scripts/bridge-benchmark-r58.mjs +67 -0
- package/scripts/deploy-r48.mjs +2 -3
- package/scripts/gate-windows-r57.ps1 +27 -0
- 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-r57.mjs +81 -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 +117 -3
- 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 +14 -10
- package/test/r56-release-contract.test.mjs +5 -3
- package/test/r57-core-startup-race.test.mjs +85 -0
- package/test/r57-release-contract.test.mjs +67 -0
- 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,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 }',
|
|
@@ -8,6 +8,10 @@ import { windowsPowerShellEnvironment } from './runtime-update.mjs';
|
|
|
8
8
|
import { resolveStateRoot } from './state-root.mjs';
|
|
9
9
|
|
|
10
10
|
const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost','park'];
|
|
11
|
+
// This inherited IPC channel is an OS-owned parent lifetime handle, not a PID
|
|
12
|
+
// poll. A reused PID cannot keep a new foreground guardian alive.
|
|
13
|
+
let ownerDisconnected=false;
|
|
14
|
+
process.once('disconnect',()=>{ownerDisconnected=true;});
|
|
11
15
|
|
|
12
16
|
function parseArgs(argv){
|
|
13
17
|
const out={};
|
|
@@ -51,7 +55,10 @@ async function main(){
|
|
|
51
55
|
const root=resolveStateRoot(args['state-root']);
|
|
52
56
|
const ownerPid=Number(args['owner-pid']);
|
|
53
57
|
const nonce=String(args['session-nonce']||'');
|
|
58
|
+
const ipcOwner=args['owner-channel']==='ipc';
|
|
59
|
+
if(ipcOwner&&typeof process.send!=='function')throw new Error('guardian owner IPC channel missing');
|
|
54
60
|
if(!Number.isInteger(ownerPid)||ownerPid<1) throw new Error('guardian owner pid invalid');
|
|
61
|
+
if(ipcOwner&&process.ppid!==ownerPid)throw new Error('guardian IPC parent does not match declared owner');
|
|
55
62
|
if(!/^[0-9a-f]{64}$/.test(nonce)) throw new Error('guardian session nonce invalid');
|
|
56
63
|
|
|
57
64
|
const launcher=await requireRegular(path.join(root,'bin','DEADBYTE-MCP.ps1'),'stable DEADBYTE launcher');
|
|
@@ -63,7 +70,7 @@ async function main(){
|
|
|
63
70
|
for(;;){
|
|
64
71
|
const marker=await readMarker(markerPath);
|
|
65
72
|
if(!marker||!markerMatches(marker,{ownerPid,nonce})) return;
|
|
66
|
-
if(pidAlive(ownerPid)){
|
|
73
|
+
if(ipcOwner?!ownerDisconnected:pidAlive(ownerPid)){
|
|
67
74
|
await sleep(250);
|
|
68
75
|
continue;
|
|
69
76
|
}
|
package/src/runtime-update.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, createPrivateKey, createPublicKey, randomBytes, sign as cryptoSign, verify as cryptoVerify } from 'node:crypto';
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -306,6 +306,69 @@ async function normalizeComponentIdentities(config){
|
|
|
306
306
|
return next;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
function bridgeDependencyName(relativePath,usedNames){
|
|
310
|
+
const normalized=String(relativePath).replaceAll('\\','/').replaceAll('/','_');
|
|
311
|
+
let name=normalized;
|
|
312
|
+
if(!/^[A-Za-z0-9._-]{1,32}$/.test(name)||usedNames.has(name)){
|
|
313
|
+
name=`dep-${sha256Bytes(Buffer.from(String(relativePath),'utf8')).slice(0,20)}`;
|
|
314
|
+
}
|
|
315
|
+
if(usedNames.has(name)) throw new Error(`bridge dependency name collision: ${relativePath}`);
|
|
316
|
+
usedNames.add(name);
|
|
317
|
+
return name;
|
|
318
|
+
}
|
|
319
|
+
function bridgeStaticRelativeImports(source,file){
|
|
320
|
+
const found=[];
|
|
321
|
+
for(const [index,line] of String(source).split(/\r?\n/).entries()){
|
|
322
|
+
if(/\bimport\s*\(\s*['"]\.\.?\//.test(line)) throw new Error(`bridge dynamic relative import is forbidden: ${file}:${index+1}`);
|
|
323
|
+
const importStart=/^\s*import\b/.test(line);
|
|
324
|
+
const exportStart=/^\s*export\b/.test(line)&&/\bfrom\b/.test(line);
|
|
325
|
+
if(!importStart&&!exportStart) continue;
|
|
326
|
+
const match=importStart
|
|
327
|
+
? /^\s*import\s+(?:(?:[^'"]+?)\s+from\s*)?['"]([^'"]+)['"]\s*;?\s*$/.exec(line)
|
|
328
|
+
: /^\s*export\s+[^'"]*?\s+from\s*['"]([^'"]+)['"]\s*;?\s*$/.exec(line);
|
|
329
|
+
if(!match) throw new Error(`bridge import syntax unsupported for dependency binding: ${file}:${index+1}`);
|
|
330
|
+
if(match[1].startsWith('.')) found.push(match[1]);
|
|
331
|
+
}
|
|
332
|
+
return found;
|
|
333
|
+
}
|
|
334
|
+
async function discoverBridgeLocalDependencies(root,entryScript){
|
|
335
|
+
const absoluteRoot=path.resolve(root),realRoot=await realpath(absoluteRoot);
|
|
336
|
+
const srcRoot=path.join(absoluteRoot,'src');
|
|
337
|
+
const queue=[path.resolve(entryScript)],visited=new Set(),files=[];
|
|
338
|
+
while(queue.length){
|
|
339
|
+
const current=queue.shift();
|
|
340
|
+
const currentReal=await realpath(current);
|
|
341
|
+
const relativeReal=path.relative(realRoot,currentReal);
|
|
342
|
+
if(relativeReal.startsWith('..')||path.isAbsolute(relativeReal)) throw new Error(`bridge dependency escapes bridge root: ${current}`);
|
|
343
|
+
if(visited.has(currentReal)) continue;
|
|
344
|
+
visited.add(currentReal);
|
|
345
|
+
if(visited.size>16) throw new Error('bridge local dependency closure exceeds 16 files');
|
|
346
|
+
const stat=await lstat(current);
|
|
347
|
+
if(!stat.isFile()||stat.isSymbolicLink()) throw new Error(`bridge dependency must be a regular file: ${current}`);
|
|
348
|
+
const source=await readFile(current,'utf8');
|
|
349
|
+
const specifiers=bridgeStaticRelativeImports(source,current);
|
|
350
|
+
if(currentReal!==await realpath(entryScript)) files.push(currentReal);
|
|
351
|
+
for(const specifier of specifiers){
|
|
352
|
+
if(!specifier.startsWith('./')) throw new Error(`bridge relative import escapes bridge root: ${specifier}`);
|
|
353
|
+
if(specifier.includes('?')||specifier.includes('#')) throw new Error(`bridge relative import query/fragment unsupported: ${specifier}`);
|
|
354
|
+
const resolved=path.resolve(path.dirname(current),specifier);
|
|
355
|
+
const relative=path.relative(absoluteRoot,resolved);
|
|
356
|
+
if(relative.startsWith('..')||path.isAbsolute(relative)) throw new Error(`bridge dependency escapes bridge root: ${specifier}`);
|
|
357
|
+
if(!/\.(?:mjs|js)$/i.test(resolved)) throw new Error(`bridge local dependency must be .mjs/.js: ${specifier}`);
|
|
358
|
+
queue.push(resolved);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const unique=[...new Set(files)].sort((a,b)=>a.localeCompare(b));
|
|
362
|
+
const usedNames=new Set();
|
|
363
|
+
const dependencies=[];
|
|
364
|
+
for(const file of unique){
|
|
365
|
+
const relative=path.relative(srcRoot,file).replaceAll('\\','/');
|
|
366
|
+
if(relative.startsWith('..')||path.isAbsolute(relative)) throw new Error(`bridge dependency must remain under src: ${file}`);
|
|
367
|
+
dependencies.push({Name:bridgeDependencyName(relative,usedNames),Path:file,Sha256:await sha256File(file)});
|
|
368
|
+
}
|
|
369
|
+
return dependencies;
|
|
370
|
+
}
|
|
371
|
+
|
|
309
372
|
export async function prepareBridgeSpec(config,bootstrap){
|
|
310
373
|
if(!bootstrap) return structuredClone(config);
|
|
311
374
|
if(typeof bootstrap.root!=='string'||!path.isAbsolute(bootstrap.root)||!HEX64.test(String(bootstrap.scriptSha256??''))) {
|
|
@@ -327,6 +390,7 @@ export async function prepareBridgeSpec(config,bootstrap){
|
|
|
327
390
|
next.Bridge.Arguments='src\\bridge.mjs';
|
|
328
391
|
next.Bridge.ScriptPath=script;
|
|
329
392
|
next.Bridge.ScriptSha256=actual;
|
|
393
|
+
next.Bridge.Dependencies=await discoverBridgeLocalDependencies(root,script);
|
|
330
394
|
return next;
|
|
331
395
|
}
|
|
332
396
|
|
|
@@ -743,6 +807,37 @@ export function selectCoreProcess(processes,coreRoot,bridgePid,verifiedBridge=nu
|
|
|
743
807
|
function coreProcess(coreRoot,bridge){
|
|
744
808
|
return selectCoreProcess(nodeProcesses(),coreRoot,bridge.pid,bridge);
|
|
745
809
|
}
|
|
810
|
+
export async function waitForCoreProcess(coreRoot,bridge,{
|
|
811
|
+
timeoutMs=10000,
|
|
812
|
+
pollMs=100,
|
|
813
|
+
processProvider=nodeProcesses,
|
|
814
|
+
sleepFn=sleep,
|
|
815
|
+
nowFn=Date.now
|
|
816
|
+
}={}){
|
|
817
|
+
const bridgePid=Number(bridge?.pid);
|
|
818
|
+
if(!Number.isInteger(bridgePid)||bridgePid<1) throw new Error('downstream core wait requires verified bridge PID');
|
|
819
|
+
if(!Number.isFinite(timeoutMs)||timeoutMs<0) throw new Error('downstream core wait timeout invalid');
|
|
820
|
+
if(!Number.isFinite(pollMs)||pollMs<1) throw new Error('downstream core wait poll invalid');
|
|
821
|
+
if(typeof processProvider!=='function'||typeof sleepFn!=='function'||typeof nowFn!=='function') throw new Error('downstream core wait hooks invalid');
|
|
822
|
+
const started=Number(nowFn());
|
|
823
|
+
let lastError=null;
|
|
824
|
+
while(true){
|
|
825
|
+
try {
|
|
826
|
+
const processes=await processProvider();
|
|
827
|
+
return selectCoreProcess(processes,coreRoot,bridgePid,bridge);
|
|
828
|
+
} catch(error){
|
|
829
|
+
const message=String(error?.message??error);
|
|
830
|
+
const transient=`expected one unambiguous downstream core for bridge ${bridgePid}, found children=0 visible_matches=0`;
|
|
831
|
+
if(message!==transient) throw error;
|
|
832
|
+
lastError=error;
|
|
833
|
+
}
|
|
834
|
+
const elapsed=Math.max(0,Number(nowFn())-started);
|
|
835
|
+
if(elapsed>=timeoutMs){
|
|
836
|
+
throw new Error(`downstream core did not appear for bridge ${bridgePid} within ${timeoutMs}ms: ${String(lastError?.message??lastError)}`);
|
|
837
|
+
}
|
|
838
|
+
await sleepFn(Math.min(pollMs,Math.max(1,timeoutMs-elapsed)));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
746
841
|
async function captureRuntime(ctx){
|
|
747
842
|
const statusText=controllerCaptured(ctx,'status',15000); const status=parseControllerStatus(statusText);
|
|
748
843
|
if(!status.online||!status.bridge_pid||!status.cloud_pid||!status.local_ready) throw new Error(`slot update requires ONLINE runtime: ${statusText}`);
|
|
@@ -826,7 +921,7 @@ async function startAndVerify(ctx,oldRuntime){
|
|
|
826
921
|
catch {} await sleep(500);
|
|
827
922
|
}
|
|
828
923
|
if(!status?.online||!status.bridge_pid||!status.cloud_pid||!status.local_ready) throw new Error(`new runtime failed ONLINE gate: ${statusText}`);
|
|
829
|
-
const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=
|
|
924
|
+
const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=await waitForCoreProcess(ctx.core_root,bridge);
|
|
830
925
|
if(oldRuntime?.bridge&&sameGeneration(oldRuntime.bridge,bridge)) throw new Error('fresh bridge generation gate failed');
|
|
831
926
|
if(oldRuntime?.cloud&&sameGeneration(oldRuntime.cloud,cloud)) throw new Error('fresh cloud generation gate failed');
|
|
832
927
|
if(oldRuntime?.core&&oldRuntime.core.pid===core.pid&&oldRuntime.core.creation_date===core.creation_date) throw new Error('fresh downstream core generation gate failed');
|
|
@@ -1303,6 +1398,23 @@ function handoffPriorPointer(body){
|
|
|
1303
1398
|
function handoffNewPointer(body){
|
|
1304
1399
|
return body.new_pointer?parseActivePointer(JSON.stringify(body.new_pointer)):null;
|
|
1305
1400
|
}
|
|
1401
|
+
async function verifyCompletedUpdateReceipt(body,state,statePath,key){
|
|
1402
|
+
const candidate=handoffNewPointer(body),prior=handoffPriorPointer(body);
|
|
1403
|
+
if(!candidate||!samePublicPointer(candidate,statePointer(state,'new_pointer'))||!samePublicPointer(prior,statePointer(state,'prior_pointer'))) throw new Error('completed update handoff pointer binding mismatch');
|
|
1404
|
+
if(state.expected_version!==candidate.version||state.expected_manifest_sha256!==candidate.manifest_sha256) throw new Error('completed update journal subject mismatch');
|
|
1405
|
+
const expectedPath=path.join(path.dirname(statePath),'receipt.json');
|
|
1406
|
+
if(typeof state.receipt_path!=='string'||!path.isAbsolute(state.receipt_path)||path.resolve(state.receipt_path)!==expectedPath||!HEX64.test(String(state.receipt_sha256??''))) throw new Error('completed update receipt reference invalid');
|
|
1407
|
+
const info=await lstat(expectedPath);
|
|
1408
|
+
if(!info.isFile()||info.isSymbolicLink()) throw new Error('completed update receipt must be a regular file');
|
|
1409
|
+
const receipt=await readJson(expectedPath);
|
|
1410
|
+
if(!verifySlotUpdateEnvelope(receipt,key.publicKey)||receipt.receipt_sha256!==state.receipt_sha256) throw new Error('completed update receipt signature or digest mismatch');
|
|
1411
|
+
const subject=receipt.body;
|
|
1412
|
+
if(subject.schema!==UPDATE_SCHEMA||subject.rollback_performed!==false||subject.update_id!==body.update_id||subject.release_version!==candidate.version||subject.candidate_manifest_sha256!==candidate.manifest_sha256||
|
|
1413
|
+
!samePublicPointer(statePointer(subject,'active_pointer'),candidate)||!samePublicPointer(statePointer(subject,'prior_pointer'),prior)||
|
|
1414
|
+
subject.release_slot?.release_id!==candidate.release_id||subject.release_slot?.release_root!==candidate.release_root||subject.release_slot?.manifest_sha256!==candidate.manifest_sha256||
|
|
1415
|
+
subject.generation?.generation_root!==candidate.generation_root||subject.generation?.generation_manifest_sha256!==candidate.generation_manifest_sha256||subject.launcher?.sha256!==candidate.launcher_sha256) throw new Error('completed update receipt subject binding mismatch');
|
|
1416
|
+
return {receipt_path:expectedPath,receipt_sha256:receipt.receipt_sha256};
|
|
1417
|
+
}
|
|
1306
1418
|
async function resumeSlotUpdate({handoffPath}){
|
|
1307
1419
|
const absoluteHandoff=path.resolve(handoffPath),probe=await readJson(absoluteHandoff);
|
|
1308
1420
|
const stateRoot=path.resolve(String(probe?.body?.state_root??path.dirname(path.dirname(path.dirname(path.dirname(absoluteHandoff))))));
|
|
@@ -1311,8 +1423,9 @@ async function resumeSlotUpdate({handoffPath}){
|
|
|
1311
1423
|
if(releaseState.schema!=='deadbyte.r27-slot-update-state.v1'||releaseState.update_id!==body.update_id) throw new Error('handoff state binding mismatch');
|
|
1312
1424
|
let phase=String(releaseState.phase??'');
|
|
1313
1425
|
if(phase==='completed'){
|
|
1426
|
+
const verifiedReceipt=await verifyCompletedUpdateReceipt(body,releaseState,statePath,key);
|
|
1314
1427
|
return {ok:true,resumed:false,completed:true,update_id:body.update_id,state_path:statePath,
|
|
1315
|
-
|
|
1428
|
+
...verifiedReceipt,historical_receipt_verified:true,current_runtime_verified:false};
|
|
1316
1429
|
}
|
|
1317
1430
|
if(phase.startsWith('failed')) throw new Error('slot update already failed at phase='+phase);
|
|
1318
1431
|
const priorPointer=handoffPriorPointer(body),candidatePointer=handoffNewPointer(body);
|
|
@@ -1704,6 +1817,7 @@ export async function runSlotUpdateCli(argv=process.argv.slice(2),{stateRoot=DEF
|
|
|
1704
1817
|
console.log('UPDATE_ID '+result.update_id);
|
|
1705
1818
|
console.log('STATE_PATH '+result.state_path);
|
|
1706
1819
|
if(result.receipt_sha256) console.log('RECEIPT_SHA256 '+result.receipt_sha256);
|
|
1820
|
+
if(result.historical_receipt_verified===true) console.log('VERIFICATION_SCOPE historical-signed-receipt; current-runtime-not-observed');
|
|
1707
1821
|
return result;
|
|
1708
1822
|
}
|
|
1709
1823
|
if(argv.includes('--recover-interrupted')){
|
package/src/version.mjs
CHANGED
|
@@ -17,5 +17,8 @@ test('R21.1 binds controller execution to CoreRoot controller directory', async
|
|
|
17
17
|
|
|
18
18
|
test('R21.1 validates controller root before creating command runtime directories', async () => {
|
|
19
19
|
const source = await readFile(controller, 'utf8');
|
|
20
|
-
|
|
20
|
+
const dispatch = source.slice(source.indexOf('function Run-CommandCore('), source.indexOf('function Run-Command(', source.indexOf('function Run-CommandCore(')));
|
|
21
|
+
assert.match(dispatch, /\$cfg = Get-Config\s+Assert-ControllerRootBound \$cfg\s+\$readOnly = \$Name -in/);
|
|
22
|
+
assert.match(dispatch, /if \(-not \$readOnly\) \{ Ensure-Directory \$RuntimeDir; Ensure-Directory \$LogDir \}/);
|
|
23
|
+
assert.ok(dispatch.indexOf('Assert-ControllerRootBound $cfg') < dispatch.indexOf('Ensure-Directory $RuntimeDir'));
|
|
21
24
|
});
|
package/test/controller.test.mjs
CHANGED
|
@@ -12,7 +12,8 @@ test('controller derives runtime state from observed identities and readiness',
|
|
|
12
12
|
assert.match(source, /'OFFLINE'/);
|
|
13
13
|
assert.match(source, /'PARKED'/);
|
|
14
14
|
assert.match(source, /'ONLINE'/);
|
|
15
|
-
assert.match(source, /'
|
|
15
|
+
assert.match(source, /'STARTING'/);
|
|
16
|
+
assert.match(source, /'FAILED'/);
|
|
16
17
|
assert.doesNotMatch(source, /state\.txt/i);
|
|
17
18
|
});
|
|
18
19
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {cp,mkdir,mkdtemp,readFile,writeFile,readdir,symlink} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const source=fileURLToPath(new URL('../..',import.meta.url));
|
|
7
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
8
|
+
export async function makeDoctorCliFixture(){
|
|
9
|
+
const base='D:/Projects/deadbyte-mcp/testing/0.15.35/fixtures';await mkdir(base,{recursive:true});
|
|
10
|
+
const root=await mkdtemp(path.join(base,'doctor-cli-')),pkg=path.join(root,'package'),state=path.join(root,'state'),temp=path.join(root,'temp');
|
|
11
|
+
await mkdir(pkg);await mkdir(state);await mkdir(temp);await mkdir(path.join(pkg,'bin'));
|
|
12
|
+
// Test-only package copy: current source bytes plus required doctor metadata.
|
|
13
|
+
// This is NOT an npm pack, immutable slot, or release acceptance artifact.
|
|
14
|
+
await cp(path.join(source,'src'),path.join(pkg,'src'),{recursive:true});
|
|
15
|
+
await cp(path.join(source,'bridge'),path.join(pkg,'bridge'),{recursive:true});
|
|
16
|
+
for(const file of ['package.json','RELEASE.json','TEST-MATRIX.json','PROVENANCE.json','SBOM.cdx.json','bin/deadbyte-hardware-observer-r45.exe'])await cp(path.join(source,file),path.join(pkg,file));
|
|
17
|
+
const rows=[];
|
|
18
|
+
const walk=async dir=>{for(const entry of await readdir(dir,{withFileTypes:true})){const file=path.join(dir,entry.name);if(entry.isDirectory())await walk(file);else if(entry.isFile())rows.push(sha(await readFile(file))+' '+path.relative(pkg,file).replaceAll('\\','/'));else throw new Error('unexpected fixture source link');}};
|
|
19
|
+
await walk(pkg);rows.sort();await writeFile(path.join(pkg,'MANIFEST.SHA256'),rows.join('\n')+'\n');
|
|
20
|
+
// Dependency junction belongs exclusively to this fresh fixture and points to
|
|
21
|
+
// candidate dependencies, never production state or a release slot.
|
|
22
|
+
await symlink(path.join(source,'node_modules'),path.join(pkg,'node_modules'),'junction');
|
|
23
|
+
const env={...process.env};for(const key of Object.keys(env))if(key.startsWith('DEADBYTE_'))delete env[key];
|
|
24
|
+
Object.assign(env,{DEADBYTE_STATE_ROOT:state,TEMP:temp,TMP:temp});
|
|
25
|
+
return {root,pkg,state,env,entry:path.join(pkg,'src','deadbyte-cli.mjs'),manifestFiles:rows.length};
|
|
26
|
+
}
|