Plugin Development

RTFMv2 supports custom plugins for the desktop client, server, and console. Plugins are compiled .NET assemblies that implement the extension interface for the component they target. This page explains the plugin systems generically and shows how to build your own extensions.

This page does not cover specific shipped plugins. Use it as a reference for creating your own custom functionality.

Plugin Types

Target Purpose Base contract
Desktop client Adds a UI window to the client Plugins menu IRTFMv2Plugin
Server Adds a web UI page, event hooks, optional JSON API, and optional scheduled work IServerPlugin
Console Adds custom console commands OptionsBase

Build plugins against the same RTFMv2 version you deploy them to. Server plugin loading checks assembly compatibility and may report a rebuild hint if the plugin targets a different server assembly version.

Desktop Client Plugins

Desktop client plugins implement IRTFMv2Plugin. At startup, the client scans the Plugins directory under the application base directory, loads DLL files recursively, finds classes that implement IRTFMv2Plugin, calls Initialize(), and adds each plugin to the client Plugins menu.

Client Plugin Contract

public interface IRTFMv2Plugin
{
    string Name { get; }
    string Description { get; }
    string Version { get; }

    void Initialize();
    void Shutdown();
    Avalonia.Controls.Window GetUIElement();
}

Minimal Client Plugin

using Avalonia.Controls;
using RTFMv2;

namespace MyClientPlugin;

public sealed class MyPlugin : IRTFMv2Plugin
{
    public string Name => "My Client Tool";
    public string Description => "Custom client workflow";
    public string Version => "1.0.0";

    public void Initialize()
    {
        // Allocate resources or load configuration here.
    }

    public void Shutdown()
    {
        // Release resources here.
    }

    public Window GetUIElement()
    {
        return new Window
        {
            Title = Name,
            Width = 900,
            Height = 650,
            Content = new TextBlock { Text = "Hello from a custom client plugin." }
        };
    }
}

Deploy a Client Plugin

  1. Create a .NET class library targeting the same runtime as the client.
  2. Reference the client assembly that contains IRTFMv2Plugin.
  3. Compile the plugin.
  4. Copy the plugin DLL and any required private dependencies into the client Plugins directory.
  5. Restart the client.
  6. Open the plugin from the Plugins menu.

Server Plugins

Server plugins implement IServerPlugin. The server expects each plugin to live in its own subdirectory:

Plugins/
  MyServerPlugin/
    MyServerPlugin.dll
    Views/
    other-plugin-files

The subdirectory name and entry DLL name should match. On startup, the server scans Plugins/{Name}/{Name}.dll, loads the assembly, finds IServerPlugin implementations, calls InitializeAsync(), then calls OnLoadedAsync().

Server Plugin Types

Server plugins declare a PluginType:

  • Admin: shown as an admin dashboard card.
  • User: shown in the user sidebar.

Use RequiresSession when a plugin should only be available after a session is selected.

Minimal Server Plugin

using RTFMv2.Server.Plugins;

namespace MyServerPlugin;

public sealed class MyServerPlugin : IServerPlugin
{
    public string Id => "my-server-plugin";
    public string Name => "My Server Plugin";
    public string Description => "Custom server workflow";
    public string Version => "1.0.0";
    public PluginType Type => PluginType.User;

    public string Icon => "bi-puzzle";
    public string CardBorderClass => "border-secondary";
    public string ButtonText => "Open";
    public int MenuOrder => 100;
    public bool RequiresSession => false;

    public Task InitializeAsync(IServiceProvider services, string pluginDirectory) => Task.CompletedTask;
    public Task OnLoadedAsync() => Task.CompletedTask;
    public Task OnReloadingAsync() => Task.CompletedTask;
    public Task ShutdownAsync() => Task.CompletedTask;

    public Task<PluginResult> HandleRequestAsync(PluginContext context)
    {
        var html = "<div class='card'><div class='card-body'>Hello from a custom server plugin.</div></div>";
        return Task.FromResult(PluginResult.Html(html));
    }

    public Task OnSessionCreatedAsync(int sessionId, string sessionName) => Task.CompletedTask;
    public Task OnSessionDeletedAsync(int sessionId, string sessionName) => Task.CompletedTask;
    public Task OnFindingCreatedAsync(int sessionId, int findingId, string title) => Task.CompletedTask;
    public Task OnReportGeneratedAsync(int sessionId, string reportType) => Task.CompletedTask;
    public Task OnDataExportedAsync(int sessionId, string exportType) => Task.CompletedTask;
    public Task OnScheduledExecuteAsync(IServiceProvider services) => Task.CompletedTask;
}

Request Routing

Server plugin pages are routed through:

/Plugin/{pluginId}
/Plugin/{pluginId}/{path}

The path after the plugin ID is provided in PluginContext.RequestPath. Use it to switch between pages, actions, or forms inside the plugin.

Return one of these result types:

  • PluginResult.Html(html) to render HTML inside the server plugin view.
  • PluginResult.Redirect(url) to redirect the browser.
  • PluginResult.Error(message) to show an error.

Optional JSON API

If a server plugin needs JSON or non-HTML endpoints, also implement IPluginApiHandler.

public sealed class MyServerPlugin : IServerPlugin, IPluginApiHandler
{
    public async Task<bool> HandleApiAsync(PluginApiContext context)
    {
        if (context.RequestPath == "status" && context.HttpMethod == "GET")
        {
            context.HttpContext.Response.ContentType = "application/json";
            await context.HttpContext.Response.WriteAsync("""{"ok":true}""");
            return true;
        }

        return false;
    }

    // IServerPlugin members omitted for brevity.
}

API requests are routed through:

/api/plugin/{pluginId}
/api/plugin/{pluginId}/{path}

Server Events

Server plugins can react to lifecycle and engagement events:

  • Session created
  • Session deleted
  • Finding created
  • Report generated
  • Data exported
  • Scheduled execution
  • Plugin reload
  • Server shutdown

Return Task.CompletedTask for event hooks you do not need.

Generate a Server Plugin Skeleton

The server includes a skeleton generator in the admin workflow. Provide a PascalCase plugin name and choose Admin or User. The generated project includes a .csproj and a starter plugin class you can build from.

Console Plugins

Console plugins add new CLI commands. A command plugin is a .NET class library containing one or more non-abstract classes that derive from OptionsBase.

Minimal Console Plugin

using System.CommandLine;
using System.CommandLine.Parsing;
using RTFMv2.Core.CLI;

namespace MyConsolePlugin;

public sealed class EchoCommand : OptionsBase
{
    private readonly Argument<string[]> _text = new("text", "Text to echo")
    {
        Arity = ArgumentArity.ZeroOrMore
    };

    public EchoCommand()
    {
        InteractiveOnly = false;
        _command = new Command("my-echo", "Echo text back to the console");
        _command.AddArgument(_text);
    }

    protected override bool ExecuteCommandCore(ParseResult parseResult)
    {
        var words = parseResult.GetValueForArgument(_text) ?? [];
        Console.WriteNormal(string.Join(" ", words));
        return true;
    }
}

Load Console Plugins

Build the class library, then load the directory containing the DLL:

plugins load --dir ./CustomConsoleCommands

Confirm it loaded:

list --plugins

Then run the new command by name:

my-echo hello from a custom command

Development Checklist

  • Build against the same RTFMv2 version you will deploy to.
  • Keep plugin IDs stable after release.
  • Keep plugin dependencies private to the plugin directory when possible.
  • Avoid blocking work inside UI or request handlers.
  • Log meaningful errors and fail gracefully when optional tools are missing.
  • Treat session IDs, credentials, and uploaded files as sensitive data.
  • Restart the target component after deploying a new client or server plugin unless you are using a supported reload path.