Files
overlord/plugins/docs/legacy-native-plugins.md
T
2026-06-22 13:04:33 -05:00

283 lines
9.8 KiB
Markdown

# Legacy Native Plugins
Legacy native v1 plugins run as shared libraries inside the agent. They are still supported and still useful when you need capabilities that the WASM runtime intentionally does not expose.
Use legacy native plugins when you need direct OS APIs, native libraries, custom process/thread behavior, platform-specific integrations, or code that cannot reasonably be compiled to the Plugin 2.0 WASM ABI.
Native plugins run with the same privileges as the agent. They are not sandboxed.
## What Legacy 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. |
WASM Plugin 2.0 is preferred for sandboxed, portable agent modules. Legacy native plugins are the escape hatch for privileged native behavior.
## Bundle Layout
A legacy bundle is a zip with web assets plus one or more platform binaries:
```text
<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:
```text
<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:
```json
{
"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`:
```c
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`:
```c
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
```c
#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 are loaded from memory through the custom PE loader. |
| 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.
## 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:
```text
[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
Legacy samples include `build.bat` and `build.sh`. They compile native binaries and zip them with web assets.
Default usage:
```bash
cd plugins/sample-c
./build.sh
```
Multiple targets:
```bash
BUILD_TARGETS="linux-amd64 linux-arm64 windows-amd64 darwin-arm64" ./build.sh
```
Windows:
```bat
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:
```text
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
Legacy 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.