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.
- package/dashboard/index.html +30 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +199 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +11 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CartographerSweepEngine.d.ts +55 -11
- package/dist/core/CartographerSweepEngine.d.ts.map +1 -1
- package/dist/core/CartographerSweepEngine.js +276 -95
- package/dist/core/CartographerSweepEngine.js.map +1 -1
- package/dist/core/CartographerTree.d.ts +77 -1
- package/dist/core/CartographerTree.d.ts.map +1 -1
- package/dist/core/CartographerTree.js +257 -5
- package/dist/core/CartographerTree.js.map +1 -1
- package/dist/core/GuardPostureStore.d.ts +38 -0
- package/dist/core/GuardPostureStore.d.ts.map +1 -0
- package/dist/core/GuardPostureStore.js +87 -0
- package/dist/core/GuardPostureStore.js.map +1 -0
- package/dist/core/MachinePoolRegistry.d.ts +16 -0
- package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
- package/dist/core/MachinePoolRegistry.js +24 -1
- package/dist/core/MachinePoolRegistry.js.map +1 -1
- package/dist/core/PeerPresencePuller.d.ts +9 -0
- package/dist/core/PeerPresencePuller.d.ts.map +1 -1
- package/dist/core/PeerPresencePuller.js +1 -1
- package/dist/core/PeerPresencePuller.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +43 -2
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/cartographerDetect.d.ts +133 -0
- package/dist/core/cartographerDetect.d.ts.map +1 -0
- package/dist/core/cartographerDetect.js +431 -0
- package/dist/core/cartographerDetect.js.map +1 -0
- package/dist/core/cartographerDetect.worker.d.ts +2 -0
- package/dist/core/cartographerDetect.worker.d.ts.map +1 -0
- package/dist/core/cartographerDetect.worker.js +37 -0
- package/dist/core/cartographerDetect.worker.js.map +1 -0
- package/dist/core/types.d.ts +29 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +8 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.js +8 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -1
- package/dist/monitoring/ContextWedgeSentinel.d.ts +8 -0
- package/dist/monitoring/ContextWedgeSentinel.d.ts.map +1 -1
- package/dist/monitoring/ContextWedgeSentinel.js +8 -0
- package/dist/monitoring/ContextWedgeSentinel.js.map +1 -1
- package/dist/monitoring/GuardPostureTripwire.d.ts +3 -27
- package/dist/monitoring/GuardPostureTripwire.d.ts.map +1 -1
- package/dist/monitoring/GuardPostureTripwire.js +8 -76
- package/dist/monitoring/GuardPostureTripwire.js.map +1 -1
- package/dist/monitoring/GuardRegistry.d.ts +48 -0
- package/dist/monitoring/GuardRegistry.d.ts.map +1 -0
- package/dist/monitoring/GuardRegistry.js +49 -0
- package/dist/monitoring/GuardRegistry.js.map +1 -0
- package/dist/monitoring/SessionReaper.d.ts +7 -0
- package/dist/monitoring/SessionReaper.d.ts.map +1 -1
- package/dist/monitoring/SessionReaper.js +9 -0
- package/dist/monitoring/SessionReaper.js.map +1 -1
- package/dist/monitoring/SessionWatchdog.d.ts +8 -0
- package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
- package/dist/monitoring/SessionWatchdog.js +8 -0
- package/dist/monitoring/SessionWatchdog.js.map +1 -1
- package/dist/monitoring/SocketDisconnectSentinel.d.ts +8 -0
- package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -1
- package/dist/monitoring/SocketDisconnectSentinel.js +8 -0
- package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts +67 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -0
- package/dist/monitoring/guardManifest.js +536 -0
- package/dist/monitoring/guardManifest.js.map +1 -0
- package/dist/monitoring/guardPosture.d.ts +77 -0
- package/dist/monitoring/guardPosture.d.ts.map +1 -0
- package/dist/monitoring/guardPosture.js +198 -0
- package/dist/monitoring/guardPosture.js.map +1 -0
- package/dist/monitoring/guardPostureView.d.ts +100 -0
- package/dist/monitoring/guardPostureView.d.ts.map +1 -0
- package/dist/monitoring/guardPostureView.js +294 -0
- package/dist/monitoring/guardPostureView.js.map +1 -0
- package/dist/monitoring/probes/GuardPostureProbe.d.ts +82 -0
- package/dist/monitoring/probes/GuardPostureProbe.d.ts.map +1 -0
- package/dist/monitoring/probes/GuardPostureProbe.js +384 -0
- package/dist/monitoring/probes/GuardPostureProbe.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +9 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts +10 -0
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +12 -0
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/AgentServer.d.ts +8 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +11 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/peerUrlGuard.d.ts +35 -0
- package/dist/server/peerUrlGuard.d.ts.map +1 -0
- package/dist/server/peerUrlGuard.js +93 -0
- package/dist/server/peerUrlGuard.js.map +1 -0
- package/dist/server/routes.d.ts +14 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +239 -25
- package/dist/server/routes.js.map +1 -1
- package/package.json +3 -2
- package/scripts/lint-guard-manifest.js +264 -0
- package/scripts/lint-no-direct-destructive.js +4 -0
- package/scripts/lint-no-mainthread-cartographer-walk.js +101 -0
- package/src/data/builtin-manifest.json +65 -65
- package/src/data/state-coherence-registry.json +54 -18
- package/src/scaffold/templates.ts +9 -0
- package/upgrades/1.3.491.md +37 -0
- package/upgrades/1.3.492.md +31 -0
- package/upgrades/side-effects/cartographer-sweep-eventloop-safety.md +110 -0
- package/upgrades/side-effects/guard-posture-endpoint.md +101 -0
package/dist/server/routes.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
3506
|
-
if (!index)
|
|
3507
|
-
index = ctx.cartographer.scaffold();
|
|
3536
|
+
const snap = ctx.cartographer.readSnapshot();
|
|
3508
3537
|
if (req.query.format === 'compact') {
|
|
3509
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
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
|
-
|
|
3555
|
-
|
|
3556
|
-
//
|
|
3557
|
-
|
|
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
|
-
|
|
3561
|
-
|
|
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
|
-
|
|
3614
|
-
|
|
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
|
-
|
|
3692
|
-
|
|
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
|