deadbyte-mcp 0.15.34 → 0.15.36
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 +100 -38
- package/PROVENANCE.json +87 -41
- package/README.txt +72 -14
- package/RELEASE.json +187 -150
- package/SBOM.cdx.json +25 -3
- package/TEST-MATRIX.json +258 -151
- package/bridge/package.json +13 -0
- package/bridge/src/bridge-health-r59.mjs +52 -0
- package/bridge/src/bridge.mjs +487 -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 +206 -0
- package/controller/deadbyte-controller.ps1 +152 -18
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R59-TASK-CONTRACT.md +26 -0
- package/package.json +7 -3
- package/proof/r59-full-regression-current.json +13 -0
- package/proof/r59-full-regression-current.stderr.log +0 -0
- package/proof/r59-full-regression-current.stdout.log +1268 -0
- 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/gate-windows-r59-prepublish.ps1 +41 -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/r59-full-regression-capture.mjs +35 -0
- package/scripts/windows-gate-evidence-r58.mjs +93 -0
- package/src/capability-resolver.mjs +25 -7
- package/src/capability-snapshot.mjs +52 -0
- package/src/compact-gateway-mcp-tools.mjs +10 -11
- 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/mcp-server.mjs +179 -118
- package/src/r40-compact-disarmed-probe.mjs +1 -1
- package/src/r44-compact-disarmed-probe.mjs +1 -1
- package/src/r45-compact-disarmed-probe.mjs +1 -1
- package/src/release-generation.mjs +1 -0
- package/src/release-slot.mjs +2 -1
- package/src/remote-mcp-r40-parity-client.mjs +1 -1
- package/src/remote-mcp-r44-parity-client.mjs +1 -1
- package/src/remote-mcp-r45-parity-client.mjs +1 -1
- package/src/remote-mcp-smoke-client.mjs +1 -1
- package/src/remote-session-guardian.mjs +8 -1
- package/src/runtime-plane-registry.mjs +74 -0
- package/src/runtime-update.mjs +85 -2
- package/src/version.mjs +1 -1
- package/test/bridge-runtime-r59.test.mjs +60 -0
- package/test/capability-resolver.test.mjs +16 -0
- package/test/capability-snapshot-r59.test.mjs +39 -0
- package/test/compact-gateway-r36.test.mjs +22 -2
- package/test/compact-max-catalog-r44.test.mjs +1 -1
- package/test/compact-max-catalog-r45.test.mjs +1 -1
- package/test/contained.test.mjs +2 -1
- package/test/controller-policy-preflight-r59.test.mjs +39 -0
- package/test/controller-r211.test.mjs +4 -1
- package/test/controller.test.mjs +2 -1
- package/test/fixtures/r59-compact-surface-v0.15.35.json +222 -0
- package/test/helpers/r58-doctor-fixture.mjs +26 -0
- package/test/observation-integration.test.mjs +2 -3
- package/test/optional-plane-degraded-r59.test.mjs +44 -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 +24 -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 +98 -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 +50 -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 +32 -0
- package/test/r59-release-contract.test.mjs +61 -0
- package/test/r59-tool-surface-compat.test.mjs +90 -0
- package/test/release-generation.test.mjs +4 -1
- package/test/remote-session-cli.test.mjs +55 -2
- package/test/runtime-plane-registry-r59.test.mjs +40 -0
|
@@ -146,7 +146,8 @@ export async function resolveCapabilityStates({
|
|
|
146
146
|
desktopPolicy = null,
|
|
147
147
|
autonomousRuntime = null,
|
|
148
148
|
skillRuntime = null,
|
|
149
|
-
hostPolicy = null
|
|
149
|
+
hostPolicy = null,
|
|
150
|
+
runtimePlanes = null
|
|
150
151
|
} = {}) {
|
|
151
152
|
const capabilities = ledger?.entries ?? [];
|
|
152
153
|
const resolved = [];
|
|
@@ -201,9 +202,26 @@ export async function resolveCapabilityStates({
|
|
|
201
202
|
let state = CAPABILITY_STATES.AVAILABLE;
|
|
202
203
|
let ready = true;
|
|
203
204
|
let diagnostic = null;
|
|
205
|
+
const livePlane = cap.runtime_flag && runtimePlanes && typeof runtimePlanes === 'object'
|
|
206
|
+
? runtimePlanes[cap.runtime_flag] ?? null
|
|
207
|
+
: null;
|
|
204
208
|
|
|
205
|
-
// 1.
|
|
206
|
-
|
|
209
|
+
// 1. A live plane overlay may only reduce availability declared by the
|
|
210
|
+
// signed ledger. It cannot create a capability or bypass later authority
|
|
211
|
+
// checks.
|
|
212
|
+
if (livePlane && livePlane.ready !== true) {
|
|
213
|
+
const observed=String(livePlane.state??'UNAVAILABLE');
|
|
214
|
+
state=observed==='DEGRADED'?CAPABILITY_STATES.DEGRADED:
|
|
215
|
+
observed==='DISABLED'?CAPABILITY_STATES.DISABLED:CAPABILITY_STATES.UNAVAILABLE;
|
|
216
|
+
ready=false;
|
|
217
|
+
diagnostic={
|
|
218
|
+
code:String(livePlane.diagnostic?.code??`RUNTIME_PLANE_${observed}`).slice(0,128),
|
|
219
|
+
reason:String(livePlane.diagnostic?.reason??`Runtime plane ${cap.runtime_flag} is ${observed}`).slice(0,1024),
|
|
220
|
+
recoverable:livePlane.diagnostic?.recoverable===true
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// 2. Runtime Flag Disabled check
|
|
224
|
+
else if (cap.runtime_flag && env[cap.runtime_flag] === '0') {
|
|
207
225
|
state = CAPABILITY_STATES.DISABLED;
|
|
208
226
|
ready = false;
|
|
209
227
|
diagnostic = {
|
|
@@ -212,7 +230,7 @@ export async function resolveCapabilityStates({
|
|
|
212
230
|
recoverable: false
|
|
213
231
|
};
|
|
214
232
|
}
|
|
215
|
-
//
|
|
233
|
+
// 3. Tool presence check
|
|
216
234
|
else if (!catalogSet.has(activeTool)) {
|
|
217
235
|
state = CAPABILITY_STATES.UNAVAILABLE;
|
|
218
236
|
ready = false;
|
|
@@ -222,7 +240,7 @@ export async function resolveCapabilityStates({
|
|
|
222
240
|
recoverable: false
|
|
223
241
|
};
|
|
224
242
|
}
|
|
225
|
-
//
|
|
243
|
+
// 4. Subsystem runtime instantiation check
|
|
226
244
|
else if (cap.id.startsWith('machine_fs.') && !machineFsRuntime) {
|
|
227
245
|
state = CAPABILITY_STATES.UNAVAILABLE;
|
|
228
246
|
ready = false;
|
|
@@ -256,7 +274,7 @@ export async function resolveCapabilityStates({
|
|
|
256
274
|
recoverable: true
|
|
257
275
|
};
|
|
258
276
|
}
|
|
259
|
-
//
|
|
277
|
+
// 5. Session Authority Checks
|
|
260
278
|
else if (requires.includes('coding_session.armed') && !codingStatus?.armed) {
|
|
261
279
|
state = CAPABILITY_STATES.DENIED;
|
|
262
280
|
ready = false;
|
|
@@ -298,7 +316,7 @@ export async function resolveCapabilityStates({
|
|
|
298
316
|
recoverable: true
|
|
299
317
|
};
|
|
300
318
|
}
|
|
301
|
-
//
|
|
319
|
+
// 6. Subsystem Health Probes (e.g. Skill Plane envelope verification)
|
|
302
320
|
else if (cap.id === 'skill.catalog') {
|
|
303
321
|
if (skillListResult?.invalid_skills && skillListResult.invalid_skills.length > 0) {
|
|
304
322
|
state = CAPABILITY_STATES.DEGRADED;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
4
|
+
function canonicalJson(value){
|
|
5
|
+
if(value===null||typeof value!=='object') return JSON.stringify(value);
|
|
6
|
+
if(Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
7
|
+
return `{${Object.keys(value).sort((a,b)=>a.localeCompare(b,'en')).map(key=>`${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
8
|
+
}
|
|
9
|
+
const sha=value=>createHash('sha256').update(canonicalJson(value),'utf8').digest('hex');
|
|
10
|
+
function iso(ms){return new Date(ms).toISOString();}
|
|
11
|
+
function boundedPlaneState(raw){
|
|
12
|
+
const diagnostic=raw?.diagnostic&&typeof raw.diagnostic==='object'?{
|
|
13
|
+
code:String(raw.diagnostic.code??'UNKNOWN').slice(0,128),
|
|
14
|
+
reason:String(raw.diagnostic.reason??'').replace(/[\r\n\t]+/g,' ').slice(0,512),
|
|
15
|
+
recoverable:raw.diagnostic.recoverable===true
|
|
16
|
+
}:null;
|
|
17
|
+
return Object.freeze({state:String(raw?.state??'UNAVAILABLE'),ready:raw?.ready===true,
|
|
18
|
+
policy_sha256:HEX64.test(String(raw?.policy_sha256??''))?raw.policy_sha256:null,diagnostic});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createCapabilitySnapshotTracker({clock=Date.now,ttlMs=5000}={}){
|
|
22
|
+
if(typeof clock!=='function'||!Number.isInteger(ttlMs)||ttlMs<1000||ttlMs>15000) throw new Error('capability snapshot tracker options invalid');
|
|
23
|
+
let lastFingerprint=null,stateEpoch=0,lastSnapshotId=null;
|
|
24
|
+
function snapshot({profile,ledger,toolCatalog,catalogSha256,resolution,bindings={},planes={}}){
|
|
25
|
+
if(!ledger||!HEX64.test(String(ledger.ledger_sha256??''))||!Array.isArray(ledger.entries)) throw new Error('capability snapshot ledger invalid');
|
|
26
|
+
if(!Array.isArray(toolCatalog)||toolCatalog.some(name=>typeof name!=='string')||new Set(toolCatalog).size!==toolCatalog.length) throw new Error('capability snapshot catalog invalid');
|
|
27
|
+
if(!resolution||!Array.isArray(resolution.capabilities)) throw new Error('capability snapshot resolution invalid');
|
|
28
|
+
const signedIds=new Set(ledger.entries.map(entry=>entry.id));
|
|
29
|
+
for(const capability of resolution.capabilities) if(!signedIds.has(capability.id)) throw new Error(`capability '${capability.id}' is outside signed ledger`);
|
|
30
|
+
const safePlanes=Object.freeze(Object.fromEntries(Object.entries(planes).sort(([a],[b])=>a.localeCompare(b,'en')).map(([id,value])=>[id,boundedPlaneState(value)])));
|
|
31
|
+
const capabilities=resolution.capabilities.map(capability=>Object.freeze({...capability,
|
|
32
|
+
selected_route:Object.freeze({tool:capability.tool,operations:[...(capability.operations??[])]})
|
|
33
|
+
}));
|
|
34
|
+
const safeBindings={
|
|
35
|
+
release_id:typeof bindings.release_id==='string'?bindings.release_id:null,
|
|
36
|
+
release_manifest_sha256:HEX64.test(String(bindings.release_manifest_sha256??''))?bindings.release_manifest_sha256:null,
|
|
37
|
+
generation_manifest_sha256:HEX64.test(String(bindings.generation_manifest_sha256??''))?bindings.generation_manifest_sha256:null
|
|
38
|
+
};
|
|
39
|
+
const body={profile,ledger_sha256:ledger.ledger_sha256,tool_catalog:[...toolCatalog],tool_count:toolCatalog.length,
|
|
40
|
+
catalog_sha256:catalogSha256,capabilities,effective_capabilities:[...(resolution.effective_capabilities??[])],
|
|
41
|
+
unavailable_capabilities:[...(resolution.unavailable_capabilities??[])],degraded_capabilities:[...(resolution.degraded_capabilities??[])],
|
|
42
|
+
bindings:safeBindings,planes:safePlanes};
|
|
43
|
+
const fingerprint=sha(body);
|
|
44
|
+
if(fingerprint!==lastFingerprint){stateEpoch++;lastFingerprint=fingerprint;lastSnapshotId=sha({fingerprint,state_epoch:stateEpoch});}
|
|
45
|
+
const observed=Number(clock());
|
|
46
|
+
if(!Number.isFinite(observed)) throw new Error('capability snapshot clock invalid');
|
|
47
|
+
return Object.freeze({schema:'deadbyte.capability-manifest.v3',...body,snapshot_id:lastSnapshotId,state_epoch:stateEpoch,
|
|
48
|
+
observed_at_utc:iso(observed),valid_until_utc:iso(observed+ttlMs)});
|
|
49
|
+
}
|
|
50
|
+
return Object.freeze({snapshot});
|
|
51
|
+
}
|
|
52
|
+
|
|
@@ -3,6 +3,7 @@ import * as z from 'zod/v4';
|
|
|
3
3
|
import { compactGatewayOutputSchema, compactGatewayResult } from './compact-gateway-output-contracts.mjs';
|
|
4
4
|
import { desktopControlInputSchema,invokeDesktopOperation } from './desktop-mcp-tools-r40.mjs';
|
|
5
5
|
import { resolveCapabilityStates } from './capability-resolver.mjs';
|
|
6
|
+
import { createCapabilitySnapshotTracker } from './capability-snapshot.mjs';
|
|
6
7
|
|
|
7
8
|
const id=z.string().regex(/^[A-Za-z0-9._-]{1,64}$/);
|
|
8
9
|
const rel=z.string().min(1).max(2048);
|
|
@@ -71,7 +72,7 @@ function unavailable(op,name){return compactGatewayResult({status:'unavailable',
|
|
|
71
72
|
async function invoke(op,runtime,name,call){
|
|
72
73
|
if(!runtime) return unavailable(op,name);
|
|
73
74
|
try{return compactGatewayResult({status:'ok',op,result:await call(runtime)},false);}
|
|
74
|
-
catch(error){return compactGatewayResult({status:'denied',op,error:String(error)},true);}
|
|
75
|
+
catch(error){return compactGatewayResult({status:error?.code==='DEADBYTE_RUNTIME_UNAVAILABLE'?'unavailable':'denied',op,error:String(error)},true);}
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
async function codingInspect(args,codingRuntime,semanticRuntime){
|
|
@@ -138,27 +139,25 @@ async function machineObserve(args,machineRuntime,observationRuntime){
|
|
|
138
139
|
return invoke(args.op,observationRuntime,'observation',map[args.op]);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
export function registerCompactGatewayTools(server,{profile,ledger,codingRuntime,semanticRuntime,machineFsRuntime,processRuntime,observationRuntime,machineRuntime,desktopRuntime,skillRuntime,autonomousRuntime,hostPolicy,env=process.env,catalogProvider}){
|
|
142
|
+
export function registerCompactGatewayTools(server,{profile,ledger,codingRuntime,semanticRuntime,machineFsRuntime,processRuntime,observationRuntime,machineRuntime,desktopRuntime,skillRuntime,autonomousRuntime,hostPolicy,env=process.env,catalogProvider,runtimePlanes=null,releaseBindingProvider=null}){
|
|
142
143
|
if(!server) throw new Error('compact gateway registration requires server');
|
|
143
144
|
if(profile!=='compact') return;
|
|
144
145
|
if(typeof catalogProvider!=='function') throw new Error('compact gateway requires catalogProvider');
|
|
146
|
+
const capabilitySnapshots=createCapabilitySnapshotTracker({ttlMs:5000});
|
|
145
147
|
server.registerTool('capability_manifest',{description:'Read the canonical capability ledger and exact active compact tool catalog with runtime-aware resolution.',inputSchema:capabilityManifestInputSchema,outputSchema:compactGatewayOutputSchema},async()=>{
|
|
146
148
|
const toolCatalog=[...catalogProvider()].map(String).sort();
|
|
147
149
|
if(toolCatalog.length<1||new Set(toolCatalog).size!==toolCatalog.length) throw new Error('compact catalog provider returned invalid catalog');
|
|
148
150
|
const catalogSha256=createHash('sha256').update(JSON.stringify(toolCatalog),'utf8').digest('hex');
|
|
151
|
+
const livePlanes=typeof runtimePlanes==='function'?await runtimePlanes():runtimePlanes;
|
|
149
152
|
const dynamicResolution = await resolveCapabilityStates({
|
|
150
153
|
ledger, toolCatalog, profile, env,
|
|
151
154
|
codingRuntime, processRuntime, machineFsRuntime, observationRuntime, desktopRuntime,
|
|
152
|
-
autonomousRuntime, skillRuntime, hostPolicy
|
|
155
|
+
autonomousRuntime, skillRuntime, hostPolicy, runtimePlanes:livePlanes
|
|
153
156
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
effective_capabilities:dynamicResolution.effective_capabilities,
|
|
159
|
-
unavailable_capabilities:dynamicResolution.unavailable_capabilities,
|
|
160
|
-
degraded_capabilities:dynamicResolution.degraded_capabilities
|
|
161
|
-
}},false);
|
|
157
|
+
const bindings=typeof releaseBindingProvider==='function'?await releaseBindingProvider():{};
|
|
158
|
+
return compactGatewayResult({status:'ok',op:'manifest',result:capabilitySnapshots.snapshot({
|
|
159
|
+
profile,ledger,toolCatalog,catalogSha256,resolution:dynamicResolution,bindings,planes:livePlanes??{}
|
|
160
|
+
})},false);
|
|
162
161
|
});
|
|
163
162
|
server.registerTool('coding_inspect',{description:'Typed compact coding inspection and semantic navigation gateway. No arbitrary tool dispatch.',inputSchema:codingInspectInputSchema,outputSchema:compactGatewayOutputSchema},args=>codingInspect(args,codingRuntime,semanticRuntime));
|
|
164
163
|
server.registerTool('machine_fs',{description:'Typed compact machine filesystem gateway preserving root policy and mutation preconditions.',inputSchema:machineFsGatewayInputSchema,outputSchema:compactGatewayOutputSchema},args=>machineFs(args,machineFsRuntime));
|
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
|
+
}
|