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](PluginBridges.md) for the unified bridge manager.
Related: Developer-Extensibility.md (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):
[cpp]
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
[cpp]
// 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.
4. Command schema (optional but recommended)
Publishing a Command schema gets you argument validation, usage/help text, and tab-completion for free, and feeds the NLU natural-language→command mapping.
[cpp]
struct Arg { string name; string type; bool required; string help; };
struct Command { string verb; string help; vector<Arg> args; };
- type ∈ string | 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.
[cpp]
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) |
[cpp]
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:
[cpp]
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:
[cpp]
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:
- Put pure logic in a header-only MyPluginCore.hpp (std-only) so it compiles under the g++ harness.
- Write tests/suite_myplugin.cpp exercising that logic (see tests/suite_plugin.cpp for the pattern).
- 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):
- Build: add PluginCore.hpp, PluginManager.hpp, PluginManager.cpp to _AugmentedIntelligence.vcxproj (+ .filters).
- 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.
- 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 |
Related documents
- Developer-Extensibility.md (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
Filed under: Uncategorized - @ July 18, 2026 6:47 am