deadbyte-mcp 0.15.33 → 0.15.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/MANIFEST.SHA256 +71 -22
  2. package/PROVENANCE.json +57 -28
  3. package/README.txt +22 -14
  4. package/RELEASE.json +41 -29
  5. package/SBOM.cdx.json +25 -3
  6. package/TEST-MATRIX.json +135 -43
  7. package/bridge/package.json +11 -0
  8. package/bridge/src/bridge.mjs +412 -0
  9. package/bridge/src/node-http-adapter.mjs +56 -0
  10. package/bridge/src/public-tool-definitions.mjs +14 -0
  11. package/bridge/src/r40-governor.mjs +160 -0
  12. package/controller/deadbyte-controller.ps1 +84 -8
  13. package/controller/deadbyte-process-policy.json +4 -4
  14. package/package.json +6 -3
  15. package/scripts/bridge-benchmark-r58.mjs +67 -0
  16. package/scripts/deploy-r48.mjs +2 -3
  17. package/scripts/gate-windows-r57.ps1 +27 -0
  18. package/scripts/gate-windows-r58.ps1 +33 -0
  19. package/scripts/owner-probe-r58.mjs +31 -0
  20. package/scripts/performance-r58.mjs +57 -0
  21. package/scripts/public-baseline-r58.mjs +72 -0
  22. package/scripts/public-surface-r58.mjs +84 -0
  23. package/scripts/r58-cross-plane.ps1 +6 -0
  24. package/scripts/r58-focused.ps1 +6 -0
  25. package/scripts/windows-gate-evidence-r57.mjs +81 -0
  26. package/scripts/windows-gate-evidence-r58.mjs +93 -0
  27. package/src/deadbyte-cli.mjs +40 -12
  28. package/src/doctor-foreground-r58.mjs +44 -0
  29. package/src/doctor-installation-r58.mjs +63 -0
  30. package/src/doctor-r45.mjs +21 -4
  31. package/src/doctor-runtime-r58.mjs +82 -0
  32. package/src/doctor-transactions-r58.mjs +40 -0
  33. package/src/foreground-process-identity.mjs +27 -0
  34. package/src/foreground-session-lock.mjs +20 -0
  35. package/src/foreground-session-lock.ps1 +15 -0
  36. package/src/release-generation.mjs +1 -0
  37. package/src/release-slot.mjs +2 -1
  38. package/src/remote-session-guardian.mjs +8 -1
  39. package/src/runtime-update.mjs +117 -3
  40. package/src/version.mjs +1 -1
  41. package/test/controller-r211.test.mjs +4 -1
  42. package/test/controller.test.mjs +2 -1
  43. package/test/helpers/r58-doctor-fixture.mjs +26 -0
  44. package/test/r49-windows-gate.test.mjs +14 -10
  45. package/test/r56-release-contract.test.mjs +5 -3
  46. package/test/r57-core-startup-race.test.mjs +85 -0
  47. package/test/r57-release-contract.test.mjs +67 -0
  48. package/test/r58-bridge-dependency-binding.test.mjs +62 -0
  49. package/test/r58-bundled-bridge.test.mjs +22 -0
  50. package/test/r58-cleanup-retention.test.mjs +45 -0
  51. package/test/r58-completed-update-receipt.test.mjs +76 -0
  52. package/test/r58-concurrent-recovery.test.mjs +54 -0
  53. package/test/r58-controller-core-observation.test.mjs +36 -0
  54. package/test/r58-controller-json.test.mjs +48 -0
  55. package/test/r58-controller-live-local.test.mjs +85 -0
  56. package/test/r58-controller-readonly.test.mjs +23 -0
  57. package/test/r58-controller-state-coherence.test.mjs +45 -0
  58. package/test/r58-doctor-cli.test.mjs +20 -0
  59. package/test/r58-doctor-foreground.test.mjs +50 -0
  60. package/test/r58-doctor-installation.test.mjs +55 -0
  61. package/test/r58-doctor-pointer.test.mjs +41 -0
  62. package/test/r58-doctor-runtime.test.mjs +45 -0
  63. package/test/r58-doctor-transactions.test.mjs +21 -0
  64. package/test/r58-guardian-channel.test.mjs +26 -0
  65. package/test/r58-launcher-shell.test.mjs +28 -0
  66. package/test/r58-owner-identity.test.mjs +20 -0
  67. package/test/r58-release-contract.test.mjs +49 -0
  68. package/test/r58-session-lock.test.mjs +26 -0
  69. package/test/r58-stale-owner.test.mjs +71 -0
  70. package/test/r58-windows-gate.test.mjs +31 -0
  71. package/test/release-generation.test.mjs +4 -1
  72. package/test/remote-session-cli.test.mjs +55 -2
@@ -0,0 +1,57 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { Client } from '@modelcontextprotocol/client';
6
+ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
7
+ import { generateTrustKeypair } from '../src/trust.mjs';
8
+
9
+ // Stage-one stdio measurements only. Never substitutes for bridge/public,
10
+ // machine-bound, mutating or durable-job release acceptance measurements.
11
+ const [targetArg,runArg]=process.argv.slice(2);
12
+ if(!targetArg||!runArg) throw new Error('usage: performance-r58.mjs <package-root> <new-D-testing-run-root>');
13
+ const target=path.resolve(targetArg), run=path.resolve(runArg);
14
+ const allowed=path.resolve('D:/Projects/deadbyte-mcp/testing/0.15.35');
15
+ const relative=path.relative(allowed,run);
16
+ if(!relative||relative.startsWith('..')||path.isAbsolute(relative)) throw new Error('benchmark run must be inside R58 D: testing root');
17
+ await mkdir(run); // Exclusive run identity; never rerun into existing evidence.
18
+ const state=path.join(run,'state'),workspace=path.join(run,'workspace'),demo=path.join(workspace,'demo');
19
+ await mkdir(demo,{recursive:true});await mkdir(state,{recursive:true});
20
+ const fixture='alpha benchmark fixture\n'.repeat(100);
21
+ await writeFile(path.join(demo,'input.txt'),fixture,{flag:'wx'});
22
+ const keys=await generateTrustKeypair({privateKeyPath:path.join(run,'private.pem'),publicKeyPath:path.join(run,'public.pem')});
23
+ const policyPath=path.join(run,'coding-policy.json');
24
+ await writeFile(policyPath,JSON.stringify({schema:'deadbyte.coding-policy.v1',enabled:true,lease_file:path.join(state,'coding-grant.json'),evidence_dir:path.join(state,'evidence'),roots:{bench:{path:demo,read:true,write:false,exec:false,deny_paths:[]}},commands:{},max_read_bytes:65536,max_write_bytes:65536}),{flag:'wx'});
25
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
26
+ const sourceHashes={};
27
+ for(const file of ['package.json','MANIFEST.SHA256','src/mcp-server.mjs','src/coding-plane.mjs','src/compact-gateway-mcp-tools.mjs']) sourceHashes[file]=sha(await readFile(path.join(target,file)));
28
+ const workload=[
29
+ {name:'workspace_open',arguments:{workspace_id:'demo'}},
30
+ {name:'file_read',arguments:{workspace_id:'demo',path:'input.txt'}},
31
+ {name:'coding_session_status',arguments:{}}
32
+ ];
33
+ const samples=[],cold=[];
34
+ let catalog;
35
+ const start=new Date().toISOString();
36
+ for(let batch=0;batch<3;batch++){
37
+ const client=new Client({name:'deadbyte-r58-performance',version:'1.0.0'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
38
+ const transport=new StdioClientTransport({command:process.execPath,args:[path.join(target,'src/mcp-server.mjs')],cwd:target,stderr:'pipe',env:{...getDefaultEnvironment(),DEADBYTE_STATE_ROOT:state,DEADBYTE_WORKSPACE_ROOT:workspace,DEADBYTE_SIGNING_KEY:keys.privateKeyPath,DEADBYTE_VERIFY_KEY:keys.publicKeyPath,DEADBYTE_PRIVILEGED_POLICY:'',DEADBYTE_CODING_RUNTIME:'1',DEADBYTE_CODING_POLICY:policyPath,DEADBYTE_MACHINE_RUNTIME:'0',DEADBYTE_PROCESS_RUNTIME:'0',DEADBYTE_AUTONOMOUS_RUNTIME:'0',DEADBYTE_DESKTOP_RUNTIME:'0',DEADBYTE_CAPABILITY_PROFILE:'full'}});
39
+ const errors=[];transport.stderr?.on('data',b=>errors.push(b.toString()));
40
+ try{
41
+ const connectStart=performance.now();await client.connect(transport);cold.push({batch,connect_ms:performance.now()-connectStart});
42
+ const tools=(await client.listTools()).tools;
43
+ if(!catalog){catalog=tools;await writeFile(path.join(run,'catalog.json'),JSON.stringify(tools,null,2),{flag:'wx'});}
44
+ for(let i=0;i<220;i++)for(const request of workload){
45
+ const t=performance.now();const result=await client.callTool(request);const elapsed=performance.now()-t;
46
+ if(result.isError) throw new Error(`benchmark ${request.name} error: ${JSON.stringify(result.structuredContent??result.content)}`);
47
+ if(request.name==='file_read'&&result.structuredContent?.text!==fixture)throw new Error('file read behavioral oracle failed');
48
+ if(i>=20)samples.push({batch,iteration:i-20,tool:request.name,total_stdio_roundtrip_ms:elapsed});
49
+ }
50
+ }finally{await client.close();await writeFile(path.join(run,`stderr-${batch}.log`),errors.join(''),{flag:'wx'});}
51
+ }
52
+ function summary(values){const v=[...values].sort((a,b)=>a-b);const q=p=>v[Math.ceil(p*v.length)-1];return {n:v.length,min:v[0],p50:q(.5),p90:q(.9),p95:q(.95),p99:q(.99),max:v.at(-1)};}
53
+ const rows=Object.fromEntries(workload.map(w=>[w.name,summary(samples.filter(s=>s.tool===w.name).map(s=>s.total_stdio_roundtrip_ms))]));
54
+ const result={schema:'deadbyte.r58.performance-stage1.v1',status:'MEASURED_NOT_RELEASE_ACCEPTANCE',start,end:new Date().toISOString(),target,node:process.version,sourceHashes,workload_sha256:sha(JSON.stringify(workload)),fixture_sha256:sha(fixture),cold_start:cold,rows,aggregate:summary(samples.map(s=>s.total_stdio_roundtrip_ms)),failures:0,timeouts:0,scope:'stdio client through MCP dispatch/core/response; coding disarmed; no live bridge/cloud or active machine binding',missing:['capability_manifest (compact profile)','machine_status','machine_ping','mutating actions','durable jobs','bridge/cloud','latency decomposition','contention','candidate comparison']};
55
+ await writeFile(path.join(run,'raw.json'),JSON.stringify(samples),{flag:'wx'});
56
+ await writeFile(path.join(run,'summary.json'),JSON.stringify(result,null,2),{flag:'wx'});
57
+ console.log(JSON.stringify(result,null,2));
@@ -0,0 +1,72 @@
1
+ import { readFile, mkdir, writeFile } from 'node:fs/promises';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { performance } from 'node:perf_hooks';
4
+ import path from 'node:path';
5
+ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
6
+ import { verifyMachinePingPublic } from '../src/machine-identity.mjs';
7
+
8
+ const run=path.resolve(process.argv[2]||'');
9
+ const mode=process.argv[3]||'public';
10
+ if(!['public','local'].includes(mode))throw new Error('transport must be public or local');
11
+ const base=path.resolve('D:/Projects/deadbyte-mcp/testing/0.15.35');
12
+ const rel=path.relative(base,run);
13
+ if(!rel||rel.startsWith('..')||path.isAbsolute(rel))throw new Error('new R58 testing run root required');
14
+ await mkdir(run);
15
+ const sha=b=>createHash('sha256').update(b).digest('hex');
16
+ const prod='C:/Users/DEADBYTE/.deadbyte-mcp';
17
+ const before=await readFile(path.join(prod,'active-release.json'));
18
+ const pointer=JSON.parse(before);
19
+ if(pointer.release_id!=='v0.15.34-2199bb59f0e0fc20'||pointer.manifest_sha256!=='2199bb59f0e0fc205d470d11f990d3ce3df444be5eba2a7d8dbc60d5f5b790bb')throw new Error('production baseline changed');
20
+ const config=await readFile(pointer.config_path,'utf8');
21
+ const urlPath=config.match(/McpUrlFile\s*=\s*'([^']+)'/)?.[1];
22
+ const urlHash=config.match(/McpUrlFileSha256\s*=\s*'([0-9a-f]{64})'/)?.[1];
23
+ if(!urlPath||!urlHash)throw new Error('pinned public endpoint metadata missing');
24
+ const bytes=await readFile(urlPath);
25
+ if(sha(bytes)!==urlHash)throw new Error('public endpoint metadata hash mismatch');
26
+ const endpoint=new URL(bytes.toString().trim());
27
+ if(endpoint.protocol!=='https:')throw new Error('HTTPS endpoint required');
28
+ let transportEndpoint=endpoint;
29
+ if(mode==='local'){
30
+ const readyText=config.match(/LocalReadyUri\s*=\s*'([^']+)'/)?.[1];
31
+ if(!readyText)throw new Error('pinned loopback readiness address missing');
32
+ const ready=new URL(readyText);
33
+ if(ready.protocol!=='http:'||ready.hostname!=='127.0.0.1')throw new Error('local benchmark must use configured loopback only');
34
+ transportEndpoint=new URL(endpoint.pathname+endpoint.search,ready.origin);
35
+ }
36
+ const client=new Client({name:'deadbyte-r58-public-readonly-baseline',version:'1.0.0'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
37
+ const traceFetch=process.argv[4]==='--trace-fetch';
38
+ const httpEvents=[];
39
+ const measuredFetch=async(input,init)=>{
40
+ let request;try{request=JSON.parse(init?.body||'null');}catch{}
41
+ const event={method:request?.method??init?.method??'GET',tool:request?.params?.name??null,started_ms:performance.now()};
42
+ httpEvents.push(event);
43
+ const response=await fetch(input,init);
44
+ event.headers_ms=performance.now();event.fetch_to_headers_ms=event.headers_ms-event.started_ms;
45
+ event.http_status=response.status;event.content_type=response.headers.get('content-type');
46
+ const json=response.json.bind(response);
47
+ response.json=async()=>{const t=performance.now();try{return await json();}finally{event.json_body_ms=performance.now()-t;event.body_finished_ms=performance.now();}};
48
+ return response;
49
+ };
50
+ const transport=new StreamableHTTPClientTransport(transportEndpoint,traceFetch?{fetch:measuredFetch}:undefined);
51
+ const samples=[];const start=new Date().toISOString();
52
+ function body(result){if(result.isError)throw new Error('public read-only call failed');const s=result.structuredContent;if(!s||typeof s!=='object')throw new Error('structured response missing');return s.result??s;}
53
+ try{
54
+ await client.connect(transport);
55
+ const catalog=(await client.listTools()).tools;
56
+ await writeFile(path.join(run,'catalog.json'),JSON.stringify(catalog,null,2),{flag:'wx'});
57
+ const status=body(await client.callTool({name:'machine_observe',arguments:{op:'machine_status'}}));
58
+ if(status.release_id!==pointer.release_id||status.release_manifest_sha256!==pointer.manifest_sha256||status.generation_manifest_sha256!==pointer.generation_manifest_sha256)throw new Error('public machine baseline binding mismatch');
59
+ for(let i=0;i<100;i++){
60
+ const nonce=randomBytes(24).toString('hex');const t=performance.now();const eventOffset=httpEvents.length;
61
+ const ping=body(await client.callTool({name:'machine_observe',arguments:{op:'machine_ping',nonce}}));
62
+ const elapsed=performance.now()-t;
63
+ if(ping.nonce!==nonce||ping.release_id!==pointer.release_id||ping.generation_manifest_sha256!==pointer.generation_manifest_sha256||!await verifyMachinePingPublic({ping,publicKeySpkiBase64:status.public_key_spki_base64}))throw new Error('public signed ping independent verification failed');
64
+ samples.push({tool:'machine_ping',iteration:i,public_roundtrip_ms:elapsed,...(traceFetch?{http_requests:httpEvents.slice(eventOffset).map(e=>({...e,after_body_to_client_ms:e.body_finished_ms===undefined?null:t+elapsed-e.body_finished_ms}))}:{})});
65
+ }
66
+ if(!before.equals(await readFile(path.join(prod,'active-release.json'))))throw new Error('production pointer changed during snapshot');
67
+ const v=samples.map(x=>x.public_roundtrip_ms).sort((a,b)=>a-b),q=p=>v[Math.ceil(v.length*p)-1];
68
+ const result={schema:'deadbyte.r58.public-baseline.v1',status:'OBSERVED_BASELINE_NOT_CANDIDATE_ACCEPTANCE',transport_scope:mode==='local'?'LOOPBACK_BRIDGE':'HTTPS_PUBLIC',start,end:new Date().toISOString(),pointer,endpoint_metadata_sha256:urlHash,catalog_count:catalog.length,catalog_sha256:sha(JSON.stringify(catalog)),signed_ping_count:samples.length,latency:{n:v.length,min:v[0],p50:q(.5),p90:q(.9),p95:q(.95),p99:q(.99),max:v.at(-1)},limitations:['100 samples only','transport difference is not exact causal layer decomposition','no candidate comparison','read-only observation not authority lifecycle acceptance']};
69
+ await writeFile(path.join(run,'raw.json'),JSON.stringify(samples),{flag:'wx'});
70
+ await writeFile(path.join(run,'summary.json'),JSON.stringify(result,null,2),{flag:'wx'});
71
+ console.log(JSON.stringify({status:result.status,catalog_count:catalog.length,signed_ping_count:samples.length,latency:result.latency}));
72
+ } finally {await client.close();}
@@ -0,0 +1,84 @@
1
+ import {createHash,randomBytes} from 'node:crypto';
2
+ import {createServer} from 'node:net';
3
+ import {mkdir,readFile,writeFile} from 'node:fs/promises';
4
+ import {spawn} from 'node:child_process';
5
+ import {setTimeout as delay} from 'node:timers/promises';
6
+ import path from 'node:path';
7
+ import {Client,StreamableHTTPClientTransport} from '@modelcontextprotocol/client';
8
+ import {generateTrustKeypair} from '../src/trust.mjs';
9
+
10
+ const [generationArg,baselineArg,runArg]=process.argv.slice(2);
11
+ if(!generationArg||!baselineArg||!runArg) throw new Error('usage: public-surface-r58.mjs <generation-root> <baseline-catalog-json> <new-run-root>');
12
+ const root=path.resolve(new URL('..',import.meta.url).pathname.replace(/^\/(?:[A-Za-z]:)/,m=>m.slice(1)));
13
+ const gen=path.resolve(generationArg),baselinePath=path.resolve(baselineArg),run=path.resolve(runArg);
14
+ const allowed=path.resolve('D:/Projects/deadbyte-mcp/testing/0.15.35'),rel=path.relative(allowed,run);
15
+ if(!rel||rel.startsWith('..')||path.isAbsolute(rel)) throw new Error('run root must be inside R58 testing root');
16
+ await mkdir(run);
17
+ const state=path.join(run,'state');await mkdir(state);
18
+ const keys=await generateTrustKeypair({privateKeyPath:path.join(run,'private.pem'),publicKeyPath:path.join(run,'public.pem')});
19
+ const semanticPath=path.join(gen,'semantic-lsp-config.json');
20
+ const sha=b=>createHash('sha256').update(b).digest('hex');
21
+ const semanticSha=sha(await readFile(semanticPath));
22
+ const baseline=JSON.parse(await readFile(baselinePath,'utf8'));
23
+ const baseSurface=baseline.map(x=>({name:x.name,inputSchema:x.inputSchema,outputSchema:x.outputSchema})).sort((a,b)=>a.name.localeCompare(b.name));
24
+ const reservation=createServer();await new Promise(resolve=>reservation.listen(0,'127.0.0.1',resolve));const port=reservation.address().port;await new Promise(resolve=>reservation.close(resolve));
25
+ const token=randomBytes(32).toString('hex');
26
+ const env={...process.env};for(const key of Object.keys(env))if(key.startsWith('DEADBYTE_'))delete env[key];
27
+ Object.assign(env,{
28
+ DEADBYTE_CORE_ROOT:root,
29
+ DEADBYTE_STATE_ROOT:state,
30
+ DEADBYTE_WORKSPACE_ROOT:path.join(root,'workspaces'),
31
+ DEADBYTE_BRIDGE_PORT:String(port),
32
+ DEADBYTE_BRIDGE_PATH_TOKEN:token,
33
+ DEADBYTE_SIGNING_KEY:keys.privateKeyPath,
34
+ DEADBYTE_VERIFY_KEY:keys.publicKeyPath,
35
+ DEADBYTE_CAPABILITY_PROFILE:'compact',
36
+ DEADBYTE_AUDIT_RUNTIME:'0',
37
+ DEADBYTE_MACHINE_RUNTIME:'1',
38
+ DEADBYTE_AGENT_RUNTIME:'1',
39
+ DEADBYTE_PROCESS_RUNTIME:'1',
40
+ DEADBYTE_PROCESS_POLICY:path.join(gen,'process-policy.json'),
41
+ DEADBYTE_CODING_RUNTIME:'1',
42
+ DEADBYTE_CODING_POLICY:path.join(gen,'coding-policy.json'),
43
+ DEADBYTE_MACHINE_FS_RUNTIME:'1',
44
+ DEADBYTE_OBSERVATION_RUNTIME:'1',
45
+ DEADBYTE_DESKTOP_RUNTIME:'1',
46
+ DEADBYTE_DESKTOP_POLICY:path.join(gen,'desktop-policy.json'),
47
+ DEADBYTE_SEMANTIC_LSP_CONFIG:semanticPath,
48
+ DEADBYTE_SEMANTIC_LSP_CONFIG_SHA256:semanticSha,
49
+ DEADBYTE_AUTONOMOUS_RUNTIME:'1',
50
+ DEADBYTE_AUTONOMOUS_POLICY:path.join(gen,'autonomous-policy.json'),
51
+ DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE:'external',
52
+ DEADBYTE_PRIVILEGED_POLICY:path.join(gen,'host-policy.json')
53
+ });
54
+ const bridge=path.join(root,'bridge','src','bridge.mjs');
55
+ const child=spawn(process.execPath,[bridge],{cwd:path.join(root,'bridge'),env,windowsHide:true,stdio:['ignore','pipe','pipe']});
56
+ let stdout='',stderr='';child.stdout.on('data',b=>stdout+=b);child.stderr.on('data',b=>stderr+=b);
57
+ const exited=new Promise(resolve=>child.once('exit',code=>resolve(code)));
58
+ try{
59
+ const deadline=Date.now()+20000;while(!stdout.includes('BRIDGE_READY')&&child.exitCode===null&&Date.now()<deadline)await delay(20);
60
+ if(!stdout.includes('BRIDGE_READY')) throw new Error('candidate bridge failed startup: '+stderr);
61
+ const client=new Client({name:'deadbyte-r58-public-surface',version:'1.0.0'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
62
+ try{
63
+ await client.connect(new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp/${token}`)));
64
+ const listed=(await client.listTools()).tools;
65
+ const candidate=listed.map(x=>({name:x.name,inputSchema:x.inputSchema,outputSchema:x.outputSchema})).sort((a,b)=>a.name.localeCompare(b.name));
66
+ const baseNames=baseSurface.map(x=>x.name),candidateNames=candidate.map(x=>x.name);
67
+ if(JSON.stringify(candidateNames)!==JSON.stringify(baseNames)) throw new Error(`public compact name drift baseline=${baseNames.length} candidate=${candidateNames.length}`);
68
+ const drift=[];
69
+ for(let i=0;i<baseSurface.length;i++){
70
+ if(JSON.stringify(baseSurface[i].inputSchema)!==JSON.stringify(candidate[i].inputSchema)||JSON.stringify(baseSurface[i].outputSchema)!==JSON.stringify(candidate[i].outputSchema)) drift.push(baseSurface[i].name);
71
+ }
72
+ if(drift.length) throw new Error('public schema drift: '+drift.join(','));
73
+ const result={schema:'deadbyte.r58.public-surface-compat.v1',status:'PASS',tool_count:candidate.length,names_sha256:sha(Buffer.from(JSON.stringify(candidateNames))),schema_sha256:sha(Buffer.from(JSON.stringify(candidate))),baseline_catalog_sha256:sha(await readFile(baselinePath)),candidate_bridge_sha256:sha(await readFile(bridge)),drift:[]};
74
+ await writeFile(path.join(run,'candidate-catalog.json'),JSON.stringify(listed,null,2));
75
+ await writeFile(path.join(run,'summary.json'),JSON.stringify(result,null,2));
76
+ console.log(JSON.stringify(result,null,2));
77
+ }finally{await client.close().catch(()=>{});}
78
+ }finally{
79
+ child.kill('SIGTERM');
80
+ await Promise.race([exited,delay(5000)]);
81
+ if(child.exitCode===null) child.kill();
82
+ await writeFile(path.join(run,'bridge.stdout.log'),stdout);
83
+ await writeFile(path.join(run,'bridge.stderr.log'),stderr);
84
+ }
@@ -0,0 +1,6 @@
1
+ $ErrorActionPreference='Stop'
2
+ Set-StrictMode -Version 3.0
3
+ $root=Split-Path $PSScriptRoot -Parent
4
+ Set-Location -LiteralPath $root
5
+ & node.exe --test --test-concurrency=1 test/r58-concurrent-recovery.test.mjs test/r58-controller-core-observation.test.mjs test/r58-controller-state-coherence.test.mjs test/r58-guardian-channel.test.mjs test/r58-owner-identity.test.mjs test/r58-session-lock.test.mjs test/r58-stale-owner.test.mjs test/remote-session-cli.test.mjs test/coding-runtime.test.mjs test/durable-job-r44.test.mjs test/external-job-provider-r44.test.mjs test/autonomous-local-supervisor.test.mjs test/autonomous-supervisor-integration.test.mjs test/runtime-self-update.test.mjs test/release-slot.test.mjs test/release-activation-gate.test.mjs test/named-tunnel-host.test.mjs test/r56-rollback-launcher-restore.test.mjs test/r57-core-startup-race.test.mjs
6
+ exit $LASTEXITCODE
@@ -0,0 +1,6 @@
1
+ $ErrorActionPreference='Stop'
2
+ Set-StrictMode -Version 3.0
3
+ $root=Split-Path $PSScriptRoot -Parent
4
+ Set-Location -LiteralPath $root
5
+ & node.exe --test --test-concurrency=1 test/r58-*.test.mjs test/release-generation.test.mjs test/runtime-self-update.test.mjs test/r57-core-startup-race.test.mjs test/r56-rollback-launcher-restore.test.mjs test/r51-runtime-coherence.test.mjs test/production-docs.test.mjs
6
+ exit $LASTEXITCODE
@@ -0,0 +1,81 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, open, readFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { doctor } from '../src/doctor-r45.mjs';
7
+
8
+ const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
9
+ const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
10
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
11
+ const insist=(ok,message)=>{if(!ok)throw new Error(message);};
12
+ const pkg=JSON.parse(await readFile(path.join(root,'package.json'),'utf8'));
13
+ const release=JSON.parse(await readFile(path.join(root,'RELEASE.json'),'utf8'));
14
+ const provenance=JSON.parse(await readFile(path.join(root,'PROVENANCE.json'),'utf8'));
15
+ const matrix=JSON.parse(await readFile(path.join(root,'TEST-MATRIX.json'),'utf8'));
16
+ const active=JSON.parse(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
17
+ const manifestSha256=sha256(await readFile(path.join(root,'MANIFEST.SHA256')));
18
+
19
+ insist(pkg.version==='0.15.34'&&release.version===pkg.version&&release.train==='R57','R57 candidate identity mismatch');
20
+ insist(release.parent.version==='0.15.33','R57 parent mismatch');
21
+ insist(release.parent.manifest_sha256==='776a51cb916e28ddc3cbb2ecd3857151a0e73e0aaf83d4e6308e6674125dd01c','R57 parent manifest mismatch');
22
+ insist(release.parent.generation_manifest_sha256==='20ccc5e4bbf8470b61e0a6d2ad181dad5e8533da980ceba7749e1d6f48d9dbd3','R57 parent generation mismatch');
23
+ insist(release.parent.tarball_sha256==='d84a6df0a972a43c50d5e31242d9217f59858b2d73c4284af296da76d2048e5f','R57 parent tarball mismatch');
24
+ insist(
25
+ active.release_id===release.runtime_predecessor.release_id&&
26
+ active.manifest_sha256===release.runtime_predecessor.manifest_sha256&&
27
+ active.generation_manifest_sha256===release.runtime_predecessor.generation_manifest_sha256,
28
+ 'active runtime is not exact sealed 0.15.33 predecessor'
29
+ );
30
+ insist(release.hotfix?.bounded_core_discovery_after_online===true,'R57 bounded core discovery missing');
31
+ insist(release.hotfix?.retry_only_missing_core===true,'R57 zero-child-only retry contract missing');
32
+ insist(release.hotfix?.ambiguous_core_fail_closed===true,'R57 ambiguous-core fail-closed contract missing');
33
+ insist(release.hotfix?.timeout_fail_closed===true,'R57 timeout fail-closed contract missing');
34
+ insist(release.hotfix?.full_regression==='PASS','R57 full regression metadata is not PASS');
35
+ insist(matrix.observed_pre_release_evidence?.full_regression?.status==='PASS','R57 full regression evidence is not PASS');
36
+ insist(provenance.operator_override?.stable_final_claim===false,'R57 must not claim stable/final');
37
+ insist(matrix.operator_override?.four_hour_soak==='NOT_COMPLETED','R57 matrix must preserve soak bypass');
38
+
39
+ const health=await doctor();
40
+ insist(health.status==='ok'&&health.package_version===pkg.version&&health.manifest?.verified===true,'R57 doctor evidence invalid');
41
+
42
+ const evidenceBody={
43
+ schema:'deadbyte.r57-windows-gate.v1',
44
+ status:'passed',
45
+ exit_code:0,
46
+ version:pkg.version,
47
+ train:'R57',
48
+ completed_at_utc:new Date().toISOString(),
49
+ manifest_sha256:manifestSha256,
50
+ parent:release.parent,
51
+ runtime_predecessor:release.runtime_predecessor,
52
+ hotfix:release.hotfix,
53
+ operator_override:provenance.operator_override,
54
+ active_predecessor:{
55
+ release_id:active.release_id,
56
+ version:active.version,
57
+ manifest_sha256:active.manifest_sha256,
58
+ generation_manifest_sha256:active.generation_manifest_sha256
59
+ },
60
+ doctor:{verified_entries:health.manifest.verified_entries},
61
+ gates:{
62
+ r57_focused:true,
63
+ package_boundary:true,
64
+ full_regression:true,
65
+ manifest_final_verify:true,
66
+ four_hour_soak_completed:false
67
+ }
68
+ };
69
+ const bytes=Buffer.from(JSON.stringify(evidenceBody,null,2)+'\n');
70
+ const dir=path.join(stateRoot,'evidence','release');
71
+ await mkdir(dir,{recursive:true});
72
+ const target=path.join(dir,`r57-windows-gate-${manifestSha256.slice(0,16)}.json`);
73
+ try{
74
+ const handle=await open(target,'wx',0o600);
75
+ try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
76
+ }catch(error){
77
+ if(error?.code!=='EEXIST')throw error;
78
+ const prior=JSON.parse(await readFile(target,'utf8'));
79
+ insist(prior.schema===evidenceBody.schema&&prior.status==='passed'&&prior.version===evidenceBody.version&&prior.manifest_sha256===manifestSha256,'conflicting R57 gate evidence identity');
80
+ }
81
+ console.log(JSON.stringify({status:'passed',path:target,sha256:sha256(bytes),manifest_sha256:manifestSha256}));
@@ -0,0 +1,93 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {mkdir,open,readFile} from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {doctor} from '../src/doctor-r45.mjs';
7
+
8
+ const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
9
+ const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
10
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
11
+ const insist=(ok,message)=>{if(!ok)throw new Error(message);};
12
+ const [pkg,release,provenance,matrix,active]=await Promise.all([
13
+ readFile(path.join(root,'package.json'),'utf8').then(JSON.parse),
14
+ readFile(path.join(root,'RELEASE.json'),'utf8').then(JSON.parse),
15
+ readFile(path.join(root,'PROVENANCE.json'),'utf8').then(JSON.parse),
16
+ readFile(path.join(root,'TEST-MATRIX.json'),'utf8').then(JSON.parse),
17
+ readFile(path.join(stateRoot,'active-release.json'),'utf8').then(JSON.parse)
18
+ ]);
19
+ const manifestSha256=sha256(await readFile(path.join(root,'MANIFEST.SHA256')));
20
+
21
+ insist(pkg.version==='0.15.35'&&release.version===pkg.version&&release.train==='R58','R58 candidate identity mismatch');
22
+ insist(provenance.version===pkg.version&&matrix.version===pkg.version&&matrix.train==='R58','R58 release metadata identity mismatch');
23
+ insist(release.parent?.version==='0.15.34','R58 parent version mismatch');
24
+ insist(release.parent?.release_id==='v0.15.34-2199bb59f0e0fc20','R58 parent release mismatch');
25
+ insist(release.parent?.manifest_sha256==='2199bb59f0e0fc205d470d11f990d3ce3df444be5eba2a7d8dbc60d5f5b790bb','R58 parent manifest mismatch');
26
+ insist(release.parent?.generation_manifest_sha256==='1a20a62aaeb324a9b5c03d158952f35092d58fedf2ac7725820ba2b094a4a88f','R58 parent generation mismatch');
27
+ insist(release.parent?.tarball_sha256==='a6f7b21faf34793ca17dfb6d0e9ec03320f1468d011e75d8349098df82fc2639','R58 parent tarball mismatch');
28
+ insist(
29
+ active.release_id===release.runtime_predecessor.release_id&&
30
+ active.manifest_sha256===release.runtime_predecessor.manifest_sha256&&
31
+ active.generation_manifest_sha256===release.runtime_predecessor.generation_manifest_sha256,
32
+ 'active runtime is not exact sealed 0.15.34 predecessor'
33
+ );
34
+ insist(release.consolidation?.bridge_bundled_inside_release===true,'R58 bundled bridge contract missing');
35
+ insist(release.consolidation?.bridge_recursive_local_dependency_binding===true,'R58 recursive bridge dependency contract missing');
36
+ insist(release.consolidation?.performance_acceptance==='PASS_BOUNDED_PREPUBLISH','R58 performance acceptance metadata not green');
37
+ insist(release.consolidation?.cross_plane_acceptance==='PASS','R58 cross-plane acceptance metadata not green');
38
+ insist(release.consolidation?.public_surface_compatibility==='PASS_43_TOOLS_NO_SCHEMA_DRIFT','R58 public surface contract not green');
39
+ insist(matrix.observed_pre_release_evidence?.full_regression?.status==='PASS','R58 fresh full regression evidence not green');
40
+ insist(matrix.observed_pre_release_evidence?.performance?.status==='PASS_BOUNDED_PREPUBLISH','R58 performance evidence not green');
41
+ insist(matrix.observed_pre_release_evidence?.cross_plane?.status==='PASS','R58 cross-plane evidence not green');
42
+ insist(matrix.observed_pre_release_evidence?.public_surface?.status==='PASS','R58 public surface evidence not green');
43
+ insist(provenance.operator_override?.four_hour_soak==='NOT_COMPLETED','R58 must preserve incomplete soak truth');
44
+ insist(provenance.operator_override?.clean_machine==='NOT_COMPLETED','R58 must preserve incomplete clean-machine truth');
45
+ insist(provenance.operator_override?.stable_final_claim===false,'R58 must not claim stable/final before external evidence');
46
+
47
+ const health=await doctor();
48
+ insist(health.assessment!=='FAIL'&&['ok','warn'].includes(health.status)&&health.package_version===pkg.version&&health.manifest?.verified===true,'R58 doctor evidence invalid');
49
+
50
+ const evidenceBody={
51
+ schema:'deadbyte.r58-windows-gate.v1',
52
+ status:'passed',
53
+ exit_code:0,
54
+ version:pkg.version,
55
+ train:'R58',
56
+ completed_at_utc:new Date().toISOString(),
57
+ manifest_sha256:manifestSha256,
58
+ parent:release.parent,
59
+ runtime_predecessor:release.runtime_predecessor,
60
+ consolidation:release.consolidation,
61
+ operator_override:provenance.operator_override,
62
+ active_predecessor:{
63
+ release_id:active.release_id,
64
+ version:active.version,
65
+ manifest_sha256:active.manifest_sha256,
66
+ generation_manifest_sha256:active.generation_manifest_sha256
67
+ },
68
+ doctor:{status:health.status,assessment:health.assessment,verified_entries:health.manifest.verified_entries,warnings:health.findings.filter(item=>item.level==='WARN').length},
69
+ gates:{
70
+ r58_focused:true,
71
+ package_boundary:true,
72
+ process_policy:true,
73
+ performance_recorded:true,
74
+ cross_plane_recorded:true,
75
+ public_surface_recorded:true,
76
+ full_regression_recorded:true,
77
+ manifest_final_verify:true,
78
+ four_hour_soak_completed:false,
79
+ clean_machine_completed:false
80
+ }
81
+ };
82
+ const bytes=Buffer.from(JSON.stringify(evidenceBody,null,2)+'\n');
83
+ const dir=path.join(stateRoot,'evidence','release');await mkdir(dir,{recursive:true});
84
+ const target=path.join(dir,`r58-windows-gate-${manifestSha256.slice(0,16)}.json`);
85
+ try{
86
+ const handle=await open(target,'wx',0o600);
87
+ try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
88
+ }catch(error){
89
+ if(error?.code!=='EEXIST')throw error;
90
+ const prior=JSON.parse(await readFile(target,'utf8'));
91
+ insist(prior.schema===evidenceBody.schema&&prior.status==='passed'&&prior.version===evidenceBody.version&&prior.manifest_sha256===manifestSha256,'conflicting R58 gate evidence identity');
92
+ }
93
+ console.log(JSON.stringify({status:'passed',path:target,sha256:sha256(bytes),manifest_sha256:manifestSha256}));
@@ -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
+ }