Skip to content

feat(plugin): demand-start shared mc-host lazily - #54

Draft
ahrav wants to merge 1 commit into
stack/mc-host-06-harness-runtimefrom
stack/mc-host-07-plugin-demand
Draft

feat(plugin): demand-start shared mc-host lazily#54
ahrav wants to merge 1 commit into
stack/mc-host-06-harness-runtimefrom
stack/mc-host-07-plugin-demand

Conversation

@ahrav

@ahrav ahrav commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • wire managed Rust and Synapse demand to the shared lifecycle owner
  • keep import, registration, wake, explicit, and injected paths passive
  • detach cancelled waiters without cancelling shared startup

Stack

PR 7 of 10. Base: stack/mc-host-06-harness-runtime.

Validation

  • plugin demand and Pi integration tests
  • storage-readiness and cancellation coverage

Post-Deploy Monitoring & Validation

Watch managed start outcomes, storage-starting waits, Synapse readiness, and fallback rates for one release cycle. Roll back if passive probes start a daemon or cancelled callers terminate shared startup. Owner: plugin maintainers.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

) {
return cached.handle;
}
this.liveRoutes.delete(cached.handle.channel);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: this.liveRoutes does not exist on McHostClient.

liveRoutes is a field of ActiveConnection (declared at line 182 and accessed everywhere else as active.liveRoutes / conn.liveRoutes, e.g. line 1803), not of McHostClient. This line calls this.liveRoutes.delete(...) from inside managedRouteHandle, where this is the client.

This branch runs whenever a cached managed route's credential fingerprints go stale (e.g. a rotated ANTHROPIC_API_KEY for an opencode/pi session) — exactly the eviction path this PR adds. It will throw TypeError: Cannot read properties of undefined (reading 'delete') instead of transparently reopening the route.

Should this be active?.liveRoutes.delete(cached.handle.channel) (guarding for active === null, mirroring the null-check just above for currentIdentity)?

if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (value !== null && typeof value === "object") {
return `{${Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locale-sensitive sort in a digest computation used for trust verification.

canonicalJson sorts object keys with left.localeCompare(right). This directly contradicts the codebase's existing canonical-JSON convention in packages/plugin/src/shared/stable-json.ts, which explicitly sorts by code-point order and documents why:

Code-point sort (NOT localeCompare). Stable across runtimes/locales.

verifyPackage (this file, ~line 232) computes sha256(canonicalJson(manifest)) and compares it against entry.payload_manifest_digest from the trust index for native payload verification. If the digest was produced with code-point ordering (or on a runtime/locale where ICU orders some key pair differently than this locale-aware sort), a legitimate untampered manifest can fail verification and verifyPackage throws native_payload_invalid, breaking managed native-host launch in a way that's very hard to reproduce/debug (it depends on the runtime's default locale).

Suggest reusing stableStringify from shared/stable-json.ts or switching this sort to plain </> comparison to match it.

const daemonVer = active.generation.daemonVer;
if (daemonVer === null) return null;
const daemonId = active.generation.authenticatedDaemonId;
if (daemonVer === null || daemonId === null) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior change: authenticated now returns null for a connected peer with no daemon id, instead of a partial authenticated record.

Previously (per the diff) this getter returned { daemonVer, daemonId: active.generation.authenticatedDaemonId, proof: "current" } whenever daemonVer !== null, even if daemonId was null. Now it returns null outright when daemonId === null.

connection.ts:752 sets authenticatedDaemonId = null whenever the handshake result's daemonId isn't a Uint8Array, while still setting daemonVer and advancing phase to "frames" — i.e. a live, usable connection can have a null daemon id.

probeManagedReadiness in managed-policy.ts (~line 79) treats authenticated === null as fatal: throw new Error("authenticated peer disappeared"). With this change, a channel/provider whose handshake legitimately omits a daemon id gets misclassified as "peer disappeared" rather than a degraded-but-authenticated/live state.

Today this only seems to be exercised by ShmFrameChannel's test-only path, but flagging since it's a real contract change on a shared getter — worth confirming no production channel can produce a null daemon id, or restoring the partial-record behavior if one can.

"missing_identity",
);
}
const identity = baseIdentity;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

routeCacheKey (used a few lines below via identity) never actually varies by credential fingerprint on first use.

routeCacheKey (line ~2210) includes identity.credential_fingerprints in the key it builds, but identity here is just baseIdentity — the caller-supplied identity — before it's ever passed through identityForConnection. identityForConnection only runs later: in the new staleness-recheck branch below (this.identityForConnection(active, baseIdentity)) or inside openCachedRoute.

So on first use, two calls with the same project_root/harness/session but different credential fingerprints hash to the same this.routes key — the credential-scoping this PR adds only takes effect via the post-hoc recheck-and-evict branch a few lines down (which itself has a bug, see the this.liveRoutes.delete comment on that branch), not at the cache-key level where routeCacheKey's new logic implies it should apply.

Is relying entirely on the recheck-and-evict path intentional, or should identityForConnection be applied before computing key?

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review summary

Reviewed the demand-start lifecycle changes for the shared mc-host (PR 7/10 of the stack). Posted 4 inline comments on the highest-confidence issues:

  1. Likely runtime crashclient.ts:1686 calls this.liveRoutes.delete(...), but liveRoutes is a field of ActiveConnection, not McHostClient. This is on the new credential-staleness eviction path (triggered when a cached managed routes fingerprints no longer match, e.g. a rotated API key), so it will throw TypeError instead of transparently reopening the route.
  2. Security-relevant divergenceowner.ts's new canonicalJson() sorts keys with localeCompare for a digest used in native-payload trust verification, while the codebases existing canonical-JSON convention (shared/stable-json.ts) explicitly mandates code-point order specifically to avoid locale/runtime-dependent digests. This risks legitimate manifests failing verification depending on the runtimes default locale.
  3. Cache key likely doesnt do what it impliesrouteCacheKey includes credential_fingerprints, but at the call site the identity used to build the key hasnt been through identityForConnection yet, so the key doesnt actually vary by credential on first use. Credential-scoping ends up depending entirely on the (buggy, see perf(search): remove six search and clustering hot-path costs #1) post-hoc recheck-and-evict branch.
  4. Getter contract changeauthenticated now returns null (instead of a partial record) when authenticatedDaemonId is null, which managed-policy.ts's probeManagedReadiness treats as a fatal "authenticated peer disappeared". Currently only exercised by a test-only channel, but worth confirming no real channel/provider can hit this path before relying on it.

Also noted but not inline (lower severity / cleanup):

  • bootstrap.ts's trust-index schema change (release_versionrelease: {id, version}, launcher digest field rename) has no handling for old-format index files already on disk from a prior release — they'd fail with the same generic native_payload_invalid as a tampered index.
  • Credential-fingerprint staleness handling is duplicated across two call sites (managedRouteHandle's fast-path and openCachedRoute's unconditional recompute) rather than centralized.
  • identityForConnection's HMAC fingerprint computation is not memoized and reruns on every warm-cache managed call.
  • The opencode/pi harness allowlist for requiring credential fingerprints is an inline special case in client.ts rather than derived from the credential model — easy to forget when adding a new harness.
  • parseHostStatusResponse re-implements the same sorted-keys exact-match validation already factored out as requireExactKeys in contract.ts, now duplicated a third time.
  • transform-mode.ts dropped the guard requiring a user-tier subc for the rust path, but userTierHasSubc is still threaded through call sites even though its no longer consulted — worth cleaning up if the guard removal is intentional.

Tests, docs, and the overall lifecycle-owner design (bounded deadlines, KTD invariants, passive probing) look solid — the concerns above are all in the credential-fingerprint routing and digest-verification code added in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant