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.
Files changed (101) hide show
  1. package/MANIFEST.SHA256 +100 -38
  2. package/PROVENANCE.json +87 -41
  3. package/README.txt +72 -14
  4. package/RELEASE.json +187 -150
  5. package/SBOM.cdx.json +25 -3
  6. package/TEST-MATRIX.json +258 -151
  7. package/bridge/package.json +13 -0
  8. package/bridge/src/bridge-health-r59.mjs +52 -0
  9. package/bridge/src/bridge.mjs +487 -0
  10. package/bridge/src/node-http-adapter.mjs +56 -0
  11. package/bridge/src/public-tool-definitions.mjs +14 -0
  12. package/bridge/src/r40-governor.mjs +206 -0
  13. package/controller/deadbyte-controller.ps1 +152 -18
  14. package/controller/deadbyte-process-policy.json +4 -4
  15. package/docs/R59-TASK-CONTRACT.md +26 -0
  16. package/package.json +7 -3
  17. package/proof/r59-full-regression-current.json +13 -0
  18. package/proof/r59-full-regression-current.stderr.log +0 -0
  19. package/proof/r59-full-regression-current.stdout.log +1268 -0
  20. package/scripts/bridge-benchmark-r58.mjs +67 -0
  21. package/scripts/deploy-r48.mjs +2 -3
  22. package/scripts/gate-windows-r58.ps1 +33 -0
  23. package/scripts/gate-windows-r59-prepublish.ps1 +41 -0
  24. package/scripts/owner-probe-r58.mjs +31 -0
  25. package/scripts/performance-r58.mjs +57 -0
  26. package/scripts/public-baseline-r58.mjs +72 -0
  27. package/scripts/public-surface-r58.mjs +84 -0
  28. package/scripts/r58-cross-plane.ps1 +6 -0
  29. package/scripts/r58-focused.ps1 +6 -0
  30. package/scripts/r59-full-regression-capture.mjs +35 -0
  31. package/scripts/windows-gate-evidence-r58.mjs +93 -0
  32. package/src/capability-resolver.mjs +25 -7
  33. package/src/capability-snapshot.mjs +52 -0
  34. package/src/compact-gateway-mcp-tools.mjs +10 -11
  35. package/src/deadbyte-cli.mjs +40 -12
  36. package/src/doctor-foreground-r58.mjs +44 -0
  37. package/src/doctor-installation-r58.mjs +63 -0
  38. package/src/doctor-r45.mjs +21 -4
  39. package/src/doctor-runtime-r58.mjs +82 -0
  40. package/src/doctor-transactions-r58.mjs +40 -0
  41. package/src/foreground-process-identity.mjs +27 -0
  42. package/src/foreground-session-lock.mjs +20 -0
  43. package/src/foreground-session-lock.ps1 +15 -0
  44. package/src/mcp-server.mjs +179 -118
  45. package/src/r40-compact-disarmed-probe.mjs +1 -1
  46. package/src/r44-compact-disarmed-probe.mjs +1 -1
  47. package/src/r45-compact-disarmed-probe.mjs +1 -1
  48. package/src/release-generation.mjs +1 -0
  49. package/src/release-slot.mjs +2 -1
  50. package/src/remote-mcp-r40-parity-client.mjs +1 -1
  51. package/src/remote-mcp-r44-parity-client.mjs +1 -1
  52. package/src/remote-mcp-r45-parity-client.mjs +1 -1
  53. package/src/remote-mcp-smoke-client.mjs +1 -1
  54. package/src/remote-session-guardian.mjs +8 -1
  55. package/src/runtime-plane-registry.mjs +74 -0
  56. package/src/runtime-update.mjs +85 -2
  57. package/src/version.mjs +1 -1
  58. package/test/bridge-runtime-r59.test.mjs +60 -0
  59. package/test/capability-resolver.test.mjs +16 -0
  60. package/test/capability-snapshot-r59.test.mjs +39 -0
  61. package/test/compact-gateway-r36.test.mjs +22 -2
  62. package/test/compact-max-catalog-r44.test.mjs +1 -1
  63. package/test/compact-max-catalog-r45.test.mjs +1 -1
  64. package/test/contained.test.mjs +2 -1
  65. package/test/controller-policy-preflight-r59.test.mjs +39 -0
  66. package/test/controller-r211.test.mjs +4 -1
  67. package/test/controller.test.mjs +2 -1
  68. package/test/fixtures/r59-compact-surface-v0.15.35.json +222 -0
  69. package/test/helpers/r58-doctor-fixture.mjs +26 -0
  70. package/test/observation-integration.test.mjs +2 -3
  71. package/test/optional-plane-degraded-r59.test.mjs +44 -0
  72. package/test/r49-windows-gate.test.mjs +2 -2
  73. package/test/r57-release-contract.test.mjs +5 -3
  74. package/test/r58-bridge-dependency-binding.test.mjs +62 -0
  75. package/test/r58-bundled-bridge.test.mjs +24 -0
  76. package/test/r58-cleanup-retention.test.mjs +45 -0
  77. package/test/r58-completed-update-receipt.test.mjs +76 -0
  78. package/test/r58-concurrent-recovery.test.mjs +54 -0
  79. package/test/r58-controller-core-observation.test.mjs +36 -0
  80. package/test/r58-controller-json.test.mjs +48 -0
  81. package/test/r58-controller-live-local.test.mjs +98 -0
  82. package/test/r58-controller-readonly.test.mjs +23 -0
  83. package/test/r58-controller-state-coherence.test.mjs +45 -0
  84. package/test/r58-doctor-cli.test.mjs +20 -0
  85. package/test/r58-doctor-foreground.test.mjs +50 -0
  86. package/test/r58-doctor-installation.test.mjs +55 -0
  87. package/test/r58-doctor-pointer.test.mjs +41 -0
  88. package/test/r58-doctor-runtime.test.mjs +45 -0
  89. package/test/r58-doctor-transactions.test.mjs +21 -0
  90. package/test/r58-guardian-channel.test.mjs +26 -0
  91. package/test/r58-launcher-shell.test.mjs +28 -0
  92. package/test/r58-owner-identity.test.mjs +20 -0
  93. package/test/r58-release-contract.test.mjs +50 -0
  94. package/test/r58-session-lock.test.mjs +26 -0
  95. package/test/r58-stale-owner.test.mjs +71 -0
  96. package/test/r58-windows-gate.test.mjs +32 -0
  97. package/test/r59-release-contract.test.mjs +61 -0
  98. package/test/r59-tool-surface-compat.test.mjs +90 -0
  99. package/test/release-generation.test.mjs +4 -1
  100. package/test/remote-session-cli.test.mjs +55 -2
  101. 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. Runtime Flag Disabled check
206
- if (cap.runtime_flag && env[cap.runtime_flag] === '0') {
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
- // 2. Tool presence check
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
- // 3. Subsystem runtime instantiation check
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
- // 4. Session Authority Checks
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
- // 5. Subsystem Health Probes (e.g. Skill Plane envelope verification)
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
- return compactGatewayResult({status:'ok',op:'manifest',result:{
155
- schema:'deadbyte.capability-manifest.v2',profile,ledger_sha256:ledger?.ledger_sha256??null,
156
- tool_catalog:toolCatalog,tool_count:toolCatalog.length,catalog_sha256:catalogSha256,
157
- capabilities:dynamicResolution.capabilities,
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));
@@ -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) writeRemote(process.stderr,`${remoteTag('warn',displayMode)} cleanup ${command} exit=${result.status}\n`,displayMode);
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<1||
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)) throw new Error(`remote session already active pid=${ownerPid}`);
392
- if(prior.schema==='deadbyte.remote-session.v2'&&pidAlive(prior.guardian_pid)){
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 recoverRemoteSession(ctx,{displayMode});
443
- const marker=await claimRemoteSession(ctx);
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 DEGRADED; restarting verified owned components...\n':'[BOOT] Starting bridge/runtime...\n');
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
+ }
@@ -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 jsonOrNull(path.join(stateRoot(),'active-release.json'));
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
+ }