deadbyte-mcp 0.15.35 → 0.15.37

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 (92) hide show
  1. package/MANIFEST.SHA256 +91 -46
  2. package/PROVENANCE.json +76 -30
  3. package/README.txt +62 -8
  4. package/RELEASE.json +78 -23
  5. package/SBOM.cdx.json +10 -25
  6. package/TEST-MATRIX.json +109 -44
  7. package/bridge/package.json +4 -2
  8. package/bridge/src/bridge-health-r59.mjs +53 -0
  9. package/bridge/src/bridge.mjs +158 -26
  10. package/bridge/src/canonical-json-r60.mjs +13 -0
  11. package/bridge/src/r40-governor.mjs +56 -10
  12. package/bridge/src/reconnect-delay-r60.mjs +31 -0
  13. package/bridge/src/remote-call-claims-r60.mjs +157 -0
  14. package/bridge/src/remote-dispatch-r60.mjs +75 -0
  15. package/controller/deadbyte-controller.ps1 +76 -13
  16. package/controller/deadbyte-process-policy.json +4 -4
  17. package/docs/R59-PROVENANCE.json +87 -0
  18. package/docs/R59-RELEASE.json +187 -0
  19. package/docs/R59-SBOM.cdx.json +68 -0
  20. package/docs/R59-TASK-CONTRACT.md +26 -0
  21. package/docs/R59-TEST-MATRIX.json +258 -0
  22. package/docs/R59-bridge-package.json +13 -0
  23. package/docs/R59-package-lock.json +184 -0
  24. package/docs/R59-package.json +106 -0
  25. package/docs/R59-version.mjs +6 -0
  26. package/docs/R60-TASK-CONTRACT.md +22 -0
  27. package/package.json +7 -3
  28. package/proof/r59-full-regression-current.json +13 -0
  29. package/proof/r59-full-regression-current.stderr.log +0 -0
  30. package/proof/r59-full-regression-current.stdout.log +1268 -0
  31. package/scripts/full-regression-r60.mjs +32 -0
  32. package/scripts/gate-windows-r59-prepublish.ps1 +41 -0
  33. package/scripts/gate-windows-r60-prepublish.ps1 +74 -0
  34. package/scripts/r59-full-regression-capture.mjs +35 -0
  35. package/src/capability-resolver.mjs +25 -7
  36. package/src/capability-snapshot.mjs +56 -0
  37. package/src/compact-gateway-mcp-tools.mjs +10 -11
  38. package/src/deadbyte-cli.mjs +80 -34
  39. package/src/mcp-server.mjs +179 -118
  40. package/src/r40-compact-disarmed-probe.mjs +1 -1
  41. package/src/r44-compact-disarmed-probe.mjs +1 -1
  42. package/src/r45-compact-disarmed-probe.mjs +1 -1
  43. package/src/remote-authority.mjs +56 -0
  44. package/src/remote-enforcement-probe-r60.mjs +17 -0
  45. package/src/remote-live-log.mjs +19 -2
  46. package/src/remote-log-decode-r60.mjs +32 -0
  47. package/src/remote-mcp-machine-smoke-client.mjs +19 -1
  48. package/src/remote-mcp-r40-parity-client.mjs +1 -1
  49. package/src/remote-mcp-r44-parity-client.mjs +1 -1
  50. package/src/remote-mcp-r45-parity-client.mjs +1 -1
  51. package/src/remote-mcp-smoke-client.mjs +1 -1
  52. package/src/remote-session-guardian.mjs +20 -3
  53. package/src/runtime-plane-registry.mjs +74 -0
  54. package/src/tool-audit.mjs +5 -0
  55. package/src/version.mjs +1 -1
  56. package/test/bridge-runtime-r59.test.mjs +60 -0
  57. package/test/capability-resolver.test.mjs +16 -0
  58. package/test/capability-snapshot-r59.test.mjs +39 -0
  59. package/test/compact-gateway-r36.test.mjs +22 -2
  60. package/test/compact-max-catalog-r44.test.mjs +1 -1
  61. package/test/compact-max-catalog-r45.test.mjs +1 -1
  62. package/test/contained.test.mjs +2 -1
  63. package/test/controller-policy-preflight-r59.test.mjs +39 -0
  64. package/test/fixtures/r59-compact-surface-v0.15.35.json +222 -0
  65. package/test/observation-integration.test.mjs +2 -3
  66. package/test/optional-plane-degraded-r59.test.mjs +44 -0
  67. package/test/r48-release-contract.test.mjs +1 -1
  68. package/test/r49-windows-gate.test.mjs +1 -1
  69. package/test/r51-runtime-coherence.test.mjs +2 -2
  70. package/test/r58-bundled-bridge.test.mjs +3 -1
  71. package/test/r58-cleanup-retention.test.mjs +9 -3
  72. package/test/r58-concurrent-recovery.test.mjs +9 -3
  73. package/test/r58-controller-live-local.test.mjs +36 -4
  74. package/test/r58-controller-state-coherence.test.mjs +1 -1
  75. package/test/r58-release-contract.test.mjs +3 -2
  76. package/test/r58-stale-owner.test.mjs +2 -2
  77. package/test/r58-windows-gate.test.mjs +2 -1
  78. package/test/r59-release-contract.test.mjs +61 -0
  79. package/test/r59-tool-surface-compat.test.mjs +90 -0
  80. package/test/r60-authority-coherence.test.mjs +36 -0
  81. package/test/r60-authority-enforcement.test.mjs +28 -0
  82. package/test/r60-bridge-dispatch.test.mjs +119 -0
  83. package/test/r60-clock-discipline.test.mjs +15 -0
  84. package/test/r60-live-timeline.test.mjs +29 -0
  85. package/test/r60-reconnect-errors.test.mjs +21 -0
  86. package/test/r60-reconnect-jitter.test.mjs +18 -0
  87. package/test/r60-release-contract.test.mjs +81 -0
  88. package/test/r60-remote-call-recovery.test.mjs +170 -0
  89. package/test/r60-windows-log-decode.test.mjs +25 -0
  90. package/test/remote-live-log-r37.test.mjs +1 -1
  91. package/test/remote-session-cli.test.mjs +200 -43
  92. package/test/runtime-plane-registry-r59.test.mjs +40 -0
@@ -1,4 +1,5 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
3
  import { createServer } from 'node:http';
3
4
  import { readFileSync } from 'node:fs';
4
5
  import os from 'node:os';
@@ -9,8 +10,24 @@ import { McpServer, createMcpHandler, createRequestStateCodec, inputRequired, in
9
10
  import {preparePublicToolDefinitions} from './public-tool-definitions.mjs';
10
11
  import {createDeadbyteNodeHandler} from './node-http-adapter.mjs';
11
12
  import { bridgeCatalogSha256, bridgeDeadlineForTool, createBridgeGovernor, enforceDesktopBridgeResult, verifyCompactCatalogAttestation } from './r40-governor.mjs';
13
+ import { createBridgeHealth } from './bridge-health-r59.mjs';
14
+ import { reconnectDelayMs, waitForReconnect, RECONNECT_ATTEMPT_BUDGET, classifyReconnectFailure } from './reconnect-delay-r60.mjs';
15
+ import {openRemoteCallClaims} from './remote-call-claims-r60.mjs';
16
+ import {dispatchBridgeCall,classifyRemoteOperation} from './remote-dispatch-r60.mjs';
17
+ import {canonicalJson} from './canonical-json-r60.mjs';
12
18
 
13
19
  const coreRoot = path.resolve(process.env.DEADBYTE_CORE_ROOT ?? 'C:/Users/DEADBYTE/Downloads/deadbyte-mcp-v0.6.0');
20
+ const configuredStateRoot=process.env.DEADBYTE_STATE_ROOT;
21
+ if(configuredStateRoot&&(!path.isAbsolute(configuredStateRoot)||configuredStateRoot.trim()===''))throw new Error('bridge state root must be absolute');
22
+ const stateRoot=path.resolve(configuredStateRoot||path.join(os.homedir(),'.deadbyte-mcp'));
23
+ const remoteClaims=openRemoteCallClaims({root:path.join(stateRoot,'runtime','remote-call-claims')});
24
+ let recoveryCursor=null,recoveredClaims=0;
25
+ do{
26
+ const batch=await remoteClaims.recover({after:recoveryCursor,limit:100});
27
+ recoveredClaims+=batch.records.length;
28
+ if(recoveredClaims>100000)throw new Error('remote claim recovery exceeds bounded startup inventory');
29
+ recoveryCursor=batch.next_cursor;
30
+ }while(recoveryCursor!==null);
14
31
  const coreServer = path.join(coreRoot, 'src', 'mcp-server.mjs');
15
32
  const workspaceRoot = path.resolve(process.env.DEADBYTE_WORKSPACE_ROOT ?? path.join(coreRoot, 'workspaces'));
16
33
  const port = Number(process.env.DEADBYTE_BRIDGE_PORT ?? '8787');
@@ -68,6 +85,7 @@ const SENSITIVE_AUDIT_KEY = /(token|secret|password|authorization|cookie|recover
68
85
  const PAYLOAD_AUDIT_KEY = /^(content|raw_text|text)$/i;
69
86
  let auditSequence = 0;
70
87
  const bridgeGovernor = createBridgeGovernor();
88
+ const bridgeHealth=createBridgeHealth({maxCapabilityAgeMs:15000});
71
89
 
72
90
  function auditDigest(text) {
73
91
  return createHash('sha256').update(text, 'utf8').digest('hex');
@@ -220,23 +238,18 @@ process.on('exit', code => {
220
238
  console.error(`[bridge-exit] code=${code}`);
221
239
  });
222
240
 
223
- const downstream = new Client(
224
- { name: 'deadbyte-chatgpt-bridge', version: '0.2.0' },
225
- { versionNegotiation: { mode: { pin: '2026-07-28' } } }
226
- );
227
- const downstreamTransport = new StdioClientTransport({
228
- command: process.execPath,
229
- args: [coreServer],
230
- cwd: coreRoot,
231
- env: {
232
- ...getDefaultEnvironment(),
233
- ...process.env,
234
- DEADBYTE_WORKSPACE_ROOT: workspaceRoot
235
- },
236
- stderr: 'pipe'
237
- });
241
+ function newDownstreamClient(){return new Client(
242
+ {name:'deadbyte-chatgpt-bridge',version:'0.2.0'},
243
+ {versionNegotiation:{mode:{pin:'2026-07-28'}}}
244
+ );}
245
+ function newDownstreamTransport(){return new StdioClientTransport({command:process.execPath,args:[coreServer],cwd:coreRoot,
246
+ env:{...getDefaultEnvironment(),...process.env,DEADBYTE_WORKSPACE_ROOT:workspaceRoot},stderr:'pipe'});}
247
+ function pipeCoreStderr(transport){transport.stderr?.on('data',chunk=>process.stderr.write(`[deadbyte-core] ${chunk}`));}
248
+
249
+ let downstream=newDownstreamClient();
250
+ let downstreamTransport=newDownstreamTransport();
238
251
 
239
- downstreamTransport.stderr?.on('data', chunk => process.stderr.write(`[deadbyte-core] ${chunk}`));
252
+ pipeCoreStderr(downstreamTransport);
240
253
  await downstream.connect(downstreamTransport);
241
254
  if (downstream.getProtocolEra() !== 'modern') throw new Error('DEADBYTE downstream is not modern MCP');
242
255
 
@@ -245,6 +258,7 @@ const actualNames = listed.tools.map(tool => tool.name).sort();
245
258
  const exactR36Compact = JSON.stringify(actualNames) === JSON.stringify(r36CompactTools);
246
259
  let compactCatalogAttestation = null;
247
260
  let compactCatalogLegacy = false;
261
+ let currentCapabilityManifest = null;
248
262
  if (actualNames.includes('capability_manifest')) {
249
263
  const manifestResult = await downstream.callTool(
250
264
  { name:'capability_manifest', arguments:{} },
@@ -253,6 +267,7 @@ if (actualNames.includes('capability_manifest')) {
253
267
  const manifestSchema = manifestResult?.structuredContent?.result?.schema ?? null;
254
268
  if (manifestSchema === 'deadbyte.capability-manifest.v2' || manifestSchema === 'deadbyte.capability-manifest.v3') {
255
269
  compactCatalogAttestation = verifyCompactCatalogAttestation(actualNames, manifestResult);
270
+ if(manifestSchema==='deadbyte.capability-manifest.v3') currentCapabilityManifest=manifestResult.structuredContent.result;
256
271
  } else if (manifestSchema === 'deadbyte.capability-manifest.v1' && exactR36Compact) {
257
272
  compactCatalogLegacy = true;
258
273
  } else {
@@ -268,6 +283,82 @@ const agentConfigured = agentTools.every(name => actualNames.includes(name));
268
283
  const codingConfigured = codingTools.every(name => actualNames.includes(name));
269
284
  const autonomousConfigured = autonomousTools.every(name => actualNames.includes(name));
270
285
  const bridgeCatalogDigest = bridgeCatalogSha256(actualNames);
286
+ const coreIdentity=createHash('sha256').update(readFileSync(coreServer)).digest('hex');
287
+ bridgeHealth.coreConnected({childIdentity:coreIdentity,protocolEra:downstream.getProtocolEra(),catalogSha256:bridgeCatalogDigest});
288
+ if(currentCapabilityManifest) bridgeHealth.capabilityObserved(currentCapabilityManifest);
289
+ else bridgeHealth.degraded('capability manifest v3 is required for fresh readiness');
290
+
291
+ async function refreshCapabilityAttestation(){
292
+ if(reconnecting||!bridgeHealth.snapshot().core)return;
293
+ try{
294
+ const result=await downstream.callTool({name:'capability_manifest',arguments:{}},bridgeDeadlineForTool('capability_manifest'));
295
+ verifyCompactCatalogAttestation(actualNames,result);
296
+ bridgeHealth.capabilityObserved(result.structuredContent.result);
297
+ }catch(error){bridgeHealth.degraded(`capability refresh failed: ${error?.message??String(error)}`);}
298
+ }
299
+ const capabilityRefreshTimer=setInterval(()=>{void refreshCapabilityAttestation();},4000);
300
+ capabilityRefreshTimer.unref?.();
301
+ let shuttingDown=false;
302
+ let reconnecting=false;
303
+ let downstreamGeneration=1;
304
+ const reconnectAbort=new AbortController();
305
+
306
+ async function attestReplacement(client){
307
+ if(client.getProtocolEra()!=='modern') throw Object.assign(new Error('replacement core protocol is not modern'),{kind:'terminal',code:'DEADBYTE_SCHEMA_INVALID'});
308
+ const replacement=(await client.listTools()).tools.map(tool=>tool.name).sort();
309
+ if(JSON.stringify(replacement)!==JSON.stringify(actualNames)) throw Object.assign(new Error('replacement core catalog drift'),{kind:'terminal',code:'DEADBYTE_SCHEMA_INVALID'});
310
+ const result=await client.callTool({name:'capability_manifest',arguments:{}},bridgeDeadlineForTool('capability_manifest'));
311
+ verifyCompactCatalogAttestation(actualNames,result);
312
+ const manifest=result?.structuredContent?.result;
313
+ if(manifest?.schema!=='deadbyte.capability-manifest.v3') throw Object.assign(new Error('replacement core capability manifest v3 missing'),{kind:'terminal',code:'DEADBYTE_SCHEMA_INVALID'});
314
+ const status=(await client.callTool({name:'machine_status',arguments:{}},bridgeDeadlineForTool('machine_status')))?.structuredContent;
315
+ if(status?.status!=='ok'||!HEX64.test(String(status.machine_id??''))||!HEX64.test(String(status.online_epoch??'')))
316
+ throw Object.assign(new Error('replacement core execution probe failed'),{kind:'transient',code:'DEADBYTE_EXECUTION_PROBE_FAILED'});
317
+ if(status.release_manifest_sha256!==manifest.bindings?.release_manifest_sha256||
318
+ status.generation_manifest_sha256!==manifest.bindings?.generation_manifest_sha256)
319
+ throw Object.assign(new Error('replacement core execution binding mismatch'),{kind:'terminal',code:'DEADBYTE_GENERATION_MISMATCH'});
320
+ return manifest;
321
+ }
322
+ function bindDownstreamClose(transport){
323
+ const sdkClose=transport.onclose;
324
+ transport.onclose=()=>{
325
+ sdkClose?.();
326
+ if(shuttingDown)return;
327
+ bridgeHealth.reconciling(`downstream core generation ${downstreamGeneration} closed`);
328
+ bridgeGovernor.reconcile('downstream core unavailable before dispatch');
329
+ void reconcileDownstream();
330
+ };
331
+ }
332
+ async function reconcileDownstream(){
333
+ if(reconnecting||shuttingDown)return;
334
+ reconnecting=true;
335
+ try{
336
+ for(let attempt=0;attempt<RECONNECT_ATTEMPT_BUDGET&&!shuttingDown;attempt++){
337
+ if(!(await waitForReconnect(reconnectDelayMs(attempt),reconnectAbort.signal))||shuttingDown)return;
338
+ const nextClient=newDownstreamClient(),nextTransport=newDownstreamTransport();
339
+ pipeCoreStderr(nextTransport);
340
+ try{
341
+ if(createHash('sha256').update(readFileSync(coreServer)).digest('hex')!==coreIdentity) throw new Error('core script identity drift');
342
+ await nextClient.connect(nextTransport);
343
+ const manifest=await attestReplacement(nextClient);
344
+ downstream=nextClient;downstreamTransport=nextTransport;downstreamGeneration++;
345
+ bindDownstreamClose(nextTransport);
346
+ bridgeHealth.coreConnected({childIdentity:coreIdentity,protocolEra:nextClient.getProtocolEra(),catalogSha256:bridgeCatalogDigest});
347
+ bridgeHealth.capabilityObserved(manifest);
348
+ return;
349
+ }catch(error){
350
+ await nextClient.close().catch(()=>{});
351
+ bridgeHealth.reconciling(`core reconnect ${attempt+1}/${RECONNECT_ATTEMPT_BUDGET} failed: ${error?.message??String(error)}`);
352
+ if(classifyReconnectFailure(error)==='TERMINAL'){
353
+ bridgeHealth.degraded(`core reconnect terminal failure: ${error?.message??String(error)}`);
354
+ return;
355
+ }
356
+ }
357
+ }
358
+ bridgeHealth.degraded('core restart budget exhausted; bridge quarantined');
359
+ }finally{reconnecting=false;}
360
+ }
361
+ bindDownstreamClose(downstreamTransport);
271
362
 
272
363
  function plannerTextFromInput(ctx) {
273
364
  const response = inputResponse(ctx.mcpReq.inputResponses, 'planner');
@@ -280,6 +371,36 @@ function plannerTextFromInput(ctx) {
280
371
 
281
372
  const publicToolDefinitions=preparePublicToolDefinitions(listed.tools);
282
373
 
374
+ const remoteDeviceId=createHash('sha256').update(endpointPath,'utf8').digest('hex');
375
+ const ingressDelivery=new AsyncLocalStorage();
376
+ const HEX64=/^[0-9a-f]{64}$/;
377
+ async function dispatchDownstream(tool,args,ctx,budget){
378
+ if(ctx.mcpReq.signal?.aborted)throw new Error('remote request cancelled before claim');
379
+ const observed=(await downstream.callTool({name:'machine_status',arguments:{}},bridgeDeadlineForTool('machine_status')))?.structuredContent;
380
+ const capability=bridgeHealth.snapshot().capability;
381
+ if(observed?.status!=='ok'||!HEX64.test(String(observed.machine_id??''))||!HEX64.test(String(observed.online_epoch??''))||
382
+ !HEX64.test(String(observed.generation_manifest_sha256??''))||typeof observed.release_id!=='string'||
383
+ observed.generation_manifest_sha256!==capability?.generation_manifest_sha256||
384
+ observed.release_manifest_sha256!==capability?.release_manifest_sha256)
385
+ throw new Error('remote claim machine/release/generation observation invalid');
386
+ const requestId=createHash('sha256').update(canonicalJson(ctx.mcpReq.id),'utf8').digest('hex');
387
+ const metadata=ctx.mcpReq._meta;
388
+ const idempotencyKey=metadata?.idempotency_key;
389
+ if(idempotencyKey!==undefined&&typeof idempotencyKey!=='string')throw new Error('remote idempotency_key invalid');
390
+ const classification=classifyRemoteOperation(tool,args);
391
+ // A stateless HTTP delivery is not a durable logical session. Only an
392
+ // explicit idempotency key may identify a mutation across deliveries.
393
+ const deliveryId=ingressDelivery.getStore()?.delivery_id;
394
+ if(!deliveryId)throw new Error('remote ingress delivery identity missing');
395
+ const binding={device_id:remoteDeviceId,session_id:deliveryId,request_id:requestId,
396
+ ...(idempotencyKey===undefined?{}:{idempotency_key:idempotencyKey}),tool:tool.name,arguments:args,
397
+ expires_at_utc:new Date(Date.now()+budget.maxTotalTimeout).toISOString(),
398
+ machine_id:observed.machine_id,online_epoch:observed.online_epoch,release_id:observed.release_id,
399
+ generation_manifest_sha256:observed.generation_manifest_sha256,classification};
400
+ return dispatchBridgeCall({claims:remoteClaims,binding,classification,signal:ctx.mcpReq.signal,
401
+ invoke:()=>downstream.callTool({name:tool.name,arguments:args},budget)});
402
+ }
403
+
283
404
  function buildFrontServer() {
284
405
  const front = new McpServer(
285
406
  { name: 'deadbyte-chatgpt-bridge', version: '0.2.0' },
@@ -294,17 +415,20 @@ function buildFrontServer() {
294
415
  outputSchema,
295
416
  annotations: tool.annotations
296
417
  }, async (args, ctx) => {
418
+ const currentHealth=bridgeHealth.snapshot();
419
+ if(!currentHealth.core||['STARTING','RECONCILING','STOPPING'].includes(currentHealth.state)) throw new Error(`bridge correlation generation=${downstreamGeneration}; downstream ${currentHealth.state.toLowerCase()}; request not dispatched`);
297
420
  if (tool.name !== 'autonomous_goal_run') {
298
421
  const audit = auditToolStart(tool.name, args);
299
- const ticket = await bridgeGovernor.acquire(tool.name);
422
+ const ticket = await bridgeGovernor.acquire(tool.name,{signal:ctx.mcpReq.signal});
300
423
  try {
301
424
  const budget = bridgeDeadlineForTool(tool.name);
302
- const result = await downstream.callTool({ name: tool.name, arguments: args }, budget);
425
+ const result = await dispatchDownstream(tool,args,ctx,budget);
303
426
  enforceDesktopBridgeResult(tool.name, result);
304
427
  auditToolFinish(audit, result);
305
428
  return result;
306
429
  } catch (error) {
307
430
  auditToolFail(audit, error);
431
+ if(classifyRemoteOperation(tool,args)!=='read_only'&&['RECONCILING','DEGRADED'].includes(bridgeHealth.snapshot().state)) throw new Error(`bridge correlation call=${audit.id} generation=${downstreamGeneration}; mutation outcome unknown; reconcile before retry`,{cause:error});
308
432
  throw error;
309
433
  } finally {
310
434
  ticket.release();
@@ -318,14 +442,15 @@ function buildFrontServer() {
318
442
  callArgs.planner_response = { request_id: state.request_id, raw_text: planner.raw_text, model: planner.model };
319
443
  }
320
444
  const audit = auditToolStart(tool.name, callArgs);
321
- const ticket = await bridgeGovernor.acquire(tool.name);
445
+ const ticket = await bridgeGovernor.acquire(tool.name,{signal:ctx.mcpReq.signal});
322
446
  let result;
323
447
  try {
324
448
  const budget = bridgeDeadlineForTool(tool.name);
325
- result = await downstream.callTool({ name: tool.name, arguments: callArgs }, budget);
449
+ result = await dispatchDownstream(tool,callArgs,ctx,budget);
326
450
  auditToolFinish(audit, result);
327
451
  } catch (error) {
328
452
  auditToolFail(audit, error);
453
+ if(['RECONCILING','DEGRADED'].includes(bridgeHealth.snapshot().state)) throw new Error(`bridge correlation call=${audit.id} generation=${downstreamGeneration}; autonomous outcome unknown; reconcile before retry`,{cause:error});
329
454
  throw error;
330
455
  } finally {
331
456
  ticket.release();
@@ -359,13 +484,15 @@ const httpServer = createServer(async (req, res) => {
359
484
  try {
360
485
  const pathname = new URL(req.url ?? '/', 'http://127.0.0.1').pathname;
361
486
  if (pathname === '/healthz') {
362
- res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
363
- return res.end('ok');
487
+ const health=bridgeHealth.snapshot();
488
+ res.writeHead(health.http_status, { 'content-type': 'application/json; charset=utf-8','cache-control':'no-store' });
489
+ return res.end(JSON.stringify(health));
364
490
  }
365
491
  if (pathname === '/healthz/r40') {
366
- res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
492
+ const health=bridgeHealth.snapshot();
493
+ res.writeHead(health.http_status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
367
494
  return res.end(JSON.stringify({
368
- status:'ok',catalog_sha256:bridgeCatalogDigest,
495
+ status:health.ready?'ok':'degraded',runtime:health,catalog_sha256:bridgeCatalogDigest,
369
496
  catalog_attestation:compactCatalogAttestation?.ok===true?'verified':compactCatalogLegacy?'legacy-r39':'not-applicable',
370
497
  ledger_sha256:compactCatalogAttestation?.ledger_sha256??null,
371
498
  governor:bridgeGovernor.snapshot()
@@ -380,7 +507,7 @@ const httpServer = createServer(async (req, res) => {
380
507
  res.writeHead(413, { 'content-type': 'text/plain; charset=utf-8' });
381
508
  return res.end('request too large');
382
509
  }
383
- await nodeMcpHandler(req, res);
510
+ await ingressDelivery.run({delivery_id:randomUUID()},()=>nodeMcpHandler(req, res));
384
511
  } catch (error) {
385
512
  if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
386
513
  if (!res.writableEnded) res.end('bridge error');
@@ -402,7 +529,12 @@ console.log(`BRIDGE_CODING_RUNTIME ${codingConfigured ? 'ENABLED' : 'DISABLED'}`
402
529
  console.log(`BRIDGE_AUTONOMOUS_RUNTIME ${autonomousConfigured ? 'ENABLED' : 'DISABLED'}`);
403
530
 
404
531
  async function shutdown() {
532
+ shuttingDown=true;
533
+ reconnectAbort.abort();
534
+ bridgeHealth.stopping();
535
+ clearInterval(capabilityRefreshTimer);
405
536
  httpServer.close();
537
+ await Promise.race([bridgeGovernor.close('bridge stopping'),new Promise(resolve=>setTimeout(resolve,5000))]);
406
538
  await mcpHandler.close().catch(() => {});
407
539
  await downstream.close().catch(() => {});
408
540
  process.exit(0);
@@ -0,0 +1,13 @@
1
+ // Keep byte-for-byte canonicalization compatible with src/canonical-json.mjs.
2
+ // Bridge dependencies are pinned inside bridge/, so this local copy is tested
3
+ // against the Core primitive rather than reaching outside the Bridge root.
4
+ function canonicalize(value){
5
+ if(Array.isArray(value))return value.map(canonicalize);
6
+ if(value!==null&&typeof value==='object'){
7
+ const out={};
8
+ for(const key of Object.keys(value).sort())out[key]=canonicalize(value[key]);
9
+ return out;
10
+ }
11
+ return value;
12
+ }
13
+ export function canonicalJson(value){return JSON.stringify(canonicalize(value));}
@@ -103,15 +103,22 @@ export function createBridgeGovernor({limits=DEFAULT_LIMITS}={}){
103
103
  insist(Number.isInteger(cfg.max_queue)&&cfg.max_queue>=0&&cfg.max_queue<=256,`bridge limit ${name}.max_queue invalid`);
104
104
  insist(Number.isInteger(cfg.queue_timeout_ms)&&cfg.queue_timeout_ms>=100&&cfg.queue_timeout_ms<=120000,`bridge limit ${name}.queue_timeout_ms invalid`);
105
105
  }
106
- const state=new Map(classes.map(name=>[name,{active:0,queue:[]}]));
106
+ const state=new Map(classes.map(name=>[name,{active:0,queue:[],admitted:0,completed:0,rejected:0,timed_out:0,cancelled:0}]));
107
107
  let sequence=0;
108
+ let closed=false;
109
+ const drainWaiters=[];
110
+
111
+ function finishDrain(){
112
+ if(!closed||[...state.values()].some(item=>item.active>0||item.queue.some(waiter=>!waiter.done))) return;
113
+ while(drainWaiters.length) drainWaiters.shift()();
114
+ }
108
115
 
109
116
  function dispatch(name){
110
117
  const cfg=limits[name],s=state.get(name);
111
118
  while(s.active<cfg.max_active&&s.queue.length){
112
119
  const waiter=s.queue.shift();
113
120
  if(waiter.done) continue;
114
- waiter.done=true; clearTimeout(waiter.timer); s.active++;
121
+ waiter.done=true; clearTimeout(waiter.timer); waiter.cleanup(); s.active++; s.admitted++;
115
122
  waiter.resolve(makeTicket(name,waiter.tool,waiter.id,waiter.queuedAt));
116
123
  }
117
124
  }
@@ -123,37 +130,76 @@ export function createBridgeGovernor({limits=DEFAULT_LIMITS}={}){
123
130
  if(released) return false;
124
131
  released=true;
125
132
  const s=state.get(name);
126
- s.active=Math.max(0,s.active-1);
133
+ s.active=Math.max(0,s.active-1);s.completed++;
127
134
  dispatch(name);
135
+ finishDrain();
128
136
  return true;
129
137
  }
130
138
  });
131
139
  }
132
- async function acquire(tool){
140
+ async function acquire(tool,{signal}={}){
133
141
  const name=toolClass(tool),cfg=limits[name],s=state.get(name),id=++sequence,queuedAt=Date.now();
142
+ if(closed){s.rejected++;throw new Error('bridge governor closed');}
143
+ if(signal?.aborted){s.cancelled++;throw signal.reason instanceof Error?signal.reason:new Error('bridge request aborted');}
134
144
  if(s.active<cfg.max_active){
135
- s.active++;
145
+ s.active++;s.admitted++;
136
146
  return makeTicket(name,String(tool),id,queuedAt);
137
147
  }
138
- if(s.queue.length>=cfg.max_queue) throw new Error(`bridge backpressure queue full for ${name}`);
148
+ if(s.queue.length>=cfg.max_queue){s.rejected++;throw new Error(`bridge backpressure queue full for ${name}`);}
139
149
  return await new Promise((resolve,reject)=>{
140
- const waiter={id,tool:String(tool),queuedAt,resolve,reject,done:false,timer:null};
150
+ const waiter={id,tool:String(tool),queuedAt,resolve,reject,done:false,timer:null,cleanup:()=>{}};
151
+ const abort=()=>{
152
+ if(waiter.done)return;
153
+ waiter.done=true;clearTimeout(waiter.timer);waiter.cleanup();
154
+ const index=s.queue.indexOf(waiter);if(index>=0)s.queue.splice(index,1);
155
+ s.cancelled++;reject(signal?.reason instanceof Error?signal.reason:new Error('bridge request aborted'));finishDrain();
156
+ };
157
+ waiter.cleanup=()=>signal?.removeEventListener?.('abort',abort);
158
+ signal?.addEventListener?.('abort',abort,{once:true});
141
159
  waiter.timer=setTimeout(()=>{
142
160
  if(waiter.done)return;
143
- waiter.done=true;
161
+ waiter.done=true;waiter.cleanup();s.timed_out++;
144
162
  const index=s.queue.indexOf(waiter); if(index>=0)s.queue.splice(index,1);
145
163
  reject(new Error(`bridge backpressure queue timeout for ${name}`));
164
+ finishDrain();
146
165
  },cfg.queue_timeout_ms);
147
166
  s.queue.push(waiter);
148
167
  });
149
168
  }
169
+ function close(reason='bridge governor closed'){
170
+ if(!closed){
171
+ closed=true;
172
+ for(const s of state.values()){
173
+ for(const waiter of s.queue.splice(0)){
174
+ if(waiter.done)continue;
175
+ waiter.done=true;clearTimeout(waiter.timer);waiter.cleanup();s.cancelled++;
176
+ waiter.reject(new Error(String(reason)));
177
+ }
178
+ }
179
+ }
180
+ if([...state.values()].every(item=>item.active===0)) return Promise.resolve();
181
+ return new Promise(resolve=>drainWaiters.push(resolve));
182
+ }
183
+ function reconcile(reason='bridge downstream reconciling'){
184
+ if(closed) return 0;
185
+ let removed=0;
186
+ for(const s of state.values()){
187
+ for(const waiter of s.queue.splice(0)){
188
+ if(waiter.done)continue;
189
+ waiter.done=true;clearTimeout(waiter.timer);waiter.cleanup();s.cancelled++;removed++;
190
+ waiter.reject(new Error(String(reason)));
191
+ }
192
+ }
193
+ return removed;
194
+ }
150
195
  function snapshot(){
151
196
  return Object.freeze(Object.fromEntries(classes.map(name=>{
152
197
  const s=state.get(name),cfg=limits[name];
153
- return [name,Object.freeze({active:s.active,queued:s.queue.filter(x=>!x.done).length,max_active:cfg.max_active,max_queue:cfg.max_queue})];
198
+ return [name,Object.freeze({active:s.active,queued:s.queue.filter(x=>!x.done).length,max_active:cfg.max_active,max_queue:cfg.max_queue,
199
+ admitted:s.admitted,completed:s.completed,rejected:s.rejected,timed_out:s.timed_out,cancelled:s.cancelled,saturated:s.active>=cfg.max_active})];
154
200
  })));
155
201
  }
156
- return Object.freeze({acquire,snapshot});
202
+ return Object.freeze({acquire,reconcile,close,snapshot});
157
203
  }
158
204
 
159
205
  export const BRIDGE_R40_DEFAULT_LIMITS=DEFAULT_LIMITS;
@@ -0,0 +1,31 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+
3
+ const BASE_DELAYS_MS=Object.freeze([250,500,1000,2000,4000]);
4
+
5
+ export function reconnectDelayMs(attempt,random=Math.random){
6
+ if(!Number.isInteger(attempt)||attempt<0||attempt>=BASE_DELAYS_MS.length) throw new Error('reconnect attempt outside budget');
7
+ const sample=Number(random());
8
+ if(!Number.isFinite(sample)||sample<0||sample>1) throw new Error('reconnect jitter source invalid');
9
+ return Math.round(BASE_DELAYS_MS[attempt]*(0.75+sample*0.5));
10
+ }
11
+
12
+ export const RECONNECT_ATTEMPT_BUDGET=BASE_DELAYS_MS.length;
13
+
14
+ export async function waitForReconnect(milliseconds,signal){
15
+ if(signal?.aborted)return false;
16
+ try{await delay(milliseconds,undefined,{signal});return !signal?.aborted;}
17
+ catch(error){if(error?.name==='AbortError')return false;throw error;}
18
+ }
19
+
20
+ export function classifyReconnectFailure(error){
21
+ const code=String(error?.code??'').toUpperCase();
22
+ if(error?.kind==='terminal')return 'TERMINAL';
23
+ if(error?.kind==='transient')return 'TRANSIENT';
24
+ if(new Set(['DEADBYTE_AUTHORITY_REVOKED','DEADBYTE_POLICY_DENIED','DEADBYTE_SCHEMA_INVALID',
25
+ 'DEADBYTE_RELEASE_MISMATCH','DEADBYTE_GENERATION_MISMATCH','DEADBYTE_IDEMPOTENCY_CONFLICT',
26
+ 'DEADBYTE_CLAIM_CORRUPT','DEADBYTE_CREDENTIAL_EXPIRED']).has(code))return 'TERMINAL';
27
+ const message=String(error?.message??error??'').toLowerCase();
28
+ if(['EACCES','EPERM','ENOENT'].includes(code)||/identity drift|catalog drift|protocol is not modern|attestation invalid|generation mismatch|policy mismatch/.test(message))return 'TERMINAL';
29
+ if(['ECONNRESET','ECONNREFUSED','EPIPE','ETIMEDOUT','EHOSTUNREACH'].includes(code)||/connection reset|transport closed|timed out|temporarily unavailable/.test(message))return 'TRANSIENT';
30
+ return 'TERMINAL';
31
+ }
@@ -0,0 +1,157 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { link, lstat, mkdir, open, readFile, readdir, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { canonicalJson } from './canonical-json-r60.mjs';
5
+
6
+ const HEX64=/^[0-9a-f]{64}$/;
7
+ const ID=/^[A-Za-z0-9._:-]{1,160}$/;
8
+ const sha=value=>createHash('sha256').update(canonicalJson(value),'utf8').digest('hex');
9
+
10
+ async function publishOnce(target,value){
11
+ const temp=`${target}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
12
+ const handle=await open(temp,'wx');
13
+ const record={...value,record_sha256:sha(value)};
14
+ try{await handle.writeFile(`${canonicalJson(record)}\n`,'utf8');await handle.sync();}
15
+ finally{await handle.close();}
16
+ try{await link(temp,target);return true;}
17
+ catch(error){if(error?.code==='EEXIST')return false;throw error;}
18
+ finally{await rm(temp,{force:true});}
19
+ }
20
+ async function readExact(file){
21
+ try{
22
+ const info=await lstat(file);
23
+ if(!info.isFile()||info.isSymbolicLink()||info.size>2*1024*1024)throw new Error('remote claim record type or size corrupt');
24
+ const record=JSON.parse(await readFile(file,'utf8'));
25
+ if(!record||typeof record!=='object'||Array.isArray(record))throw new Error('remote claim record corrupt');
26
+ const {record_sha256,...body}=record;
27
+ if(!HEX64.test(String(record_sha256??''))||record_sha256!==sha(body))throw new Error('remote claim record hash corrupt');
28
+ return body;
29
+ }
30
+ catch(error){if(error?.code==='ENOENT')return null;throw new Error(`remote claim state unreadable: ${String(error?.message||error)}`);}
31
+ }
32
+ function validate(binding,now){
33
+ if(!binding||typeof binding!=='object'||Array.isArray(binding))throw new Error('remote claim binding invalid');
34
+ for(const key of ['device_id','session_id','request_id','tool','release_id'])if(!ID.test(String(binding[key]??'')))throw new Error(`remote claim ${key} invalid`);
35
+ if(binding.idempotency_key!==undefined&&!ID.test(String(binding.idempotency_key)))throw new Error('remote claim idempotency_key invalid');
36
+ if(!HEX64.test(String(binding.machine_id??'')))throw new Error('remote claim machine_id invalid');
37
+ if(!HEX64.test(String(binding.generation_manifest_sha256??'')))throw new Error('remote claim generation binding invalid');
38
+ if(!HEX64.test(String(binding.online_epoch??'')))throw new Error('remote claim online epoch invalid');
39
+ if(!['read_only','mutating','unknown'].includes(binding.classification))throw new Error('remote claim classification invalid');
40
+ if(!binding.arguments||typeof binding.arguments!=='object'||Array.isArray(binding.arguments))throw new Error('remote claim arguments invalid');
41
+ const expires=Date.parse(binding.expires_at_utc);
42
+ if(!Number.isFinite(expires))throw new Error('remote claim deadline invalid');
43
+ if(expires<=now)throw new Error('remote request expired');
44
+ return Object.freeze({schema:'deadbyte.remote-call-claim.v1',device_id:binding.device_id,session_id:binding.session_id,
45
+ idempotency_key:binding.idempotency_key??null,
46
+ request_id:String(binding.request_id),tool:binding.tool,payload_sha256:sha(binding.arguments),
47
+ received_at_utc:new Date(now).toISOString(),expires_at_utc:new Date(expires).toISOString(),
48
+ release_id:binding.release_id,generation_manifest_sha256:binding.generation_manifest_sha256,
49
+ machine_id:binding.machine_id,online_epoch:binding.online_epoch,classification:binding.classification});
50
+ }
51
+
52
+ export function openRemoteCallClaims({root,clock=Date.now}={}){
53
+ if(typeof root!=='string'||!path.isAbsolute(root)||typeof clock!=='function')throw new Error('remote claim store options invalid');
54
+ const directory=path.resolve(root);
55
+ function files(id){
56
+ if(!HEX64.test(String(id)))throw new Error('remote claim identity invalid');
57
+ const base=path.join(directory,id.slice(0,2),id);
58
+ return {base,claim:`${base}.claim.json`,claimed:`${base}.claimed.json`,dispatch:`${base}.dispatch.json`,interrupted:`${base}.interrupted.json`,terminal:`${base}.terminal.json`};
59
+ }
60
+ async function state(claimId){
61
+ const f=files(claimId);
62
+ const claim=await readExact(f.claim);
63
+ if(!claim||claim.schema!=='deadbyte.remote-call-claim.v1')throw new Error('remote claim missing or invalid');
64
+ const terminal=await readExact(f.terminal);
65
+ if(terminal){
66
+ if(terminal.schema!=='deadbyte.remote-call-terminal.v1'||terminal.claim_id!==claimId||
67
+ !HEX64.test(String(terminal.result_sha256??''))||terminal.result_sha256!==sha(terminal.result))
68
+ throw new Error('remote claim terminal hash corrupt');
69
+ return Object.freeze({claim_id:claimId,owner:false,state:'TERMINAL',side_effect_state:claim.classification==='read_only'?'none':'unknown',result:terminal.result});
70
+ }
71
+ const interrupted=await readExact(f.interrupted);
72
+ if(interrupted){
73
+ if(interrupted.schema!=='deadbyte.remote-call-interrupted.v1'||interrupted.claim_id!==claimId)throw new Error('remote claim interruption corrupt');
74
+ return Object.freeze({claim_id:claimId,owner:false,state:'INTERRUPTED',side_effect_state:claim.classification==='read_only'?'none':'unknown'});
75
+ }
76
+ const dispatched=await readExact(f.dispatch);
77
+ if(dispatched)return Object.freeze({claim_id:claimId,owner:false,state:'EXECUTING',side_effect_state:claim.classification==='read_only'?'none':'may_have_started'});
78
+ const claimed=await readExact(f.claimed);
79
+ return Object.freeze({claim_id:claimId,owner:false,state:claimed?'CLAIMED':'RECEIVED',side_effect_state:'not_started'});
80
+ }
81
+ async function claim(binding){
82
+ const record=validate(binding,Number(clock()));
83
+ const claimId=record.idempotency_key===null
84
+ ?sha({device_id:record.device_id,session_id:record.session_id,request_id:record.request_id})
85
+ :sha({device_id:record.device_id,idempotency_key:record.idempotency_key});
86
+ const f=files(claimId);
87
+ await mkdir(path.dirname(f.base),{recursive:true});
88
+ if(await publishOnce(f.claim,record)){
89
+ const owner=await publishOnce(f.claimed,{schema:'deadbyte.remote-call-claimed.v1',claim_id:claimId,execution_state:'CLAIMED',side_effect_state:'not_started'});
90
+ return owner?Object.freeze({claim_id:claimId,owner:true,state:'CLAIMED',side_effect_state:'not_started'}):state(claimId);
91
+ }
92
+ const prior=await readExact(f.claim);
93
+ if(!prior||prior.schema!==record.schema)throw new Error('remote claim identity corrupt');
94
+ if(Date.parse(prior.expires_at_utc)<=Number(clock()))throw new Error('remote request expired');
95
+ if(prior.tool!==record.tool||prior.payload_sha256!==record.payload_sha256)throw new Error(record.idempotency_key===null?'request_id_conflict':'idempotency_key_conflict');
96
+ if(prior.device_id!==record.device_id||prior.idempotency_key!==record.idempotency_key||
97
+ (record.idempotency_key===null&&(prior.session_id!==record.session_id||prior.request_id!==record.request_id)))throw new Error('remote claim identity drift');
98
+ if(prior.release_id!==record.release_id||prior.generation_manifest_sha256!==record.generation_manifest_sha256)throw new Error('remote claim release identity drift');
99
+ if(prior.machine_id!==record.machine_id||prior.online_epoch!==record.online_epoch||prior.classification!==record.classification)throw new Error('remote claim machine/epoch/classification drift');
100
+ const current=await state(claimId);
101
+ if(current.state==='RECEIVED'&&await publishOnce(f.claimed,{schema:'deadbyte.remote-call-claimed.v1',claim_id:claimId,execution_state:'CLAIMED',side_effect_state:'not_started'}))
102
+ return Object.freeze({claim_id:claimId,owner:true,state:'CLAIMED',side_effect_state:'not_started'});
103
+ return state(claimId);
104
+ }
105
+ async function dispatch(claimId){
106
+ const f=files(claimId);
107
+ if(!(await readExact(f.claimed)))throw new Error('remote claim not claimed before dispatch');
108
+ if(!(await publishOnce(f.dispatch,{schema:'deadbyte.remote-call-dispatch.v1',claim_id:claimId,at_utc:new Date(Number(clock())).toISOString(),execution_state:'EXECUTING',side_effect_state:'may_have_started'})))
109
+ throw new Error('remote claim already dispatched; outcome unknown');
110
+ return Object.freeze({claim_id:claimId,state:'EXECUTING',side_effect_state:'may_have_started'});
111
+ }
112
+ async function terminal(claimId,result){
113
+ const f=files(claimId);
114
+ if(!(await readExact(f.dispatch)))throw new Error('remote claim not dispatched');
115
+ const bytes=Buffer.byteLength(canonicalJson(result),'utf8');
116
+ if(bytes>1048576)throw new Error('remote claim terminal result exceeds bound');
117
+ if(!(await publishOnce(f.terminal,{schema:'deadbyte.remote-call-terminal.v1',claim_id:claimId,
118
+ at_utc:new Date(Number(clock())).toISOString(),result_sha256:sha(result),result})))throw new Error('remote claim already terminal');
119
+ return state(claimId);
120
+ }
121
+ async function recover({after=null,limit=100}={}){
122
+ if(after!==null&&!HEX64.test(String(after)))throw new Error('remote recovery cursor invalid');
123
+ if(!Number.isInteger(limit)||limit<1||limit>100)throw new Error('remote recovery batch limit invalid');
124
+ const records=[];let more=false;
125
+ for(let shard=0;shard<256;shard++){
126
+ const prefix=shard.toString(16).padStart(2,'0');
127
+ if(after&&prefix<after.slice(0,2))continue;
128
+ const folder=path.join(directory,prefix);
129
+ let names;
130
+ try{
131
+ const info=await lstat(folder);
132
+ if(!info.isDirectory()||info.isSymbolicLink())throw new Error('remote recovery shard corrupt');
133
+ names=await readdir(folder);
134
+ }catch(error){if(error?.code==='ENOENT')continue;throw error;}
135
+ if(names.length>4096)throw new Error('remote recovery shard exceeds bounded inventory');
136
+ for(const name of names.filter(item=>/^[0-9a-f]{64}\.claim\.json$/.test(item)).sort()){
137
+ const id=name.slice(0,64);
138
+ if(after&&id<=after)continue;
139
+ if(records.length>=limit){more=true;break;}
140
+ const claimRecord=await readExact(files(id).claim);
141
+ if(!claimRecord||claimRecord.schema!=='deadbyte.remote-call-claim.v1')throw new Error('remote recovery claim corrupt');
142
+ if(Date.parse(claimRecord.expires_at_utc)<=Number(clock()))continue;
143
+ let current=await state(id);
144
+ if(current.state==='TERMINAL')continue;
145
+ if(current.state==='EXECUTING'){
146
+ await publishOnce(files(id).interrupted,{schema:'deadbyte.remote-call-interrupted.v1',claim_id:id,
147
+ at_utc:new Date(Number(clock())).toISOString(),execution_state:'INTERRUPTED',side_effect_state:current.side_effect_state==='none'?'none':'unknown'});
148
+ current=await state(id);
149
+ }
150
+ records.push(current);
151
+ }
152
+ if(more)break;
153
+ }
154
+ return Object.freeze({records,next_cursor:more?records.at(-1)?.claim_id??after:null});
155
+ }
156
+ return Object.freeze({claim,dispatch,terminal,state,recover});
157
+ }