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
Introduce the discovery and loading foundation for plugins: swappable discovery/loader interfaces, declarative plugin manifest carrying the A1–A13 metadata, and host compatibility gate whose first rule is MinHostVersion with clear separation between discovery, compatibility decisions, loading (construct instance), and activation (start runtime behavior).
Goal
Let the host discover and load plugins through pluggable mechanism, read plugin metadata before activation, and reject incompatible plugins with the compatibility gate running before loading and the full contract validation running after the isolated load.
Background
Plugins are currently registered implicitly by the host, with no abstraction that lets the host choose how and from where plugins are discovered. Metadata lives only on the IAuthKitPlugin instance, so the host cannot reason about plugin until it is constructed, and a plugin requiring newer host features fails only at runtime. These gaps block the rest of Section G (isolation, ordering, validation).
Separate discovery (finding candidate plugins and reading their manifest) from loading (constructing the IAuthKitPlugin instance). The loader receives exactly what the discoverer produced it does not rediscover and it does not activate runtime behavior.
G2. Plugin manifest
Add PluginManifest record that mirrors the metadata surface from A1–A13. The discoverer reads it from plugin.json / plugin.manifest during discovery, before the plugin assembly is loaded or activated, so the compatibility gate can run first.
G3. Compatibility gate MinHostVersion
The compatibility gate (not the loader) compares manifest.MinHostVersion (semver) to the host's reported version and rejects mismatches before loading. MinHostVersion is the first rule of gate that may later also cover enabled, required host capabilities, platform, and architecture but only MinHostVersion is implemented now.
Proposed Contract
publicinterfaceIPluginDiscoverer{IAsyncEnumerable<DiscoveredPlugin>DiscoverAsync(CancellationTokenct=default);}publicsealedrecordDiscoveredPlugin{publicrequiredPluginManifestManifest{get;init;}// Location identifies the source from which the loader can load the plugin.// Its format is discovery source specific (directory, dll, zip, package, uri, ...)// and must not be interpreted by the contract.publicrequiredstringLocation{get;init;}}publicinterfaceIPluginLoader{Task<IReadOnlyList<LoadedPlugin>>LoadAsync(IReadOnlyList<DiscoveredPlugin>plugins,CancellationTokenct=default);}publicsealedrecordLoadedPlugin{publicrequiredPluginManifestManifest{get;init;}publicrequiredTypePluginType{get;init;}publicrequiredIAuthKitPluginInstance{get;init;}}publicsealedrecordPluginManifest{publicrequiredstringId{get;init;}publicrequiredstringName{get;init;}publicrequiredSemanticVersionVersion{get;init;}publicSemanticVersion?MinHostVersion{get;init;}publicIReadOnlyList<string>DependsOn{get;init;}=Array.Empty<string>();// A6–A8/A11 — classification/lifecycle/feature metadata, available pre activationpublicIReadOnlyList<string>Tags{get;init;}=Array.Empty<string>();publicintPriority{get;init;}publicboolIsEnabled{get;init;}=true;publicIReadOnlySet<string>Capabilities{get;init;}=ImmutableHashSet.Create<string>(StringComparer.OrdinalIgnoreCase);// remaining metadata mirrors A1–A13}
The host owns the default discoverer/loader implementation plugins never implement them. IPluginLoaderloads and constructs the plugin instance (returns LoadedPlugin) but does not start/activate its runtime behavior activation is the host's separate step after validation, consistency, and ordering.
Compatibility check (host pipeline, not inside the loader):
if(manifest.MinHostVersionis not null&&hostVersion<manifest.MinHostVersion){reject;// hard failure, no "warn only" at this stage}
Requirements
G1. Loader interface
The discoverer returns an async stream of DiscoveredPluginwithout activating any plugin; it reads the manifest during discovery.
The loader receives the discovered plugins (IReadOnlyList<DiscoveredPlugin>) and returns IReadOnlyList<LoadedPlugin> it does not re discover.
LoadAsyncloads and constructs the instance but does not activate runtime behavior; activation is separate host step.
The loader implementation is swappable: the host can supply custom IPluginLoader/IPluginDiscoverer.
Discovery MUST NOT activate plugins. All activation/compatibility decisions (enabled G8, version gate G3, validation G5) happen in the host pipeline around the loader, never inside discovery or the loader.
G2. Manifest
Id, Name, Version are required MinHostVersion and DependsOn are optional.
The manifest is read by the discoverer during discovery, before the plugin assembly is loaded or activated.
Semantic versions follow SemVer 2.0.0 (e.g. 1.2.3, 1.2.3-alpha, 1.2.3-beta.1, 1.2.3+build). Comparison must use SemVer-2.0.0-aware type, neverSystem.Version.
Build metadata does not affect precedence: 1.0.0+abc and 1.0.0+xyz have the same precedence.
The manifest declares pre activation metadata IAuthKitPlugin is the runtime contract. They share semantics (Id/Name/Version/DependsOn/IsEnabled/Capabilities/...) but are not the same model and must not be coupled via PluginManifest : IAuthKitPlugin.
The loaded manifest must be consistent with the IAuthKitPlugin instance (same Id, Name, Version, IsEnabled, and set-equal Capabilities with OrdinalIgnoreCase). A mismatch is hard validation failure -> REJECT.
IsEnabled and Capabilities (A6–A8/A11) are projected into the manifest so the compatibility gate can decide before loading the post load consistency check re verifies them against the instance.
Duplicate Ids: multiple discovered manifests declaring the same Id must be rejected deterministically before activation (required for the dependency graph in G7).
Prerelease ordering must hold: 1.2.3-alpha < 1.2.3 and 1.2.3-alpha < 1.2.3-alpha.1 < 1.2.3-beta.
A plugin without MinHostVersion is unaffected.
No "warn only" mode at this stage. Loading plugin that requires newer host risks MissingMethodException / TypeLoadException / behavior corruption, so the gate is hard reject. A Strict / Warn / Ignore policy engine may be added later as a separate host feature, but it is not part of the basic compatibility gate.
MinHostVersion is one rule of the compatibility gate. The gate also consumes manifest.IsEnabled (A8/G8) and manifest.Capabilities (A11) before loading; host capability checks use the manifest's capability set pre activation, and the manifest/instance capability sets are re verified for consistency after load.
Backward Compatibility
Two discovery paths coexist:
Manifest-based plugin pre-activation metadata is available from PluginManifest enables G3/G8/G7 before loading.
Legacy plugin (no manifest) — metadata is available only after activation (read from IAuthKitPlugin). It falls back to the existing implicit loading path; G3/G8 cannot run for it because the metadata is not known beforehand.
Plugins that do not declare MinHostVersion are unaffected. The new loader is an opt in discovery path layered on top of the current registration.
Loading Pipeline
Realistic host pipeline order (cross references other Section G items):
flowchart TD
A["DISCOVERY<br/>reads PluginManifest"] --> B{"Pre-load validation<br/>structural + duplicate Id?"}
B -- REJECT --> R["REJECT"]
B -- ok --> C{"Compatibility Gate"}
C -- "IsEnabled==false → SKIP" --> S["SKIP"]
C -- "Host < MinHostVersion → REJECT" --> R
C -- ok --> D["ISOLATED LOAD → LoadedPlugin<br/>G4"]
D --> E["PluginContractValidator<br/>G5/G6"]
E --> F["Manifest ↔ IAuthKitPlugin consistency<br/>Id/Name/Version/IsEnabled/Capabilities"]
F --> G["DEPENDENCY GRAPH G7<br/>Priority asc → RegistrationOrder asc"]
G --> H["ACTIVATION"]
classDef gate fill:#fef3c7,stroke:#f59e0b,color:#92400e
classDef reject fill:#fee2e2,stroke:#ef4444,color:#991b1b
classDef ok fill:#dcfce7,stroke:#22c55e,color:#14532d
class B,C gate
class R,S reject
class H ok
Loading
Pre load manifest validation (structural only, before the assembly is loaded):
Id / Name not empty
Version is valid SemVer
MinHostVersion is valid SemVer
DependsOn entries valid
no duplicate Id
manifest schema valid
Post load plugin validation (after the isolated load, may need the loaded type/assembly):
IAuthKitPlugin implementation
Middleware (C)
Security schemes (F)
Health checks (D)
etc. (full PluginContractValidator, G5/G6)
Validation
G1. Loader interface
IPluginDiscoverer and IPluginLoader interfaces exist.
The discoverer returns DiscoveredPlugin (with manifest read during discovery) without activating plugins.
IPluginLoader.LoadAsync accepts discovered plugins and returns IReadOnlyList<LoadedPlugin>.
LoadAsync constructs the instance but does not activate runtime behavior.
A custom loader/discoverer can replace the default.
DevTokens still loads after the contract change.
PluginContractValidator passes.
G2. Manifest
PluginManifest record exists with the A1–A13 fields, including Tags/Priority/IsEnabled/Capabilities (A6–A8/A11) for pre activation.
The discoverer reads the manifest before activation.
A manifest/plugin Id/Name/Version mismatch is hard rejection.
A manifest/instance IsEnabled mismatch or non set equal Capabilities is hard rejection (consistency).
Duplicate Id across discovered plugins is rejected before activation.
Semantic version comparison uses SemVer 2.0.0 semantics; build metadata ignored for precedence.
PluginManifest does not inherit from IAuthKitPlugin.
G3. Compatibility gate
Reject when HostVersion < MinHostVersion (with the 2.4.0 examples above).
A plugin without manifest loads via the legacy path (metadata after activation).
A manifest based plugin is rejected on manifest <-> instance inconsistency.
Acceptance Criteria
The loader implementation is swappable: the host can supply custom IPluginLoader/IPluginDiscoverer.
The discoverer returns DiscoveredPlugin (manifest read during discovery) without activating plugins discovery MUST NOT activate plugins.
IPluginLoader.LoadAsync receives the discovered plugins and returns LoadedPlugin instances (constructed, not activated).
Metadata from A1–A13 loads from file before activation.
The loaded manifest is consistent (hard failure on mismatch: Id/Name/Version/IsEnabled/set equal Capabilities) with the IAuthKitPlugin instance the plugin ultimately provides.
Reject rule: HostVersion < MinHostVersion.
Duplicate Id across discovered manifests is rejected before activation.
A plugin without MinHostVersion is unaffected.
Legacy plugins without manifest still load via the fallback path.
Non Goals
No "warn only" policy in the basic compatibility gate (a separate host policy engine may add Strict/Warn/Ignore later).
DiscoveredPlugin does not carry AssemblyLoadContext that is added by G4 via LoadedPlugin.
No PluginManifest : IAuthKitPlugin inheritance the two share semantics but are separate models.
The loader does not decide compatibility (enabled/version/validation) those live in the host pipeline around it.
IPluginLoader.LoadAsync does not return PluginLoadResult, PluginLoadResult is an internal host concept (see below).
No specific discovery source (filesystem, package feed, database) is mandated; Location format is discovery source specific.
No new metadata schema beyond A1–A13 is introduced.
No maximum host version ceiling is enforced here (can be added later).
No per feature capability negotiation beyond the version check (left as future compatibility gate rule).
PluginLoadResult (host pipeline, not contract)
IPluginLoader returns only the accepted LoadedPlugin list, but the host pipeline should track outcomes for diagnostics:
This PluginLoadResult is an internal host concept and is intentionally not part of IAuthKitPlugin or the base loader contract. It is useful for startup logs, diagnostics, health, admin endpoints, and telemetry.
Summary
Introduce the discovery and loading foundation for plugins: swappable discovery/loader interfaces, declarative plugin manifest carrying the A1–A13 metadata, and host compatibility gate whose first rule is
MinHostVersionwith clear separation between discovery, compatibility decisions, loading (construct instance), and activation (start runtime behavior).Goal
Let the host discover and load plugins through pluggable mechanism, read plugin metadata before activation, and reject incompatible plugins with the compatibility gate running before loading and the full contract validation running after the isolated load.
Background
Plugins are currently registered implicitly by the host, with no abstraction that lets the host choose how and from where plugins are discovered. Metadata lives only on the
IAuthKitPlugininstance, so the host cannot reason about plugin until it is constructed, and a plugin requiring newer host features fails only at runtime. These gaps block the rest of Section G (isolation, ordering, validation).Architecture
The responsibilities are deliberately separated:
flowchart TD D1["IPluginDiscoverer<br/>reads plugin.json → PluginManifest"] --> D2["DiscoveredPlugin<br/>Manifest + Location"] D2 --> Gate{"Compatibility Gate"} Gate -- accepted --> Loader["IPluginLoader<br/>constructs instance"] Gate -- rejected --> Rej["REJECT / SKIP<br/>disabled G8<br/>invalid manifest<br/>duplicate Id<br/>incompatible host G3"] Loader --> Loaded["LoadedPlugin<br/>Manifest + Instance"] Loaded --> V1["Plugin validation<br/>G5/G6"] V1 --> V2["Manifest ↔ Plugin<br/>consistency"] V2 --> Dep["Dependency ordering<br/>G7"] Dep --> Act["ACTIVATION"] classDef gate fill:#fef3c7,stroke:#f59e0b,color:#92400e classDef reject fill:#fee2e2,stroke:#ef4444,color:#991b1b classDef ok fill:#dcfce7,stroke:#22c55e,color:#14532d class Gate gate class Rej reject class Act okTarget shape for G1–G3:
flowchart TD Disc["IPluginDiscoverer"] -- "reads plugin.json" --> DP["DiscoveredPlugin"] DP --> M1["Manifest"] DP --> Loc["Location"] DP --> Gate2{"Compatibility Gate"} Gate2 -- disabled --> SK2["SKIP"] Gate2 -- incompatible --> RJ2["REJECT"] Gate2 -- accepted --> Acc["accepted plugin"] Acc --> Loader2["IPluginLoader"] Loader2 --> LP["LoadedPlugin<br/>Manifest + Instance"] LP --> Act2["ACTIVATION"] classDef gate fill:#fef3c7,stroke:#f59e0b,color:#92400e classDef reject fill:#fee2e2,stroke:#ef4444,color:#991b1b classDef ok fill:#dcfce7,stroke:#22c55e,color:#14532d class Gate2 gate class SK2,RJ2 reject class Act2 okScope
G1.
IPluginLoader/IPluginDiscovererSeparate discovery (finding candidate plugins and reading their manifest) from loading (constructing the
IAuthKitPlugininstance). The loader receives exactly what the discoverer produced it does not rediscover and it does not activate runtime behavior.G2. Plugin manifest
Add
PluginManifestrecord that mirrors the metadata surface from A1–A13. The discoverer reads it fromplugin.json/plugin.manifestduring discovery, before the plugin assembly is loaded or activated, so the compatibility gate can run first.G3. Compatibility gate
MinHostVersionThe compatibility gate (not the loader) compares
manifest.MinHostVersion(semver) to the host's reported version and rejects mismatches before loading.MinHostVersionis the first rule of gate that may later also cover enabled, required host capabilities, platform, and architecture but onlyMinHostVersionis implemented now.Proposed Contract
The host owns the default discoverer/loader implementation plugins never implement them.
IPluginLoaderloads and constructs the plugin instance (returnsLoadedPlugin) but does not start/activate its runtime behavior activation is the host's separate step after validation, consistency, and ordering.Compatibility check (host pipeline, not inside the loader):
Requirements
G1. Loader interface
DiscoveredPluginwithout activating any plugin; it reads the manifest during discovery.IReadOnlyList<DiscoveredPlugin>) and returnsIReadOnlyList<LoadedPlugin>it does not re discover.LoadAsyncloads and constructs the instance but does not activate runtime behavior; activation is separate host step.IPluginLoader/IPluginDiscoverer.G2. Manifest
Id,Name,Versionare requiredMinHostVersionandDependsOnare optional.1.2.3,1.2.3-alpha,1.2.3-beta.1,1.2.3+build). Comparison must use SemVer-2.0.0-aware type, neverSystem.Version.1.0.0+abcand1.0.0+xyzhave the same precedence.IAuthKitPluginis the runtime contract. They share semantics (Id/Name/Version/DependsOn/IsEnabled/Capabilities/...) but are not the same model and must not be coupled viaPluginManifest : IAuthKitPlugin.IAuthKitPlugininstance (sameId,Name,Version,IsEnabled, and set-equalCapabilitieswithOrdinalIgnoreCase). A mismatch is hard validation failure -> REJECT.IsEnabledandCapabilities(A6–A8/A11) are projected into the manifest so the compatibility gate can decide before loading the post load consistency check re verifies them against the instance.Idmust be rejected deterministically before activation (required for the dependency graph in G7).G3. Compatibility gate (
MinHostVersion)HostVersion < MinHostVersion-> reject.MinHostVersion = 2.4.0:2.3.9-> reject2.4.0-> accept2.4.1-> accept3.0.0-> accept1.2.3-alpha < 1.2.3and1.2.3-alpha < 1.2.3-alpha.1 < 1.2.3-beta.MinHostVersionis unaffected.MissingMethodException/TypeLoadException/ behavior corruption, so the gate is hard reject. AStrict/Warn/Ignorepolicy engine may be added later as a separate host feature, but it is not part of the basic compatibility gate.MinHostVersionis one rule of the compatibility gate. The gate also consumesmanifest.IsEnabled(A8/G8) andmanifest.Capabilities(A11) before loading; host capability checks use the manifest's capability set pre activation, and the manifest/instance capability sets are re verified for consistency after load.Backward Compatibility
Two discovery paths coexist:
PluginManifestenables G3/G8/G7 before loading.IAuthKitPlugin). It falls back to the existing implicit loading path; G3/G8 cannot run for it because the metadata is not known beforehand.Plugins that do not declare
MinHostVersionare unaffected. The new loader is an opt in discovery path layered on top of the current registration.Loading Pipeline
Realistic host pipeline order (cross references other Section G items):
flowchart TD A["DISCOVERY<br/>reads PluginManifest"] --> B{"Pre-load validation<br/>structural + duplicate Id?"} B -- REJECT --> R["REJECT"] B -- ok --> C{"Compatibility Gate"} C -- "IsEnabled==false → SKIP" --> S["SKIP"] C -- "Host < MinHostVersion → REJECT" --> R C -- ok --> D["ISOLATED LOAD → LoadedPlugin<br/>G4"] D --> E["PluginContractValidator<br/>G5/G6"] E --> F["Manifest ↔ IAuthKitPlugin consistency<br/>Id/Name/Version/IsEnabled/Capabilities"] F --> G["DEPENDENCY GRAPH G7<br/>Priority asc → RegistrationOrder asc"] G --> H["ACTIVATION"] classDef gate fill:#fef3c7,stroke:#f59e0b,color:#92400e classDef reject fill:#fee2e2,stroke:#ef4444,color:#991b1b classDef ok fill:#dcfce7,stroke:#22c55e,color:#14532d class B,C gate class R,S reject class H okPre load manifest validation (structural only, before the assembly is loaded):
Id/Namenot emptyVersionis valid SemVerMinHostVersionis valid SemVerDependsOnentries validIdPost load plugin validation (after the isolated load, may need the loaded type/assembly):
IAuthKitPluginimplementationPluginContractValidator, G5/G6)Validation
G1. Loader interface
IPluginDiscovererandIPluginLoaderinterfaces exist.DiscoveredPlugin(with manifest read during discovery) without activating plugins.IPluginLoader.LoadAsyncaccepts discovered plugins and returnsIReadOnlyList<LoadedPlugin>.LoadAsyncconstructs the instance but does not activate runtime behavior.DevTokensstill loads after the contract change.PluginContractValidatorpasses.G2. Manifest
PluginManifestrecord exists with the A1–A13 fields, includingTags/Priority/IsEnabled/Capabilities(A6–A8/A11) for pre activation.Id/Name/Versionmismatch is hard rejection.IsEnabledmismatch or non set equalCapabilitiesis hard rejection (consistency).Idacross discovered plugins is rejected before activation.PluginManifestdoes not inherit fromIAuthKitPlugin.G3. Compatibility gate
HostVersion < MinHostVersion(with the2.4.0examples above).1.2.3-alpha < 1.2.3;1.2.3-alpha < 1.2.3-alpha.1 < 1.2.3-beta.MinHostVersionis unaffected.Legacy
Acceptance Criteria
IPluginLoader/IPluginDiscoverer.DiscoveredPlugin(manifest read during discovery) without activating plugins discovery MUST NOT activate plugins.IPluginLoader.LoadAsyncreceives the discovered plugins and returnsLoadedPlugininstances (constructed, not activated).Id/Name/Version/IsEnabled/set equalCapabilities) with theIAuthKitPlugininstance the plugin ultimately provides.HostVersion < MinHostVersion.Idacross discovered manifests is rejected before activation.MinHostVersionis unaffected.Non Goals
Strict/Warn/Ignorelater).DiscoveredPlugindoes not carryAssemblyLoadContextthat is added by G4 viaLoadedPlugin.PluginManifest : IAuthKitPlugininheritance the two share semantics but are separate models.IPluginLoader.LoadAsyncdoes not returnPluginLoadResult,PluginLoadResultis an internal host concept (see below).Locationformat is discovery source specific.PluginLoadResult (host pipeline, not contract)
IPluginLoaderreturns only the acceptedLoadedPluginlist, but the host pipeline should track outcomes for diagnostics:This
PluginLoadResultis an internal host concept and is intentionally not part ofIAuthKitPluginor the base loader contract. It is useful for startup logs, diagnostics, health, admin endpoints, and telemetry.