fix(plugin): fence lifecycle compatibility by daemon - #60
Conversation
| if (status === null) { | ||
| return { | ||
| ...compatibility, | ||
| readiness: { | ||
| transport: { state: "ready", reason: "healthy" }, | ||
| storage: { state: "unavailable", reason: "storage_unavailable" }, | ||
| synapse: { state: "degraded", reason: "synapse_degraded" }, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
When the compatibility probe short-circuits before host.status (daemon or module mismatch, status === null), this fabricates storage: "unavailable" / synapse: "degraded" readiness records that were never actually observed.
In McHostLifecyclePolicy.observationalCommand (policy.ts), addCheck runs unconditionally over observed.readiness.*, so these fabricated values get published as real readiness.storage/readiness.synapse "fail" checks (with specific reasons like storage_unavailable) in the status/doctor output — even though storage/synapse were never probed. The top-level ok/reason correctly prefer the compatibility verdict, but the checks array itself misrepresents unknown state as a specific, actionable failure, which will mislead anyone reading mc-host doctor output to troubleshoot storage/synapse when the real (and only known) problem is a daemon/module version mismatch.
Consider marking these as "unsupported"/not-yet-known rather than synthesizing "unavailable"/"degraded", or omitting the storage/synapse checks entirely when status === null.
| const expectedDaemonKey = | ||
| options.expectedDaemonId === undefined | ||
| ? "" | ||
| : Array.from(options.expectedDaemonId, (byte) => | ||
| byte.toString(16).padStart(2, "0"), | ||
| ).join(""); | ||
| const key = `${routeCacheKey(target, identity, consumerIdentity)}\0${expectedDaemonKey}`; |
There was a problem hiding this comment.
Folding expectedDaemonId into the managed-route cache key means every daemon restart mints a brand-new this.routes entry (a fresh key per daemon incarnation) instead of reusing/replacing the old one for the same (target, identity, consumerIdentity). Entries are only ever removed from this.routes via closeRoute's explicit this.routes.delete(key) (client.ts:739) — there's no bulk eviction on connection invalidation/generation change and no cleanup keyed on "stale expectedDaemonId no longer matches the current daemon". Over a long-lived process that lives through many mc-host restarts (e.g. upgrades), this.routes will accumulate one dead CachedManagedRoute per prior daemon generation indefinitely.
The per-entry cost is small, but this is an unbounded-growth pattern tied directly to a mechanism (daemon rotation) this PR now actively causes to happen more visibly. Worth considering pruning entries whose expectedDaemonKey no longer matches this.compatibleDaemonId-equivalent state when a new connection is established, or keying by target/identity only and revalidating identity per-use (as assertExpectedDaemon already does) rather than fragmenting the cache.
Review summaryThis PR extends lifecycle compatibility checking to fence live traffic to a specific authenticated daemon generation ( Two lower-severity issues found, posted inline:
Also worth noting (not flagged inline, lower confidence): No security concerns beyond the two items above — the fencing logic is a genuine hardening (defense against cross-daemon-generation request delivery), not a regression. |
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
Summary
Managed callers now evaluate daemon, module, epoch, and readiness data from one authenticated daemon identity. Route and application publication fail before use when that identity rotates, while unsupported-platform and typed compatibility outcomes remain intact.
Verification
bun run typecheckbun run lintStack