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:
NuGet.Versioning.SemanticVersion(fromNuGet.Versioning.dll).Json.Schema.JsonSchema/Json.More.*(fromJsonSchema.Net.dll+Json.More.dll) onIJsonSchemaValidator.Validate(JsonDocument, Json.Schema.JsonSchema, string).
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.
- Durable (baked into packaging):
devops/build/package-runtime-plugins.ps1(Publish-DotNetRuntimePluginBundle, ~L970–1018) auto-prunes, formodule=concelier, everyStellaOps.*(except the connector’s own) plus$contractSurfaceThirdParty = @('NuGet.Versioning.dll','JsonSchema.Net.dll','Json.More.dll'). Logs[concelier-bundle-prune] <PluginId>: pruned N host-shared assembly(ies). A plain rebuild re-introduces nothing for concelier (auto-prune is automatic) — but a NEW third-party contract DLL must be ADDED to$contractSurfaceThirdPartyor it will reappear. - Runtime/manual (no rebuild):
devops/scripts/prune-concelier-bundle-shared-assemblies.sh [container] [bundle-glob](defaultvndr-cisco; use'*'for all), then restart the service to reload the plugin.
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
- Host
/app/*.Models.dllnewer than the plugins (connector plugins ship no ownModels.dll):Advisory..ctorin a mapper throwsMissingMethodException, 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; seedocs/agent-playbooks/live-deploy-operator.md. - Plugin transitive
StellaOps.*deps failGetTypes()because the default ALC only probes the TPA list and the host doesn’t carry them. Fix = aResolvinghook 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.
