7.9 KiB
Overlord Plugins
This document explains how to build Overlord plugins, how the runtime works, and what you can do from a plugin UI and a WASM module.
TL;DR: A plugin is a zip file with 4 files at the root:
<id>.wasm,<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), these four files are required:
<pluginId>.wasm
<pluginId>.html
<pluginId>.css
<pluginId>.js
Example for plugin ID sample:
sample.zip
├─ sample.wasm
├─ sample.html
├─ sample.css
└─ sample.js
When the server sees <pluginId>.zip, it extracts to:
Overlord-Server/plugins/<pluginId>/
├─ <pluginId>.wasm
├─ manifest.json
└─ assets/
├─ <pluginId>.html
├─ <pluginId>.css
└─ <pluginId>.js
The server generates manifest.json automatically. You do not need to include a manifest inside the zip.
Manifest fields
The auto-generated manifest looks like:
{
"id": "sample",
"name": "sample",
"version": "1.0.0",
"binary": "sample.wasm",
"entry": "sample.html",
"assets": {
"html": "sample.html",
"css": "sample.css",
"js": "sample.js"
}
}
2) Build a WASM plugin (Go WASI)
The plugin runtime expects a WASI-compatible WebAssembly binary. Go 1.21+ builds work well with the current wazero runtime.
Build steps (from the plugin folder):
GOOS=wasip1 GOARCH=wasm go build -o <pluginId>.wasm ./wasm
There is a working example in:
- plugin-sample-go/wasm/main.go
3) Install & open a plugin
Install / upload
- Use the UI at /plugins to upload the zip
- Or drop
<pluginId>.zipinto Overlord-Server/plugins and restart the server
Open the UI
The plugin UI is served from:
/plugins/<pluginId>?clientId=<CLIENT_ID>
Your HTML should load 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 and can call server APIs or open WebSockets.
- WASM module — Runs in the agent (client) process via wazero.
UI → agent (plugin event)
From your UI JS, you can send an event to a plugin on a client:
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 your event
- deliver it when the plugin reports
loaded
Agent → plugin (WASM stdin)
The agent forwards that event into the WASM plugin over msgpack via stdin.
The message envelope looks like:
{
"type": "event",
"event": "ui_message",
"payload": { ... }
}
Plugin → agent (WASM stdout)
Your WASM can reply by writing msgpack messages to stdout. Two supported types:
type: "event"→ forwarded back to the server asplugin_eventtype: "log"→ logged by the agent
Examples:
{ "type": "event", "event": "ready", "payload": "plugin ready" }
{ "type": "log", "payload": "starting capture" }
Plugin lifecycle events
The agent sends lifecycle events to the server:
loadedon successful loadunloadedwhen unloadederrorif load or runtime fails
These update the server-side plugin status and error display.
Note: plugin events are not pushed back to the plugin UI automatically. If you need UI feedback, call a server API that returns a response to the UI or implement your own polling/WS endpoint.
5) What can plugins do?
Plugins can do anything the server already exposes to authenticated users, such as:
- start/stop remote desktop
- open a console session
- file browser actions (list, upload, download, edit)
- process listing and kill
- run scripts
- send commands to clients
This is intentional: plugins do not modify server code and are sandboxed by the plugin CSP.
Security constraints
Plugin pages are served with a tight CSP:
- scripts must be same-origin
- no third‑party JS/CDN
- WebSocket and fetch are allowed to same origin
That means your plugin JS must be bundled into <pluginId>.js and loaded from /plugins/<pluginId>/assets/.
Sandboxed iframe isolation
Plugin UIs are rendered inside a sandboxed iframe to isolate them from the main dashboard. This provides:
- No access to main app DOM
- No access to main app cookies/localStorage
- No direct access to privileged APIs
Because of the sandbox, direct network calls from the plugin are blocked by CSP. The system injects a small bridge that replaces window.fetch and forwards requests to the parent page.
Practical implications:
- Use
fetch()as usual in your plugin JS - Only the allowed plugin API routes are permitted
- Direct WebSocket usage from the plugin frame is blocked
If your plugin needs WebSockets (e.g., remote desktop), you must build a parent‑page bridge that opens the WS in the parent and forwards frames into the plugin UI. See the remote desktop section below for the architecture.
6) API surface (what you can call)
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>/command(input, ping, scripts, etc.)WS /api/clients/<clientId>/rd/ws(remote desktop)WS /api/clients/<clientId>/console/wsWS /api/clients/<clientId>/files/wsWS /api/clients/<clientId>/processes/ws
7) Building a “pinnacle” plugin (remote desktop class)
A remote‑desktop‑class plugin typically has:
- A UI page with a
<canvas>and quality controls - A WebSocket connection to
/api/clients/<id>/rd/ws - Command messages (
desktop_start,desktop_stop,desktop_select_display, etc.) - Binary frame decoding (JPEG/raw) and drawing into the canvas
How it would work (high level)
- UI page reads
clientIdfrom query string - JS opens a WebSocket to
/api/clients/<clientId>/rd/ws - UI sends JSON messages:
{ "type": "desktop_start" }
{ "type": "desktop_select_display", "display": 0 }
{ "type": "desktop_set_quality", "quality": 90, "codec": "jpeg" }
- Incoming WS messages contain encoded frames (see
public/assets/remotedesktop.jsfor the parsing logic) - Draw frames into the canvas and expose controls to the user
You do not need to re-implement the agent capture pipeline. The agent already supports it; your plugin just drives the existing WS endpoint.
What you’d write (outline)
- HTML: Canvas + controls
- JS:
- parse
clientId - open WS
- send desktop commands
- decode frames and draw
- parse
- WASM module (optional):
- background tasks or message validation
- optional telemetry / state management
If you want a starting point, the existing remote desktop UI is in:
- Overlord-Server/public/remotedesktop.html
- Overlord-Server/public/assets/remotedesktop.js
You can reuse that logic inside your plugin UI with minimal changes.
8) Example: minimal plugin code shape
WASM side (Go, message loop)
- listen on stdin using msgpack
- on
type=init, sendevent=ready - on
type=event, process payload and emit response events
See:
- plugin-sample-go/wasm/main.go
UI side (JS)
- call
/api/clients/<id>/plugins/<pluginId>/event - show responses via your own UI (or poll your own endpoints)
See:
- plugin-sample-go/sample.js
If you want a deeper walkthrough of a specific plugin type (file explorer, custom telemetry, or a remote-desktop clone), say which one and I’ll expand that section.