6.9 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 pluginsPOST /api/plugins/upload— upload zipPOST /api/plugins/<id>/enable— enable/disableDELETE /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