Skip to content

[RFC #1] Plugin contract shape — IFalloutPlugin discovery & registration #97

@ChrisonSimtian

Description

@ChrisonSimtian

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-versionedPluginApiVersion 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

  1. Allow IFalloutPlugin to declare deps on other plugins (builder.RequirePlugin<DotNetPlugin>())? Default: no — discover capabilities via DI, not plugin-to-plugin references. Disagree?
  2. Give Configure a CancellationToken? Default: no — registration is fast and synchronous; async setup belongs in a middleware.
  3. 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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFCDesign discussion / RFC. Comment with feedback; consensus shapes the implementation.target/2027Targets the 2027 calendar-version line. See ADR-0004.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions