You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Extend AuthKit.Plugins.Abstractions with first class plugin discovery, loading, and isolation mechanism covering section G: Discovery, loading and isolation.
The contract introduces swappable plugin loader/discoverer plugin manifest, host version verification, AssemblyLoadContext isolation, dependency ordered loading, disable/cache/log support, and integration with PluginContractValidator. The host pipeline owns discovery cache, compatibility gate, ordering and logging IPluginLoader only loads what it is given in sorted order.
Goal
Provide complete, first class implementation of the "Discovery, loading and isolation" area in the plugin contract, so that plugins can be discovered, loaded, and isolated safely without dependency conflicts and with optional hot reload.
The implementation must allow the host to:
discover plugins via swappable IPluginDiscoverer without activating them,
read PluginManifest before load for pre activation decisions,
reject incompatible plugins via MinHostVersion before load,
isolate each plugin in its own AssemblyLoadContext,
validate every loaded plugin automatically,
order plugins deterministically by DependsOn + Priority,
skip disabled plugins via EffectiveIsEnabled,
avoid full rediscovery via IPluginDiscoveryCache,
and observe every outcome via structured logging.
Problem
There is no discovery/isolation mechanism for plugins (AssemblyLoadContext), causing dependency conflicts between plugins and no hot reload. Discovery is implicit, metadata lives only on IAuthKitPlugin after construction, and version/capability/disable checks happen too late (at runtime). Without it, host runtime behavior is unpredictable when multiple plugins ship conflicting transitive dependencies.
Scope
Implementation is divided into the following sub issues:
swappable discovery MUST NOT activate, loader receives sorted accepted list only
G2
Plugin manifest
PluginManifest record mirroring A1–A13, read from plugin.json during discovery before load
PluginManifest.cs
consistent with IAuthKitPlugin instance (hard failure on mismatch) duplicate Id rejected
G3
Compatibility gate MinHostVersion
HostVersion < MinHostVersion (SemVer 2.0.0) -> reject, no warn-only
host pipeline
prerelease and build metadata semantics correct
G4
AssemblyLoadContext isolation
one collectible AssemblyLoadContext per plugin, attached to LoadedPlugin
loader
no type collisions; host assemblies shared
G5
PluginContractValidator integration
runs automatically after isolation, before activation fail -> startup error
tools/PluginContractValidator, host pipeline
non-conforming -> hard reject
G6
Extend validator with new hooks
validates G1/G2/B/C/D/F presence/signatures
tools/PluginContractValidator
full contract coverage, reports member name
G7
Resolving DependsOn
topological sort ready set by Priority asc -> RegistrationOrder asc
host pipeline
no cycles/self/duplicate/missing; deps first deterministic
G8
Disable via configuration
EffectiveIsEnabled(manifest, config) checked at gate before ordering/loading
host pipeline
disabled -> not ordered/isolated/validated/loaded
G9
Cache discovered plugins
IPluginDiscoveryCache before discoverer; stores only discovery results + fingerprints
IPluginDiscoveryCache.cs (or host internal)
hit avoids rediscovery never stores ALC/runtime objects
G10
Log discovered plugins
host pipeline logs discovery and final outcome with reason
host pipeline
Information/Warning/Error per outcome
G1. Discoverer / Loader
Separate discovery (finding candidates and reading manifest) from loading (constructing IAuthKitPlugin instance). Loader receives exactly what the pipeline produced it does not rediscover and does not activate.
Requirements
Discoverer returns async stream of DiscoveredPlugin without activating any plugin it reads manifest during discovery.
Loader receives IReadOnlyList<DiscoveredPlugin> (already validated, gated, sorted) and returns IReadOnlyList<LoadedPlugin> it does not rediscover.
LoadAsync loads and constructs but does not activate runtime behavior.
Implementation is swappable via host pipeline.
Location is discovery source specific (directory/dll/zip/uri) and not interpreted by contract.
G2. Plugin Manifest
PluginManifest mirrors A1–A13 for pre activation decisions. Read by discoverer before assembly load.
Requirements
Id, Name, Version required; MinHostVersion, DependsOn optional.
SemVer 2.0.0 build metadata ignored for precedence; never System.Version.
PluginManifest does not inherit from IAuthKitPlugin.
Post load consistency: Id/Name/Version/IsEnabled/set-equal Capabilities (OrdinalIgnoreCase) must match instance; mismatch -> hard REJECT.
Duplicate Id across discovered manifests -> deterministic reject before activation.
G3. Compatibility Gate MinHostVersion
Gate (not loader) compares manifest.MinHostVersion to host version and rejects before loading.
if(manifest.MinHostVersionis not null&&hostVersion<manifest.MinHostVersion)reject;
Requirements
Host 2.3.9 with Min 2.4.0 -> reject; 2.4.0 -> accept; 2.4.1/3.0.0 -> accept.
Loader creates one AssemblyLoadContext per plugin (preferably IsCollectible). Host/framework assemblies shared only plugin private dependencies isolated. Context attached to LoadedPlugin, not DiscoveredPlugin.
Requirements
Each plugin loads into its own ALC no type collisions between conflicting dependency versions.
Host types shared, not duplicated.
IsCollectible where possible.
G5. Validator Integration
Loader pipeline invokes PluginContractValidator on each loaded plugin/type after isolation and before activation. Failure -> startup error (hard reject, Strict/Warn/Ignore is later host policy).
G6. Validator Coverage
Add rules for every new contract member: G1 loader/discoverer interfaces, G2 manifest shape, B lifecycle hooks, C middleware models, D health result, F security scheme extensions. Each rule reports specific missing/invalid member.
G7. Resolving DependsOn
Host pipeline builds dependency graph from DependsOn (plugin Id values) and topologically sorts it before invoking loader.
Algorithm
Kahn + priority queue: at each step among dependency ready set pick lowest Priority asc then RegistrationOrder asc. Never resort entire result.
Example: A p-100 -> B p-200 DependsOn A, C p0. Ready {A,C} -> pick A (-100), ready {B,C} -> pick B (-200) -> C ⇒ A, B, C. With A p100, C p0 the correct ascending result is C, A, B.
Requirements
Cycle -> startup error deps before dependents deterministic.
DependsOn entries are Id values missing/self/duplicate/invalid Id -> reject, cycle -> startup error.
Graph validation failure -> startup failure (not per plugin reject).
Dependency unavailable (disabled/incompatible/validation/load failed) -> dependent REJECTED with dependency unavailable.
No version range resolution.
G8. Disable via Configuration
Compatibility gate checks EffectiveIsEnabled(manifest, config) before ordering/loading.
Hit reuses DiscoveredPlugin[] without calling discoverer miss calls discoverer then StoreAsync. Stores only discovery results + fingerprints, never LoadedPlugin/IAuthKitPlugin/Type/ALC/process bound state. Cache key is assembly paths/hashes. Host local, no distributed cache.
G10. Log Discovered Plugins
Host pipeline logs at two moments:
Discovery: Information discovered id version
Final outcome: Information loaded / Warning skipped reason=disabled / Error rejected reason=minHostVersion|dependency-unavailable|mismatch|validation
Reasons from G3/G5/G7/G8 included. No structured telemetry here (Section H).
Update: epic implemented with two deliberate deviations from the spec.
No legacy fallback. The spec assumes manifest-less plugins keep loading via a fallback path. That path was removed along with the rest of the legacy surface: a directory without a readable manifest is Invalid and never loads. Rationale: a single loading model (manifest-first) instead of two parallel ones; every solution ships its manifest (committed like Shield/Example, generated at build like DevTokens/DevTools).
Rejections do not abort startup (except unorderable graphs). Structural problems, contract violations, loader failures, and unavailable dependencies reject only the offending plugin as Invalid/Rejected in…
Summary
Extend
AuthKit.Plugins.Abstractionswith first class plugin discovery, loading, and isolation mechanism covering section G: Discovery, loading and isolation.The contract introduces swappable plugin loader/discoverer plugin manifest, host version verification,
AssemblyLoadContextisolation, dependency ordered loading, disable/cache/log support, and integration withPluginContractValidator. The host pipeline owns discovery cache, compatibility gate, ordering and loggingIPluginLoaderonly loads what it is given in sorted order.Goal
Provide complete, first class implementation of the "Discovery, loading and isolation" area in the plugin contract, so that plugins can be discovered, loaded, and isolated safely without dependency conflicts and with optional hot reload.
The implementation must allow the host to:
IPluginDiscovererwithout activating them,PluginManifestbefore load for pre activation decisions,MinHostVersionbefore load,AssemblyLoadContext,DependsOn+Priority,EffectiveIsEnabled,IPluginDiscoveryCache,Problem
There is no discovery/isolation mechanism for plugins (
AssemblyLoadContext), causing dependency conflicts between plugins and no hot reload. Discovery is implicit, metadata lives only onIAuthKitPluginafter construction, and version/capability/disable checks happen too late (at runtime). Without it, host runtime behavior is unpredictable when multiple plugins ship conflicting transitive dependencies.Scope
Implementation is divided into the following sub issues:
Specification
IPluginDiscoverer/IPluginLoaderIAsyncEnumerable<DiscoveredPlugin> DiscoverAsync()reads manifest without activating;Task<IReadOnlyList<LoadedPlugin>> LoadAsync(IReadOnlyList<DiscoveredPlugin>)constructs without activatingIPluginDiscoverer.cs,IPluginLoader.cs,DiscoveredPlugin.cs,LoadedPlugin.csPluginManifestrecord mirroring A1–A13, read fromplugin.jsonduring discovery before loadPluginManifest.csIAuthKitPlugininstance (hard failure on mismatch) duplicate Id rejectedMinHostVersionHostVersion < MinHostVersion(SemVer 2.0.0) -> reject, no warn-onlyAssemblyLoadContextisolationAssemblyLoadContextper plugin, attached toLoadedPluginPluginContractValidatorintegrationtools/PluginContractValidator, host pipelinetools/PluginContractValidatorDependsOnPriorityasc ->RegistrationOrderascEffectiveIsEnabled(manifest, config)checked at gate before ordering/loadingIPluginDiscoveryCachebefore discoverer; stores only discovery results + fingerprintsIPluginDiscoveryCache.cs(or host internal)G1. Discoverer / Loader
Separate discovery (finding candidates and reading manifest) from loading (constructing
IAuthKitPlugininstance). Loader receives exactly what the pipeline produced it does not rediscover and does not activate.Requirements
DiscoveredPluginwithout activating any plugin it reads manifest during discovery.IReadOnlyList<DiscoveredPlugin>(already validated, gated, sorted) and returnsIReadOnlyList<LoadedPlugin>it does not rediscover.LoadAsyncloads and constructs but does not activate runtime behavior.Locationis discovery source specific (directory/dll/zip/uri) and not interpreted by contract.G2. Plugin Manifest
PluginManifestmirrors A1–A13 for pre activation decisions. Read by discoverer before assembly load.Requirements
Id,Name,Versionrequired;MinHostVersion,DependsOnoptional.System.Version.PluginManifestdoes not inherit fromIAuthKitPlugin.Id/Name/Version/IsEnabled/set-equalCapabilities(OrdinalIgnoreCase) must match instance; mismatch -> hard REJECT.Idacross discovered manifests -> deterministic reject before activation.G3. Compatibility Gate MinHostVersion
Gate (not loader) compares
manifest.MinHostVersionto host version and rejects before loading.Requirements
Host 2.3.9withMin 2.4.0-> reject;2.4.0-> accept;2.4.1/3.0.0-> accept.1.2.3-alpha < 1.2.3-alpha.1 < 1.2.3-beta < 1.2.3.MinHostVersion-> unaffected no warnonly mode.G4. AssemblyLoadContext Isolation
Loader creates one
AssemblyLoadContextper plugin (preferablyIsCollectible). Host/framework assemblies shared only plugin private dependencies isolated. Context attached toLoadedPlugin, notDiscoveredPlugin.Requirements
IsCollectiblewhere possible.G5. Validator Integration
Loader pipeline invokes
PluginContractValidatoron each loaded plugin/type after isolation and before activation. Failure -> startup error (hard reject, Strict/Warn/Ignore is later host policy).G6. Validator Coverage
Add rules for every new contract member: G1 loader/discoverer interfaces, G2 manifest shape, B lifecycle hooks, C middleware models, D health result, F security scheme extensions. Each rule reports specific missing/invalid member.
G7. Resolving DependsOn
Host pipeline builds dependency graph from
DependsOn(plugin Id values) and topologically sorts it before invoking loader.Algorithm
Kahn + priority queue: at each step among dependency ready set pick lowest
Priorityasc thenRegistrationOrderasc. Never resort entire result.Example:
Ap-100 ->Bp-200DependsOn A,Cp0. Ready {A,C} -> pickA(-100), ready {B,C} -> pickB(-200) ->C⇒A, B, C. WithAp100,Cp0 the correct ascending result isC, A, B.Requirements
DependsOnentries are Id values missing/self/duplicate/invalid Id -> reject, cycle -> startup error.dependency unavailable.G8. Disable via Configuration
Compatibility gate checks
EffectiveIsEnabled(manifest, config)before ordering/loading.Host may disable
true -> false, never enablefalse -> true. Does not mutatePluginManifestconsistency check staysmanifest.IsEnabled == plugin.IsEnabled.Disabled plugin is not ordered (G7), not isolated (G4), not validated (G5), not loaded. Composes with
IsMiddlewareEnabled(C).G9. Cache Discovered Plugins
Host owned
IPluginDiscoveryCachesits before discovery:Hit reuses
DiscoveredPlugin[]without calling discoverer miss calls discoverer thenStoreAsync. Stores only discovery results + fingerprints, neverLoadedPlugin/IAuthKitPlugin/Type/ALC/process bound state. Cache key is assembly paths/hashes. Host local, no distributed cache.G10. Log Discovered Plugins
Host pipeline logs at two moments:
Information discovered id versionInformation loaded/Warning skipped reason=disabled/Error rejected reason=minHostVersion|dependency-unavailable|mismatch|validationReasons from G3/G5/G7/G8 included. No structured telemetry here (Section H).
Architecture
flowchart TD Cache["Discovery Cache G9<br/>IPluginDiscoveryCache"] --> Check{"Cache valid?"} Check -- yes --> Reuse["HIT → DiscoveredPlugin[]"] Check -- no --> Disc["IPluginDiscoverer G1<br/>plugin.json → Manifest"] Disc --> Store["StoreAsync<br/>Manifest + Location<br/>+ fingerprint"] Store --> Merge Reuse --> Merge["DiscoveredPlugin[]"] Merge --> PreVal["Pre-load validation G2<br/>structural + duplicate Id"] PreVal -- invalid → startup error --> Err0["startup failure"] PreVal -- ok --> Gate{"Gate G3/G8<br/>EffectiveIsEnabled<br/>MinHostVersion"} Gate -- disabled → SKIP --> L1["LOG Warning"] Gate -- incompatible → REJECT --> L2["LOG Error"] Gate -- accepted --> Graph["Dependency Graph G7"] Graph -- invalid → startup error --> Err1["startup failure"] Graph -- valid --> Sort["Topo sort<br/>Priority asc → RegistrationOrder"] Sort -- dependency unavailable --> DepRej["REJECT dependency-unavailable"] Sort -- ok --> Loader["Loader G1/G4<br/>sorted accepted list"] Loader --> Loaded["LoadedPlugin[]"] Loaded --> Val["Validator G5/G6<br/>+ manifest consistency"] Val --> Act["ACTIVATION"] Act --> Log["LOG G10<br/>discovery + outcome"] classDef gate fill:#fef3c7,stroke:#f59e0b,color:#92400e classDef err fill:#fee2e2,stroke:#ef4444,color:#991b1b classDef ok fill:#dcfce7,stroke:#22c55e,color:#14532d classDef cache fill:#dbeafe,stroke:#3b82f6,color:#1e40af class Cache,Check,Store,Reuse cache class PreVal,Gate,Graph,Sort gate class Err0,Err1,DepRej,L1,L2 err class Act okIPluginDiscovererIPluginDiscoveryCacheIPluginLoaderBackward Compatibility
Versionstring->SemanticVersion(A2) is breaking requiring explicit migration.IAuthKitPluginregistration.MinHostVersion,IsEnabled,Capabilities,DependsOnread fromPluginManifestbefore loading without manifest unaffected.IAuthKitPluginorAuthKitSecuritySchemeDescriptor.Validation
dotnet build AuthKit.Plugins.Abstractions(0 errors).tools/PluginContractValidatoron DevTokens ->[PASS].Acceptance Criteria
IPluginDiscoverer/IPluginLoaderswappable discovery does not activate.PluginManifestconsistent with instance (mismatch -> reject) duplicate Id rejected.HostVersion < MinHostVersion-> reject (SemVer 2.0.0, no warn only).AssemblyLoadContext(collectible), host shared.EffectiveIsEnabled(host can disable, never enable).Non Goals
Update: epic implemented with two deliberate deviations from the spec.
No legacy fallback. The spec assumes manifest-less plugins keep loading via a fallback path. That path was removed along with the rest of the legacy surface: a directory without a readable manifest is Invalid and never loads. Rationale: a single loading model (manifest-first) instead of two parallel ones; every solution ships its manifest (committed like Shield/Example, generated at build like DevTokens/DevTools).
Rejections do not abort startup (except unorderable graphs). Structural problems, contract violations, loader failures, and unavailable dependencies reject only the offending plugin as Invalid/Rejected in…