Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Docs/content/en/adr/030-plugin-middleware-pipeline.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./029-structured-plugin-health-contract.md) | [Next]()
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./029-structured-plugin-health-contract.md) | [Next](./031-plugin-discovery-manifest-gate.md)

# [ADR-030] Declare Plugin Middleware As A First-Class Transport-Explicit Pipeline

Expand Down Expand Up @@ -54,4 +54,4 @@ A single implicit middleware slot cannot express ordering across plugins, per-en
- [ADR-013](./013-dual-rest-and-grpc-transport.md) - dual REST and gRPC transport surfaces
- [ADR-009](./009-dynamic-plugin-discovery.md) - plugin discovery and contract boundary

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./029-structured-plugin-health-contract.md) | [Next]()
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./029-structured-plugin-health-contract.md) | [Next](./031-plugin-discovery-manifest-gate.md)
57 changes: 57 additions & 0 deletions Docs/content/en/adr/031-plugin-discovery-manifest-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./030-plugin-middleware-pipeline.md) | [Next](./032-plugin-isolation-ordering.md)

# [ADR-031] Discover Plugins Through Manifests With A Pre-Load Compatibility Gate

*2026-09* | Status: accepted

**Tag:** #adr_031

**Date:** 2026-09-24

**Scope:** AuthKit.Plugins.Abstractions + Host plugin loading

## Context

Plugins were loaded by single static host routine that located an entry assembly, constructed the instance, and only then read metadata from the instance itself. The host could not reason about a plugin before constructing it, so a plugin requiring a newer host failed late (or crashed startup), disabled plugins were still constructed, and duplicate identities surfaced far from their cause. There was no seam for custom discovery or loading.

## Problem

Metadata lived only on the constructed `IAuthKitPlugin` instance, which forced the host to build the object before any compatibility decision. A single `LoadPlugins` routine mixed discovery, compatibility, construction, and validation, so none of those stages could be replaced, tested, or reasoned about in isolation.

## Decision

- Split the pipeline into swappable stages with explicit ownership: `IPluginDiscoverer` finds candidates and reads manifests (never activates), `IPluginLoader` loads assemblies and constructs instances (never judges compatibility), and the host pipeline owns validation, gating, consistency, and activation around them.
- The discoverer reads `plugin.json` / `plugin.manifest` / `manifest.json` from disk without loading assemblies, yielding `DiscoveredPlugin` (manifest + opaque location + discovery error). Order per stage: discovery errors → structural validation → duplicate Id rejection → compatibility gate → load → manifest/instance consistency → contract validation.
- The compatibility gate runs pre-load on the manifest: `IsEnabled == false` skips quietly, `HostVersion < MinHostVersion` hard-rejects (no warn-only — a too-new plugin risks `MissingMethodException` / `TypeLoadException`). Undefined `MinHostVersion` means unaffected.
- Manifest/instance consistency (`Id`, `Name`, `Version`, `IsEnabled`, set-equal `Capabilities`, `MinHostVersion`, `DependsOn`) is a hard failure. Duplicate manifest Ids are rejected deterministically (first by location wins).
- Outcomes are captured in the host-internal `PluginLoadResult` (loaded / skipped-disabled / rejected / invalid with reasons) for startup logs and diagnostics; it is deliberately not part of the plugin contract.
- A manifest is required: directories without a readable manifest are invalid and never load. There is no legacy fallback. `PluginManifest` never inherits `IAuthKitPlugin` — shared semantics, separate models.

### Design Rationale

- Reading metadata before loading moves failures (bad manifest, duplicate Id, too-new plugin, disabled plugin) ahead of assembly loading, where they are cheap and diagnosable.
- Opaque `Location` keeps the contract source-agnostic (directory today, feed or package tomorrow) without leaking loader details into discovery.
- Per-candidate loader attribution makes every outcome explainable in `PluginLoadResult` instead of failing the whole batch or crashing startup (the previous behavior on contract violation).
- The loader stays dumb on purpose: compatibility policy lives in exactly one place (the gate + pipeline), so custom loaders cannot silently change acceptance rules.

## Rejected

- Keeping the monolithic static loader: no seam for custom discovery/loading, untestable stages, late failures.
- Warn-only compatibility mode: loading a plugin built for a newer host corrupts behavior instead of degrading gracefully; policy engines (`Strict`/`Warn`/`Ignore`) belong to a future host feature, not the basic gate.
- Max-host-version ceiling and per-feature capability negotiation now: reserved for future gate rules.
- Coupling manifest to instance via inheritance: binds the pre-activation model to runtime construction, defeating the purpose of the gate.

## Consequences

- Every plugin solution must ship its `manifest.json` (committed like Shield and Example, or generated at build like DevTokens and DevTools via `AuthKit.ManifestGenerator`, which the Dockerfile runs after publish) — without it the plugin is rejected at startup.
- Graph problems split by severity: structural issues (self/duplicate/invalid dependency entries, duplicate Ids) reject only the offending plugin as `Invalid`; hard startup failure is reserved for unorderable graphs (unknown dependency, cycle).
- `SemanticVersion` (SemVer 2.0.0, build metadata ignored for precedence) is the only version comparison; `System.Version` must never be used.
- Future gate rules (capabilities, platform, max version) plug into `CompatibilityGate` without touching discovery or loading.

## Related

- [ADR-009](./009-dynamic-plugin-discovery.md) - plugin discovery and contract boundary
- [ADR-010](./010-plugin-loading-from-directory.md) - plugin loading and legacy middleware slot
- [ADR-028](./028-plugin-contract-and-dynamic-loading-architecture.md) - plugin contract and dynamic loading architecture

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./030-plugin-middleware-pipeline.md) | [Next](./032-plugin-isolation-ordering.md)
56 changes: 56 additions & 0 deletions Docs/content/en/adr/032-plugin-isolation-ordering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./031-plugin-discovery-manifest-gate.md) | [Next]()

# [ADR-032] Isolate Plugins In Per-Plugin Load Contexts With Deterministic Ordering

*2026-09* | Status: accepted

**Tag:** #adr_032

**Date:** 2026-09-29

**Scope:** Host plugin loading (isolation, ordering, discovery cache)

## Context

All plugins loaded into the default `AssemblyLoadContext`, so conflicting transitive dependencies collapsed into one universe: whichever copy won broke the others with `TypeLoadException` or silent wrong-version binds. Load order followed discovery order, so dependencies routinely lost to dependents. Every restart paid full rediscovery.

## Problem

One shared context cannot host conflicting dependency versions, and filesystem order is not a loading order. Without isolation, adding any plugin risks every other plugin; without ordering, dependency edges are luck; without caching, startup redoes all discovery work including assembly probing.

## Decision

- Each plugin loads into its own collectible `AssemblyLoadContext` (`PluginLoadContext`), attached to the contract `LoadedPlugin` — never to `DiscoveredPlugin`. Collectible enables future unload orchestration; the loader itself never unloads.
- Sharing wins by rule, in order: explicit shared contracts (`AuthKit.Plugins.Abstractions`, `Grpc.Core.Api`, `Google.Protobuf`), anything already loaded by default, anything shipped in the host application directory. Everything else resolves privately from the plugin directory. A plugin therefore always sees the host's `Core` types (DI identity holds) and never its own `Interceptor` copy.
- Accepted plugins load in Kahn topological order: dependencies first, ties by `Priority` ascending then registration order, never re-sorted afterward. Unknown dependency ids and cycles are startup errors; dependents of unavailable plugins are rejected as dependency-unavailable with propagation.
- Discovery results cache in a host-local file (`PluginManifest` + location + source fingerprint + schema version; never load contexts, types, or instances). Stale or corrupt cache is a miss, never a failure.
- The compatibility gate reads the effective enabled flag (manifest `IsEnabled` anded with host config, which may disable but never re-enable) before ordering, so disabled plugins are never ordered, isolated, or loaded.

### Design Rationale

- Returning null (fall back to default) for shared assemblies keeps one type universe for contracts while private universes diverge per plugin — the exact property DI and `is` checks need.
- The host-directory rule (not just already-loaded) closes the startup-ordering hole: loading runs before first gRPC/DI use, so an explicit list alone would still duplicate copies on a cold host.
- Kahn with priority-then-registration is deterministic for a given discovered set and reviewable in logs; discovery order alone stays the final tiebreak, never the strategy.
- Cache stores fingerprints, not results trust: any input change (manifest or dll bytes) invalidates, so stale entries cannot load.

## Rejected

- Single shared context with version unification: forces one dependency version on all plugins and the host — the original problem.
- Copying host assemblies per plugin directory: duplicates contract types per ALC and breaks DI identity silently.
- Ordering by discovery order or full re-sort by priority afterward: the former is nondeterministic, the latter breaks dependency edges.
- Distributed or in-memory-only cache: host-local file survives restarts with zero infrastructure; caching activated instances is explicitly out.

## Consequences

- Plugin solutions must not rely on privately loading assemblies the host ships; host-owned wins by rule and the plugin gets the host copy.
- Every plugin directory still needs its manifest (ADR-031); ordering and validation keys off manifest identity.
- `PluginLoadResult` distinguishes terminal outcomes (loaded / skipped-disabled / rejected / invalid) so each candidate is explainable.
- Future work (unload orchestration, max-version ceiling, capability rules) plugs into `PluginLoadContext`, `CompatibilityGate`, or `DependencyGraph` without touching discovery.

## Related

- [ADR-031](./031-plugin-discovery-manifest-gate.md) - manifest discovery with pre-load compatibility gate
- [ADR-009](./009-dynamic-plugin-discovery.md) - plugin discovery and contract boundary
- [ADR-028](./028-plugin-contract-and-dynamic-loading-architecture.md) - plugin contract and dynamic loading architecture

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./031-plugin-discovery-manifest-gate.md) | [Next]()
2 changes: 2 additions & 0 deletions Docs/content/en/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ The table below shows the architecture areas and their current scope.
| [ADR-028](./028-plugin-contract-and-dynamic-loading-architecture.md) | Define The Plugin Contract And Dynamic Loading Architecture | Plugins | accepted | 2026-09-11 |
| [ADR-029](./029-structured-plugin-health-contract.md) | Expose Structured And Cancellable Plugin Health Results | Plugins | accepted | 2026-09-13 |
| [ADR-030](./030-plugin-middleware-pipeline.md) | Declare Plugin Middleware As A First-Class Transport-Explicit Pipeline | Plugins | accepted | 2026-09-24 |
| [ADR-031](./031-plugin-discovery-manifest-gate.md) | Discover Plugins Through Manifests With A Pre-Load Compatibility Gate | Plugins | accepted | 2026-09-24 |
| [ADR-032](./032-plugin-isolation-ordering.md) | Isolate Plugins In Per-Plugin Load Contexts With Deterministic Ordering | Plugins | accepted | 2026-09-29 |

## Relationships Between Areas

Expand Down
4 changes: 2 additions & 2 deletions Docs/content/pl/adr/030-plugin-middleware-pipeline.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[/pl/](/pl/) | [Indeks kategorii](/pl/adr/) | [Poprzedni](/pl/adr/029-structured-plugin-health-contract/) | [Następny]()
[/pl/](/pl/) | [Indeks kategorii](/pl/adr/) | [Poprzedni](/pl/adr/029-structured-plugin-health-contract/) | [Następny](/pl/adr/031-plugin-discovery-manifest-gate/)

# [ADR-030] Deklaratywny Pipeline Middleware Pluginów Z Jawnym Transportem

Expand Down Expand Up @@ -54,4 +54,4 @@ Pojedynczy implicit slot nie wyraża kolejności między pluginami, włączania
- [ADR-013](/pl/adr/013-dual-rest-and-grpc-transport/) - dualny transport REST i gRPC
- [ADR-009](/pl/adr/009-dynamic-plugin-discovery/) - discovery pluginów i granica kontraktu

[/pl/](/pl/) | [Indeks kategorii](/pl/adr/) | [Poprzedni](/pl/adr/029-structured-plugin-health-contract/) | [Następny]()
[/pl/](/pl/) | [Indeks kategorii](/pl/adr/) | [Poprzedni](/pl/adr/029-structured-plugin-health-contract/) | [Następny](/pl/adr/031-plugin-discovery-manifest-gate/)
57 changes: 57 additions & 0 deletions Docs/content/pl/adr/031-plugin-discovery-manifest-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[/pl/](/pl/) | [Indeks kategorii](/pl/adr/) | [Poprzedni](/pl/adr/030-plugin-middleware-pipeline/) | [Następny](/pl/adr/032-plugin-isolation-ordering/)

# [ADR-031] Odkrywanie Pluginów Przez Manifesty Z Bramką Kompatybilności Przed Ładowaniem

*2026-09* | Status: accepted

**Tag:** #adr_031

**Date:** 2026-09-24

**Scope:** AuthKit.Plugins.Abstractions + ładowanie pluginów hosta

## Context

Pluginy ładowała jedna statyczna procedura hosta: znajdowała assembly wejściowe, konstruowała instancję i dopiero potem czytała metadane z samej instancji. Host nie mógł rozumować o pluginie przed jego konstrukcją, więc plugin wymagający nowszego hosta padał późno (albo kładł start), wyłączone pluginy i tak były konstruowane, a zduplikowane tożsamości wychodziły daleko od przyczyny. Nie było szwu na własne discovery ani ładowanie.

## Problem

Metadane żyły tylko na skonstruowanej instancji `IAuthKitPlugin`, co zmuszało hosta do zbudowania obiektu przed jakąkolwiek decyzją o kompatybilności. Jedna procedura `LoadPlugins` mieszała discovery, kompatybilność, konstrukcję i walidację, więc żaden z tych etapów nie dał się wymienić, testować ani analizować w izolacji.

## Decision

- Podział pipeline na wymienne etapy z jawną własnością: `IPluginDiscoverer` znajduje kandydatów i czyta manifesty (nigdy nie aktywuje), `IPluginLoader` ładuje assembly i konstruuje instancje (nigdy nie ocenia kompatybilności), a pipeline hosta odpowiada za walidację, bramkę, spójność i aktywację wokół nich.
- Discoverer czyta `plugin.json` / `plugin.manifest` / `manifest.json` z dysku bez ładowania assembly, zwracając `DiscoveredPlugin` (manifest + nieprzezroczysta lokalizacja + błąd discovery). Kolejność etapów: błędy discovery → walidacja strukturalna → odrzucenie duplikatów Id → bramka kompatybilności → ładowanie → spójność manifest/instancja → walidacja kontraktu.
- Bramka kompatybilności działa przed ładowaniem na manifeście: `IsEnabled == false` cicho pomija, `HostVersion < MinHostVersion` twardo odrzuca (bez trybu warn-only — za nowy plugin grozi `MissingMethodException` / `TypeLoadException`). Brak `MinHostVersion` znaczy brak wpływu.
- Spójność manifest/instancja (`Id`, `Name`, `Version`, `IsEnabled`, równe zbiory `Capabilities`, `MinHostVersion`, `DependsOn`) to twardy błąd. Zduplikowane Id manifestów odrzucane deterministycznie (pierwszy po lokalizacji wygrywa).
- Wyniki zbiera hostowy `PluginLoadResult` (załadowane / pominięte-wyłączone / odrzucone / niepoprawne, z powodami) do logów startowych i diagnostyki; celowo nie jest częścią kontraktu pluginu.
- Manifest jest wymagany: katalogi bez czytelnego manifestu są niepoprawne i nigdy się nie ładują. Nie ma ścieżki legacy. `PluginManifest` nigdy nie dziedziczy po `IAuthKitPlugin` — wspólna semantyka, osobne modele.

### Design Rationale

- Czytanie metadanych przed ładowaniem przesuwa błędy (zły manifest, duplikat Id, za nowy plugin, wyłączony plugin) przed ładowanie assembly — tam są tanie i diagnozowalne.
- Nieprzezroczysta `Location` uniezależnia kontrakt od źródła (dziś katalog, jutro feed albo pakiet) bez przeciekania detali loadera do discovery.
- Atrybucja per kandydat sprawia, że każdy wynik da się wyjaśnić w `PluginLoadResult`, zamiast kłaść cały batch albo crashować start (poprzednie zachowanie przy naruszeniu kontraktu).
- Loader jest celowo głupi: polityka kompatybilności żyje w dokładnie jednym miejscu (bramka + pipeline), więc własne loadery nie zmienią po cichu reguł akceptacji.

## Rejected

- Monolityczny statyczny loader: brak szwu na własne discovery/ładowanie, nietestowalne etapy, późne błędy.
- Tryb warn-only: ładowanie pluginu pod nowszego hosta psuje zachowanie zamiast graceful degradation; silniki polityk (`Strict`/`Warn`/`Ignore`) to przyszła funkcja hosta, nie podstawowa bramka.
- Sufit max-host-version i negocjacja capabilities teraz: zarezerwowane na przyszłe reguły bramki.
- Sprzężenie manifestu z instancją przez dziedziczenie: wiąże model pre-aktywacyjny z konstrukcją runtime, niwecząc sens bramki.

## Consequences

- Każde rozwiązanie pluginu musi dostarczać swój `manifest.json` (commitowany jak Shield i Example albo generowany przy buildzie jak DevTokens i DevTools przez `AuthKit.ManifestGenerator`, który Dockerfile odpala po publikacji) — bez niego plugin odpada na starcie.
- Problemy grafu dzielą się po wadze: strukturalne (self/duplikaty/złe wpisy zależności, duplikaty Id) odrzucają tylko winny plugin jako `Invalid`; twardy błąd startu rezerwujemy dla grafów nieuporządkowalnych (nieznana zależność, cykl).
- `SemanticVersion` (SemVer 2.0.0, build metadata ignorowane w precedencji) to jedyne porównywanie wersji; `System.Version` nigdy.
- Przyszłe reguły bramki (capabilities, platforma, max version) wpina się w `CompatibilityGate` bez ruszania discovery ani ładowania.

## Powiązane

- [ADR-009](/pl/adr/009-dynamic-plugin-discovery/) - discovery pluginów i granica kontraktu
- [ADR-010](/pl/adr/010-plugin-loading-from-directory/) - ładowanie pluginów i legacy slot middleware
- [ADR-028](/pl/adr/028-plugin-contract-and-dynamic-loading-architecture/) - kontrakt pluginu i architektura dynamicznego ładowania

[/pl/](/pl/) | [Indeks kategorii](/pl/adr/) | [Poprzedni](/pl/adr/030-plugin-middleware-pipeline/) | [Następny](/pl/adr/032-plugin-isolation-ordering/)
Loading
Loading