Files
overlord/PLUGINS.md
T
2026-03-05 00:04:26 -06:00

7.9 KiB
Raw Blame History

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>.zip into 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:

  1. UI (HTML/CSS/JS) — Runs in the browser and can call server APIs or open WebSockets.
  2. 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 as plugin_event
  • type: "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:

  • loaded on successful load
  • unloaded when unloaded
  • error if 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 plugins
  • POST /api/plugins/upload — upload zip
  • POST /api/plugins/<id>/enable — enable/disable
  • 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

  • POST /api/clients/<clientId>/command (input, ping, scripts, etc.)
  • 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

7) Building a “pinnacle” plugin (remote desktop class)

A remote‑desktop‑class plugin typically has:

  1. A UI page with a <canvas> and quality controls
  2. A WebSocket connection to /api/clients/<id>/rd/ws
  3. Command messages (desktop_start, desktop_stop, desktop_select_display, etc.)
  4. Binary frame decoding (JPEG/raw) and drawing into the canvas

How it would work (high level)

  1. UI page reads clientId from query string
  2. JS opens a WebSocket to /api/clients/<clientId>/rd/ws
  3. UI sends JSON messages:
{ "type": "desktop_start" }
{ "type": "desktop_select_display", "display": 0 }
{ "type": "desktop_set_quality", "quality": 90, "codec": "jpeg" }
  1. Incoming WS messages contain encoded frames (see public/assets/remotedesktop.js for the parsing logic)
  2. 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
  • 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, send event=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.