organize repo

This commit is contained in:
KingKDot
2026-04-15 11:11:53 -05:00
parent f6c9337141
commit 7367923ac5
36 changed files with 39 additions and 25 deletions
+882
View File
@@ -0,0 +1,882 @@
# 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 system APIs.
Plugins can be written in **any language** that can produce a C-ABI shared library: **Go**, **C**, **C++**, **Rust**, or anything else that compiles to native code.
> 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
```
### Architecture validation
The server matches binaries to clients by OS and architecture. Binary filenames **must** follow the naming convention `<pluginId>-<os>-<arch>.<ext>`:
| OS | Arch | Example filename |
|----------|---------|------------------------------|
| windows | amd64 | `sample-windows-amd64.dll` |
| windows | arm64 | `sample-windows-arm64.dll` |
| linux | amd64 | `sample-linux-amd64.so` |
| linux | arm64 | `sample-linux-arm64.so` |
| darwin | amd64 | `sample-darwin-amd64.dylib` |
| darwin | arm64 | `sample-darwin-arm64.dylib` |
The server will **never** send an x64 binary to an ARM client (or vice versa). If no binary matches the client's platform, loading is skipped with an error.
### Manifest fields
The auto-generated manifest:
```json
{
"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"
}
}
```
## 2) Plugin ABI (all languages)
Every plugin — regardless of language — must export **C-callable functions** with specific signatures. The ABI differs slightly between Windows and Unix.
### Required exports
| Export | Signature (Windows) | Signature (Linux / macOS) | Required |
|--------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------|----------|
| `PluginOnLoad` | `int PluginOnLoad(char* hostInfo, int hostInfoLen, uint64 callback)` | `int PluginOnLoad(char* hostInfo, int hostInfoLen, uintptr callback, uintptr ctx)` | Yes |
| `PluginOnEvent` | `int PluginOnEvent(char* event, int eventLen, char* payload, int payloadLen)` | same | Yes |
| `PluginOnUnload` | `void PluginOnUnload()` | same | Yes |
| `PluginSetCallback`| `void PluginSetCallback(uint64 callback)` | — | Windows only |
| `PluginGetRuntime` | `const char* PluginGetRuntime()` | same | Recommended |
### Host callback
The host passes a callback function pointer during `PluginOnLoad`. The plugin uses it to send events back to the agent.
**Windows** — The callback is a `__stdcall` function:
```c
void __stdcall callback(const char *event, uintptr_t eventLen,
const char *payload, uintptr_t payloadLen);
```
**Linux / macOS** — The callback is a standard C function with an opaque context pointer:
```c
void callback(uintptr_t ctx,
const char *event, int eventLen,
const char *payload, int payloadLen);
```
The `ctx` value received in `PluginOnLoad` must be passed back as the first argument to every callback invocation.
### PluginGetRuntime (runtime detection)
If a plugin exports `PluginGetRuntime`, the host calls it after loading. It must return a pointer to a static, null-terminated string identifying the runtime:
| Return value | Meaning |
|-------------|---------------------------------------|
| `"c"` | Pure C plugin — fully unloadable |
| `"cpp"` | C++ plugin — fully unloadable |
| `"rust"` | Rust plugin — fully unloadable |
| *(absent)* | Defaults to `"go"` — **not** freed |
This matters because **Go plugins cannot be fully unloaded** due to [golang/go#11100](https://github.com/golang/go/issues/11100) — Go's runtime spawns threads that cannot be stopped, so calling `FreeLibrary`/`dlclose` on a Go shared library causes a crash. The host uses this information to skip freeing Go plugins while properly reclaiming memory for C/C++/Rust plugins.
The runtime is logged on load and unload:
```
[plugins] loaded plugin "sample-c" (runtime=c, freeable=true)
[plugins] unloaded plugin "sample-c" — memory freed
[plugins] unloaded plugin "sample-go" — memory leaked (go runtime)
```
### HostInfo JSON
The `hostInfo` buffer passed to `PluginOnLoad` is JSON:
```json
{
"clientId": "abc123",
"os": "windows",
"arch": "amd64",
"version": "1.0.0"
}
```
### Loading mechanism
- **Windows** — DLLs are loaded **entirely in memory** via a custom PE loader. No files are written to disk. This is why Rust's `std::sync::Mutex` (backed by `SRWLOCK`) may not initialize correctly — see the Rust section below.
- **Linux** — Shared libraries are loaded in memory via `memfd_create` + `dlopen` on `/proc/self/fd/`. No files touch disk.
- **macOS** — Shared libraries are written to a temp file, `dlopen`'d, then the temp file is deleted.
## 3) Sample plugins by language
### Go (`sample-go/`)
Go plugins use `-buildmode=c-shared` with platform-specific export wrappers.
**Project structure:**
```
sample-go/native/
├─ main.go (shared core logic)
├─ exports_unix.go (exports for Linux/macOS)
├─ exports_windows.go (exports for Windows)
└─ go.mod
```
**Linux / macOS exports:**
```go
//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()
```
**Windows exports:**
```go
//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)
```
**Build:**
```bash
CGO_ENABLED=1 go build -buildmode=c-shared -o sample-windows-amd64.dll ./native
```
> **Note:** Go plugins do **not** export `PluginGetRuntime` and default to the `"go"` runtime. They are intentionally never freed on unload to prevent crashes from orphaned Go runtime threads.
---
### C (`sample-c/`)
Pure C plugins are the simplest — no runtime, no GC, fully unloadable.
**Project structure:**
```
sample-c/
├─ native/plugin.c
├─ sample-c.html
├─ sample-c.css
└─ sample-c.js
```
**Key points:**
- Uses `__declspec(dllexport)` on Windows, `__attribute__((visibility("default")))` on Unix
- Has a `DllMain` entry point on Windows (receives `DLL_PROCESS_ATTACH` / `DLL_PROCESS_DETACH`)
- Exports `PluginGetRuntime` → returns `"c"`
- No heap allocations required — can use stack buffers for all responses
**Minimal example (Windows callback):**
```c
#ifdef _WIN32
typedef void (__stdcall *host_callback_t)(
const char *event, uintptr_t eventLen,
const char *payload, uintptr_t payloadLen);
#else
typedef void (*host_callback_t)(
uintptr_t ctx,
const char *event, int eventLen,
const char *payload, int payloadLen);
#endif
static host_callback_t g_callback;
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, ...) {
// Parse hostInfo JSON, store callback, return 0 on success
}
EXPORT int PluginOnEvent(const char *event, int eventLen,
const char *payload, int payloadLen) {
// Handle events, call g_callback() to respond
return 0;
}
EXPORT void PluginOnUnload(void) {
// Cleanup
}
EXPORT const char *PluginGetRuntime(void) { return "c"; }
```
**Build:**
```bash
# MSVC
cl /LD /O2 plugin.c /Fe:sample-c-windows-amd64.dll
# MinGW / gcc
gcc -shared -O2 -o sample-c-windows-amd64.dll plugin.c
# Linux
gcc -shared -fPIC -O2 -o sample-c-linux-amd64.so plugin.c
```
---
### C++ (`sample-cpp/`)
C++ plugins can use the full standard library (STL containers, `std::mutex`, `std::string`, etc.) while still being fully unloadable.
**Project structure:**
```
sample-cpp/
├─ native/plugin.cpp
├─ sample-cpp.html
├─ sample-cpp.css
└─ sample-cpp.js
```
**Key points:**
- All exports wrapped in `extern "C"` to prevent name mangling
- Can freely use `std::string`, `std::unordered_map`, `std::mutex`, etc.
- Exports `PluginGetRuntime` → returns `"cpp"`
**Build:**
```bash
# MSVC
cl /LD /EHsc /O2 plugin.cpp /Fe:sample-cpp-windows-amd64.dll
# MinGW / g++
g++ -shared -O2 -o sample-cpp-windows-amd64.dll plugin.cpp
# Linux
g++ -shared -fPIC -O2 -o sample-cpp-linux-amd64.so plugin.cpp
```
---
### Rust (`sample-rust/`)
Rust plugins compile as `cdylib` crates. Exports use `#[no_mangle]` and `extern "C"`.
**Project structure:**
```
sample-rust/
├─ native/
│ ├─ Cargo.toml
│ └─ src/lib.rs
├─ sample-rust.html
├─ sample-rust.css
└─ sample-rust.js
```
**Key points:**
- `crate-type = ["cdylib"]` in Cargo.toml
- Uses `extern "stdcall"` for the callback type on Windows, `extern "C"` on Unix
- Exports `PluginGetRuntime` → returns `"rust"`
**⚠️ Important: avoid `std::sync::Mutex` on Windows.** Because the DLL is loaded via an in-memory PE loader (not `LoadLibrary`), Rust's standard `Mutex` (backed by Windows `SRWLOCK`) may not initialize correctly, causing an access violation. Use C-style `static mut` globals instead — this is safe because the Go host serializes all calls through its own mutex.
**Cargo.toml:**
```toml
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "z"
lto = true
strip = true
```
**Build:**
```bash
# Native
cargo build --release
# Cross-compile
cargo build --release --target=x86_64-pc-windows-msvc
cargo build --release --target=aarch64-unknown-linux-gnu
```
## 4) Build scripts & cross-compilation
Each language has `.bat` (Windows) and `.sh` (Unix) build scripts. All scripts support the `BUILD_TARGETS` environment variable to compile for multiple platforms in one invocation.
### Available build scripts
| Language | Windows | Unix |
|----------|--------------------------|-------------------------|
| Go | `build-plugin.bat` | `build-plugin.sh` |
| C | `build-plugin-c.bat` | `build-plugin-c.sh` |
| C++ | `build-plugin-cpp.bat` | `build-plugin-cpp.sh` |
| Rust | `build-plugin-rust.bat` | `build-plugin-rust.sh` |
### Usage
All scripts default to building for the host platform only:
```bash
# Build for current platform
./build-plugin-c.sh
# Build for multiple targets
BUILD_TARGETS="linux-amd64 linux-arm64 windows-amd64" ./build-plugin-c.sh
# Custom plugin directory
./build-plugin-c.sh /path/to/my-plugin
```
```bat
REM Build for current platform
build-plugin-c.bat
REM Build for multiple targets
set BUILD_TARGETS=windows-amd64 windows-arm64
build-plugin-c.bat
REM Custom plugin directory
build-plugin-c.bat C:\path\to\my-plugin
```
### Cross-compilation
**Go** — Cross-compilation is built-in via `GOOS` / `GOARCH` env vars. No extra toolchains needed (unless CGo calls platform-specific APIs).
**Rust** — Cross-compilation uses `--target=` with Rust target triples. Install targets with `rustup target add <triple>`. The build scripts map `os-arch` pairs to triples automatically:
| Target | Rust triple (`.bat` / MSVC) | Rust triple (`.sh` / GNU) |
|-----------------|-------------------------------------|---------------------------------------|
| windows-amd64 | `x86_64-pc-windows-msvc` | `x86_64-pc-windows-gnu` |
| windows-arm64 | `aarch64-pc-windows-msvc` | `aarch64-pc-windows-gnullvm` |
| linux-amd64 | `x86_64-unknown-linux-gnu` | `x86_64-unknown-linux-gnu` |
| linux-arm64 | `aarch64-unknown-linux-gnu` | `aarch64-unknown-linux-gnu` |
| darwin-amd64 | `x86_64-apple-darwin` | `x86_64-apple-darwin` |
| darwin-arm64 | `aarch64-apple-darwin` | `aarch64-apple-darwin` |
**C / C++** — Cross-compilation requires the appropriate cross-compiler toolchain to be installed:
| Target | C compiler | C++ compiler |
|-----------------|-------------------------------|--------------------------------|
| linux-amd64 | `x86_64-linux-gnu-gcc` | `x86_64-linux-gnu-g++` |
| linux-arm64 | `aarch64-linux-gnu-gcc` | `aarch64-linux-gnu-g++` |
| linux-arm | `arm-linux-gnueabihf-gcc` | `arm-linux-gnueabihf-g++` |
| windows-amd64 | `x86_64-w64-mingw32-gcc` | `x86_64-w64-mingw32-g++` |
| windows-arm64 | `aarch64-w64-mingw32-gcc` | `aarch64-w64-mingw32-g++` |
On Windows `.bat` scripts, MSVC (`cl`) is tried first with the appropriate `/link /machine:` flag, falling back to MinGW if `cl` fails.
On Unix `.sh` scripts, override the compiler with `CC=<compiler>` (C) or `CXX=<compiler>` (C++):
```bash
CC=zig-cc BUILD_TARGETS="linux-arm64" ./build-plugin-c.sh
CXX=zig-c++ BUILD_TARGETS="linux-arm64" ./build-plugin-cpp.sh
```
### Installing cross-compiler toolchains
**Debian/Ubuntu:**
```bash
# ARM64 Linux cross-compilers
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# Windows cross-compilers (MinGW)
sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64
# Rust targets
rustup target add aarch64-unknown-linux-gnu x86_64-pc-windows-gnu
```
**macOS (Homebrew):**
```bash
# Linux cross-compilers via musl-cross
brew install FiloSottile/musl-cross/musl-cross
# Rust targets
rustup target add aarch64-apple-darwin x86_64-apple-darwin
```
## 5) 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
### Open the UI
Plugin UI is served from:
```
/plugins/<pluginId>?clientId=<CLIENT_ID>
```
Your HTML loads its JS/CSS from `/plugins/<pluginId>/assets/`.
## 6) Runtime: how events flow
Overlord plugins have **two parts**:
1. **UI (HTML/CSS/JS)** — Runs in the browser, calls server APIs.
2. **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 callback received during `OnLoad`:
**Go:**
```go
send("echo", []byte(`{"message":"hello back"}`))
```
**C / C++:**
```c
g_callback("echo", 4, "{\"message\":\"hello back\"}", 24);
```
**Rust:**
```rust
let event = b"echo";
let payload = b"{\"message\":\"hello back\"}";
(g_callback)(event.as_ptr(), event.len(), payload.as_ptr(), payload.len());
```
On Windows, the callback uses `__stdcall` calling convention. On Unix, it's a standard C call with the context pointer as the first argument.
### Plugin lifecycle events
The agent sends these events to the server:
- `loaded` on successful load
- `unloaded` when unloaded
- `error` if load or runtime fails
## 7) What can plugins do?
Since plugins run as native code, they can:
- Call any system API (file I/O, network, processes, etc.)
- Use any library available to their language
- Spawn threads / goroutines
- Access hardware
- Do anything a normal native program can do
Plugins have the same capabilities as the agent itself.
| Language | Can be fully unloaded? | Runtime overhead |
|----------|----------------------|------------------|
| C | Yes | None |
| C++ | Yes | Minimal (STL) |
| Rust | Yes | Minimal |
| Go | **No** ([#11100](https://github.com/golang/go/issues/11100)) | Go runtime + GC |
### 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.
## 8) API surface
### Plugin management
- `GET /api/plugins` — list installed plugins (includes `autoLoad` and `autoStartEvents` per plugin)
- `POST /api/plugins/upload` — upload zip
- `POST /api/plugins/<id>/enable` — enable/disable
- `POST /api/plugins/<id>/autoload` — configure auto-load on client connect
- `DELETE /api/plugins/<id>` — remove (preserves `data/` directory)
### Plugin data directory
- `GET /api/plugins/<id>/data` — list files in the plugin's persistent data directory
- `GET /api/plugins/<id>/data/<path>` — read a file
- `PUT /api/plugins/<id>/data/<path>` — write a file
- `DELETE /api/plugins/<id>/data/<path>` — delete a file or directory
- `POST /api/plugins/<id>/exec` — execute a stored binary (admin/operator only)
See [section 10](#10-server-side-plugin-data-directory) for full details.
### 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`
- `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`
## 9) 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
1. Client connects and completes the enrollment handshake
2. Server sends `hello_ack` (as usual)
3. Server dispatches auto-scripts (as usual)
4. Server checks all plugins with `autoLoad: true` and `enabled: true`
5. For each, if the plugin is **not already loaded** on that client:
- Sends the plugin binary bundle (chunked)
- Queues any `autoStartEvents`
6. 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:
```json
{
"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 `enabled` flag — disabled plugins are never auto-loaded
- Auto-load state is persisted in `.plugin-state.json` and 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
## 10) Server-side plugin data directory
Each plugin has a **persistent data directory** on the server:
```
Overlord-Server/plugins/<pluginId>/data/
```
This directory is **never deleted** when a plugin is removed or reinstalled. It survives the entire plugin lifecycle and gives server-side plugins a dedicated place to store files — SQLite databases, config files, cached data, executables, etc.
### Read/write files
From your plugin UI JS (or any authenticated API consumer):
**List all files**
```
GET /api/plugins/<pluginId>/data
```
```json
{
"ok": true,
"files": [
{ "path": "config.json", "size": 128, "isDir": false },
{ "path": "cache", "size": 0, "isDir": true },
{ "path": "cache/data.db", "size": 40960, "isDir": false }
]
}
```
**Read a file**
```
GET /api/plugins/<pluginId>/data/<path>
```
Returns the raw file bytes with an appropriate `Content-Type`.
**Write a file**
```
PUT /api/plugins/<pluginId>/data/<path>
Content-Type: application/octet-stream
<raw bytes>
```
Parent directories are created automatically.
```json
{ "ok": true, "path": "config.json", "size": 128 }
```
**Delete a file or directory**
```
DELETE /api/plugins/<pluginId>/data/<path>
```
Deleting a directory removes it recursively.
```json
{ "ok": true, "path": "cache" }
```
### Execute a stored binary
Plugins can store executables in their data directory and run them on the server. This endpoint is restricted to **admin and operator** roles.
```
POST /api/plugins/<pluginId>/exec
Content-Type: application/json
{
"file": "mytool",
"args": ["--flag", "value"],
"stdin": "optional stdin string",
"timeoutMs": 10000
}
```
| Field | Type | Description |
|-------|------|-------------|
| `file` | string | Path to the binary, relative to the plugin's `data/` directory |
| `args` | string[] | Command-line arguments (optional) |
| `stdin` | string | Text to pipe to stdin (optional) |
| `timeoutMs` | number | Max run time in ms, max 60000, default 30000 |
```json
{
"ok": true,
"exitCode": 0,
"stdout": "tool output here",
"stderr": ""
}
```
The binary is run from the `data/` directory as the working directory. The server sets the executable bit automatically before running on Unix.
### Example: storing a SQLite database from plugin JS
```js
// Write a new database seed file
await fetch(`/api/plugins/myplugin/data/app.db`, {
method: "PUT",
body: dbBytes, // ArrayBuffer
});
// Read it back
const res = await fetch(`/api/plugins/myplugin/data/app.db`);
const db = await res.arrayBuffer();
// List all stored files
const { files } = await (await fetch(`/api/plugins/myplugin/data`)).json();
```
### Notes
- Path traversal is blocked — paths may not escape the `data/` directory
- Null bytes in paths are rejected
- The data directory is created automatically on first use (no pre-setup needed)
- Deleting and reinstalling a plugin leaves `data/` untouched
---
## Plugin Signing
Plugins can be cryptographically signed with Ed25519 keys. The server verifies signatures on upload and displays trust status in the dashboard. Loading unsigned or untrusted plugins requires explicit confirmation.
### Trust Levels
| Status | Badge | Behavior |
|--------|-------|----------|
| **Signed + Trusted** | Green shield | Loads immediately |
| **Signed + Untrusted** | Yellow shield | Must type "confirm" to load |
| **Unsigned** | Orange shield | Must type "confirm" to load |
| **Invalid Signature** | Red shield | Blocked — cannot load |
### Generate a Signing Key
```bash
cd Overlord-Server
bun run scripts/plugin-keygen.ts --out my-signing-key
```
This creates:
- `my-signing-key.key` — private key (keep secret!)
- `my-signing-key.pub` — public key + fingerprint
The fingerprint (64-char hex SHA-256) is printed to the console. Add it to your server config to trust plugins signed with this key.
### Sign a Plugin
```bash
cd Overlord-Server
bun run scripts/plugin-sign.ts --key my-signing-key.key ../plugins/sample-go/sample.zip
```
This injects a `signature.json` into the ZIP containing the Ed25519 public key and signature.
### Add a Trusted Key
**Via config.json:**
```json
{
"plugins": {
"trustedKeys": [
"a1b2c3d4e5f6...64-char-hex-fingerprint..."
]
}
}
```
**Via environment variable:**
```
TRUSTED_PLUGIN_KEYS=fingerprint1,fingerprint2
```
**Via the web UI:** Go to the Plugins page → Trusted Signing Keys section → paste the fingerprint and click Add Key.
**Via API:**
```bash
# Add a trusted key
curl -X POST /api/plugins/trusted-keys \
-H "Content-Type: application/json" \
-d '{"fingerprint": "a1b2c3..."}'
# List trusted keys
curl /api/plugins/trusted-keys
# Remove a trusted key
curl -X DELETE /api/plugins/trusted-keys/a1b2c3...
```
### Build Script Integration
Set the `PLUGIN_SIGN_KEY` environment variable to automatically sign plugins during build:
```bash
# Unix
PLUGIN_SIGN_KEY=path/to/my-signing-key.key ./build-plugin.sh
# Windows
set PLUGIN_SIGN_KEY=path\to\my-signing-key.key
build-plugin.bat
```
### How Signing Works
1. The canonical content digest is computed by hashing every file in the ZIP (excluding `signature.json`), sorting filenames alphabetically, and concatenating `filename:sha256hex\n` for each
2. The digest is signed with Ed25519 using the private key
3. The signature, public key, and algorithm are stored as `signature.json` inside the ZIP
4. On upload, the server extracts `signature.json`, recomputes the digest, and verifies the signature using `crypto.subtle.verify("Ed25519", ...)`
5. The signer's fingerprint (`hex(SHA-256(raw_public_key))`) is compared against `plugins.trustedKeys` in the config
+111
View File
@@ -0,0 +1,111 @@
@echo off
setlocal enabledelayedexpansion
set "ROOT=%~dp0"
set "PLUGIN_DIR=%ROOT%sample-c"
if not "%~1"=="" set "PLUGIN_DIR=%~1"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "PLUGIN_NAME=sample-c"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%NATIVE_DIR%\plugin.c" (
echo [error] native\plugin.c not found in %NATIVE_DIR%
exit /b 1
)
REM Build targets - default to windows-amd64 on Windows
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
REM Detect host architecture
set "HOST_ARCH=amd64"
if "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "HOST_ARCH=arm64"
set "BUILT_FILES="
for %%T in (%BUILD_TARGETS%) do (
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
set "TARGET_OS=%%A"
set "TARGET_ARCH=%%B"
)
if "!TARGET_OS!"=="windows" (
set "EXT=dll"
) else if "!TARGET_OS!"=="darwin" (
set "EXT=dylib"
) else (
set "EXT=so"
)
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
if "!TARGET_OS!"=="windows" (
REM Pick cross-compiler flags for MSVC or gcc
set "CL_MACHINE="
set "GCC_CMD=gcc"
if "!TARGET_ARCH!"=="arm64" (
set "CL_MACHINE=/machine:ARM64"
set "GCC_CMD=aarch64-w64-mingw32-gcc"
) else if "!TARGET_ARCH!"=="amd64" (
set "CL_MACHINE=/machine:X64"
set "GCC_CMD=x86_64-w64-mingw32-gcc"
)
echo [build] cl /LD /O2 "%NATIVE_DIR%\plugin.c" /Fe:"!OUTFILE!" /link !CL_MACHINE!
cl /LD /O2 "%NATIVE_DIR%\plugin.c" /Fe:"!OUTFILE!" /link !CL_MACHINE! >nul 2>&1
if errorlevel 1 (
echo [build] cl failed, trying !GCC_CMD!...
!GCC_CMD! -shared -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
if errorlevel 1 (
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
exit /b 1
)
)
) else (
REM Cross-compile for non-Windows targets
set "GCC_CMD=gcc"
if "!TARGET_OS!"=="linux" (
if "!TARGET_ARCH!"=="arm64" set "GCC_CMD=aarch64-linux-gnu-gcc"
if "!TARGET_ARCH!"=="amd64" set "GCC_CMD=x86_64-linux-gnu-gcc"
)
echo [build] !GCC_CMD! -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
!GCC_CMD! -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
if errorlevel 1 (
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
exit /b 1
)
)
set "BUILT_FILES=!BUILT_FILES! '%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!'"
)
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
REM Collect all build outputs and web assets
set "ZIP_SOURCES="
for %%T in (%BUILD_TARGETS%) do (
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
set "TARGET_OS=%%A"
set "TARGET_ARCH=%%B"
)
if "!TARGET_OS!"=="windows" (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
) else if "!TARGET_OS!"=="darwin" (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
) else (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
)
)
REM Add web assets
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
REM Remove leading comma
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
if errorlevel 1 (
echo [error] zip failed
exit /b 1
)
echo [ok] %ZIP_OUT%
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_DIR="${1:-${ROOT_DIR}/sample-c}"
NATIVE_DIR="${PLUGIN_DIR}/native"
PLUGIN_NAME="sample-c"
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
if [[ ! -f "${NATIVE_DIR}/plugin.c" ]]; then
echo "[error] native/plugin.c not found in ${NATIVE_DIR}" >&2
exit 1
fi
# Detect current OS/arch
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
HOST_ARCH="$(uname -m)"
case "${HOST_ARCH}" in
x86_64) HOST_ARCH="amd64" ;;
aarch64) HOST_ARCH="arm64" ;;
arm64) HOST_ARCH="arm64" ;;
esac
case "${HOST_OS}" in
linux) HOST_OS="linux" ;;
darwin) HOST_OS="darwin" ;;
esac
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
# Resolve the C compiler for a given target os-arch pair.
# Override with CC=<compiler> for custom toolchains.
resolve_cc() {
local t_os="$1" t_arch="$2"
# Honour explicit CC override
if [[ -n "${CC:-}" ]]; then echo "${CC}"; return; fi
# Native compilation
if [[ "${t_os}" == "${HOST_OS}" && "${t_arch}" == "${HOST_ARCH}" ]]; then
echo "gcc"; return
fi
# Cross-compilation
case "${t_os}-${t_arch}" in
linux-amd64) echo "x86_64-linux-gnu-gcc" ;;
linux-arm64) echo "aarch64-linux-gnu-gcc" ;;
linux-arm) echo "arm-linux-gnueabihf-gcc" ;;
windows-amd64) echo "x86_64-w64-mingw32-gcc" ;;
windows-arm64) echo "aarch64-w64-mingw32-gcc" ;;
darwin-amd64) echo "x86_64-apple-darwin-gcc" ;;
darwin-arm64) echo "aarch64-apple-darwin-gcc" ;;
*) echo "gcc" ;;
esac
}
BUILT_FILES=()
for target in ${BUILD_TARGETS}; do
os="${target%%-*}"
arch="${target#*-}"
if [[ "${os}" == "windows" ]]; then
ext="dll"
elif [[ "${os}" == "darwin" ]]; then
ext="dylib"
else
ext="so"
fi
cc="$(resolve_cc "${os}" "${arch}")"
if ! command -v "${cc}" >/dev/null 2>&1; then
echo "[error] cross-compiler '${cc}' not found for ${os}-${arch}. Install it or set CC=<compiler>." >&2
exit 1
fi
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
echo "[build] ${cc} -shared -fPIC -O2 -o ${outfile} ${NATIVE_DIR}/plugin.c"
${cc} -shared -fPIC -O2 -o "${outfile}" "${NATIVE_DIR}/plugin.c"
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
done
rm -f "${ZIP_OUT}"
# Collect files to zip
ZIP_FILES=()
for bf in "${BUILT_FILES[@]}"; do
ZIP_FILES+=("${bf}")
done
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
ZIP_FILES+=("${asset}")
fi
done
if command -v zip >/dev/null 2>&1; then
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
else
echo "[error] zip not found. Please install zip." >&2
exit 1
fi
echo "[ok] ${ZIP_OUT}"
+111
View File
@@ -0,0 +1,111 @@
@echo off
setlocal enabledelayedexpansion
set "ROOT=%~dp0"
set "PLUGIN_DIR=%ROOT%sample-cpp"
if not "%~1"=="" set "PLUGIN_DIR=%~1"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "PLUGIN_NAME=sample-cpp"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%NATIVE_DIR%\plugin.cpp" (
echo [error] native\plugin.cpp not found in %NATIVE_DIR%
exit /b 1
)
REM Build targets - default to windows-amd64 on Windows
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
REM Detect host architecture
set "HOST_ARCH=amd64"
if "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "HOST_ARCH=arm64"
set "BUILT_FILES="
for %%T in (%BUILD_TARGETS%) do (
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
set "TARGET_OS=%%A"
set "TARGET_ARCH=%%B"
)
if "!TARGET_OS!"=="windows" (
set "EXT=dll"
) else if "!TARGET_OS!"=="darwin" (
set "EXT=dylib"
) else (
set "EXT=so"
)
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
if "!TARGET_OS!"=="windows" (
REM Pick cross-compiler flags for MSVC or g++
set "CL_MACHINE="
set "GXX_CMD=g++"
if "!TARGET_ARCH!"=="arm64" (
set "CL_MACHINE=/machine:ARM64"
set "GXX_CMD=aarch64-w64-mingw32-g++"
) else if "!TARGET_ARCH!"=="amd64" (
set "CL_MACHINE=/machine:X64"
set "GXX_CMD=x86_64-w64-mingw32-g++"
)
echo [build] cl /LD /EHsc /O2 "%NATIVE_DIR%\plugin.cpp" /Fe:"!OUTFILE!" /link !CL_MACHINE!
cl /LD /EHsc /O2 "%NATIVE_DIR%\plugin.cpp" /Fe:"!OUTFILE!" /link !CL_MACHINE! >nul 2>&1
if errorlevel 1 (
echo [build] cl failed, trying !GXX_CMD!...
!GXX_CMD! -shared -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
if errorlevel 1 (
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
exit /b 1
)
)
) else (
REM Cross-compile for non-Windows targets
set "GXX_CMD=g++"
if "!TARGET_OS!"=="linux" (
if "!TARGET_ARCH!"=="arm64" set "GXX_CMD=aarch64-linux-gnu-g++"
if "!TARGET_ARCH!"=="amd64" set "GXX_CMD=x86_64-linux-gnu-g++"
)
echo [build] !GXX_CMD! -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
!GXX_CMD! -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
if errorlevel 1 (
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
exit /b 1
)
)
set "BUILT_FILES=!BUILT_FILES! '%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!'"
)
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
REM Collect all build outputs and web assets
set "ZIP_SOURCES="
for %%T in (%BUILD_TARGETS%) do (
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
set "TARGET_OS=%%A"
set "TARGET_ARCH=%%B"
)
if "!TARGET_OS!"=="windows" (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
) else if "!TARGET_OS!"=="darwin" (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
) else (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
)
)
REM Add web assets
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
REM Remove leading comma
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
if errorlevel 1 (
echo [error] zip failed
exit /b 1
)
echo [ok] %ZIP_OUT%
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_DIR="${1:-${ROOT_DIR}/sample-cpp}"
NATIVE_DIR="${PLUGIN_DIR}/native"
PLUGIN_NAME="sample-cpp"
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
if [[ ! -f "${NATIVE_DIR}/plugin.cpp" ]]; then
echo "[error] native/plugin.cpp not found in ${NATIVE_DIR}" >&2
exit 1
fi
# Detect current OS/arch
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
HOST_ARCH="$(uname -m)"
case "${HOST_ARCH}" in
x86_64) HOST_ARCH="amd64" ;;
aarch64) HOST_ARCH="arm64" ;;
arm64) HOST_ARCH="arm64" ;;
esac
case "${HOST_OS}" in
linux) HOST_OS="linux" ;;
darwin) HOST_OS="darwin" ;;
esac
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
# Resolve the C++ compiler for a given target os-arch pair.
# Override with CXX=<compiler> for custom toolchains.
resolve_cxx() {
local t_os="$1" t_arch="$2"
# Honour explicit CXX override
if [[ -n "${CXX:-}" ]]; then echo "${CXX}"; return; fi
# Native compilation
if [[ "${t_os}" == "${HOST_OS}" && "${t_arch}" == "${HOST_ARCH}" ]]; then
echo "g++"; return
fi
# Cross-compilation
case "${t_os}-${t_arch}" in
linux-amd64) echo "x86_64-linux-gnu-g++" ;;
linux-arm64) echo "aarch64-linux-gnu-g++" ;;
linux-arm) echo "arm-linux-gnueabihf-g++" ;;
windows-amd64) echo "x86_64-w64-mingw32-g++" ;;
windows-arm64) echo "aarch64-w64-mingw32-g++" ;;
darwin-amd64) echo "x86_64-apple-darwin-g++" ;;
darwin-arm64) echo "aarch64-apple-darwin-g++" ;;
*) echo "g++" ;;
esac
}
BUILT_FILES=()
for target in ${BUILD_TARGETS}; do
os="${target%%-*}"
arch="${target#*-}"
if [[ "${os}" == "windows" ]]; then
ext="dll"
elif [[ "${os}" == "darwin" ]]; then
ext="dylib"
else
ext="so"
fi
cxx="$(resolve_cxx "${os}" "${arch}")"
if ! command -v "${cxx}" >/dev/null 2>&1; then
echo "[error] cross-compiler '${cxx}' not found for ${os}-${arch}. Install it or set CXX=<compiler>." >&2
exit 1
fi
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
echo "[build] ${cxx} -shared -fPIC -O2 -o ${outfile} ${NATIVE_DIR}/plugin.cpp"
${cxx} -shared -fPIC -O2 -o "${outfile}" "${NATIVE_DIR}/plugin.cpp"
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
done
rm -f "${ZIP_OUT}"
# Collect files to zip
ZIP_FILES=()
for bf in "${BUILT_FILES[@]}"; do
ZIP_FILES+=("${bf}")
done
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
ZIP_FILES+=("${asset}")
fi
done
if command -v zip >/dev/null 2>&1; then
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
else
echo "[error] zip not found. Please install zip." >&2
exit 1
fi
echo "[ok] ${ZIP_OUT}"
+100
View File
@@ -0,0 +1,100 @@
@echo off
setlocal enabledelayedexpansion
set "ROOT=%~dp0"
set "PLUGIN_DIR=%ROOT%sample-rust"
if not "%~1"=="" set "PLUGIN_DIR=%~1"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "PLUGIN_NAME=sample-rust"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%NATIVE_DIR%\Cargo.toml" (
echo [error] native\Cargo.toml not found in %NATIVE_DIR%
exit /b 1
)
REM Build targets - default to windows-amd64 on Windows
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
set "BUILT_FILES="
for %%T in (%BUILD_TARGETS%) do (
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
set "TARGET_OS=%%A"
set "TARGET_ARCH=%%B"
)
if "!TARGET_OS!"=="windows" (
set "EXT=dll"
set "RUST_TARGET=x86_64-pc-windows-msvc"
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-pc-windows-msvc"
) else if "!TARGET_OS!"=="darwin" (
set "EXT=dylib"
set "RUST_TARGET=x86_64-apple-darwin"
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-apple-darwin"
) else (
set "EXT=so"
set "RUST_TARGET=x86_64-unknown-linux-gnu"
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-unknown-linux-gnu"
)
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
echo [build] cargo build --release --target=!RUST_TARGET! in %NATIVE_DIR%
pushd "%NATIVE_DIR%"
cargo build --release --target=!RUST_TARGET!
if errorlevel 1 (
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
popd
exit /b 1
)
popd
REM Cargo outputs to target/<triple>/release/ — find the cdylib
if "!TARGET_OS!"=="windows" (
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\sample_rust.dll" "!OUTFILE!" >nul
) else if "!TARGET_OS!"=="darwin" (
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\libsample_rust.dylib" "!OUTFILE!" >nul
) else (
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\libsample_rust.so" "!OUTFILE!" >nul
)
if errorlevel 1 (
echo [error] copy failed for !TARGET_OS!-!TARGET_ARCH!
exit /b 1
)
set "BUILT_FILES=!BUILT_FILES! '!OUTFILE!'"
)
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
REM Collect all build outputs and web assets
set "ZIP_SOURCES="
for %%T in (%BUILD_TARGETS%) do (
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
set "TARGET_OS=%%A"
set "TARGET_ARCH=%%B"
)
if "!TARGET_OS!"=="windows" (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
) else if "!TARGET_OS!"=="darwin" (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
) else (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
)
)
REM Add web assets
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
REM Remove leading comma
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
if errorlevel 1 (
echo [error] zip failed
exit /b 1
)
echo [ok] %ZIP_OUT%
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_DIR="${1:-${ROOT_DIR}/sample-rust}"
NATIVE_DIR="${PLUGIN_DIR}/native"
PLUGIN_NAME="sample-rust"
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
if [[ ! -f "${NATIVE_DIR}/Cargo.toml" ]]; then
echo "[error] native/Cargo.toml not found in ${NATIVE_DIR}" >&2
exit 1
fi
# Detect current OS/arch
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
HOST_ARCH="$(uname -m)"
case "${HOST_ARCH}" in
x86_64) HOST_ARCH="amd64" ;;
aarch64) HOST_ARCH="arm64" ;;
arm64) HOST_ARCH="arm64" ;;
esac
case "${HOST_OS}" in
linux) HOST_OS="linux" ;;
darwin) HOST_OS="darwin" ;;
esac
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
BUILT_FILES=()
for target in ${BUILD_TARGETS}; do
os="${target%%-*}"
arch="${target#*-}"
if [[ "${os}" == "windows" ]]; then
ext="dll"
rust_target="x86_64-pc-windows-gnu"
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-pc-windows-gnullvm"
lib_name="sample_rust.dll"
elif [[ "${os}" == "darwin" ]]; then
ext="dylib"
rust_target="x86_64-apple-darwin"
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-apple-darwin"
lib_name="libsample_rust.dylib"
else
ext="so"
rust_target="x86_64-unknown-linux-gnu"
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-unknown-linux-gnu"
lib_name="libsample_rust.so"
fi
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
echo "[build] cargo build --release --target=${rust_target} in ${NATIVE_DIR}"
(cd "${NATIVE_DIR}" && cargo build --release --target="${rust_target}")
cp "${NATIVE_DIR}/target/${rust_target}/release/${lib_name}" "${outfile}"
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
done
rm -f "${ZIP_OUT}"
# Collect files to zip
ZIP_FILES=()
for bf in "${BUILT_FILES[@]}"; do
ZIP_FILES+=("${bf}")
done
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
ZIP_FILES+=("${asset}")
fi
done
if command -v zip >/dev/null 2>&1; then
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
else
echo "[error] zip not found. Please install zip." >&2
exit 1
fi
echo "[ok] ${ZIP_OUT}"
+101
View File
@@ -0,0 +1,101 @@
@echo off
setlocal enabledelayedexpansion
set "ROOT=%~dp0"
set "PLUGIN_DIR=%ROOT%sample-go"
if not "%~1"=="" set "PLUGIN_DIR=%~1"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "PLUGIN_NAME=sample"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%NATIVE_DIR%" (
echo [error] native folder not found: %NATIVE_DIR%
exit /b 1
)
pushd "%NATIVE_DIR%"
REM Build targets - default to windows-amd64 on Windows
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
set "BUILT_FILES="
for %%T in (%BUILD_TARGETS%) do (
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
set "TARGET_OS=%%A"
set "TARGET_ARCH=%%B"
)
if "!TARGET_OS!"=="windows" (
set "EXT=dll"
) else if "!TARGET_OS!"=="darwin" (
set "EXT=dylib"
) else (
set "EXT=so"
)
set "BUILDMODE=c-shared"
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
echo [build] GOOS=!TARGET_OS! GOARCH=!TARGET_ARCH! CGO_ENABLED=1 go build -buildmode=!BUILDMODE! -o "!OUTFILE!"
set "GOOS=!TARGET_OS!"
set "GOARCH=!TARGET_ARCH!"
set "CGO_ENABLED=1"
go build -buildmode=!BUILDMODE! -o "!OUTFILE!" .
if errorlevel 1 (
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
popd
exit /b 1
)
set "BUILT_FILES=!BUILT_FILES! '%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!'"
)
set "GOOS="
set "GOARCH="
set "CGO_ENABLED="
popd
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
REM Collect all build outputs and web assets
set "ZIP_SOURCES="
for %%T in (%BUILD_TARGETS%) do (
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
set "TARGET_OS=%%A"
set "TARGET_ARCH=%%B"
)
if "!TARGET_OS!"=="windows" (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
) else if "!TARGET_OS!"=="darwin" (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
) else (
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
)
)
REM Add web assets
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
REM Remove leading comma
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
if errorlevel 1 (
echo [error] zip failed
exit /b 1
)
REM Optional: sign the plugin if PLUGIN_SIGN_KEY is set
if defined PLUGIN_SIGN_KEY (
where bun >nul 2>&1
if not errorlevel 1 (
echo [sign] Signing plugin with key: %PLUGIN_SIGN_KEY%
bun run "%ROOT%Overlord-Server\scripts\plugin-sign.ts" --key "%PLUGIN_SIGN_KEY%" "%ZIP_OUT%"
) else (
echo [warn] bun not found, skipping plugin signing
)
)
echo [ok] %ZIP_OUT%
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_DIR="${1:-${ROOT_DIR}/sample-go}"
NATIVE_DIR="${PLUGIN_DIR}/native"
PLUGIN_NAME="sample"
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
if [[ ! -d "${NATIVE_DIR}" ]]; then
echo "[error] native folder not found: ${NATIVE_DIR}" >&2
exit 1
fi
pushd "${NATIVE_DIR}" >/dev/null
# Detect current OS/arch for default build
HOST_OS="$(go env GOOS)"
HOST_ARCH="$(go env GOARCH)"
# Build matrix - can be overridden with BUILD_TARGETS env var
# Format: "os-arch os-arch ..." e.g. "linux-amd64 linux-arm64 darwin-arm64"
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
BUILT_FILES=()
for target in ${BUILD_TARGETS}; do
os="${target%%-*}"
arch="${target#*-}"
if [[ "${os}" == "windows" ]]; then
ext="dll"
elif [[ "${os}" == "darwin" ]]; then
ext="dylib"
else
ext="so"
fi
buildmode="c-shared"
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
echo "[build] GOOS=${os} GOARCH=${arch} CGO_ENABLED=1 go build -buildmode=${buildmode} -o ${outfile}"
CGO_ENABLED=1 GOOS="${os}" GOARCH="${arch}" go build -buildmode="${buildmode}" -o "${outfile}" .
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
done
popd >/dev/null
rm -f "${ZIP_OUT}"
# Collect files to zip
ZIP_FILES=()
for bf in "${BUILT_FILES[@]}"; do
ZIP_FILES+=("${bf}")
done
# Add web assets
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
ZIP_FILES+=("${asset}")
fi
done
if command -v zip >/dev/null 2>&1; then
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
else
echo "[error] zip not found. Please install zip." >&2
exit 1
fi
# Optional: sign the plugin if PLUGIN_SIGN_KEY is set
if [[ -n "${PLUGIN_SIGN_KEY:-}" ]]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if command -v bun >/dev/null 2>&1; then
echo "[sign] Signing plugin with key: ${PLUGIN_SIGN_KEY}"
bun run "${SCRIPT_DIR}/Overlord-Server/scripts/plugin-sign.ts" --key "${PLUGIN_SIGN_KEY}" "${ZIP_OUT}"
else
echo "[warn] bun not found, skipping plugin signing" >&2
fi
fi
echo "[ok] ${ZIP_OUT}"
+160
View File
@@ -0,0 +1,160 @@
/*
* Sample C Plugin — fully unloadable, no runtime baggage.
*
* Exports the standard Overlord plugin ABI:
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
* PluginGetRuntime (returns "c" — tells the host this DLL can be freed)
*
* Build (Windows):
* cl /LD /O2 plugin.c /Fe:sample-c-windows-amd64.dll
* — or with MinGW —
* x86_64-w64-mingw32-gcc -shared -O2 -o sample-c-windows-amd64.dll plugin.c
*
* Build (Linux):
* gcc -shared -fPIC -O2 -o sample-c-linux-amd64.so plugin.c
*/
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
/* ------------------------------------------------------------------ */
/* Platform helpers */
/* ------------------------------------------------------------------ */
#ifdef _WIN32
# define EXPORT __declspec(dllexport)
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
(void)hDll; (void)reserved;
switch (reason) {
case DLL_PROCESS_ATTACH: break;
case DLL_PROCESS_DETACH: break;
}
return TRUE;
}
#else
# define EXPORT __attribute__((visibility("default")))
#endif
/* ------------------------------------------------------------------ */
/* Host callback */
/* ------------------------------------------------------------------ */
#ifdef _WIN32
/* Windows: callback is void(__stdcall*)(ptr,len,ptr,len) */
typedef void (__stdcall *host_callback_t)(
const char *event, uintptr_t eventLen,
const char *payload, uintptr_t payloadLen);
#else
/* Unix: callback is void(*)(ctx, event, eventLen, payload, payloadLen) */
typedef void (*host_callback_t)(
uintptr_t ctx,
const char *event, int eventLen,
const char *payload, int payloadLen);
#endif
static host_callback_t g_callback = NULL;
#ifndef _WIN32
static uintptr_t g_callback_ctx = 0;
#endif
static void send_event(const char *event, const char *payload) {
if (!g_callback) return;
int elen = event ? (int)strlen(event) : 0;
int plen = payload ? (int)strlen(payload) : 0;
#ifdef _WIN32
g_callback(event, (uintptr_t)elen, payload, (uintptr_t)plen);
#else
g_callback(g_callback_ctx, event, elen, payload, plen);
#endif
}
/* ------------------------------------------------------------------ */
/* Plugin state (trivial example) */
/* ------------------------------------------------------------------ */
static char g_client_id[256];
/* ------------------------------------------------------------------ */
/* Exported ABI */
/* ------------------------------------------------------------------ */
EXPORT const char *PluginGetRuntime(void) {
return "c";
}
#ifdef _WIN32
EXPORT void PluginSetCallback(uint64_t cb) {
g_callback = (host_callback_t)(uintptr_t)cb;
}
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
g_callback = (host_callback_t)(uintptr_t)cb;
#else
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen,
uintptr_t cb, uintptr_t ctx) {
g_callback = (host_callback_t)cb;
g_callback_ctx = ctx;
#endif
/* Minimal JSON-ish parse to grab clientId — keeps the example dependency-free */
g_client_id[0] = '\0';
if (hostInfo && hostInfoLen > 0) {
const char *key = "\"clientId\":\"";
const char *p = strstr(hostInfo, key); /* safe: hostInfo is nul-terminated by the Go caller copying it */
if (!p) {
key = "\"clientId\": \"";
p = strstr(hostInfo, key);
}
if (p) {
p += strlen(key);
const char *end = strchr(p, '"');
if (end) {
size_t n = (size_t)(end - p);
if (n >= sizeof(g_client_id)) n = sizeof(g_client_id) - 1;
memcpy(g_client_id, p, n);
g_client_id[n] = '\0';
}
}
}
fprintf(stderr, "[sample-c] loaded, clientId=%s\n", g_client_id);
send_event("ready", "{\"message\":\"sample-c plugin ready\"}");
return 0;
}
EXPORT int PluginOnEvent(const char *event, int eventLen,
const char *payload, int payloadLen) {
(void)payloadLen;
/* Simple echo for "ping" */
if (eventLen == 4 && memcmp(event, "ping", 4) == 0) {
send_event("pong", NULL);
return 0;
}
/* Echo back ui_message */
if (eventLen == 10 && memcmp(event, "ui_message", 10) == 0) {
fprintf(stderr, "[sample-c] got ui_message: %.*s\n", payloadLen, payload ? payload : "");
/* Build a tiny JSON response — payload is already JSON from the host */
char buf[512];
snprintf(buf, sizeof(buf), "{\"message\":\"echo from C: %.*s\"}",
payloadLen < 400 ? payloadLen : 400, payload ? payload : "");
send_event("echo", buf);
return 0;
}
fprintf(stderr, "[sample-c] unhandled event: %.*s\n", eventLen, event);
return 0;
}
EXPORT void PluginOnUnload(void) {
fprintf(stderr, "[sample-c] unloading\n");
g_callback = NULL;
#ifndef _WIN32
g_callback_ctx = 0;
#endif
g_client_id[0] = '\0';
}
+93
View File
@@ -0,0 +1,93 @@
:root {
color-scheme: dark;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Segoe UI", system-ui, sans-serif;
background: #0f1115;
color: #e8ecf1;
}
.container {
max-width: 820px;
margin: 40px auto;
padding: 0 20px;
}
.header h1 {
margin: 0 0 4px 0;
font-size: 28px;
}
.sub {
margin: 0 0 24px 0;
color: #9aa4b2;
}
.card {
background: #161a22;
border: 1px solid #222938;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.label {
color: #9aa4b2;
font-size: 14px;
}
.input {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #2a3346;
background: #0f131b;
color: #e8ecf1;
}
.actions {
display: flex;
gap: 10px;
margin-top: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: #2563eb;
color: #fff;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #1d4ed8;
}
.log {
background: #0b0e14;
border: 1px solid #1e2533;
border-radius: 8px;
padding: 14px;
font-size: 13px;
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
+41
View File
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sample C Plugin</title>
<link rel="stylesheet" href="/plugins/sample-c/assets/sample-c.css" />
</head>
<body>
<main class="container">
<header class="header">
<h1>Sample C Plugin</h1>
<p class="sub">Pure C &mdash; fully unloadable, zero runtime leak</p>
</header>
<section class="card">
<div class="row">
<label class="label">Client ID</label>
<input id="client-id" class="input" placeholder="clientId" />
</div>
<div class="row">
<label class="label">Message</label>
<input id="message" class="input" placeholder="hello from UI" />
</div>
<div class="actions">
<button id="send-btn" class="btn">Send event to client</button>
<button id="ping-btn" class="btn">Ping</button>
</div>
</section>
<section class="card">
<h2>Log</h2>
<pre id="log" class="log"></pre>
</section>
</main>
<script src="/plugins/sample-c/assets/sample-c.js"></script>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
const params = new URLSearchParams(window.location.search);
const clientIdInput = document.getElementById("client-id");
const messageInput = document.getElementById("message");
const logEl = document.getElementById("log");
const sendBtn = document.getElementById("send-btn");
const pingBtn = document.getElementById("ping-btn");
const pluginId = "sample-c";
clientIdInput.value = params.get("clientId") || "";
messageInput.value = "hello from sample-c UI";
function log(line) {
const ts = new Date().toISOString();
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
}
async function sendEvent(event, payload) {
const clientId = clientIdInput.value.trim();
if (!clientId) { log("Missing clientId"); return; }
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event, payload }),
});
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
log(`Sent event: ${event}`);
}
sendBtn.addEventListener("click", () => {
sendEvent("ui_message", { message: messageInput.value.trim() });
});
pingBtn.addEventListener("click", () => {
sendEvent("ping", {});
});
// Listen for plugin events via SSE / polling (same pattern as Go sample)
if (typeof EventSource !== "undefined") {
const clientId = params.get("clientId");
if (clientId) {
log("Ready — C plugin (fully unloadable)");
}
}
+179
View File
@@ -0,0 +1,179 @@
/*
* Sample C++ Plugin — fully unloadable, no runtime baggage.
*
* Exports the standard Overlord plugin ABI:
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
* PluginGetRuntime (returns "cpp" — tells the host this DLL can be freed)
*
* Build (Windows / MSVC):
* cl /LD /EHsc /O2 plugin.cpp /Fe:sample-cpp-windows-amd64.dll
* — or with MinGW —
* x86_64-w64-mingw32-g++ -shared -O2 -o sample-cpp-windows-amd64.dll plugin.cpp
*
* Build (Linux):
* g++ -shared -fPIC -O2 -o sample-cpp-linux-amd64.so plugin.cpp
*/
#include <cstdint>
#include <cstring>
#include <cstdio>
#include <string>
#include <mutex>
#include <unordered_map>
/* ------------------------------------------------------------------ */
/* Platform helpers */
/* ------------------------------------------------------------------ */
#ifdef _WIN32
# define EXPORT extern "C" __declspec(dllexport)
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
(void)hDll; (void)reserved;
return TRUE;
}
#else
# define EXPORT extern "C" __attribute__((visibility("default")))
#endif
/* ------------------------------------------------------------------ */
/* Host callback */
/* ------------------------------------------------------------------ */
#ifdef _WIN32
typedef void (__stdcall *host_callback_t)(
const char *event, uintptr_t eventLen,
const char *payload, uintptr_t payloadLen);
#else
typedef void (*host_callback_t)(
uintptr_t ctx,
const char *event, int eventLen,
const char *payload, int payloadLen);
#endif
static host_callback_t g_callback = nullptr;
#ifndef _WIN32
static uintptr_t g_callback_ctx = 0;
#endif
static void send_event(const char *event, const char *payload) {
if (!g_callback) return;
int elen = event ? static_cast<int>(strlen(event)) : 0;
int plen = payload ? static_cast<int>(strlen(payload)) : 0;
#ifdef _WIN32
g_callback(event, static_cast<uintptr_t>(elen),
payload, static_cast<uintptr_t>(plen));
#else
g_callback(g_callback_ctx, event, elen, payload, plen);
#endif
}
/* ------------------------------------------------------------------ */
/* Plugin state — demonstrates C++ features (std::string, maps, mutex) */
/* ------------------------------------------------------------------ */
static std::mutex g_mu;
static std::string g_client_id;
static std::unordered_map<std::string, int> g_event_counts;
/* Extract a JSON string value by key (simple, no full JSON parser) */
static std::string json_extract(const char *json, int len, const char *key) {
if (!json || len <= 0) return "";
std::string haystack(json, static_cast<size_t>(len));
std::string needle = std::string("\"") + key + "\":\"";
auto pos = haystack.find(needle);
if (pos == std::string::npos) {
needle = std::string("\"") + key + "\": \"";
pos = haystack.find(needle);
}
if (pos == std::string::npos) return "";
pos += needle.size();
auto end = haystack.find('"', pos);
if (end == std::string::npos) return "";
return haystack.substr(pos, end - pos);
}
/* ------------------------------------------------------------------ */
/* Exported ABI */
/* ------------------------------------------------------------------ */
EXPORT const char *PluginGetRuntime() {
return "cpp";
}
#ifdef _WIN32
EXPORT void PluginSetCallback(uint64_t cb) {
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
}
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
#else
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen,
uintptr_t cb, uintptr_t ctx) {
g_callback = reinterpret_cast<host_callback_t>(cb);
g_callback_ctx = ctx;
#endif
{
std::lock_guard<std::mutex> lk(g_mu);
g_client_id = json_extract(hostInfo, hostInfoLen, "clientId");
g_event_counts.clear();
}
fprintf(stderr, "[sample-cpp] loaded, clientId=%s\n", g_client_id.c_str());
send_event("ready", "{\"message\":\"sample-cpp plugin ready\"}");
return 0;
}
EXPORT int PluginOnEvent(const char *event, int eventLen,
const char *payload, int payloadLen) {
std::string ev(event, static_cast<size_t>(eventLen));
{
std::lock_guard<std::mutex> lk(g_mu);
g_event_counts[ev]++;
}
if (ev == "ping") {
send_event("pong", nullptr);
return 0;
}
if (ev == "ui_message") {
std::string pl(payload ? payload : "", payload ? static_cast<size_t>(payloadLen) : 0u);
fprintf(stderr, "[sample-cpp] got ui_message: %s\n", pl.c_str());
std::string resp = "{\"message\":\"echo from C++: " + pl + "\"}";
send_event("echo", resp.c_str());
return 0;
}
if (ev == "stats") {
std::lock_guard<std::mutex> lk(g_mu);
std::string json = "{";
bool first = true;
for (auto &kv : g_event_counts) {
if (!first) json += ",";
json += "\"" + kv.first + "\":" + std::to_string(kv.second);
first = false;
}
json += "}";
send_event("stats_reply", json.c_str());
return 0;
}
fprintf(stderr, "[sample-cpp] unhandled event: %s\n", ev.c_str());
return 0;
}
EXPORT void PluginOnUnload() {
fprintf(stderr, "[sample-cpp] unloading\n");
g_callback = nullptr;
#ifndef _WIN32
g_callback_ctx = 0;
#endif
std::lock_guard<std::mutex> lk(g_mu);
g_client_id.clear();
g_event_counts.clear();
}
+93
View File
@@ -0,0 +1,93 @@
:root {
color-scheme: dark;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Segoe UI", system-ui, sans-serif;
background: #0f1115;
color: #e8ecf1;
}
.container {
max-width: 820px;
margin: 40px auto;
padding: 0 20px;
}
.header h1 {
margin: 0 0 4px 0;
font-size: 28px;
}
.sub {
margin: 0 0 24px 0;
color: #9aa4b2;
}
.card {
background: #161a22;
border: 1px solid #222938;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.label {
color: #9aa4b2;
font-size: 14px;
}
.input {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #2a3346;
background: #0f131b;
color: #e8ecf1;
}
.actions {
display: flex;
gap: 10px;
margin-top: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: #2563eb;
color: #fff;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #1d4ed8;
}
.log {
background: #0b0e14;
border: 1px solid #1e2533;
border-radius: 8px;
padding: 14px;
font-size: 13px;
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
+42
View File
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sample C++ Plugin</title>
<link rel="stylesheet" href="/plugins/sample-cpp/assets/sample-cpp.css" />
</head>
<body>
<main class="container">
<header class="header">
<h1>Sample C++ Plugin</h1>
<p class="sub">C++ &mdash; fully unloadable, zero runtime leak</p>
</header>
<section class="card">
<div class="row">
<label class="label">Client ID</label>
<input id="client-id" class="input" placeholder="clientId" />
</div>
<div class="row">
<label class="label">Message</label>
<input id="message" class="input" placeholder="hello from UI" />
</div>
<div class="actions">
<button id="send-btn" class="btn">Send event to client</button>
<button id="ping-btn" class="btn">Ping</button>
<button id="stats-btn" class="btn">Get Stats</button>
</div>
</section>
<section class="card">
<h2>Log</h2>
<pre id="log" class="log"></pre>
</section>
</main>
<script src="/plugins/sample-cpp/assets/sample-cpp.js"></script>
</body>
</html>
+47
View File
@@ -0,0 +1,47 @@
const params = new URLSearchParams(window.location.search);
const clientIdInput = document.getElementById("client-id");
const messageInput = document.getElementById("message");
const logEl = document.getElementById("log");
const sendBtn = document.getElementById("send-btn");
const pingBtn = document.getElementById("ping-btn");
const statsBtn = document.getElementById("stats-btn");
const pluginId = "sample-cpp";
clientIdInput.value = params.get("clientId") || "";
messageInput.value = "hello from sample-cpp UI";
function log(line) {
const ts = new Date().toISOString();
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
}
async function sendEvent(event, payload) {
const clientId = clientIdInput.value.trim();
if (!clientId) { log("Missing clientId"); return; }
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event, payload }),
});
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
log(`Sent event: ${event}`);
}
sendBtn.addEventListener("click", () => {
sendEvent("ui_message", { message: messageInput.value.trim() });
});
pingBtn.addEventListener("click", () => {
sendEvent("ping", {});
});
statsBtn.addEventListener("click", () => {
sendEvent("stats", {});
});
if (typeof EventSource !== "undefined") {
const clientId = params.get("clientId");
if (clientId) {
log("Ready — C++ plugin (fully unloadable)");
}
}
+49
View File
@@ -0,0 +1,49 @@
# Sample Go Plugin (sample)
This sample lives in the `plugins/` directory and builds a native Go plugin.
## Files to include in the plugin zip
At the root of the zip:
- sample-linux-amd64.so / sample-darwin-arm64.dylib / sample-windows-amd64.dll
- sample.html
- sample.css
- sample.js
## Build
Use the provided build scripts:
```bash
# Linux/macOS
./build-plugin.sh
# Multiple targets
BUILD_TARGETS="linux-amd64 linux-arm64 darwin-arm64" ./build-plugin.sh
# Windows
build-plugin.bat
```
## Create the zip
The build script automatically creates `sample.zip` containing the built
binaries and web assets.
Then place `sample.zip` in `Overlord-Server/plugins`.
## Open the UI
Navigate to:
- /plugins/sample?clientId=<CLIENT_ID>
Click "Send event" and the plugin will echo back to the server logs.
## OS detection (Windows-only logic)
The plugin receives host metadata in the `init` payload. The sample plugin shows
how to detect the host OS and branch logic (see
`sample-go/native/main.go`). For example:
- If `host.os == "windows"`, you can safely run Windows-only flows (e.g., WinAPI-backed behavior).
- Otherwise, skip or provide a fallback path.
+75
View File
@@ -0,0 +1,75 @@
//go:build linux || darwin || freebsd
package main
/*
#include <stdint.h>
typedef void (*host_callback_t)(uintptr_t ctx,
const char* event, int eventLen,
const char* payload, int payloadLen);
static void invoke_host_callback(uintptr_t fn, uintptr_t ctx,
const char* ev, int evLen, const char* pl, int plLen) {
((host_callback_t)fn)(ctx, ev, evLen, pl, plLen);
}
*/
import "C"
import "unsafe"
var (
hostCallbackFn C.uintptr_t
hostCallbackCtx C.uintptr_t
)
func hostSendViaCallback(event string, payload []byte) {
fn := hostCallbackFn
if fn == 0 {
return
}
evBytes := []byte(event)
var evPtr, plPtr unsafe.Pointer
if len(evBytes) > 0 {
evPtr = unsafe.Pointer(&evBytes[0])
}
if len(payload) > 0 {
plPtr = unsafe.Pointer(&payload[0])
}
C.invoke_host_callback(fn, hostCallbackCtx,
(*C.char)(evPtr), C.int(len(evBytes)),
(*C.char)(plPtr), C.int(len(payload)))
}
//export PluginOnLoad
func PluginOnLoad(hostInfo *C.char, hostInfoLen C.int, cb C.uintptr_t, ctx C.uintptr_t) C.int {
hostCallbackFn = cb
hostCallbackCtx = ctx
setSend(hostSendViaCallback)
data := C.GoBytes(unsafe.Pointer(hostInfo), hostInfoLen)
if err := handleInit(data); err != nil {
return 1
}
return 0
}
//export PluginOnEvent
func PluginOnEvent(event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) C.int {
ev := C.GoStringN(event, eventLen)
var pl []byte
if payloadLen > 0 {
pl = C.GoBytes(unsafe.Pointer(payload), payloadLen)
}
if err := handleEvent(ev, pl); err != nil {
return 1
}
return 0
}
//export PluginOnUnload
func PluginOnUnload() {
handleUnload()
}
func main() {}
@@ -0,0 +1,67 @@
//go:build windows
package main
import "C"
import (
"syscall"
"unsafe"
)
var callbackPtr uintptr
func hostSendViaCallback(event string, payload []byte) {
cb := callbackPtr
if cb == 0 {
return
}
eventBytes := []byte(event)
var evPtr, plPtr uintptr
evLen := uintptr(len(eventBytes))
plLen := uintptr(len(payload))
if len(eventBytes) > 0 {
evPtr = uintptr(unsafe.Pointer(&eventBytes[0]))
}
if len(payload) > 0 {
plPtr = uintptr(unsafe.Pointer(&payload[0]))
}
syscall.SyscallN(cb, evPtr, evLen, plPtr, plLen)
}
//export PluginSetCallback
func PluginSetCallback(cb C.ulonglong) {
callbackPtr = uintptr(cb)
setSend(hostSendViaCallback)
}
//export PluginOnLoad
func PluginOnLoad(hostInfo *C.char, hostInfoLen C.int, cb C.ulonglong) C.int {
callbackPtr = uintptr(cb)
setSend(hostSendViaCallback)
data := C.GoBytes(unsafe.Pointer(hostInfo), hostInfoLen)
if err := handleInit(data); err != nil {
return 1
}
return 0
}
//export PluginOnEvent
func PluginOnEvent(event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) C.int {
ev := C.GoStringN(event, eventLen)
var pl []byte
if payloadLen > 0 {
pl = C.GoBytes(unsafe.Pointer(payload), payloadLen)
}
if err := handleEvent(ev, pl); err != nil {
return 1
}
return 0
}
//export PluginOnUnload
func PluginOnUnload() {
handleUnload()
}
func main() {}
+3
View File
@@ -0,0 +1,3 @@
module sample-plugin
go 1.24
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"encoding/json"
"log"
"sync"
)
type HostInfo struct {
ClientID string `json:"clientId"`
OS string `json:"os"`
Arch string `json:"arch"`
Version string `json:"version"`
}
var (
hostInfo HostInfo
sendFn func(event string, payload []byte)
mu sync.Mutex
)
func setSend(fn func(event string, payload []byte)) {
mu.Lock()
sendFn = fn
mu.Unlock()
}
func sendEvent(event string, payload interface{}) {
mu.Lock()
fn := sendFn
mu.Unlock()
if fn == nil {
return
}
data, err := json.Marshal(payload)
if err != nil {
log.Printf("[sample] marshal error: %v", err)
return
}
fn(event, data)
}
func handleInit(hostJSON []byte) error {
if err := json.Unmarshal(hostJSON, &hostInfo); err != nil {
return err
}
log.Printf("[sample] init: clientId=%s os=%s arch=%s", hostInfo.ClientID, hostInfo.OS, hostInfo.Arch)
sendEvent("ready", map[string]string{"message": "sample plugin ready"})
return nil
}
func handleEvent(event string, payload []byte) error {
switch event {
case "ui_message":
var msg struct {
Message string `json:"message"`
}
_ = json.Unmarshal(payload, &msg)
log.Printf("[sample] got ui_message: %s", msg.Message)
sendEvent("echo", map[string]string{"message": msg.Message})
case "ping":
sendEvent("pong", nil)
default:
log.Printf("[sample] unhandled event: %s", event)
}
return nil
}
func handleUnload() {
log.Printf("[sample] unloading")
}
+92
View File
@@ -0,0 +1,92 @@
/* Code generated by cmd/cgo; DO NOT EDIT. */
/* package sample-plugin */
#line 1 "cgo-builtin-export-prolog"
#include <stddef.h>
#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
extern size_t _GoStringLen(_GoString_ s);
extern const char *_GoStringPtr(_GoString_ s);
#endif
#endif
/* Start of preamble from import "C" comments. */
/* End of preamble from import "C" comments. */
/* Start of boilerplate cgo prologue. */
#line 1 "cgo-gcc-export-header-prolog"
#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef size_t GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
#ifdef _MSC_VER
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
#include <complex.h>
typedef _Fcomplex GoComplex64;
typedef _Dcomplex GoComplex128;
#else
#include <complex>
typedef std::complex<float> GoComplex64;
typedef std::complex<double> GoComplex128;
#endif
#else
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
#endif
/*
static assertion to make sure the file is being used on architecture
at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
#endif
/* End of boilerplate cgo prologue. */
#ifdef __cplusplus
extern "C" {
#endif
extern __declspec(dllexport) void PluginSetCallback(long long unsigned int cb);
extern __declspec(dllexport) int PluginOnLoad(char* hostInfo, int hostInfoLen, long long unsigned int cb);
extern __declspec(dllexport) int PluginOnEvent(char* event, int eventLen, char* payload, int payloadLen);
extern __declspec(dllexport) void PluginOnUnload(void);
#ifdef __cplusplus
}
#endif
+89
View File
@@ -0,0 +1,89 @@
:root {
color-scheme: dark;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Segoe UI", system-ui, sans-serif;
background: #0f1115;
color: #e8ecf1;
}
.container {
max-width: 820px;
margin: 40px auto;
padding: 0 20px;
}
.header h1 {
margin: 0 0 4px 0;
font-size: 28px;
}
.sub {
margin: 0 0 24px 0;
color: #9aa4b2;
}
.card {
background: #161a22;
border: 1px solid #222938;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.label {
color: #9aa4b2;
font-size: 14px;
}
.input {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #2a3346;
background: #0f131b;
color: #e8ecf1;
}
.actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.btn {
background: #3b82f6;
color: #fff;
border: none;
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
.btn:hover {
background: #2563eb;
}
.log {
min-height: 120px;
background: #0f131b;
border-radius: 8px;
border: 1px solid #2a3346;
padding: 12px;
white-space: pre-wrap;
}
+48
View File
@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sample Plugin</title>
<link rel="stylesheet" href="/plugins/sample/assets/sample.css" />
</head>
<body>
<main class="container">
<header class="header">
<h1>Sample Plugin</h1>
<p class="sub">Go Native + UI plugin</p>
</header>
<section class="card">
<div class="row">
<label class="label">Client ID</label>
<input id="client-id" class="input" placeholder="clientId" />
</div>
<div class="row">
<label class="label">Message</label>
<input id="message" class="input" placeholder="hello from UI" />
</div>
<div class="row">
<label class="label">Message Box</label>
<input id="msgbox" class="input" placeholder="Windows only" />
</div>
<div class="actions">
<button id="send-btn" class="btn">Send event to client</button>
<button id="ping-btn" class="btn">Ping Test x100</button>
<button id="msgbox-btn" class="btn">Show Message Box</button>
<button id="run-cmd-btn" class="btn">Run Command (example)</button>
</div>
</section>
<section class="card">
<h2>Log</h2>
<pre id="log" class="log"></pre>
</section>
</main>
<script src="/plugins/sample/assets/sample.js"></script>
</body>
</html>
+103
View File
@@ -0,0 +1,103 @@
const params = new URLSearchParams(window.location.search);
const clientIdInput = document.getElementById("client-id");
const messageInput = document.getElementById("message");
const msgBoxInput = document.getElementById("msgbox");
const logEl = document.getElementById("log");
const sendBtn = document.getElementById("send-btn");
const pingBtn = document.getElementById("ping-btn");
const msgBoxBtn = document.getElementById("msgbox-btn");
const runCmdBtn = document.getElementById("run-cmd-btn");
const pluginId = "sample";
const clientIdFromUrl = params.get("clientId") || "";
clientIdInput.value = clientIdFromUrl;
messageInput.value = "hello from sample UI";
msgBoxInput.value = "Hello from Overlord";
function log(line) {
const ts = new Date().toISOString();
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
}
async function sendEvent(event, payload) {
const clientId = clientIdInput.value.trim();
if (!clientId) {
log("Missing clientId");
return;
}
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event, payload }),
});
if (!res.ok) {
const text = await res.text();
log(`Send failed: ${res.status} ${text}`);
return;
}
log(`Sent event: ${event}`);
}
sendBtn.addEventListener("click", () => {
const message = messageInput.value.trim();
sendEvent("ui_message", { message });
});
pingBtn?.addEventListener("click", async () => {
const clientId = clientIdInput.value.trim();
if (!clientId) {
log("Missing clientId");
return;
}
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "ping_bulk", count: 100 }),
});
if (!res.ok) {
const msg = await res.text();
log(`Ping test failed: ${msg}`);
return;
}
log("Ping test x100 dispatched (single request)");
});
msgBoxBtn?.addEventListener("click", async () => {
const clientId = clientIdInput.value.trim();
if (!clientId) {
log("Missing clientId");
return;
}
const text = msgBoxInput.value.trim() || "Hello from Overlord";
const script = `$os = $env:OS; if ($os -ne 'Windows_NT') { exit } ; Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('${text.replace(/'/g, "''")}') | Out-Null`;
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "script_exec", script, scriptType: "powershell" }),
});
if (!res.ok) {
const msg = await res.text();
log(`Message box failed: ${msg}`);
return;
}
log("Message box command sent (Windows only)");
});
runCmdBtn?.addEventListener("click", async () => {
const clientId = clientIdInput.value.trim();
if (!clientId) {
log("Missing clientId");
return;
}
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "screenshot" }),
});
if (!res.ok) {
const msg = await res.text();
log(`Command failed: ${msg}`);
return;
}
log("Command sent: screenshot");
});
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "sample-rust"
version = "0.1.0"
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "sample-rust"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "z"
lto = true
strip = true
+276
View File
@@ -0,0 +1,276 @@
/*
* Sample Rust Plugin — fully unloadable, no GC runtime.
*
* Exports the standard Overlord plugin ABI:
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
* PluginGetRuntime (returns "rust" — tells the host this DLL can be freed)
*
* Build:
* cargo build --release
* Output: target/release/sample_rust.dll (Windows) or libsample_rust.so (Linux)
*
* Note: Uses C-style globals instead of std::sync::Mutex because the DLL is
* loaded via an in-memory PE loader, and Rust's std Mutex (SRWLOCK) may not
* initialize correctly without OS-level LoadLibrary. The Go host already
* serializes all calls through its own mutex, so this is safe.
*/
use std::os::raw::{c_char, c_int};
use std::slice;
use std::ptr;
// ---------------------------------------------------------------------------
// Host callback types — platform-specific calling conventions
// ---------------------------------------------------------------------------
#[cfg(windows)]
type HostCallback = unsafe extern "stdcall" fn(
event: *const u8,
event_len: usize,
payload: *const u8,
payload_len: usize,
);
#[cfg(not(windows))]
type HostCallback = unsafe extern "C" fn(
ctx: usize,
event: *const u8,
event_len: i32,
payload: *const u8,
payload_len: i32,
);
// ---------------------------------------------------------------------------
// Global state — C-style, no Mutex.
// The Go host serializes all calls via dllPlugin.mu / soPlugin.mu.
// ---------------------------------------------------------------------------
static mut G_CALLBACK: Option<HostCallback> = None;
#[cfg(not(windows))]
static mut G_CALLBACK_CTX: usize = 0;
static mut G_CLIENT_ID: [u8; 256] = [0u8; 256];
static mut G_CLIENT_ID_LEN: usize = 0;
static mut G_EVENT_COUNT: u64 = 0;
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
unsafe fn send_event(event: &[u8], payload: &[u8]) {
if let Some(cb) = G_CALLBACK {
#[cfg(windows)]
cb(
event.as_ptr(),
event.len(),
payload.as_ptr(),
payload.len(),
);
#[cfg(not(windows))]
cb(
G_CALLBACK_CTX,
event.as_ptr(),
event.len() as i32,
payload.as_ptr(),
payload.len() as i32,
);
}
}
/// Minimal JSON string extraction by key — zero allocations.
unsafe fn json_extract_into(json: &[u8], key: &[u8], out: &mut [u8]) -> usize {
// Search for "key":" or "key": "
let klen = key.len();
if json.len() < klen + 4 {
return 0;
}
let mut i = 0;
while i + klen + 3 < json.len() {
if json[i] == b'"'
&& json[i + 1..i + 1 + klen] == *key
&& json[i + 1 + klen] == b'"'
{
// Skip past "key"
let mut j = i + 1 + klen + 1; // past closing quote
// Skip : and optional space
while j < json.len() && (json[j] == b':' || json[j] == b' ') {
j += 1;
}
// Expect opening quote
if j < json.len() && json[j] == b'"' {
j += 1;
let start = j;
while j < json.len() && json[j] != b'"' {
j += 1;
}
let vlen = j - start;
let copy_len = if vlen < out.len() { vlen } else { out.len() - 1 };
ptr::copy_nonoverlapping(json[start..].as_ptr(), out.as_mut_ptr(), copy_len);
return copy_len;
}
}
i += 1;
}
0
}
unsafe fn bytes_to_slice<'a>(p: *const c_char, len: c_int) -> &'a [u8] {
if p.is_null() || len <= 0 {
return &[];
}
slice::from_raw_parts(p as *const u8, len as usize)
}
// ---------------------------------------------------------------------------
// Exported ABI
// ---------------------------------------------------------------------------
#[no_mangle]
pub extern "C" fn PluginGetRuntime() -> *const c_char {
b"rust\0".as_ptr() as *const c_char
}
#[cfg(windows)]
#[no_mangle]
pub unsafe extern "C" fn PluginSetCallback(cb: u64) {
G_CALLBACK = Some(std::mem::transmute(cb as usize));
}
#[cfg(windows)]
#[no_mangle]
pub unsafe extern "C" fn PluginOnLoad(
host_info: *const c_char,
host_info_len: c_int,
cb: u64,
) -> c_int {
G_CALLBACK = Some(std::mem::transmute(cb as usize));
G_EVENT_COUNT = 0;
let info = bytes_to_slice(host_info, host_info_len);
G_CLIENT_ID_LEN = json_extract_into(info, b"clientId", &mut G_CLIENT_ID);
// Build a small on-stack response and send
send_event(
b"ready",
br#"{"message":"sample-rust plugin ready"}"#,
);
0
}
#[cfg(not(windows))]
#[no_mangle]
pub unsafe extern "C" fn PluginOnLoad(
host_info: *const c_char,
host_info_len: c_int,
cb: usize,
ctx: usize,
) -> c_int {
G_CALLBACK = Some(std::mem::transmute(cb));
G_CALLBACK_CTX = ctx;
G_EVENT_COUNT = 0;
let info = bytes_to_slice(host_info, host_info_len);
G_CLIENT_ID_LEN = json_extract_into(info, b"clientId", &mut G_CLIENT_ID);
send_event(
b"ready",
br#"{"message":"sample-rust plugin ready"}"#,
);
0
}
#[no_mangle]
pub unsafe extern "C" fn PluginOnEvent(
event: *const c_char,
event_len: c_int,
payload: *const c_char,
payload_len: c_int,
) -> c_int {
let ev = bytes_to_slice(event, event_len);
let pl = bytes_to_slice(payload, payload_len);
G_EVENT_COUNT += 1;
if ev == b"ping" {
send_event(b"pong", &[]);
return 0;
}
if ev == b"ui_message" {
// Build echo response on the stack — limited to 512 bytes
let prefix = br#"{"message":"echo from Rust: "#;
let suffix = br#""}"#;
let max_pl = 450;
let pl_copy = if pl.len() < max_pl { pl.len() } else { max_pl };
let total = prefix.len() + pl_copy + suffix.len();
let mut buf = [0u8; 512];
if total <= buf.len() {
ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr(), prefix.len());
ptr::copy_nonoverlapping(
pl.as_ptr(),
buf.as_mut_ptr().add(prefix.len()),
pl_copy,
);
ptr::copy_nonoverlapping(
suffix.as_ptr(),
buf.as_mut_ptr().add(prefix.len() + pl_copy),
suffix.len(),
);
send_event(b"echo", &buf[..total]);
}
return 0;
}
if ev == b"stats" {
// Format event count into a stack buffer
let mut buf = [0u8; 128];
let prefix = br#"{"event_count":"#;
let suffix = b"}";
ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr(), prefix.len());
let mut num_buf = [0u8; 20];
let num_len = fmt_u64(G_EVENT_COUNT, &mut num_buf);
ptr::copy_nonoverlapping(
num_buf[20 - num_len..].as_ptr(),
buf.as_mut_ptr().add(prefix.len()),
num_len,
);
ptr::copy_nonoverlapping(
suffix.as_ptr(),
buf.as_mut_ptr().add(prefix.len() + num_len),
suffix.len(),
);
let total = prefix.len() + num_len + suffix.len();
send_event(b"stats_reply", &buf[..total]);
return 0;
}
0
}
#[no_mangle]
pub unsafe extern "C" fn PluginOnUnload() {
G_CALLBACK = None;
#[cfg(not(windows))]
{
G_CALLBACK_CTX = 0;
}
G_CLIENT_ID_LEN = 0;
G_EVENT_COUNT = 0;
}
// ---------------------------------------------------------------------------
// Tiny u64-to-decimal without std::fmt (avoids allocator)
// ---------------------------------------------------------------------------
fn fmt_u64(mut n: u64, buf: &mut [u8; 20]) -> usize {
if n == 0 {
buf[19] = b'0';
return 1;
}
let mut i = 20;
while n > 0 {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
}
20 - i
}
@@ -0,0 +1 @@
{"rustc_fingerprint":2568544898921049753,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: x86_64-pc-windows-msvc\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""},"12004014463585500860":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\this1\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\this1\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""}},"successes":{}}
+93
View File
@@ -0,0 +1,93 @@
:root {
color-scheme: dark;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Segoe UI", system-ui, sans-serif;
background: #0f1115;
color: #e8ecf1;
}
.container {
max-width: 820px;
margin: 40px auto;
padding: 0 20px;
}
.header h1 {
margin: 0 0 4px 0;
font-size: 28px;
}
.sub {
margin: 0 0 24px 0;
color: #9aa4b2;
}
.card {
background: #161a22;
border: 1px solid #222938;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.label {
color: #9aa4b2;
font-size: 14px;
}
.input {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #2a3346;
background: #0f131b;
color: #e8ecf1;
}
.actions {
display: flex;
gap: 10px;
margin-top: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: #2563eb;
color: #fff;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #1d4ed8;
}
.log {
background: #0b0e14;
border: 1px solid #1e2533;
border-radius: 8px;
padding: 14px;
font-size: 13px;
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
+42
View File
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sample Rust Plugin</title>
<link rel="stylesheet" href="/plugins/sample-rust/assets/sample-rust.css" />
</head>
<body>
<main class="container">
<header class="header">
<h1>Sample Rust Plugin</h1>
<p class="sub">Rust &mdash; fully unloadable, zero runtime leak</p>
</header>
<section class="card">
<div class="row">
<label class="label">Client ID</label>
<input id="client-id" class="input" placeholder="clientId" />
</div>
<div class="row">
<label class="label">Message</label>
<input id="message" class="input" placeholder="hello from UI" />
</div>
<div class="actions">
<button id="send-btn" class="btn">Send event to client</button>
<button id="ping-btn" class="btn">Ping</button>
<button id="stats-btn" class="btn">Get Stats</button>
</div>
</section>
<section class="card">
<h2>Log</h2>
<pre id="log" class="log"></pre>
</section>
</main>
<script src="/plugins/sample-rust/assets/sample-rust.js"></script>
</body>
</html>
+47
View File
@@ -0,0 +1,47 @@
const params = new URLSearchParams(window.location.search);
const clientIdInput = document.getElementById("client-id");
const messageInput = document.getElementById("message");
const logEl = document.getElementById("log");
const sendBtn = document.getElementById("send-btn");
const pingBtn = document.getElementById("ping-btn");
const statsBtn = document.getElementById("stats-btn");
const pluginId = "sample-rust";
clientIdInput.value = params.get("clientId") || "";
messageInput.value = "hello from sample-rust UI";
function log(line) {
const ts = new Date().toISOString();
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
}
async function sendEvent(event, payload) {
const clientId = clientIdInput.value.trim();
if (!clientId) { log("Missing clientId"); return; }
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event, payload }),
});
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
log(`Sent event: ${event}`);
}
sendBtn.addEventListener("click", () => {
sendEvent("ui_message", { message: messageInput.value.trim() });
});
pingBtn.addEventListener("click", () => {
sendEvent("ping", {});
});
statsBtn.addEventListener("click", () => {
sendEvent("stats", {});
});
if (typeof EventSource !== "undefined") {
const clientId = params.get("clientId");
if (clientId) {
log("Ready — Rust plugin (fully unloadable)");
}
}
Binary file not shown.