Skip to content

[RFC #4] Plugin discovery and load model #100

Description

@ChrisonSimtian

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

  1. Deterministic load order? Default: yes — alphabetical by package ID. Predictable beats clever. Disagree?
  2. 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.
  3. 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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFCDesign discussion / RFC. Comment with feedback; consensus shapes the implementation.target/vNextTargets the next calendar-version

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions