RFC #4 — Plugin discovery and load model
Self-RFC. Pins the runtime mechanics: how plugins get found, when they load, and what trust posture the host assumes.
Context
RFC #97 fixes the contract (assembly attribute + IFalloutPlugin). This RFC fixes the mechanics — when the host looks for plugins, what happens if one is broken, and whether plugins run isolated or full-trust.
Proposed model
Discovery: opt-in via assembly attribute
At startup the host enumerates loaded assemblies and reads [assembly: FalloutPlugin(typeof(...))] from each. Only assemblies declaring the attribute incur scanning cost. No type-walking, no GetAssemblies().SelectMany(a => a.GetTypes()) — the attribute is a single read.
Load timing: before phase 1 (Initializing)
Discovery and IFalloutPlugin.Configure(builder) run before the orchestrator starts Initializing. The composition root is built and frozen before any middleware runs:
- Plugins can register middleware, hosts, parameter sources — anything the phases consume.
- Plugins cannot alter the composition root mid-build. (If runtime reconfiguration is ever needed, ship a new extension point; don't unfreeze the container.)
Caching
Discovery results (assembly hash → plugin types) cache at <root>/.build/plugin-cache.json. Cache key is the hash of every referenced assembly. Hits skip the attribute read; misses re-scan and update. Saves ~20–100ms on later builds.
Trust model: same as Roslyn analyzers
Plugins run in-process, full trust. A malicious or buggy plugin can call Environment.Exit(0), touch the filesystem, read env vars, make network calls — same as Roslyn analyzers, MSBuild SDKs, source generators. The right tradeoff:
- The user already added the plugin via
PackageReference — a trust decision.
- In-process .NET sandboxing is hard and incomplete (
AppDomains deprecated; collectible AssemblyLoadContexts isolate lifetime, not capability).
- Out-of-process (subprocess per plugin) adds 10–100ms per call and breaks debugging.
The host documents this in the author guide and consumer FAQ. No engineering around it.
Optional: collectible AssemblyLoadContext
If hot-reloading plugins in a long-running daemon ever matters, load each into a collectible AssemblyLoadContext. Defer to a future RFC unless someone surfaces the use case here.
Error handling
- Attribute points at a type that doesn't implement
IFalloutPlugin → throw at startup with the assembly + offending type name.
Configure throws → throw, wrapping the inner exception with "plugin {name} failed to configure". Build stops.
- Plugin requires a newer
PluginApiVersion than the host → throw with the version mismatch.
Fail-fast rationale: silently skipping a broken plugin produces "why doesn't my plugin work" tickets that are hard to debug. Loud, clear failure is kinder.
Questions for discussion
- Deterministic load order? Default: yes — alphabetical by package ID. Predictable beats clever. Disagree?
- Load-order dependencies between plugins (
[FalloutPlugin(LoadAfter = "...")])? Default: no — extension points are unordered within a phase; cross-phase order is fixed. Need another plugin's services? Resolve via DI like any consumer.
- Ship
dotnet fallout plugins list? Default: yes, useful for debugging. Belongs in Fallout.GlobalTool.
Out of scope (other RFCs)
How to engage
Use cases that don't fit this load model are most valuable. "I want plugin X to register a new tool wrapper at minute 3 of the build" tells us the freeze-after-Configure decision is wrong.
Decision lands in
When this RFC locks (2026-08-31), its shape becomes the spec for:
RFC #4 — Plugin discovery and load model
Self-RFC. Pins the runtime mechanics: how plugins get found, when they load, and what trust posture the host assumes.
Context
RFC #97 fixes the contract (assembly attribute +
IFalloutPlugin). This RFC fixes the mechanics — when the host looks for plugins, what happens if one is broken, and whether plugins run isolated or full-trust.Proposed model
Discovery: opt-in via assembly attribute
At startup the host enumerates loaded assemblies and reads
[assembly: FalloutPlugin(typeof(...))]from each. Only assemblies declaring the attribute incur scanning cost. No type-walking, noGetAssemblies().SelectMany(a => a.GetTypes())— the attribute is a single read.Load timing: before phase 1 (Initializing)
Discovery and
IFalloutPlugin.Configure(builder)run before the orchestrator starts Initializing. The composition root is built and frozen before any middleware runs:Caching
Discovery results (assembly hash → plugin types) cache at
<root>/.build/plugin-cache.json. Cache key is the hash of every referenced assembly. Hits skip the attribute read; misses re-scan and update. Saves ~20–100ms on later builds.Trust model: same as Roslyn analyzers
Plugins run in-process, full trust. A malicious or buggy plugin can call
Environment.Exit(0), touch the filesystem, read env vars, make network calls — same as Roslyn analyzers, MSBuild SDKs, source generators. The right tradeoff:PackageReference— a trust decision.AppDomains deprecated; collectibleAssemblyLoadContexts isolate lifetime, not capability).The host documents this in the author guide and consumer FAQ. No engineering around it.
Optional: collectible AssemblyLoadContext
If hot-reloading plugins in a long-running daemon ever matters, load each into a collectible
AssemblyLoadContext. Defer to a future RFC unless someone surfaces the use case here.Error handling
IFalloutPlugin→ throw at startup with the assembly + offending type name.Configurethrows → throw, wrapping the inner exception with "plugin {name} failed to configure". Build stops.PluginApiVersionthan the host → throw with the version mismatch.Fail-fast rationale: silently skipping a broken plugin produces "why doesn't my plugin work" tickets that are hard to debug. Loud, clear failure is kinder.
Questions for discussion
[FalloutPlugin(LoadAfter = "...")])? Default: no — extension points are unordered within a phase; cross-phase order is fixed. Need another plugin's services? Resolve via DI like any consumer.dotnet fallout plugins list? Default: yes, useful for debugging. Belongs inFallout.GlobalTool.Out of scope (other RFCs)
How to engage
Use cases that don't fit this load model are most valuable. "I want plugin X to register a new tool wrapper at minute 3 of the build" tells us the freeze-after-Configure decision is wrong.
Decision lands in
When this RFC locks (2026-08-31), its shape becomes the spec for: