instar 1.3.490 → 1.3.492

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 (121) hide show
  1. package/dashboard/index.html +30 -0
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +3 -0
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/server.d.ts.map +1 -1
  6. package/dist/commands/server.js +199 -1
  7. package/dist/commands/server.js.map +1 -1
  8. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  9. package/dist/config/ConfigDefaults.js +11 -0
  10. package/dist/config/ConfigDefaults.js.map +1 -1
  11. package/dist/core/CartographerSweepEngine.d.ts +55 -11
  12. package/dist/core/CartographerSweepEngine.d.ts.map +1 -1
  13. package/dist/core/CartographerSweepEngine.js +276 -95
  14. package/dist/core/CartographerSweepEngine.js.map +1 -1
  15. package/dist/core/CartographerTree.d.ts +77 -1
  16. package/dist/core/CartographerTree.d.ts.map +1 -1
  17. package/dist/core/CartographerTree.js +257 -5
  18. package/dist/core/CartographerTree.js.map +1 -1
  19. package/dist/core/GuardPostureStore.d.ts +38 -0
  20. package/dist/core/GuardPostureStore.d.ts.map +1 -0
  21. package/dist/core/GuardPostureStore.js +87 -0
  22. package/dist/core/GuardPostureStore.js.map +1 -0
  23. package/dist/core/MachinePoolRegistry.d.ts +16 -0
  24. package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
  25. package/dist/core/MachinePoolRegistry.js +24 -1
  26. package/dist/core/MachinePoolRegistry.js.map +1 -1
  27. package/dist/core/PeerPresencePuller.d.ts +9 -0
  28. package/dist/core/PeerPresencePuller.d.ts.map +1 -1
  29. package/dist/core/PeerPresencePuller.js +1 -1
  30. package/dist/core/PeerPresencePuller.js.map +1 -1
  31. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  32. package/dist/core/PostUpdateMigrator.js +43 -2
  33. package/dist/core/PostUpdateMigrator.js.map +1 -1
  34. package/dist/core/cartographerDetect.d.ts +133 -0
  35. package/dist/core/cartographerDetect.d.ts.map +1 -0
  36. package/dist/core/cartographerDetect.js +431 -0
  37. package/dist/core/cartographerDetect.js.map +1 -0
  38. package/dist/core/cartographerDetect.worker.d.ts +2 -0
  39. package/dist/core/cartographerDetect.worker.d.ts.map +1 -0
  40. package/dist/core/cartographerDetect.worker.js +37 -0
  41. package/dist/core/cartographerDetect.worker.js.map +1 -0
  42. package/dist/core/types.d.ts +29 -0
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/core/types.js.map +1 -1
  45. package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +8 -0
  46. package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -1
  47. package/dist/monitoring/ActiveWorkSilenceSentinel.js +8 -0
  48. package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -1
  49. package/dist/monitoring/ContextWedgeSentinel.d.ts +8 -0
  50. package/dist/monitoring/ContextWedgeSentinel.d.ts.map +1 -1
  51. package/dist/monitoring/ContextWedgeSentinel.js +8 -0
  52. package/dist/monitoring/ContextWedgeSentinel.js.map +1 -1
  53. package/dist/monitoring/GuardPostureTripwire.d.ts +3 -27
  54. package/dist/monitoring/GuardPostureTripwire.d.ts.map +1 -1
  55. package/dist/monitoring/GuardPostureTripwire.js +8 -76
  56. package/dist/monitoring/GuardPostureTripwire.js.map +1 -1
  57. package/dist/monitoring/GuardRegistry.d.ts +48 -0
  58. package/dist/monitoring/GuardRegistry.d.ts.map +1 -0
  59. package/dist/monitoring/GuardRegistry.js +49 -0
  60. package/dist/monitoring/GuardRegistry.js.map +1 -0
  61. package/dist/monitoring/SessionReaper.d.ts +7 -0
  62. package/dist/monitoring/SessionReaper.d.ts.map +1 -1
  63. package/dist/monitoring/SessionReaper.js +9 -0
  64. package/dist/monitoring/SessionReaper.js.map +1 -1
  65. package/dist/monitoring/SessionWatchdog.d.ts +8 -0
  66. package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
  67. package/dist/monitoring/SessionWatchdog.js +8 -0
  68. package/dist/monitoring/SessionWatchdog.js.map +1 -1
  69. package/dist/monitoring/SocketDisconnectSentinel.d.ts +8 -0
  70. package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -1
  71. package/dist/monitoring/SocketDisconnectSentinel.js +8 -0
  72. package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -1
  73. package/dist/monitoring/guardManifest.d.ts +67 -0
  74. package/dist/monitoring/guardManifest.d.ts.map +1 -0
  75. package/dist/monitoring/guardManifest.js +536 -0
  76. package/dist/monitoring/guardManifest.js.map +1 -0
  77. package/dist/monitoring/guardPosture.d.ts +77 -0
  78. package/dist/monitoring/guardPosture.d.ts.map +1 -0
  79. package/dist/monitoring/guardPosture.js +198 -0
  80. package/dist/monitoring/guardPosture.js.map +1 -0
  81. package/dist/monitoring/guardPostureView.d.ts +100 -0
  82. package/dist/monitoring/guardPostureView.d.ts.map +1 -0
  83. package/dist/monitoring/guardPostureView.js +294 -0
  84. package/dist/monitoring/guardPostureView.js.map +1 -0
  85. package/dist/monitoring/probes/GuardPostureProbe.d.ts +82 -0
  86. package/dist/monitoring/probes/GuardPostureProbe.d.ts.map +1 -0
  87. package/dist/monitoring/probes/GuardPostureProbe.js +384 -0
  88. package/dist/monitoring/probes/GuardPostureProbe.js.map +1 -0
  89. package/dist/scaffold/templates.d.ts.map +1 -1
  90. package/dist/scaffold/templates.js +9 -0
  91. package/dist/scaffold/templates.js.map +1 -1
  92. package/dist/scheduler/JobScheduler.d.ts +10 -0
  93. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  94. package/dist/scheduler/JobScheduler.js +12 -0
  95. package/dist/scheduler/JobScheduler.js.map +1 -1
  96. package/dist/server/AgentServer.d.ts +8 -0
  97. package/dist/server/AgentServer.d.ts.map +1 -1
  98. package/dist/server/AgentServer.js +2 -0
  99. package/dist/server/AgentServer.js.map +1 -1
  100. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  101. package/dist/server/CapabilityIndex.js +11 -0
  102. package/dist/server/CapabilityIndex.js.map +1 -1
  103. package/dist/server/peerUrlGuard.d.ts +35 -0
  104. package/dist/server/peerUrlGuard.d.ts.map +1 -0
  105. package/dist/server/peerUrlGuard.js +93 -0
  106. package/dist/server/peerUrlGuard.js.map +1 -0
  107. package/dist/server/routes.d.ts +14 -0
  108. package/dist/server/routes.d.ts.map +1 -1
  109. package/dist/server/routes.js +239 -25
  110. package/dist/server/routes.js.map +1 -1
  111. package/package.json +3 -2
  112. package/scripts/lint-guard-manifest.js +264 -0
  113. package/scripts/lint-no-direct-destructive.js +4 -0
  114. package/scripts/lint-no-mainthread-cartographer-walk.js +101 -0
  115. package/src/data/builtin-manifest.json +65 -65
  116. package/src/data/state-coherence-registry.json +54 -18
  117. package/src/scaffold/templates.ts +9 -0
  118. package/upgrades/1.3.491.md +37 -0
  119. package/upgrades/1.3.492.md +31 -0
  120. package/upgrades/side-effects/cartographer-sweep-eventloop-safety.md +110 -0
  121. package/upgrades/side-effects/guard-posture-endpoint.md +101 -0
@@ -32,6 +32,10 @@ import { CoherenceJournalReader, InvalidCursorError } from '../core/CoherenceJou
32
32
  import { creditUsherOnOutbound } from '../core/UsherActedCorrelator.js';
33
33
  import { validateWriteToken, canPerformOperation } from '../core/StateWriteAuthority.js';
34
34
  import { DegradationReporter } from '../monitoring/DegradationReporter.js';
35
+ import { readGuardPostureBootSnapshot, resolveGuardConfigSnapshot, } from '../monitoring/guardPosture.js';
36
+ import { buildGuardInventory } from '../monitoring/guardPostureView.js';
37
+ import { GuardRegistry } from '../monitoring/GuardRegistry.js';
38
+ import { isPeerUrlAllowedForCredentials } from './peerUrlGuard.js';
35
39
  import { FailureLedger } from '../monitoring/FailureLedger.js';
36
40
  import { FailureAttributionEngine } from '../monitoring/FailureAttributionEngine.js';
37
41
  import { FailureAnalyzer } from '../monitoring/FailureAnalyzer.js';
@@ -3496,20 +3500,70 @@ export function createRoutes(ctx) {
3496
3500
  // Cartographer doc-tree — hierarchical, semantic map of the codebase with
3497
3501
  // git-hash staleness derivation (cartographer-doc-tree-schema spec #1). Ships
3498
3502
  // dark behind cartographer.enabled (503 when disabled). Read-only — authoring
3499
- // is in-process only. Routes lazy-scaffold the structural skeleton on first use.
3503
+ // is in-process only.
3504
+ //
3505
+ // fix instar#1069: NO request path runs an O(nodeCount)/67MB synchronous op on
3506
+ // the event loop. /health + /stale serve the per-host SNAPSHOT the sweep writes;
3507
+ // /tree is byte-bounded (too-large-for-request above the ceiling); the lazy
3508
+ // scaffold()/loadIndex() preamble is GONE (routes serve indexState:'not-built'
3509
+ // when no index exists — the boot-path chunked scaffold builds it off-request).
3510
+ // The Slice-5 callsite lint forbids loadIndex/staleNodes/freshnessHealth/scaffold
3511
+ // here, so this regression cannot return by accident.
3512
+ /** Snapshot-enum + provenance, computed cheaply (one `git rev-parse --short HEAD`). */
3513
+ const cartoSnapshotMeta = (snap) => {
3514
+ if (!snap) {
3515
+ return { snapshot: 'absent', snapshotStale: true, ageMs: null, headMoved: false, lastDetectStatus: null, lastDetectAt: null, headSha: null };
3516
+ }
3517
+ const currentHead = ctx.cartographer ? ctx.cartographer.currentHeadShort() : null;
3518
+ const headMoved = snap.headSha != null && currentHead != null && snap.headSha !== currentHead;
3519
+ const ageMs = snap.generatedAt ? Math.max(0, Date.now() - Date.parse(snap.generatedAt)) : null;
3520
+ const failing = snap.lastDetectStatus !== 'ok';
3521
+ return {
3522
+ snapshot: failing ? 'detect-failing' : 'present',
3523
+ snapshotStale: headMoved || failing,
3524
+ ageMs,
3525
+ headMoved,
3526
+ lastDetectStatus: snap.lastDetectStatus,
3527
+ lastDetectAt: snap.lastDetectAt,
3528
+ headSha: snap.headSha,
3529
+ };
3530
+ };
3500
3531
  router.get('/cartographer/tree', (req, res) => {
3501
3532
  if (!ctx.cartographer) {
3502
3533
  res.status(503).json({ error: 'Cartographer not enabled' });
3503
3534
  return;
3504
3535
  }
3505
- let index = ctx.cartographer.loadIndex();
3506
- if (!index)
3507
- index = ctx.cartographer.scaffold();
3536
+ const snap = ctx.cartographer.readSnapshot();
3508
3537
  if (req.query.format === 'compact') {
3509
- res.json({ root: index.root, generatedAt: index.generatedAt, nodeCount: Object.keys(index.nodes).length });
3538
+ // Cheap: nodeCount + generatedAt from the snapshot (no index parse).
3539
+ res.json({
3540
+ root: '',
3541
+ generatedAt: snap?.generatedAt ?? null,
3542
+ nodeCount: snap?.counts.nodeCount ?? 0,
3543
+ indexState: snap ? 'built' : 'not-built',
3544
+ ...cartoSnapshotMeta(snap),
3545
+ });
3546
+ return;
3547
+ }
3548
+ // Full index: byte-bounded so a huge tree never triggers the 67MB sync parse.
3549
+ const fsCfg = ctx.config.cartographer?.freshnessSweep;
3550
+ const maxRequestNodes = typeof fsCfg?.maxRequestNodes === 'number' ? fsCfg.maxRequestNodes : 50000;
3551
+ const byteCeiling = maxRequestNodes * 512; // generous per-entry estimate
3552
+ const loaded = ctx.cartographer.loadIndexBounded(byteCeiling);
3553
+ if (loaded.state === 'not-built') {
3554
+ res.json({ root: '', generatedAt: null, nodeCount: 0, indexState: 'not-built', ...cartoSnapshotMeta(snap) });
3510
3555
  return;
3511
3556
  }
3512
- res.json(index);
3557
+ if (loaded.state === 'too-large') {
3558
+ res.json({
3559
+ indexState: 'too-large-for-request',
3560
+ nodeCount: snap?.counts.nodeCount ?? null,
3561
+ guidance: 'index too large to serve whole; use GET /cartographer/navigate?query=... for a bounded subtree',
3562
+ ...cartoSnapshotMeta(snap),
3563
+ });
3564
+ return;
3565
+ }
3566
+ res.json(loaded.index);
3513
3567
  });
3514
3568
  router.get('/cartographer/node', (req, res) => {
3515
3569
  if (!ctx.cartographer) {
@@ -3523,11 +3577,10 @@ export function createRoutes(ctx) {
3523
3577
  res.status(400).json({ error: 'invalid path' });
3524
3578
  return;
3525
3579
  }
3526
- if (!ctx.cartographer.loadIndex())
3527
- ctx.cartographer.scaffold();
3580
+ // getNode reads ONE node file (O(1)) — no index parse, no scaffold preamble.
3528
3581
  const node = ctx.cartographer.getNode(p);
3529
3582
  if (!node) {
3530
- res.status(404).json({ error: 'no such node' });
3583
+ res.status(404).json({ error: 'no such node', indexState: ctx.cartographer.readSnapshot() ? 'built' : 'not-built' });
3531
3584
  return;
3532
3585
  }
3533
3586
  res.json(node);
@@ -3537,29 +3590,57 @@ export function createRoutes(ctx) {
3537
3590
  res.status(503).json({ error: 'Cartographer not enabled' });
3538
3591
  return;
3539
3592
  }
3540
- if (!ctx.cartographer.loadIndex())
3541
- ctx.cartographer.scaffold();
3542
- const nodes = ctx.cartographer.staleNodes();
3543
- res.json({ count: nodes.length, nodes });
3593
+ const snap = ctx.cartographer.readSnapshot();
3594
+ const meta = cartoSnapshotMeta(snap);
3595
+ if (!snap) {
3596
+ res.json({ count: 0, nodes: [], total: 0, truncated: false, ...meta });
3597
+ return;
3598
+ }
3599
+ // Serve the bounded, secret-filtered sample with the honest total + truncation.
3600
+ const reasonFor = (s) => s === 'never-authored' ? 'no summary authored yet'
3601
+ : s === 'stale' ? 'code changed since the summary was authored'
3602
+ : s === 'path-gone' ? 'covered path no longer in HEAD' : '';
3603
+ const nodes = snap.staleSample.map((e) => ({ path: e.path, status: e.status, reason: reasonFor(e.status) }));
3604
+ res.json({ count: nodes.length, nodes, total: snap.staleTotal, truncated: snap.staleSampleTruncated, ...meta });
3544
3605
  });
3545
3606
  router.get('/cartographer/health', (_req, res) => {
3546
3607
  if (!ctx.cartographer) {
3547
3608
  res.status(503).json({ error: 'Cartographer not enabled' });
3548
3609
  return;
3549
3610
  }
3550
- if (!ctx.cartographer.loadIndex())
3551
- ctx.cartographer.scaffold();
3552
3611
  const sweepCfg = ctx.config
3553
3612
  .cartographer?.freshnessSweep;
3554
- // Spec #2: surface the freshness ratio + the two ABSOLUTE backlog counts so a
3555
- // green ratio can't hide a growing un-authored/quarantined backlog. Grace =
3556
- // one cadence so a just-scaffolded node isn't counted as debt immediately.
3557
- const graceMs = (sweepCfg?.cadenceMs ?? 600000) * 2;
3613
+ const snap = ctx.cartographer.readSnapshot();
3614
+ const meta = cartoSnapshotMeta(snap);
3615
+ // fix instar#1069: the entire freshness block is served from the SNAPSHOT — the
3616
+ // route never calls health()/freshnessHealth() (the request-thread starvers).
3617
+ // Legacy field names/semantics are preserved (additive contract); new fields
3618
+ // carry snapshot provenance so a stale/failing detect is visible on the route.
3619
+ if (!snap) {
3620
+ res.json({
3621
+ enabled: true,
3622
+ nodeCount: 0, authoredCount: 0, neverAuthoredCount: 0, staleCount: 0, generatedAt: null,
3623
+ freshness: {
3624
+ nodeCount: 0, authorableCount: 0, freshCount: 0, staleCount: 0, neverAuthoredCount: 0,
3625
+ neverAuthoredWithinGrace: 0, neverAuthoredPastGrace: 0, authorFailedCount: 0, freshRatio: 1, generatedAt: null,
3626
+ },
3627
+ sweepEnabled: sweepCfg?.enabled === true,
3628
+ ...meta,
3629
+ });
3630
+ return;
3631
+ }
3558
3632
  res.json({
3559
3633
  enabled: true,
3560
- ...ctx.cartographer.health(),
3561
- freshness: ctx.cartographer.freshnessHealth({ graceMs }),
3634
+ // Legacy health() fields, sourced from the snapshot:
3635
+ nodeCount: snap.counts.nodeCount,
3636
+ authoredCount: snap.counts.authoredCount,
3637
+ neverAuthoredCount: snap.counts.neverAuthored,
3638
+ staleCount: snap.counts.stale,
3639
+ generatedAt: snap.generatedAt,
3640
+ // Legacy freshnessHealth() block, sourced from the snapshot:
3641
+ freshness: snap.freshness,
3562
3642
  sweepEnabled: sweepCfg?.enabled === true,
3643
+ ...meta,
3563
3644
  });
3564
3645
  });
3565
3646
  // Tier-1 (inline opportunistic) write route (spec #2 — doc-freshness). The ONE
@@ -3610,8 +3691,8 @@ export function createRoutes(ctx) {
3610
3691
  res.status(400).json({ error: 'invalid summary' });
3611
3692
  return;
3612
3693
  }
3613
- if (!ctx.cartographer.loadIndex())
3614
- ctx.cartographer.scaffold();
3694
+ // fix instar#1069: no lazy scaffold()/loadIndex() preamble — getNode is a single
3695
+ // node-file read. An un-scaffolded node 400s (the boot scaffold builds the index).
3615
3696
  const node = ctx.cartographer.getNode(p);
3616
3697
  if (!node) {
3617
3698
  res.status(400).json({ error: 'no such node — only an existing scaffolded node can be refreshed' });
@@ -3688,8 +3769,9 @@ export function createRoutes(ctx) {
3688
3769
  res.status(400).json({ error: 'maxResults out of range (1..500)' });
3689
3770
  return;
3690
3771
  }
3691
- if (!ctx.cartographer.loadIndex())
3692
- ctx.cartographer.scaffold();
3772
+ // fix instar#1069: no lazy scaffold()/loadIndex() preamble — the navigator is
3773
+ // bounded (maxNodesVisited) and returns an empty manifest when the root node is
3774
+ // absent (index not built yet); the boot-path scaffold builds it off-request.
3693
3775
  const manifest = cartographerNavigate(ctx.cartographer, query, {
3694
3776
  maxDepth: maxDepthOverride ?? navCfg.maxDepth,
3695
3777
  branchingFactor: navCfg.branchingFactor,
@@ -4270,6 +4352,138 @@ export function createRoutes(ctx) {
4270
4352
  const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), 1000) : 200;
4271
4353
  res.json({ entries: ctx.reapLog.read(limit) });
4272
4354
  });
4355
+ // ── Guard Posture (GUARD-POSTURE-ENDPOINT-SPEC) ─────────────────────
4356
+ // Read-only, Bearer-auth (router-level middleware — NEVER added to any auth
4357
+ // exemption list: posture is an attack-timing oracle, spec §3). Reports
4358
+ // every shipped guard's HONEST effective state: config-on is not working
4359
+ // (the Mini's reaper lesson). Local inventory is fully synchronous (one
4360
+ // disk-read config snapshot + sync in-memory runtime getters, <100ms);
4361
+ // ?scope=pool fans out to peers with classified per-peer failures, never a
4362
+ // 500. Deliberately NO `guards.enabled` config gate — an off-switch on the
4363
+ // guard-visibility surface would itself be an invisible disabled guard
4364
+ // (always-on read-only observability; reap-log/TokenLedger precedent).
4365
+ const guardsPoolLimiter = rateLimiter(60_000, 6);
4366
+ router.get('/guards', async (req, res) => {
4367
+ const stateDir = ctx.config.stateDir;
4368
+ const projectDir = path.dirname(stateDir);
4369
+ const snapshot = resolveGuardConfigSnapshot(projectDir);
4370
+ if (snapshot.readError) {
4371
+ // Config-read failure is a top-level error — never a truthful-looking
4372
+ // empty inventory (spec §2.2).
4373
+ res.status(500).json({ error: `guard inventory unavailable: config read failed (${snapshot.readError})` });
4374
+ return;
4375
+ }
4376
+ const inventory = buildGuardInventory({
4377
+ snapshot,
4378
+ bootSnapshot: readGuardPostureBootSnapshot(stateDir),
4379
+ registry: ctx.guardRegistry ?? new GuardRegistry(),
4380
+ });
4381
+ const selfMachineId = ctx.meshSelfId ?? null;
4382
+ const body = {
4383
+ machineId: selfMachineId,
4384
+ nickname: selfMachineId
4385
+ ? (ctx.machinePoolRegistry?.getCapacity(selfMachineId)?.nickname ?? null)
4386
+ : null,
4387
+ version: ProcessIntegrity.getInstance()?.runningVersion || ctx.config.version || '0.0.0',
4388
+ generatedAt: new Date().toISOString(),
4389
+ guards: inventory.guards,
4390
+ summary: inventory.summary,
4391
+ };
4392
+ if (req.query.scope !== 'pool') {
4393
+ res.json(body);
4394
+ return;
4395
+ }
4396
+ // scope=pool — every registered machine accounted for: a posture row or a
4397
+ // NAMED, classified failure row; silent omission re-creates the blind spot
4398
+ // at the pool level (spec §2.3). Non-recursive (peers get plain /guards),
4399
+ // rate-limited (anti-amplification, spec §3(d)).
4400
+ guardsPoolLimiter(req, res, async () => {
4401
+ const known = (ctx.listPoolMachines?.() ?? []).filter((m) => m.machineId !== selfMachineId);
4402
+ if (known.length === 0) {
4403
+ res.json({ ...body, pool: { enabled: false, peersQueried: 0, knownMachines: 0, machines: [], failed: [] } });
4404
+ return;
4405
+ }
4406
+ const extraAllowlist = ctx.config.multiMachine
4407
+ ?.peerUrlAllowlist;
4408
+ const machines = [];
4409
+ const failed = [];
4410
+ let peersQueried = 0;
4411
+ await Promise.all(known.map(async (m) => {
4412
+ const capacity = ctx.machinePoolRegistry?.getCapacity(m.machineId) ?? null;
4413
+ const peerVersion = capacity?.hardware?.instarVersion;
4414
+ if (!m.lastKnownUrl) {
4415
+ failed.push({ machineId: m.machineId, reason: 'no-known-url' });
4416
+ return;
4417
+ }
4418
+ if (capacity && capacity.online === false) {
4419
+ // The heartbeat piggyback (durable last-known posture on the pool
4420
+ // view) covers dark peers — a doomed fetch buys nothing but timeout.
4421
+ failed.push({ machineId: m.machineId, reason: 'offline' });
4422
+ return;
4423
+ }
4424
+ const verdict = isPeerUrlAllowedForCredentials(m.lastKnownUrl, extraAllowlist);
4425
+ if (!verdict.ok) {
4426
+ // The Bearer token was NEVER attached (spec §3(c)) — visible, named.
4427
+ failed.push({ machineId: m.machineId, reason: 'url-rejected' });
4428
+ return;
4429
+ }
4430
+ peersQueried++;
4431
+ try {
4432
+ const r = await fetch(`${m.lastKnownUrl}/guards`, {
4433
+ headers: { Authorization: `Bearer ${ctx.config.authToken}` },
4434
+ signal: AbortSignal.timeout(5000),
4435
+ });
4436
+ if (r.status === 404) {
4437
+ // Pre-/guards peer version: "needs update to report", not a
4438
+ // phantom outage (rollout-skew honesty, spec §2.3).
4439
+ failed.push({ machineId: m.machineId, reason: 'route-missing', ...(peerVersion ? { version: peerVersion } : {}) });
4440
+ return;
4441
+ }
4442
+ if (r.status === 401 || r.status === 403) {
4443
+ failed.push({ machineId: m.machineId, reason: 'unauthorized' });
4444
+ return;
4445
+ }
4446
+ if (!r.ok) {
4447
+ failed.push({ machineId: m.machineId, reason: 'error' });
4448
+ return;
4449
+ }
4450
+ const peerBody = (await r.json());
4451
+ machines.push({
4452
+ // Merge identity: the row is keyed on the REGISTRY's machine
4453
+ // identity; the body's self-reported machineId is data and a
4454
+ // mismatch is FLAGGED, never allowed to shadow another row.
4455
+ machineId: m.machineId,
4456
+ nickname: capacity?.nickname ?? m.nickname,
4457
+ ...(typeof peerBody.machineId === 'string' && peerBody.machineId !== m.machineId
4458
+ ? { identityMismatch: true, claimedMachineId: peerBody.machineId }
4459
+ : {}),
4460
+ version: peerBody.version,
4461
+ generatedAt: peerBody.generatedAt,
4462
+ guards: peerBody.guards,
4463
+ summary: peerBody.summary,
4464
+ });
4465
+ }
4466
+ catch (err) {
4467
+ // Normalized enum only — raw err.message leaks URL/TLS internals.
4468
+ const name = err instanceof Error ? err.name : '';
4469
+ failed.push({
4470
+ machineId: m.machineId,
4471
+ reason: name === 'TimeoutError' || name === 'AbortError' ? 'timeout' : 'unreachable',
4472
+ });
4473
+ }
4474
+ }));
4475
+ res.json({
4476
+ ...body,
4477
+ pool: {
4478
+ enabled: !!ctx.machinePoolRegistry,
4479
+ knownMachines: known.length,
4480
+ peersQueried,
4481
+ machines,
4482
+ failed,
4483
+ },
4484
+ });
4485
+ });
4486
+ });
4273
4487
  // Coherence Journal merged read API (COHERENCE-JOURNAL-SPEC §3.5). The
4274
4488
  // pull-surface answer to "which machine was topic N on / where are its
4275
4489
  // artifacts?" — a bounded, honest-about-trust merged view over this machine's