9.8 KiB
Overlord Plugins
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.
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.
1) How plugins are structured
Required bundle format
A plugin bundle is a zip file named after the plugin ID:
<pluginId>.zip
Inside the zip (root level), you need:
- Platform-specific binaries named
<pluginId>-<os>-<arch>.<ext> - Web assets:
<pluginId>.html,<pluginId>.css,<pluginId>.js
Example for plugin ID sample:
sample.zip
├─ sample-linux-amd64.so
├─ sample-linux-arm64.so
├─ sample-darwin-arm64.dylib
├─ sample-windows-amd64.dll
├─ sample.html
├─ sample.css
└─ sample.js
When the server extracts the zip:
Overlord-Server/plugins/sample/
├─ sample-linux-amd64.so
├─ sample-linux-arm64.so
├─ sample-darwin-arm64.dylib
├─ sample-windows-amd64.dll
├─ manifest.json (auto-generated)
└─ assets/
├─ sample.html
├─ sample.css
└─ sample.js
Manifest fields
The auto-generated manifest:
{
"id": "sample",
"name": "sample",
"version": "1.0.0",
"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"
},
"entry": "sample.html",
"assets": {
"html": "sample.html",
"css": "sample.css",
"js": "sample.js"
}
}
The server picks the right binary for the target client's OS/arch when loading.
2) Build a native plugin
Plugin contract
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)
//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()
The host passes a callback function pointer and context during OnLoad. The plugin calls the callback to send events back.
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)
//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
{
"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:
# 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
3) Install & open a plugin
Install / upload
- Use the UI at
/pluginsto upload the zip - Or drop
<pluginId>.zipintoOverlord-Server/pluginsand restart
Open the UI
Plugin UI is served from:
/plugins/<pluginId>?clientId=<CLIENT_ID>
Your HTML loads its JS/CSS from /plugins/<pluginId>/assets/.
4) Runtime: how events flow
Overlord plugins have two parts:
- UI (HTML/CSS/JS) — Runs in the browser, calls server APIs.
- Native module — Runs in the agent (client) process as a loaded shared library.
UI → agent (plugin event)
From your UI JS:
POST /api/clients/<clientId>/plugins/<pluginId>/event
{
"event": "ui_message",
"payload": { "message": "hello" }
}
If the plugin is not loaded yet, the server will load it on the client, queue the event, and deliver it once ready.
Agent → plugin (direct function call)
The agent calls your OnEvent(event, payload) function directly with JSON-encoded data. No stdin/stdout pipes, no msgpack — just a direct function call.
Plugin → agent (callback)
Your plugin sends events back to the host using the send callback received during OnLoad:
send("echo", []byte(`{"message":"hello back"}`))
On Windows, the equivalent is calling the registered callback function pointer.
Plugin lifecycle events
The agent sends these events to the server:
loadedon successful loadunloadedwhen unloadederrorif load or runtime fails
5) What can plugins do?
Since plugins run as native code, they can:
- 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
Plugins have the same capabilities as the agent itself.
UI Security constraints
Plugin UI pages are still served with a tight CSP:
- Scripts must be same-origin
- No third-party JS/CDN
- WebSocket and fetch are allowed to same origin
Plugin UIs run in a sandboxed iframe with a fetch bridge.
6) API surface
Plugin management
GET /api/plugins— list installed plugins (includesautoLoadandautoStartEventsper plugin)POST /api/plugins/upload— upload zipPOST /api/plugins/<id>/enable— enable/disablePOST /api/plugins/<id>/autoload— configure auto-load on client connectDELETE /api/plugins/<id>— remove
Per-client plugin runtime
POST /api/clients/<clientId>/plugins/<pluginId>/loadPOST /api/clients/<clientId>/plugins/<pluginId>/eventPOST /api/clients/<clientId>/plugins/<pluginId>/unload
Useful built-in endpoints
POST /api/clients/<clientId>/commandWS /api/clients/<clientId>/rd/ws(remote desktop)WS /api/clients/<clientId>/console/wsWS /api/clients/<clientId>/files/wsWS /api/clients/<clientId>/processes/ws
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
- Client connects and completes the enrollment handshake
- Server sends
hello_ack(as usual) - Server dispatches auto-scripts (as usual)
- Server checks all plugins with
autoLoad: trueandenabled: true - For each, if the plugin is not already loaded on that client:
- Sends the plugin binary bundle (chunked)
- Queues any
autoStartEvents
- 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:
{
"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
enabledflag — disabled plugins are never auto-loaded - Auto-load state is persisted in
.plugin-state.jsonand 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