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.
`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.
| 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.
| `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`:
```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.
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. |
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.