RFC #1 — Plugin contract shape
Self-RFC. Comment with agreement, disagreement, or alternatives. The proposal below is what we'll ship in v12 unless this discussion produces a better answer.
Context
v12 (milestone #7) ships Fallout.Plugin.Sdk 1.0 — the public abstractions package contributors compile plugins against. This RFC pins the contract: how a plugin declares itself and how the host discovers it. IFalloutPlugin's shape is the most consequential SDK decision — it can't break without a major SDK bump, and every plugin author touches it first.
Proposal
A plugin is a NuGet package referenced by the build project. No JSON manifest, no preprocessor directives, no separate plugin folder — same convention as Roslyn analyzers, MSBuild SDKs, source generators.
The entry point is a single assembly attribute pointing at a registration class:
[assembly: FalloutPlugin(typeof(TerraformPlugin))]
public sealed class TerraformPlugin : IFalloutPlugin
{
public PluginApiVersion ApiVersion => PluginApiVersion.V1;
public void Configure(IFalloutPluginBuilder builder)
{
builder.Services.AddSingleton<ITerraformWorkspaceLocator, DefaultTerraformWorkspaceLocator>();
builder.AddToolWrapper<TerraformTasks>();
builder.AddBuildMiddleware<TerraformWorkspaceDetectionMiddleware>();
builder.AddHost<TerraformCloudHost>(detect: env => env.Has("TFC_RUN_ID"));
}
}
At startup the orchestrator scans referenced assemblies for FalloutPluginAttribute, instantiates each IFalloutPlugin, calls Configure, and merges into the composition root.
Why this shape
- Opt-in discovery via attribute — only assemblies declaring it pay any cost. No type-walking every referenced DLL.
- DI as the registration vocabulary — plugins compose against
IServiceCollection + a small IFalloutPluginBuilder. .NET devs already know this.
- One entry point per plugin — single class, single method. Easy to find, read, test.
- API-versioned —
PluginApiVersion lets the host detect a too-new plugin and fail clearly, not at random later.
Alternatives rejected
- JSON manifest (
fallout-plugin.json) — more ceremony than [assembly: FalloutPlugin], no clearer signal, still needs reflection for design-time types.
- Convention-based discovery (any
IFalloutPlugin is registered) — forces a full assembly scan, risks surprise activations from transitive deps, harder to debug "why is this loading?".
#addin directives (Cake's model) — couples the plugin model to source-file pragmas; poor fit with .NET tooling (IntelliSense, refactoring, restore); wrong audience.
Questions for discussion
- Allow
IFalloutPlugin to declare deps on other plugins (builder.RequirePlugin<DotNetPlugin>())? Default: no — discover capabilities via DI, not plugin-to-plugin references. Disagree?
- Give
Configure a CancellationToken? Default: no — registration is fast and synchronous; async setup belongs in a middleware.
- Minimum SDK target framework? Default:
net10.0 (matches global.json). Ship a netstandard2.0 flavour for older runtimes? Probably not — the build host itself needs net10.
Out of scope (separate RFCs)
How to engage
React or comment. Strong opinions backed by use cases are most useful — "this won't work because [my plugin needs X]" is gold; "what about a manifest file?" without a use case isn't.
Decision lands in
When this RFC locks (2026-08-31), its shape becomes the spec for:
RFC #1 — Plugin contract shape
Self-RFC. Comment with agreement, disagreement, or alternatives. The proposal below is what we'll ship in v12 unless this discussion produces a better answer.
Context
v12 (milestone #7) ships
Fallout.Plugin.Sdk1.0 — the public abstractions package contributors compile plugins against. This RFC pins the contract: how a plugin declares itself and how the host discovers it.IFalloutPlugin's shape is the most consequential SDK decision — it can't break without a major SDK bump, and every plugin author touches it first.Proposal
A plugin is a NuGet package referenced by the build project. No JSON manifest, no preprocessor directives, no separate plugin folder — same convention as Roslyn analyzers, MSBuild SDKs, source generators.
The entry point is a single assembly attribute pointing at a registration class:
At startup the orchestrator scans referenced assemblies for
FalloutPluginAttribute, instantiates eachIFalloutPlugin, callsConfigure, and merges into the composition root.Why this shape
IServiceCollection+ a smallIFalloutPluginBuilder. .NET devs already know this.PluginApiVersionlets the host detect a too-new plugin and fail clearly, not at random later.Alternatives rejected
fallout-plugin.json) — more ceremony than[assembly: FalloutPlugin], no clearer signal, still needs reflection for design-time types.IFalloutPluginis registered) — forces a full assembly scan, risks surprise activations from transitive deps, harder to debug "why is this loading?".#addindirectives (Cake's model) — couples the plugin model to source-file pragmas; poor fit with .NET tooling (IntelliSense, refactoring, restore); wrong audience.Questions for discussion
IFalloutPluginto declare deps on other plugins (builder.RequirePlugin<DotNetPlugin>())? Default: no — discover capabilities via DI, not plugin-to-plugin references. Disagree?ConfigureaCancellationToken? Default: no — registration is fast and synchronous; async setup belongs in a middleware.net10.0(matchesglobal.json). Ship anetstandard2.0flavour for older runtimes? Probably not — the build host itself needsnet10.Out of scope (separate RFCs)
How to engage
React or comment. Strong opinions backed by use cases are most useful — "this won't work because [my plugin needs X]" is gold; "what about a manifest file?" without a use case isn't.
Decision lands in
When this RFC locks (2026-08-31), its shape becomes the spec for: