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