Files
overlord/plugins/docs/legacy-native-plugins.md
2026-08-18 21:41:28 -05:00

11 KiB

Native Plugins

Native plugins run as shared libraries inside the agent.

Use native plugins when you need direct OS APIs, native libraries, custom process/thread behavior, or platform-specific integrations.

Native plugins run with the same privileges as the agent. They are not sandboxed.

What Native Plugins Can Do

Capability Notes
Call native OS APIs Win32, POSIX, Objective-C/C APIs, platform SDKs, and local native libraries.
Use language runtimes C, C++, Go, Rust, and other languages that can export a C ABI.
Run platform-specific code Package separate binaries per OS/architecture.
Emit plugin events Send events back to the UI/server through the host callback.
Receive UI/server commands Handle PluginOnEvent calls from plugin UI actions or server-side routing.

Bundle Layout

A native bundle is a zip with web assets plus one or more platform binaries:

<pluginId>.zip
  <pluginId>.html
  <pluginId>.css
  <pluginId>.js
  <pluginId>-windows-amd64.dll
  <pluginId>-linux-amd64.so
  <pluginId>-darwin-arm64.dylib

The browser assets provide the plugin UI. The native binary is sent to a matching client when the plugin is loaded for that client.

Binary Naming

Binary filenames must follow:

<pluginId>-<os>-<arch>.<ext>
OS Arch Example
windows amd64 sample-windows-amd64.dll
windows arm64 sample-windows-arm64.dll
linux amd64 sample-linux-amd64.so
linux arm64 sample-linux-arm64.so
darwin amd64 sample-darwin-amd64.dylib
darwin arm64 sample-darwin-arm64.dylib

The server never sends an x64 native binary to an ARM client, or the reverse. If no binary matches the target client, loading fails for that client.

Required ABI

Every native plugin exports C-callable functions. The ABI differs slightly between Windows and Unix-like systems.

Export Windows signature Linux/macOS signature Required
PluginOnLoad int PluginOnLoad(char* hostInfo, int hostInfoLen, uint64 callback) int PluginOnLoad(char* hostInfo, int hostInfoLen, uintptr callback, uintptr ctx) Yes
PluginOnEvent int PluginOnEvent(char* event, int eventLen, char* payload, int payloadLen) Same Yes
PluginOnUnload void PluginOnUnload() Same Yes
PluginSetCallback void PluginSetCallback(uint64 callback) Not used Windows only
PluginGetRuntime const char* PluginGetRuntime() Same Recommended

Return 0 from PluginOnLoad and PluginOnEvent for success. Return a non-zero value when the plugin cannot initialize or handle the event.

HostInfo JSON

PluginOnLoad receives host metadata as a JSON byte buffer:

{
  "clientId": "abc123",
  "os": "windows",
  "arch": "amd64",
  "version": "1.0.0"
}

Use this to branch by client OS, architecture, client ID, or agent version.

Host Callback

The host callback lets a native plugin emit events back to the agent/server.

Windows uses __stdcall:

void __stdcall callback(const char *event, uintptr_t eventLen,
                        const char *payload, uintptr_t payloadLen);

Linux/macOS use a normal C function plus the opaque context pointer received by PluginOnLoad:

void callback(uintptr_t ctx,
              const char *event, int eventLen,
              const char *payload, int payloadLen);

The event name and payload are byte buffers. JSON payloads are conventional, but the ABI only requires bytes plus lengths.

Minimal C Skeleton

#include <stdint.h>

#ifdef _WIN32
#define EXPORT __declspec(dllexport)
typedef void (__stdcall *host_callback_t)(
  const char *event, uintptr_t eventLen,
  const char *payload, uintptr_t payloadLen);
static host_callback_t g_callback = 0;
#else
#define EXPORT __attribute__((visibility("default")))
typedef void (*host_callback_t)(
  uintptr_t ctx,
  const char *event, int eventLen,
  const char *payload, int payloadLen);
static host_callback_t g_callback = 0;
static uintptr_t g_ctx = 0;
#endif

EXPORT const char *PluginGetRuntime(void) {
  return "c";
}

#ifdef _WIN32
EXPORT void PluginSetCallback(uint64_t callback) {
  g_callback = (host_callback_t)(uintptr_t)callback;
}

EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t callback) {
  g_callback = (host_callback_t)(uintptr_t)callback;
  if (g_callback) g_callback("ready", 5, "{\"ok\":true}", 11);
  return 0;
}
#else
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uintptr_t callback, uintptr_t ctx) {
  g_callback = (host_callback_t)callback;
  g_ctx = ctx;
  if (g_callback) g_callback(g_ctx, "ready", 5, "{\"ok\":true}", 11);
  return 0;
}
#endif

EXPORT int PluginOnEvent(const char *event, int eventLen, const char *payload, int payloadLen) {
  return 0;
}

EXPORT void PluginOnUnload(void) {
}

Runtime Detection

PluginGetRuntime should return a pointer to a static, null-terminated string.

Return value Meaning
c C plugin; fully unloadable.
cpp C++ plugin; fully unloadable.
rust Rust plugin; fully unloadable when written without unload-hostile globals.
go or absent Go runtime; intentionally not unloaded.

Go shared libraries cannot be fully unloaded safely because the Go runtime owns threads and process-global state. The host treats Go plugins as non-freeable to avoid crashes.

Loading Model

Platform Loading behavior
Windows DLLs default to the custom in-memory PE loader. Plugins can set nativeLoader: "os" to stage the DLL in the agent cache and load it with LoadLibraryExW.
Linux Shared libraries are loaded through the static-agent subprocess shim when needed.
macOS Dynamic libraries are loaded through the platform loader.

Because Windows uses an in-memory PE loader, some runtime primitives that assume normal LoadLibrary initialization can be fragile. The Rust sample avoids std::sync::Mutex for this reason and uses C-style globals; the host serializes plugin calls.

Windows Loader Selection

Set nativeLoader in config.json:

{
  "runtime": "native",
  "nativeLoader": "memory"
}
Value Behavior
memory Default. Loads the PE image from bytes without staging the plugin DLL on disk.
os Writes a content-addressed copy to the agent cache and loads it with LoadLibraryExW. Prefer this for Go DLLs or other runtimes that rely on the normal Windows loader.

The OS loader path reuses the cached DLL when the SHA-256 hash matches, so repeated loads do not rewrite the same plugin. Windows keeps loaded images locked; old cached plugin versions are left for later cleanup.

Custom Export Names

Native plugins may override the exported ABI names in config.json:

{
  "runtime": "native",
  "nativeEntrypoints": {
    "onLoad": "StartPlugin",
    "onEvent": "HandlePluginEvent",
    "onUnload": "StopPlugin",
    "setCallback": "SetHostCallback",
    "getRuntime": "RuntimeName"
  }
}

Omitted fields use the standard names. The signatures must stay ABI-compatible with PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback, and PluginGetRuntime; only the exported symbol names change. Custom names are honored by the Windows loader and direct dlopen/dlsym paths. The Linux static-agent subprocess shim still expects the standard names.

Static Linux Agent Shim

Linux agents are compiled as static musl binaries for portability. Static binaries cannot directly call dlopen, so Linux native plugin support uses an embedded plugin-host shim.

When loading a Linux native plugin, the agent:

  1. Writes the plugin .so to an anonymous in-memory file.
  2. Writes the embedded shim to another in-memory file.
  3. Creates a bidirectional socket.
  4. Starts the shim and passes file descriptors to it.
  5. The shim loads the .so and forwards events over the socket.

Plugin authors normally do not need shim-specific code. Export the normal v1 ABI and compile the .so for the target libc/architecture.

IPC Protocol Notes

The agent and Linux shim use a length-prefixed binary protocol:

[4-byte little-endian total payload length][1-byte message type][payload bytes]

Important message types:

Direction Type Meaning
agent to shim 0x01 Load plugin with HostInfo.
agent to shim 0x02 Deliver plugin event.
agent to shim 0x03 Unload plugin.
shim to agent 0x10 Plugin callback event.
shim to agent 0x11 Ready/runtime string.
shim to agent 0x12 Error string.
shim to agent 0x13 Load result.

Most plugin authors can ignore this, but it is useful when debugging Linux native plugin load failures.

Build Scripts

Native samples include build.bat and build.sh. They compile native binaries and zip them with web assets.

Default usage:

cd plugins/sample-c
./build.sh

Multiple targets:

BUILD_TARGETS="linux-amd64 linux-arm64 windows-amd64 darwin-arm64" ./build.sh

Windows:

cd plugins\sample-c
build.bat

The scripts support BUILD_TARGETS and usually accept an optional plugin directory argument.

Cross-Compilation

Install the compiler matching the target platform, or set CC for C/C++ samples.

Common compiler names used by the sample scripts:

Target Compiler
linux-amd64 x86_64-linux-gnu-gcc
linux-arm64 aarch64-linux-gnu-gcc
linux-arm arm-linux-gnueabihf-gcc
windows-amd64 x86_64-w64-mingw32-gcc
windows-arm64 aarch64-w64-mingw32-gcc
darwin-amd64 x86_64-apple-darwin-gcc
darwin-arm64 aarch64-apple-darwin-gcc

Rust samples use Cargo targets. Go samples use go build -buildmode=c-shared with CGO enabled.

Language Notes

Sample Notes
sample-c Smallest ABI surface, no runtime, fully unloadable.
sample-cpp C++ convenience with exported C ABI. Return cpp from PluginGetRuntime.
sample-rust Exports a C ABI. Avoid assumptions that require normal OS loader initialization.
sample-go Uses -buildmode=c-shared; treated as non-freeable because the Go runtime cannot be safely unloaded.

UI Event Flow

The browser UI sends events through the normal plugin event endpoint. The server forwards them to the client. The agent calls:

PluginOnEvent(event, eventLen, payload, payloadLen)

The native plugin can reply by invoking the host callback. Those callback events are delivered back to the server/UI as plugin events.

Security Notes

Native plugins are trusted executable code. They can call OS APIs, read/write files available to the agent process, spawn subprocesses, open sockets, and interact with native libraries. Use plugin signing, per-user plugin access, and code review before deploying third-party native plugins.