Runbook — Plugin MissingMethodException (cross-ALC type identity)

Applies to: any plugin loaded into a PluginLoadContext (Concelier/Excititor connectors are the usual trigger; the class is general). Companion: docs/plugins/ARCHITECTURE.md (the ALC mechanism), docs/agent-playbooks/concelier-connector.md (field summary). Verify against src/ — code wins.

Symptom

A signed/working plugin throws, at map/parse time, something like:

System.MissingMethodException: Method not found:
  '...TryParseSemVer(System.String, NuGet.Versioning.SemanticVersion ByRef, System.String ByRef)'

…even though a reflection probe shows every copy of the assembly (host /app and every plugin bundle) physically contains that method. It looks like a stale/build-skew DLL. It is not.

Root cause — second ALC type identity

The plugin bundle ships its own copy of a host-shared contract assembly (byte-identical is still wrong). The plugin loads into a plugin ALC; the shared dep resolves inconsistently, so the type at the call site and the type in the resolved method signature come from two different ALC-loaded copies → two distinct System.Type → the CLR cannot bind the method. Only the plugin whose call path crosses a doubly-loaded type trips it (e.g. a mapper calling a method whose signature carries NuGet.Versioning.SemanticVersion); siblings that don’t hit that signature map fine.

Known doubly-loaded contract surfaces:

Loader note: PluginLoadContext (StellaOps.Plugin.Hosting) does prefer host assemblies — but only those already loaded in the default ALC. A lazy-load ordering race lets the bundle copy load first.

Fix — prune host-present assemblies from the bundle

Remove from the bundle every DLL the host /app already provides, leaving only the plugin-unique assembly (+ manifest.json + .dll.sig). All shared types then resolve from the single host ALC → one canonical identity → the method binds. The manifest signs only the connector DLL, so pruning deps does not break the signature.

The narrow deny-list (StellaOps.* + the 3 third-party DLLs) was verified live-sufficient — connector- internal third-party deps (AngleSharp/BouncyCastle/Grpc/…) stay bundle-local and don’t cross the contract.

Two variants that look the same but need a different fix

  1. Host /app/*.Models.dll newer than the plugins (connector plugins ship no own Models.dll): Advisory..ctor in a mapper throws MissingMethodException, which is swallowed — the job reports “succeeded”, 0 edges change, the doc → failed. Fix = a coherent image REBUILD (not a recreate) so host and plugins are built from the same tree. Operator-scope; see docs/agent-playbooks/live-deploy-operator.md.
  2. Plugin transitive StellaOps.* deps fail GetTypes()because the default ALC only probes the TPA list and the host doesn’t carry them. Fix = a Resolving hook on the load context (not a prune).

Dev signature gotcha

The dev required-bundle gate enforces .sig presence even when EnforceSignatureVerification=false. After pruning/repackaging, sign with -SignConcelierBundles -UseOfflineDevSigner or the bundle is rejected.

Verify the fix (live forcing-function)

Re-run the map and confirm real movement: docs go failed → mapped, source_states/edges change, and no MissingMethodException in logs. A “job succeeded” with 0 changes is not proof — that’s exactly the swallowed-failure signature.