Docs  /  Plugin SDK

Plugin SDK

How to write a plugin for _AugmentedIntelligence using the _IPlugin interface and the PLUGIN_REGISTER self-registration macro. This is the modern extension path — a plugin declares a manifest, an optional command schema, and the scopes it needs, and the host loads it with no hand-edits to SpeechCommands.cpp or RSIAgent.cpp.

Status. The pure SDK core (PluginCore.hpp) is unit-tested (tests/suite_plugin.cpp). Host glue is wired: PluginManager::Init at boot, TryHandle early in SpeechCommands, Lua registration from AgentRuntime, builtin plugins in BuiltinPlugins.cpp. See also PluginBridges.md for the unified bridge manager.

Related: Developer-Extensibility.md covers the legacy module contract; this document covers the plugin SDK that supersedes it.


1. Anatomy of a plugin

A plugin is a class implementing _IPlugin (from PluginManager.hpp):

class _IPlugin
{
public:
    virtual ~_IPlugin() {}
    virtual plugincore::Manifest Manifest() const = 0;                  // required: identity + capabilities
    virtual vector<plugincore::Command> Commands() const { return {}; } // optional: declarative arg schema
    virtual void Handle(const vector<string>& command, size_t start_index) = 0; // required: do the work
    virtual void RegisterLua(lua_State*) {}                             // optional: expose Lua globals
    virtual void OnLoad() {}                                            // optional lifecycle hook
    virtual void OnUnload() {}
};

You provide at minimum Manifest() and Handle().


2. Quick start — a minimal plugin

// GreetPlugin.cpp
#include "PluginManager.hpp"

using namespace std;

class _GreetPlugin : public _IPlugin
{
public:
    plugincore::Manifest Manifest() const override
    {
        // The key=value manifest format (parsed + validated by PluginCore).
        return plugincore::parseManifest(
            "name=greet\n"
            "version=1.0.0\n"
            "min_host=11.0.0\n"
            "commands=greet\n"
            "scopes=\n");            // no host capabilities needed
    }

    vector<plugincore::Command> Commands() const override
    {
        plugincore::Command c;
        c.verb = "greet";
        c.help = "greet someone by name";
        c.args = { { "name", "string", true, "who to greet" } };
        return { c };
    }

    void Handle(const vector<string>& command, size_t) override
    {
        // command[0] == "greet"; args follow.
        const string who = command.size() > 1 ? command[1] : "world";
        cout << "Hello, " << who << "!" << endl;
    }
};

// Self-register at startup. Second arg = the capability scopes to grant.
PLUGIN_REGISTER(_GreetPlugin, {});

Add the file to the project (<ClCompile> in the vcxproj + filters), rebuild, and — once the SDK is wired into the host — typing greet Ada runs it. No dispatch or Lua-registration edits required.


3. The manifest

Declares identity and requirements. Parsed from a simple key=value block (plugincore::parseManifest) — one key per line, # comments allowed:

Key Meaning Example
name Unique plugin id (required) media
version Plugin semver (required) 1.2.0
min_host Minimum host version it needs 11.0.0
commands CSV of typed-command verbs it provides (required) media,mv
scopes CSV of capability scopes it requires db.read,llm.invoke
deps CSV of other plugin names it depends on core

Validation (plugincore::validateManifest) rejects a manifest with no name, no/invalid version, or no commands, and refuses to load if min_host exceeds the running host version. You may also construct the Manifest struct directly instead of parsing text.


Publishing a Command schema gets you argument validation, usage/help text, and tab-completion for free, and feeds the NLU natural-language→command mapping.

struct Arg     { string name; string type; bool required; string help; };
struct Command { string verb; string help; vector<Arg> args; };
  • typestring | int | float | bool — values are type-checked before your Handle runs.
  • Required args must come before the host will dispatch; too many/too few args produce a usage message instead of calling you.
plugincore::Command c;
c.verb = "play";
c.help = "play a title";
c.args = { {"title","string",true,"movie/track title"},
           {"count","int",false,"repeat N times"} };
// usageString(c) => "play <title:string> [count:int]  - play a title"

If you skip Commands(), your Handle still runs — you just parse command yourself and get no auto-validation.


5. Capability scopes (security)

Every plugin is granted only the scopes passed to PLUGIN_REGISTER (or, for DLL plugins, its manifest scopes intersected with host policy). The host checks scopes before letting a plugin reach a protected service.

Scope matching (plugincore::authorize):

Granted Matches
llm.invoke exactly llm.invoke
net.* any net.<something> (wildcard)
* everything (super-scope — grant sparingly)
PLUGIN_REGISTER(_MediaPlugin, { "db.read", "llm.invoke" });   // scoped
PLUGIN_REGISTER(_TrustedTool,  { "*" });                       // full access

Declare the same scopes in your manifest's scopes= line so they're visible in plugins info and enforced when calling the Host API.


6. The Host API facade

Plugins reach host services through a stable facade (_PluginManager::Host()), rather than touching globals — this keeps plugins decoupled and testable:

struct HostApi
{
    function<string(const string& prompt, const string& model)> Llm;
    function<void(const string& key, const string& value)>      MemoryPut;
    function<string(const string& key)>                          MemoryGet;
    function<void(const string& name, double v)>                 Metric;
    function<bool(const string& action_scope)>                   EthicsAllows;
    string host_version;
};

// inside Handle():
auto& host = _PluginManager::Host();
if (host.EthicsAllows("actuate.write")) {
    string answer = host.Llm("summarize: " + input, "gemma3:12b");
    host.MemoryPut("last_summary", answer);
}

7. Lifecycle & runtime enforcement

  • OnLoad() / OnUnload() — set up / tear down state. Called on load, unload, and plugins reload.
  • On every dispatch, _PluginManager::TryHandle wraps your Handle with:
  • kill-switch check (safe-mode blocks all plugin actuation),
  • rate limiting per plugin,
  • schema validation (if you published one),
  • timing + audit — invocation count, error count, latency, and a tamper-evident audit entry (reusing _KillSwitch / _RateLimiter / _Metrics / _AuditLog from NetFeatures).

Exceptions thrown from Handle are caught and recorded as errors, not crashes.


8. The plugins command

Runtime management (available once the SDK is wired in):

plugins list                 # all plugins, on/off, version, verbs, call/error counts
plugins info <name>          # scopes, deps, avg latency, error rate
plugins enable <name>        # turn a plugin on
plugins disable <name>       # turn it off (its verbs stop routing)
plugins reload <name>        # OnUnload + OnLoad (state reset; DLL reload for dynamic)
plugins doctor               # dependency load order + cycle / missing-dep report
plugins help <verb>          # usage string from the command schema

9. Dynamic (out-of-tree) plugins

Ship a plugin as a DLL without the host source tree. Export a C entry point:

extern "C" __declspec(dllexport) _IPlugin* ai_plugin_entry() { return new _MyPlugin(); }

The host loads it with _PluginManager::LoadDynamic("plugins\\mymod.dll", err), reads its manifest, grants scopes per policy, and registers it. Unload / Reload manage the module handle. (This path is written but depends on the full Windows/MSVC build; treat it as the design contract until exercised.)


10. Testing your plugin

Follow the project's tested-core convention:

  1. Put pure logic in a header-only MyPluginCore.hpp (std-only) so it compiles under the g++ harness.
  2. Write tests/suite_myplugin.cpp exercising that logic (see tests/suite_plugin.cpp for the pattern).
  3. Add copies + a build/run line to tests/run_math_tests.ps1, and confirm it stays green.

The SDK's own core is validated this way — PluginCore (registry, manifests, semver, scopes, schema, dependency ordering, metrics) has 42 passing checks in the plugin core suite.


Enabling the SDK in the host

The SDK isn't live until these three edits are made (kept out of this doc's scope so you can decide the integration point):

  1. Build: add PluginCore.hpp, PluginManager.hpp, PluginManager.cpp to _AugmentedIntelligence.vcxproj (+ .filters).
  2. Startup + dispatch: call _PluginManager::Init("11.0.0") during boot, and at the top of myCallback in SpeechCommands.cpp call if (_PluginManager::TryHandle(command)) return vector<string>(); so plugin verbs are routed first.
  3. Lua: add _PluginManager::RegisterLuaGlobals(L) to registerExtendedLuaGlobals in RSIAgent.cpp.

API reference (essentials)

plugincore (PluginCore.hpp) — pure, tested

Symbol Purpose
Version, parseVersion, compareVersion, hostSatisfies semver
authorize, scopeMatches, firstMissingScope capability scopes
Arg, Command, validateInvocation, usageString, completions command schema
Manifest, parseManifest, validateManifest plugin manifest
Registry add/enable/route/metrics/loadOrder (dependency topo-sort)

_IPlugin / _PluginManager (PluginManager.hpp) — host glue

Symbol Purpose
PLUGIN_REGISTER(Type, {scopes}) self-registration at startup
_PluginManager::Init/Shutdown/GetStatus lifecycle
_PluginManager::TryHandle(command) scoped, audited dispatch
_PluginManager::LoadDynamic/Unload/Reload dynamic DLL plugins
_PluginManager::Complete(prefix) REPL autocomplete
_PluginManager::Host() host-service facade

  • Developer-Extensibility.md — legacy module contract + mesh systems
  • PluginCore.hpp / PluginManager.hpp — the SDK headers (source of truth)
  • tests/suite_plugin.cpp — worked examples of the core API
Generated from the project markdown docs on 2026-07-24. This is a static, self-contained site.