# 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: `.wasm`, `.html`, `.css`, `.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: ``` .zip ``` Inside the zip (root level), these four files are required: ``` .wasm .html .css .js ``` Example for plugin ID `sample`: ``` sample.zip ├─ sample.wasm ├─ sample.html ├─ sample.css └─ sample.js ``` When the server sees `.zip`, it extracts to: ``` Overlord-Server/plugins// ├─ .wasm ├─ manifest.json └─ assets/ ├─ .html ├─ .css └─ .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 .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 `.zip` into Overlord-Server/plugins and restart the server ### Open the UI The plugin UI is served from: ``` /plugins/?clientId= ``` Your HTML should load its JS/CSS from `/plugins//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//plugins//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 `.js` and loaded from `/plugins//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//enable` — enable/disable - `DELETE /api/plugins/` — remove ### Per-client plugin runtime - `POST /api/clients//plugins//load` - `POST /api/clients//plugins//event` - `POST /api/clients//plugins//unload` ### Useful built-in endpoints - `POST /api/clients//command` (input, ping, scripts, etc.) - `WS /api/clients//rd/ws` (remote desktop) - `WS /api/clients//console/ws` - `WS /api/clients//files/ws` - `WS /api/clients//processes/ws` ## 7) Building a “pinnacle” plugin (remote desktop class) A remote‑desktop‑class plugin typically has: 1. **A UI page** with a `` and quality controls 2. **A WebSocket** connection to `/api/clients//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//rd/ws` 3. UI sends JSON messages: ``` { "type": "desktop_start" } { "type": "desktop_select_display", "display": 0 } { "type": "desktop_set_quality", "quality": 90, "codec": "jpeg" } ``` 4. Incoming WS messages contain encoded frames (see `public/assets/remotedesktop.js` for the parsing logic) 5. 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//plugins//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.