Remove WASM plugin support

This commit is contained in:
KingKDot
2026-08-18 21:41:28 -05:00
parent 4b8de29d32
commit 7800c24c65
42 changed files with 125 additions and 1773 deletions
+5 -11
View File
@@ -1,6 +1,6 @@
# Overlord Plugins
This is the short entry point for plugin authors. The detailed docs are split by topic so server-only build extensions, WASM agent plugins, legacy native plugins, signing, and examples can evolve independently.
This is the short entry point for plugin authors. The detailed docs are split by topic so server-only build extensions, native agent plugins, signing, and examples can evolve independently.
## Quick Start
@@ -12,7 +12,7 @@ A Plugin 2.0 bundle is a zip with a root `config.json`, browser assets, and opti
<pluginId>.html
<pluginId>.css
<pluginId>.js optional when src/ui.ts exists
<pluginId>.wasm optional agent-side WASM module
<pluginId>-<os>-<arch>.* optional native agent library
server.js optional server-side runtime
src/ optional TypeScript source
```
@@ -25,19 +25,16 @@ Upload the zip from the Plugins page, or place it under `Overlord-Server/plugins
|-------|-----------|
| Bundle layout, `config.json`, TypeScript UI/server entries | [docs/bundle-format.md](docs/bundle-format.md) |
| Server-only plugins, plugin data, RPC/SSE, build hooks, build settings/buttons | [docs/server-side-plugins.md](docs/server-side-plugins.md) |
| WASM Plugin 2.0 agent modules and permissioned file bridges | [docs/wasm-plugins.md](docs/wasm-plugins.md) |
| Install/upload flow, Plugin Manager behavior, auto-load rules | [docs/install-and-manage.md](docs/install-and-manage.md) |
| Sample plugin directory guide | [docs/samples.md](docs/samples.md) |
| Legacy native plugins: OS APIs, native ABI, build scripts, Linux static-agent shim | [docs/legacy-native-plugins.md](docs/legacy-native-plugins.md) |
| Native plugins: OS APIs, native ABI, build scripts, Linux static-agent shim | [docs/legacy-native-plugins.md](docs/legacy-native-plugins.md) |
| Plugin signing and trusted keys | [docs/signing.md](docs/signing.md) |
## Runtime Choices
Use `runtime: "server"` for server-side extensions. These can add server UI, store files, expose RPCs, subscribe to plugin events, and hook the build pipeline. Server-only plugins are shown as server extensions in the Plugin Manager, are not sent to clients, and do not use auto-load.
Use `runtime: "wasm"` for Plugin 2.0 agent code. WASM plugins are sandboxed on the agent and access host features only through declared needs and Overlord bridge APIs.
Use legacy native plugins when you need direct OS APIs, native libraries, platform-specific behavior, or other privileged agent-side functionality that the WASM sandbox does not expose. Native plugins run with the agent's privileges and require one binary per OS/architecture.
Use `runtime: "native"` for agent-side plugins that need direct OS APIs, native libraries, or platform-specific behavior. Native plugins run with the agent's privileges and require one binary per OS/architecture.
## Current Samples
@@ -45,9 +42,6 @@ Use legacy native plugins when you need direct OS APIs, native libraries, platfo
|-----------|---------|
| `sample-build-hooks` | Server-only build plugin with its own Build page button and artifact replacement example. |
| `sample-ts-fullstack` | TypeScript UI plus TypeScript server runtime. |
| `sample-wasm` | C/WASI Plugin 2.0 WASM example. |
| `sample-wasm-hostinfo` | TinyGo WASM HostInfo example. |
| `sample-wasm-platform-note` | Rust WASM platform-aware example. |
| `sample-c`, `sample-cpp`, `sample-go`, `sample-rust` | Legacy native v1 examples. |
| `sample-c`, `sample-cpp`, `sample-go`, `sample-rust` | Native agent plugin examples. |
Start with [docs/server-side-plugins.md](docs/server-side-plugins.md) if you are building uploaders, custom build buttons, artifact post-processing, or internal release hooks.
+1 -2
View File
@@ -6,8 +6,7 @@ These docs are split from `plugins/PLUGINS.md` so each plugin area has a smaller
|------|--------|
| [bundle-format.md](bundle-format.md) | Bundle layout, manifest fields, nav entries, TypeScript entrypoints. |
| [server-side-plugins.md](server-side-plugins.md) | Server-only plugin runtime, RPC/SSE, persistent data, build hooks, build UI settings/actions. |
| [wasm-plugins.md](wasm-plugins.md) | Plugin 2.0 WASM runtime and agent-side host bridges. |
| [install-and-manage.md](install-and-manage.md) | Uploading, opening, enabling, auto-load, Plugin Manager behavior. |
| [samples.md](samples.md) | Included sample plugins and what each demonstrates. |
| [legacy-native-plugins.md](legacy-native-plugins.md) | Legacy native plugins, direct OS/API use, native ABI, build scripts, static Linux shim. |
| [legacy-native-plugins.md](legacy-native-plugins.md) | Native plugins, direct OS/API use, native ABI, build scripts, static Linux shim. |
| [signing.md](signing.md) | Ed25519 plugin signing and trusted key management. |
+3 -5
View File
@@ -10,7 +10,7 @@ Plugin bundles are zip files named after the plugin ID. `config.json` must be at
<pluginId>.html
<pluginId>.css
<pluginId>.js optional when src/ui.ts exists
<pluginId>.wasm optional agent-side WASM module
<pluginId>-<os>-<arch>.* optional native agent library
server.js optional server runtime
src/
ui.ts compiled to assets/<pluginId>.js
@@ -25,8 +25,7 @@ After extraction the server stores assets under `Overlord-Server/plugins/<plugin
```json
{
"apiVersion": 2,
"runtime": "wasm",
"wasm": "sample-wasm.wasm"
"runtime": "server"
}
```
@@ -35,8 +34,7 @@ Use:
| Runtime | Use case |
|---------|----------|
| `server` | Server-only extension, build plugin, dashboard plugin, RPC-backed plugin. |
| `wasm` | Sandboxed agent-side Plugin 2.0 module. |
| `native` or omitted v1 fields | Legacy shared-library agent plugin. |
| `native` or omitted v1 fields | Shared-library agent plugin. |
Server-only plugins are not sent to clients and do not use auto-load.
+3 -5
View File
@@ -36,8 +36,7 @@ Navbar plugins appear in the Plugin Apps group when enabled and configured with:
| Plugin kind | Manager behavior |
|-------------|------------------|
| Server-only (`runtime: "server"`) | Shown as a server extension. Auto-load controls are hidden. It is not sent to clients. |
| WASM (`runtime: "wasm"`) | Shows declared needs and approval state. Can be loaded on compatible clients after needs approval. |
| Legacy native | Shows native binary/runtime compatibility. Requires matching OS/architecture binary. |
| Native (`runtime: "native"`) | Shows native binary/runtime compatibility. Requires a matching OS/architecture binary. |
## Auto-Load
@@ -50,16 +49,15 @@ Auto-load notes:
| Rule | Detail |
|------|--------|
| Enabled flag | Disabled plugins are never auto-loaded. |
| WASM needs | WASM plugins with unapproved needs are blocked. |
| Persistence | Auto-load state is saved in `.plugin-state.json`. |
| Platform matching | WASM is universal; legacy native requires a matching binary. |
| Platform matching | Native plugins require a matching binary. |
| Bundle default | `autoLoadByDefault: true` enables auto-load only after the plugin passes trusted enablement or an administrator explicitly confirms enablement. |
## Useful APIs
| Endpoint | Purpose |
|----------|---------|
| `GET /api/plugins` | List installed plugins, runtime, needs, enabled state, server state, and auto-load state. |
| `GET /api/plugins` | List installed plugins, runtime, enabled state, server state, and auto-load state. |
| `POST /api/plugins/<id>/autoload` | Configure auto-load for client-side plugins. Server-only plugins reject this. |
| `POST /api/plugins/<id>/rpc` | Call a server-side plugin RPC method. |
| `GET /api/plugins/<id>/stream` | Subscribe to server-side plugin broadcasts. |
+7 -9
View File
@@ -1,12 +1,12 @@
# Legacy Native Plugins
# Native Plugins
Legacy native v1 plugins run as shared libraries inside the agent. They are still supported and still useful when you need capabilities that the WASM runtime intentionally does not expose.
Native plugins run as shared libraries inside the agent.
Use legacy native plugins when you need direct OS APIs, native libraries, custom process/thread behavior, platform-specific integrations, or code that cannot reasonably be compiled to the Plugin 2.0 WASM ABI.
Use native plugins when you need direct OS APIs, native libraries, custom process/thread behavior, or platform-specific integrations.
Native plugins run with the same privileges as the agent. They are not sandboxed.
## What Legacy Plugins Can Do
## What Native Plugins Can Do
| Capability | Notes |
|------------|-------|
@@ -16,11 +16,9 @@ Native plugins run with the same privileges as the agent. They are not sandboxed
| Emit plugin events | Send events back to the UI/server through the host callback. |
| Receive UI/server commands | Handle `PluginOnEvent` calls from plugin UI actions or server-side routing. |
WASM Plugin 2.0 is preferred for sandboxed, portable agent modules. Legacy native plugins are the escape hatch for privileged native behavior.
## Bundle Layout
A legacy bundle is a zip with web assets plus one or more platform binaries:
A native bundle is a zip with web assets plus one or more platform binaries:
```text
<pluginId>.zip
@@ -253,7 +251,7 @@ Most plugin authors can ignore this, but it is useful when debugging Linux nativ
## Build Scripts
Legacy samples include `build.bat` and `build.sh`. They compile native binaries and zip them with web assets.
Native samples include `build.bat` and `build.sh`. They compile native binaries and zip them with web assets.
Default usage:
@@ -316,4 +314,4 @@ The native plugin can reply by invoking the host callback. Those callback events
## Security Notes
Legacy native plugins are trusted executable code. They can call OS APIs, read/write files available to the agent process, spawn subprocesses, open sockets, and interact with native libraries. Use plugin signing, per-user plugin access, and code review before deploying third-party native plugins.
Native plugins are trusted executable code. They can call OS APIs, read/write files available to the agent process, spawn subprocesses, open sockets, and interact with native libraries. Use plugin signing, per-user plugin access, and code review before deploying third-party native plugins.
+4 -7
View File
@@ -6,12 +6,9 @@ The `plugins/` directory includes small examples that exercise each plugin style
|-----------|--------------|
| `sample-build-hooks` | Server-only build plugin, custom Build page action, build settings, and replacing the produced artifact with a `.txt` file. |
| `sample-ts-fullstack` | TypeScript UI and TypeScript server runtime with shared local modules. |
| `sample-wasm` | Plugin 2.0 WASM module built from C/WASI. |
| `sample-wasm-hostinfo` | TinyGo WASM plugin that queries HostInfo. |
| `sample-wasm-platform-note` | Rust WASM plugin that branches by HostInfo/platform. |
| `sample-c` | Legacy native C plugin for the smallest unloadable ABI surface. |
| `sample-cpp` | Legacy native C++ plugin using exported C ABI functions. |
| `sample-go` | Legacy native Go plugin. Go plugins are intentionally not unloaded because the Go runtime cannot be safely unloaded from shared libraries. |
| `sample-rust` | Legacy native Rust plugin with C ABI exports and unload-friendly runtime handling. |
| `sample-c` | Native C plugin for the smallest unloadable ABI surface. |
| `sample-cpp` | Native C++ plugin using exported C ABI functions. |
| `sample-go` | Native Go plugin. Go plugins are intentionally not unloaded because the Go runtime cannot be safely unloaded from shared libraries. |
| `sample-rust` | Native Rust plugin with C ABI exports and unload-friendly runtime handling. |
Use `sample-build-hooks` as the starting point for uploaders, artifact publishers, or custom release buttons.
-76
View File
@@ -1,76 +0,0 @@
# WASM Plugins
Plugin 2.0 WASM plugins run inside the agent through a sandboxed runtime. They cannot access files or OS APIs directly; they declare needs in `config.json`, and the agent exposes approved host bridge operations.
## Config
```json
{
"name": "My WASM Plugin",
"version": "1.0.0",
"apiVersion": 2,
"runtime": "wasm",
"wasm": "my-plugin.wasm",
"needs": {
"files": [
{
"bucket": "pluginData",
"access": ["read", "write", "list", "mkdir"],
"reason": "Store plugin state"
}
]
}
}
```
Supported file buckets are `home`, `desktop`, `documents`, `downloads`, `temp`, `appData`, `pluginData`, and `fullDisk`.
Supported operations are `read`, `write`, `list`, `delete`, and `mkdir`. Approval is tracked by a stable hash of normalized needs; changing needs requires approval again.
## Required Exports
| Export | Signature |
|--------|-----------|
| `overlord_alloc` | `ptr(uint32 size)` |
| `overlord_free` | `void(ptr, uint32 size)` |
| `overlord_on_load` | `int32(ptr hostInfo, uint32 hostInfoLen)` |
| `overlord_on_event` | `int32(ptr event, uint32 eventLen, ptr payload, uint32 payloadLen)` |
| `overlord_on_unload` | `void()` |
## Host Imports
Imports are available from both `env` and `overlord`.
| Import | Behavior |
|--------|----------|
| `overlord_emit(event, eventLen, payload, payloadLen)` | Sends an event through the plugin event channel. |
| `overlord_host_info(out, outLen)` | Writes HostInfo JSON into `out`. |
| `overlord_fs_stat(bucket, bucketLen, path, pathLen, out, outLen)` | Writes JSON file metadata into `out`. |
| `overlord_fs_list(bucket, bucketLen, path, pathLen, out, outLen)` | Writes a JSON directory listing into `out`. |
| `overlord_fs_read(bucket, bucketLen, path, pathLen, out, outLen)` | Writes file bytes into `out`, max 32 MB. |
| `overlord_fs_write(bucket, bucketLen, path, pathLen, data, dataLen)` | Writes bytes to a bucket-relative path. |
| `overlord_fs_delete(bucket, bucketLen, path, pathLen)` | Deletes a bucket-relative file or directory. |
| `overlord_fs_mkdir(bucket, bucketLen, path, pathLen)` | Creates a bucket-relative directory. |
Bridge functions return non-negative byte counts or `0` on success. Negative return codes mean denied, invalid path/memory, not found, output buffer too small, file too large, or I/O failure.
## HostInfo
WASM plugins are universal across OS/architecture. To branch by platform, read HostInfo from `overlord_on_load(hostInfo, hostInfoLen)` or `overlord_host_info(out, outLen)`.
```json
{
"clientId": "abc123",
"os": "windows",
"arch": "amd64",
"version": "1.0.0"
}
```
## Samples
| Directory | Toolchain |
|-----------|-----------|
| `plugins/sample-wasm` | C/WASI |
| `plugins/sample-wasm-hostinfo` | TinyGo |
| `plugins/sample-wasm-platform-note` | Rust |
+1 -25
View File
@@ -4,8 +4,6 @@ setlocal enabledelayedexpansion
set "PLUGIN_DIR=%~dp0"
set "PLUGIN_NAME=sample-ts-fullstack"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "WASM_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.wasm"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%PLUGIN_DIR%\config.json" (
@@ -13,32 +11,10 @@ if not exist "%PLUGIN_DIR%\config.json" (
exit /b 1
)
set "WASM_SOURCE="
if exist "%NATIVE_DIR%" (
for %%S in ("%NATIVE_DIR%\*.c") do (
if not defined WASM_SOURCE set "WASM_SOURCE=%%~fS"
)
)
if defined WASM_SOURCE (
where clang >nul 2>&1
if errorlevel 1 (
echo [error] clang was not found. Install a WASI-capable clang or provide %WASM_OUT% yourself.
exit /b 1
)
echo [build] clang --target=wasm32-wasi -O2 "!WASM_SOURCE!" ^> "%WASM_OUT%"
clang --target=wasm32-wasi -O2 -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=overlord_alloc -Wl,--export=overlord_free -Wl,--export=overlord_on_load -Wl,--export=overlord_on_event -Wl,--export=overlord_on_unload -o "%WASM_OUT%" "!WASM_SOURCE!"
if errorlevel 1 (
echo [error] WASM build failed
exit /b 1
)
)
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
pushd "%PLUGIN_DIR%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$zip = '%ZIP_OUT%'; $items = @(); foreach ($file in @('config.json', '%PLUGIN_NAME%.html', '%PLUGIN_NAME%.css', '%PLUGIN_NAME%.js', '%PLUGIN_NAME%.wasm', 'server.js')) { if (Test-Path -LiteralPath $file -PathType Leaf) { $items += $file } }; foreach ($dir in @('src', 'assets')) { if (Test-Path -LiteralPath $dir -PathType Container) { $items += $dir } }; if ($items.Count -eq 0) { throw 'No plugin files found to package' }; Compress-Archive -Path $items -DestinationPath $zip -Force"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$zip = '%ZIP_OUT%'; $items = @(); foreach ($file in @('config.json', '%PLUGIN_NAME%.html', '%PLUGIN_NAME%.css', '%PLUGIN_NAME%.js', 'server.js')) { if (Test-Path -LiteralPath $file -PathType Leaf) { $items += $file } }; foreach ($dir in @('src', 'assets')) { if (Test-Path -LiteralPath $dir -PathType Container) { $items += $dir } }; if ($items.Count -eq 0) { throw 'No plugin files found to package' }; Compress-Archive -Path $items -DestinationPath $zip -Force"
set "ZIP_STATUS=%ERRORLEVEL%"
popd
if not "%ZIP_STATUS%"=="0" exit /b %ZIP_STATUS%
-5
View File
@@ -1,5 +0,0 @@
# Sample WASM HostInfo Plugin
This Plugin 2.0 sample is written in Go for TinyGo. It demonstrates how a universal WASM module can read HostInfo and emit it back to the UI.
On Windows, install TinyGo 0.41.0 or newer, then run this sample's local `build.bat`. TinyGo 0.41.0 added Go 1.26 support. The builder runs `tinygo build -target=wasi`, writes `sample-wasm-hostinfo.wasm`, and creates `sample-wasm-hostinfo.zip`.
-55
View File
@@ -1,55 +0,0 @@
@echo off
setlocal enabledelayedexpansion
set "PLUGIN_DIR=%~dp0"
set "PLUGIN_NAME=sample-wasm-hostinfo"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "WASM_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.wasm"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%PLUGIN_DIR%\config.json" (
echo [error] config.json not found in %PLUGIN_DIR%
exit /b 1
)
set "WASM_SOURCE=%NATIVE_DIR%\main.go"
if exist "%WASM_SOURCE%" (
where tinygo >nul 2>&1
if errorlevel 1 (
echo [error] tinygo was not found. Install TinyGo or provide %WASM_OUT% yourself.
exit /b 1
)
echo [build] tinygo build -target=wasi -opt=2 -no-debug -o "%WASM_OUT%" "%WASM_SOURCE%"
tinygo build -target=wasi -opt=2 -no-debug -o "%WASM_OUT%" "%WASM_SOURCE%"
if errorlevel 1 (
echo [error] WASM build failed
exit /b 1
)
)
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
pushd "%PLUGIN_DIR%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$zip = '%ZIP_OUT%'; $items = @(); foreach ($file in @('config.json', '%PLUGIN_NAME%.html', '%PLUGIN_NAME%.css', '%PLUGIN_NAME%.js', '%PLUGIN_NAME%.wasm', 'server.js')) { if (Test-Path -LiteralPath $file -PathType Leaf) { $items += $file } }; foreach ($dir in @('src', 'assets')) { if (Test-Path -LiteralPath $dir -PathType Container) { $items += $dir } }; if ($items.Count -eq 0) { throw 'No plugin files found to package' }; Compress-Archive -Path $items -DestinationPath $zip -Force"
set "ZIP_STATUS=%ERRORLEVEL%"
popd
if not "%ZIP_STATUS%"=="0" exit /b %ZIP_STATUS%
if errorlevel 1 (
echo [error] zip failed
exit /b 1
)
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 "%~dp0..\..\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%
@@ -1,93 +0,0 @@
package main
import "unsafe"
//go:wasmimport env overlord_emit
func overlordEmit(eventPtr, eventLen, payloadPtr, payloadLen uint32) int32
//go:wasmimport env overlord_host_info
func overlordHostInfo(outPtr, outLen uint32) int32
var heap = make([]byte, 64*1024)
var heapOffset uint32 = 8
var hostInfo [1024]byte
var hostInfoLen uint32
//go:export overlord_alloc
func overlordAlloc(size uint32) uint32 {
if size == 0 {
size = 1
}
ptr := (heapOffset + 7) &^ 7
next := ptr + size
if next >= uint32(len(heap)) {
return 0
}
heapOffset = next
return uint32(uintptr(unsafe.Pointer(&heap[ptr])))
}
//go:export overlord_free
func overlordFree(ptr, size uint32) {
_, _ = ptr, size
}
//go:export overlord_on_load
func overlordOnLoad(hostPtr, hostLen uint32) int32 {
n := hostLen
if n > uint32(len(hostInfo)) {
n = uint32(len(hostInfo))
}
copy(hostInfo[:n], bytesAt(hostPtr, n))
hostInfoLen = n
emit("ready", hostInfo[:hostInfoLen])
return 0
}
//go:export overlord_on_event
func overlordOnEvent(eventPtr, eventLen, payloadPtr, payloadLen uint32) int32 {
_, _ = payloadPtr, payloadLen
event := string(bytesAt(eventPtr, eventLen))
if event != "query_host" {
return 0
}
var buf [1024]byte
n := overlordHostInfo(uint32(uintptr(unsafe.Pointer(&buf[0]))), uint32(len(buf)))
if n < 0 {
emitString("host_info", `{"error":"host info buffer too small"}`)
return 0
}
emit("host_info", buf[:uint32(n)])
return 0
}
//go:export overlord_on_unload
func overlordOnUnload() {}
func emitString(event string, payload string) {
emit(event, []byte(payload))
}
func emit(event string, payload []byte) {
eventBytes := []byte(event)
var eventPtr uint32
var payloadPtr uint32
if len(eventBytes) > 0 {
eventPtr = uint32(uintptr(unsafe.Pointer(&eventBytes[0])))
}
if len(payload) > 0 {
payloadPtr = uint32(uintptr(unsafe.Pointer(&payload[0])))
}
overlordEmit(eventPtr, uint32(len(eventBytes)), payloadPtr, uint32(len(payload)))
}
func bytesAt(ptr, length uint32) []byte {
if length == 0 {
return nil
}
return unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), length)
}
func main() {}
@@ -1,22 +0,0 @@
#sample-wasm-hostinfo-output {
min-height: 8rem;
}
.sample-wasm-hostinfo-field {
border: 1px solid rgba(8, 145, 178, 0.45);
background: rgba(8, 47, 73, 0.28);
padding: 0.75rem;
}
.sample-wasm-hostinfo-field span {
display: block;
color: rgb(148, 163, 184);
font-size: 0.75rem;
}
.sample-wasm-hostinfo-field strong {
display: block;
color: rgb(224, 242, 254);
font-size: 0.95rem;
overflow-wrap: anywhere;
}
@@ -1,12 +0,0 @@
<div class="space-y-3">
<h2 class="text-lg font-semibold text-slate-100">Sample WASM Host Info</h2>
<button id="sample-wasm-hostinfo-run" class="px-3 py-2 rounded-lg border border-cyan-700 bg-cyan-950/40 text-cyan-100">Query host info</button>
<div id="sample-wasm-hostinfo-status" class="text-xs text-slate-400">Waiting for host info</div>
<div class="grid gap-2 sm:grid-cols-2">
<div class="sample-wasm-hostinfo-field"><span>Client</span><strong id="sample-wasm-hostinfo-client">-</strong></div>
<div class="sample-wasm-hostinfo-field"><span>Version</span><strong id="sample-wasm-hostinfo-version">-</strong></div>
<div class="sample-wasm-hostinfo-field"><span>OS</span><strong id="sample-wasm-hostinfo-os">-</strong></div>
<div class="sample-wasm-hostinfo-field"><span>Arch</span><strong id="sample-wasm-hostinfo-arch">-</strong></div>
</div>
<pre id="sample-wasm-hostinfo-output" class="text-xs text-slate-300 whitespace-pre-wrap"></pre>
</div>
@@ -1,93 +0,0 @@
(() => {
const out = document.getElementById("sample-wasm-hostinfo-output");
const btn = document.getElementById("sample-wasm-hostinfo-run");
const status = document.getElementById("sample-wasm-hostinfo-status");
const fields = {
clientId: document.getElementById("sample-wasm-hostinfo-client"),
version: document.getElementById("sample-wasm-hostinfo-version"),
os: document.getElementById("sample-wasm-hostinfo-os"),
arch: document.getElementById("sample-wasm-hostinfo-arch"),
};
const clientId = new URLSearchParams(window.location.search).get("clientId");
const pluginId = "sample-wasm-hostinfo";
let polling = false;
function log(message) {
if (out) out.textContent = `${message}\n${out.textContent || ""}`;
}
function setStatus(message) {
if (status) status.textContent = message;
}
function parsePayload(payload) {
if (payload == null) return {};
if (typeof payload === "string") {
try { return JSON.parse(payload); } catch (_) { return { raw: payload }; }
}
return payload;
}
function renderHostInfo(payload) {
const info = parsePayload(payload);
if (info.error) {
setStatus(info.error);
log(JSON.stringify(info, null, 2));
return;
}
if (fields.clientId) fields.clientId.textContent = info.clientId || "-";
if (fields.version) fields.version.textContent = info.version || "-";
if (fields.os) fields.os.textContent = info.os || "-";
if (fields.arch) fields.arch.textContent = info.arch || "-";
setStatus("Host info received");
log(JSON.stringify(info, null, 2));
}
function handlePluginEvent(event, payload) {
if (event === "ready" || event === "host_info") renderHostInfo(payload);
}
window.addEventListener("message", (e) => {
if (!e.data || e.data.type !== "plugin_event") return;
if (e.data.pluginId && e.data.pluginId !== pluginId) return;
handlePluginEvent(e.data.event, e.data.payload);
});
async function pollEvents() {
if (!clientId || polling) return;
polling = true;
try {
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/events`, {
headers: { Accept: "application/json" },
});
if (res.ok) {
const data = await res.json();
if (data.events) data.events.forEach((e) => handlePluginEvent(e.event, e.payload));
}
} catch (_) {
// Polling is best-effort; the next interval can recover.
} finally {
polling = false;
}
}
btn?.addEventListener("click", async () => {
if (!clientId) {
setStatus("Open this plugin from a client row so clientId is present.");
return;
}
setStatus("Requesting host info");
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/sample-wasm-hostinfo/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event: "query_host", payload: {} }),
});
if (!res.ok) setStatus(`Send failed: ${res.status}`);
setTimeout(pollEvents, 250);
});
if (clientId) {
setInterval(pollEvents, 1500);
setTimeout(pollEvents, 300);
}
})();
@@ -1,13 +0,0 @@
# Sample WASM Platform Note Plugin
This Plugin 2.0 sample is written in Rust. It reads HostInfo, branches on `os` and `arch`, writes a small note into the approved `pluginData` bucket, then lists the user's Desktop and attempts to read a small sample from one regular file to prove the file bridge is working.
The manifest requests `pluginData` write/mkdir access and `desktop` list/read access, so changing this sample's permissions will require approving the new needs hash before load.
On Windows, install the Rust WASI target first:
```bat
rustup target add wasm32-wasip1
```
Then run this sample's local `build.bat`. The builder runs Cargo, writes `sample-wasm-platform-note.wasm`, and creates `sample-wasm-platform-note.zip`.
@@ -1,76 +0,0 @@
@echo off
setlocal enabledelayedexpansion
set "PLUGIN_DIR=%~dp0"
set "PLUGIN_NAME=sample-wasm-platform-note"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "WASM_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.wasm"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%PLUGIN_DIR%\config.json" (
echo [error] config.json not found in %PLUGIN_DIR%
exit /b 1
)
set "RUST_MANIFEST=%NATIVE_DIR%\Cargo.toml"
if exist "%RUST_MANIFEST%" (
where cargo >nul 2>&1
if errorlevel 1 (
echo [error] cargo was not found. Install Rust or provide %WASM_OUT% yourself.
exit /b 1
)
where rustup >nul 2>&1
if errorlevel 1 (
echo [error] rustup was not found. Install the Rust target manually: rustup target add wasm32-wasip1
exit /b 1
)
rustup target list --installed | findstr /x "wasm32-wasip1" >nul 2>&1
if errorlevel 1 (
echo [setup] Installing Rust target wasm32-wasip1
rustup target add wasm32-wasip1
if errorlevel 1 (
echo [error] Could not install wasm32-wasip1. Run this manually: rustup target add wasm32-wasip1
exit /b 1
)
)
echo [build] cargo build --manifest-path "%RUST_MANIFEST%" --release --target wasm32-wasip1
cargo build --manifest-path "%RUST_MANIFEST%" --release --target wasm32-wasip1
if errorlevel 1 (
echo [error] WASM build failed
exit /b 1
)
copy /Y "%NATIVE_DIR%\target\wasm32-wasip1\release\sample_wasm_platform_note.wasm" "%WASM_OUT%" >nul
if errorlevel 1 (
echo [error] could not copy built WASM to %WASM_OUT%
exit /b 1
)
)
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
pushd "%PLUGIN_DIR%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$zip = '%ZIP_OUT%'; $items = @(); foreach ($file in @('config.json', '%PLUGIN_NAME%.html', '%PLUGIN_NAME%.css', '%PLUGIN_NAME%.js', '%PLUGIN_NAME%.wasm', 'server.js')) { if (Test-Path -LiteralPath $file -PathType Leaf) { $items += $file } }; foreach ($dir in @('src', 'assets')) { if (Test-Path -LiteralPath $dir -PathType Container) { $items += $dir } }; if ($items.Count -eq 0) { throw 'No plugin files found to package' }; Compress-Archive -Path $items -DestinationPath $zip -Force"
set "ZIP_STATUS=%ERRORLEVEL%"
popd
if not "%ZIP_STATUS%"=="0" exit /b %ZIP_STATUS%
if errorlevel 1 (
echo [error] zip failed
exit /b 1
)
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 "%~dp0..\..\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%
-7
View File
@@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "sample_wasm_platform_note"
version = "0.1.0"
@@ -1,8 +0,0 @@
[package]
name = "sample_wasm_platform_note"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
path = "src/lib.rs"
@@ -1,301 +0,0 @@
use std::alloc::{alloc, dealloc, Layout};
use std::ptr;
use std::slice;
use std::str;
extern "C" {
fn overlord_emit(event: *const u8, event_len: i32, payload: *const u8, payload_len: i32) -> i32;
fn overlord_host_info(out: *mut u8, out_len: i32) -> i32;
fn overlord_fs_list(
bucket: *const u8,
bucket_len: i32,
path: *const u8,
path_len: i32,
out: *mut u8,
out_len: i32,
) -> i32;
fn overlord_fs_read(
bucket: *const u8,
bucket_len: i32,
path: *const u8,
path_len: i32,
out: *mut u8,
out_len: i32,
) -> i32;
fn overlord_fs_mkdir(bucket: *const u8, bucket_len: i32, path: *const u8, path_len: i32) -> i32;
fn overlord_fs_write(
bucket: *const u8,
bucket_len: i32,
path: *const u8,
path_len: i32,
data: *const u8,
data_len: i32,
) -> i32;
}
#[no_mangle]
pub extern "C" fn overlord_alloc(size: u32) -> *mut u8 {
let size = size.max(1) as usize;
let layout = Layout::from_size_align(size, 8).unwrap();
unsafe { alloc(layout) }
}
#[no_mangle]
pub extern "C" fn overlord_free(ptr: *mut u8, size: u32) {
if ptr.is_null() {
return;
}
let layout = Layout::from_size_align((size.max(1)) as usize, 8).unwrap();
unsafe { dealloc(ptr, layout) };
}
#[no_mangle]
pub extern "C" fn overlord_on_load(_host: *const u8, _host_len: u32) -> i32 {
emit("ready", br#"{"sample":"platform-note"}"#);
0
}
#[no_mangle]
pub extern "C" fn overlord_on_event(
event: *const u8,
event_len: u32,
_payload: *const u8,
_payload_len: u32,
) -> i32 {
let event = unsafe { slice::from_raw_parts(event, event_len as usize) };
if event != b"write_note" {
return 0;
}
let mut host = [0u8; 1024];
let host_len = unsafe { overlord_host_info(host.as_mut_ptr(), host.len() as i32) };
let host = if host_len > 0 {
str::from_utf8(&host[..host_len as usize]).unwrap_or("")
} else {
""
};
let os = if host.contains(r#""os":"windows""#) {
"windows"
} else if host.contains(r#""os":"darwin""#) {
"darwin"
} else if host.contains(r#""os":"linux""#) {
"linux"
} else {
"unknown"
};
let arch = if host.contains(r#""arch":"arm64""#) {
"arm64"
} else if host.contains(r#""arch":"amd64""#) {
"amd64"
} else if host.contains(r#""arch":"386""#) {
"386"
} else {
"unknown"
};
let bucket = "pluginData";
let dir = "platform";
let file = format!("platform/{os}-{arch}.txt");
let note = format!("single WASM plugin running on os={os} arch={arch}\n");
let mkdir_result = unsafe {
overlord_fs_mkdir(
bucket.as_ptr(),
bucket.len() as i32,
dir.as_ptr(),
dir.len() as i32,
)
};
let write_result = unsafe {
overlord_fs_write(
bucket.as_ptr(),
bucket.len() as i32,
file.as_ptr(),
file.len() as i32,
note.as_ptr(),
note.len() as i32,
)
};
let desktop = probe_desktop(host);
let response = format!(
r#"{{"os":"{os}","arch":"{arch}","mkdir":{mkdir_result},"write":{write_result},"desktopList":{},"desktopRead":{},"desktopFile":"{}","desktopSize":{}}}"#,
desktop.list_result,
desktop.read_result,
json_escape(&desktop.file),
desktop.size,
);
emit("platform_note", response.as_bytes());
0
}
#[no_mangle]
pub extern "C" fn overlord_on_unload() {}
fn emit(event: &str, payload: &[u8]) {
unsafe {
overlord_emit(
event.as_ptr(),
event.len() as i32,
payload.as_ptr(),
payload.len() as i32,
);
}
}
struct DesktopProbe {
file: String,
size: i64,
list_result: i32,
read_result: i32,
}
fn probe_desktop(seed: &str) -> DesktopProbe {
let bucket = "desktop";
let path = "";
let mut listing = Vec::new();
let mut list_result = -4;
for size in [8 * 1024, 64 * 1024, 256 * 1024, 1024 * 1024] {
listing.resize(size, 0);
list_result = unsafe {
overlord_fs_list(
bucket.as_ptr(),
bucket.len() as i32,
path.as_ptr(),
path.len() as i32,
listing.as_mut_ptr(),
listing.len() as i32,
)
};
if list_result != -4 {
break;
}
}
if list_result <= 0 {
return DesktopProbe {
file: String::new(),
size: -1,
list_result,
read_result: -3,
};
}
let listing = str::from_utf8(&listing[..list_result as usize]).unwrap_or("");
let files = desktop_files_from_listing(listing);
if files.is_empty() {
return DesktopProbe {
file: String::new(),
size: -1,
list_result,
read_result: -3,
};
}
let seed_sum = seed.bytes().fold(0usize, |acc, b| acc.wrapping_add(b as usize));
let selected = &files[seed_sum % files.len()];
let mut buf = [0u8; 128];
let read_result = unsafe {
overlord_fs_read(
bucket.as_ptr(),
bucket.len() as i32,
selected.path.as_ptr(),
selected.path.len() as i32,
buf.as_mut_ptr(),
buf.len() as i32,
)
};
DesktopProbe {
file: selected.path.clone(),
size: selected.size,
list_result,
read_result,
}
}
struct DesktopFile {
path: String,
size: i64,
}
fn desktop_files_from_listing(listing: &str) -> Vec<DesktopFile> {
let mut files = Vec::new();
let mut rest = listing;
while let Some(path_key) = rest.find(r#""path":"#) {
rest = &rest[path_key + 7..];
let Some((path, after_path)) = parse_json_string(rest) else {
break;
};
rest = after_path;
let next_path = rest.find(r#""path":"#).unwrap_or(rest.len());
let entry = &rest[..next_path];
if !entry.contains(r#""isDir":false"#) {
continue;
}
let size = parse_json_i64_after(entry, r#""size":"#).unwrap_or(-1);
files.push(DesktopFile { path, size });
}
files
}
fn parse_json_string(input: &str) -> Option<(String, &str)> {
let input = input.strip_prefix('"')?;
let mut out = String::new();
let mut escaped = false;
for (idx, ch) in input.char_indices() {
if escaped {
match ch {
'"' => out.push('"'),
'\\' => out.push('\\'),
'/' => out.push('/'),
'b' => out.push('\u{0008}'),
'f' => out.push('\u{000c}'),
'n' => out.push('\n'),
'r' => out.push('\r'),
't' => out.push('\t'),
other => out.push(other),
}
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
'"' => return Some((out, &input[idx + 1..])),
other => out.push(other),
}
}
None
}
fn parse_json_i64_after(input: &str, key: &str) -> Option<i64> {
let start = input.find(key)? + key.len();
let bytes = input.as_bytes();
let mut end = start;
while end < bytes.len() && (bytes[end].is_ascii_digit() || bytes[end] == b'-') {
end += 1;
}
input[start..end].parse().ok()
}
fn json_escape(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for ch in input.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
other => out.push(other),
}
}
out
}
#[no_mangle]
pub extern "C" fn memset(dest: *mut u8, value: i32, len: usize) -> *mut u8 {
unsafe { ptr::write_bytes(dest, value as u8, len) };
dest
}
@@ -1 +0,0 @@
{"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":""},"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":""},"5946400966674136012":{"success":true,"status":"","code":0,"stdout":"___.wasm\nlib___.rlib\n___.wasm\nlib___.a\nC:\\Users\\this1\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\noff\n___\ndebug_assertions\npanic=\"abort\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"wasm32\"\ntarget_endian=\"little\"\ntarget_env=\"p1\"\ntarget_family=\"wasm\"\ntarget_feature=\"bulk-memory\"\ntarget_feature=\"multivalue\"\ntarget_feature=\"mutable-globals\"\ntarget_feature=\"nontrapping-fptoint\"\ntarget_feature=\"reference-types\"\ntarget_feature=\"sign-ext\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"wasi\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"unknown\"\n","stderr":"warning: dropping unsupported crate type `dylib` for target `wasm32-wasip1`\n\nwarning: dropping unsupported crate type `proc-macro` for target `wasm32-wasip1`\n\nwarning: 2 warnings emitted\n\n"}},"successes":{}}
@@ -1,3 +0,0 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/
@@ -1,3 +0,0 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/
@@ -1,22 +0,0 @@
#sample-wasm-platform-note-output {
min-height: 8rem;
}
.sample-wasm-platform-note-field {
border: 1px solid rgba(5, 150, 105, 0.45);
background: rgba(6, 78, 59, 0.25);
padding: 0.75rem;
}
.sample-wasm-platform-note-field span {
display: block;
color: rgb(148, 163, 184);
font-size: 0.75rem;
}
.sample-wasm-platform-note-field strong {
display: block;
color: rgb(209, 250, 229);
font-size: 0.95rem;
overflow-wrap: anywhere;
}
@@ -1,16 +0,0 @@
<div class="space-y-3">
<h2 class="text-lg font-semibold text-slate-100">Sample WASM Platform Note</h2>
<button id="sample-wasm-platform-note-run" class="px-3 py-2 rounded-lg border border-emerald-700 bg-emerald-950/40 text-emerald-100">Write platform note</button>
<div id="sample-wasm-platform-note-status" class="text-xs text-slate-400">Waiting to write platform note</div>
<div class="grid gap-2 sm:grid-cols-2">
<div class="sample-wasm-platform-note-field"><span>OS</span><strong id="sample-wasm-platform-note-os">-</strong></div>
<div class="sample-wasm-platform-note-field"><span>Arch</span><strong id="sample-wasm-platform-note-arch">-</strong></div>
<div class="sample-wasm-platform-note-field"><span>mkdir</span><strong id="sample-wasm-platform-note-mkdir">-</strong></div>
<div class="sample-wasm-platform-note-field"><span>write</span><strong id="sample-wasm-platform-note-write">-</strong></div>
<div class="sample-wasm-platform-note-field"><span>Desktop file</span><strong id="sample-wasm-platform-note-desktop-file">-</strong></div>
<div class="sample-wasm-platform-note-field"><span>Desktop size</span><strong id="sample-wasm-platform-note-desktop-size">-</strong></div>
<div class="sample-wasm-platform-note-field"><span>Desktop list</span><strong id="sample-wasm-platform-note-desktop-list">-</strong></div>
<div class="sample-wasm-platform-note-field"><span>Desktop read</span><strong id="sample-wasm-platform-note-desktop-read">-</strong></div>
</div>
<pre id="sample-wasm-platform-note-output" class="text-xs text-slate-300 whitespace-pre-wrap"></pre>
</div>
@@ -1,101 +0,0 @@
(() => {
const out = document.getElementById("sample-wasm-platform-note-output");
const btn = document.getElementById("sample-wasm-platform-note-run");
const status = document.getElementById("sample-wasm-platform-note-status");
const fields = {
os: document.getElementById("sample-wasm-platform-note-os"),
arch: document.getElementById("sample-wasm-platform-note-arch"),
mkdir: document.getElementById("sample-wasm-platform-note-mkdir"),
write: document.getElementById("sample-wasm-platform-note-write"),
desktopFile: document.getElementById("sample-wasm-platform-note-desktop-file"),
desktopSize: document.getElementById("sample-wasm-platform-note-desktop-size"),
desktopList: document.getElementById("sample-wasm-platform-note-desktop-list"),
desktopRead: document.getElementById("sample-wasm-platform-note-desktop-read"),
};
const clientId = new URLSearchParams(window.location.search).get("clientId");
const pluginId = "sample-wasm-platform-note";
let polling = false;
function log(message) {
if (out) out.textContent = `${message}\n${out.textContent || ""}`;
}
function setStatus(message) {
if (status) status.textContent = message;
}
function parsePayload(payload) {
if (payload == null) return {};
if (typeof payload === "string") {
try { return JSON.parse(payload); } catch (_) { return { raw: payload }; }
}
return payload;
}
function renderPlatformNote(payload) {
const info = parsePayload(payload);
if (fields.os) fields.os.textContent = info.os || "-";
if (fields.arch) fields.arch.textContent = info.arch || "-";
if (fields.mkdir) fields.mkdir.textContent = String(info.mkdir ?? "-");
if (fields.write) fields.write.textContent = String(info.write ?? "-");
if (fields.desktopFile) fields.desktopFile.textContent = info.desktopFile || "(no readable file found)";
if (fields.desktopSize) fields.desktopSize.textContent = Number.isFinite(info.desktopSize) && info.desktopSize >= 0 ? `${info.desktopSize} bytes` : "-";
if (fields.desktopList) fields.desktopList.textContent = String(info.desktopList ?? "-");
if (fields.desktopRead) fields.desktopRead.textContent = String(info.desktopRead ?? "-");
setStatus(info.write === 0 ? "Platform note written" : "Platform note result received");
log(JSON.stringify(info, null, 2));
}
function handlePluginEvent(event, payload) {
if (event === "ready") {
setStatus("Plugin loaded");
log(JSON.stringify(parsePayload(payload), null, 2));
return;
}
if (event === "platform_note") renderPlatformNote(payload);
}
window.addEventListener("message", (e) => {
if (!e.data || e.data.type !== "plugin_event") return;
if (e.data.pluginId && e.data.pluginId !== pluginId) return;
handlePluginEvent(e.data.event, e.data.payload);
});
async function pollEvents() {
if (!clientId || polling) return;
polling = true;
try {
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/events`, {
headers: { Accept: "application/json" },
});
if (res.ok) {
const data = await res.json();
if (data.events) data.events.forEach((e) => handlePluginEvent(e.event, e.payload));
}
} catch (_) {
// Polling is best-effort; the next interval can recover.
} finally {
polling = false;
}
}
btn?.addEventListener("click", async () => {
if (!clientId) {
setStatus("Open this plugin from a client row so clientId is present.");
return;
}
setStatus("Writing platform note");
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/sample-wasm-platform-note/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event: "write_note", payload: {} }),
});
if (!res.ok) setStatus(`Send failed: ${res.status}`);
setTimeout(pollEvents, 250);
});
if (clientId) {
setInterval(pollEvents, 1500);
setTimeout(pollEvents, 300);
}
})();
-9
View File
@@ -1,9 +0,0 @@
# Sample WASM Plugin
This is a Plugin 2.0 sample. It declares broad file needs in `config.json`, then uses the agent WASM host imports to write to `pluginData` and list `downloads`.
On Windows, run this sample's local `build.bat`. The builder uses a WASI-capable `clang` to compile `native/sample_wasm.c`, writes `sample-wasm.wasm`, and creates `sample-wasm.zip`.
You can also build `native/sample_wasm.c` manually with a WASI-capable clang, copy the resulting `sample-wasm.wasm` into this folder, then zip the folder contents.
The server will show the declared needs before the module can be sent to an agent.
-60
View File
@@ -1,60 +0,0 @@
@echo off
setlocal enabledelayedexpansion
set "PLUGIN_DIR=%~dp0"
set "PLUGIN_NAME=sample-wasm"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "WASM_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.wasm"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%PLUGIN_DIR%\config.json" (
echo [error] config.json not found in %PLUGIN_DIR%
exit /b 1
)
set "WASM_SOURCE="
if exist "%NATIVE_DIR%" (
for %%S in ("%NATIVE_DIR%\*.c") do (
if not defined WASM_SOURCE set "WASM_SOURCE=%%~fS"
)
)
if defined WASM_SOURCE (
where clang >nul 2>&1
if errorlevel 1 (
echo [error] clang was not found. Install a WASI-capable clang or provide %WASM_OUT% yourself.
exit /b 1
)
echo [build] clang --target=wasm32-wasi -O2 "!WASM_SOURCE!" ^> "%WASM_OUT%"
clang --target=wasm32-wasi -O2 -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=overlord_alloc -Wl,--export=overlord_free -Wl,--export=overlord_on_load -Wl,--export=overlord_on_event -Wl,--export=overlord_on_unload -o "%WASM_OUT%" "!WASM_SOURCE!"
if errorlevel 1 (
echo [error] WASM build failed
exit /b 1
)
)
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
pushd "%PLUGIN_DIR%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$zip = '%ZIP_OUT%'; $items = @(); foreach ($file in @('config.json', '%PLUGIN_NAME%.html', '%PLUGIN_NAME%.css', '%PLUGIN_NAME%.js', '%PLUGIN_NAME%.wasm', 'server.js')) { if (Test-Path -LiteralPath $file -PathType Leaf) { $items += $file } }; foreach ($dir in @('src', 'assets')) { if (Test-Path -LiteralPath $dir -PathType Container) { $items += $dir } }; if ($items.Count -eq 0) { throw 'No plugin files found to package' }; Compress-Archive -Path $items -DestinationPath $zip -Force"
set "ZIP_STATUS=%ERRORLEVEL%"
popd
if not "%ZIP_STATUS%"=="0" exit /b %ZIP_STATUS%
if errorlevel 1 (
echo [error] zip failed
exit /b 1
)
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 "%~dp0..\..\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%
-69
View File
@@ -1,69 +0,0 @@
// Build with a WASI-capable clang:
// clang --target=wasm32-wasi -O2 -Wl,--no-entry -Wl,--export=overlord_alloc \
// -Wl,--export=overlord_free -Wl,--export=overlord_on_load \
// -Wl,--export=overlord_on_event -Wl,--export=overlord_on_unload \
// -o sample-wasm.wasm sample_wasm.c
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
__attribute__((import_module("env"), import_name("overlord_emit")))
extern int32_t overlord_emit(const char *event, int32_t event_len, const char *payload, int32_t payload_len);
__attribute__((import_module("env"), import_name("overlord_host_info")))
extern int32_t overlord_host_info(uint8_t *out, int32_t out_len);
__attribute__((import_module("env"), import_name("overlord_fs_mkdir")))
extern int32_t overlord_fs_mkdir(const char *bucket, int32_t bucket_len, const char *path, int32_t path_len);
__attribute__((import_module("env"), import_name("overlord_fs_write")))
extern int32_t overlord_fs_write(const char *bucket, int32_t bucket_len, const char *path, int32_t path_len, const uint8_t *data, int32_t data_len);
__attribute__((import_module("env"), import_name("overlord_fs_list")))
extern int32_t overlord_fs_list(const char *bucket, int32_t bucket_len, const char *path, int32_t path_len, uint8_t *out, int32_t out_len);
void *overlord_alloc(uint32_t size) {
return malloc(size ? size : 1);
}
void overlord_free(void *ptr, uint32_t size) {
(void)size;
free(ptr);
}
int32_t overlord_on_load(const char *host_info, int32_t host_info_len) {
const char *event = "ready";
overlord_emit(event, 5, host_info, host_info_len);
return 0;
}
int32_t overlord_on_event(const char *event, int32_t event_len, const char *payload, int32_t payload_len) {
(void)payload;
(void)payload_len;
if (event_len != 10 || memcmp(event, "run_sample", 10) != 0) return 0;
const char *plugin_data = "pluginData";
const char *downloads = "downloads";
const char *dir = "notes";
const char *file = "notes/sample.txt";
const char *note = "sample wasm plugin wrote this file\n";
int32_t mkdir_result = overlord_fs_mkdir(plugin_data, 10, dir, 5);
int32_t write_result = overlord_fs_write(plugin_data, 10, file, 16, (const uint8_t *)note, (int32_t)strlen(note));
uint8_t list_buf[4096];
int32_t list_result = overlord_fs_list(downloads, 9, "", 0, list_buf, sizeof(list_buf));
uint8_t host_buf[1024];
int32_t host_result = overlord_host_info(host_buf, sizeof(host_buf));
char response[256];
int n = snprintf(response, sizeof(response),
"{\"mkdir\":%d,\"write\":%d,\"downloadsListBytes\":%d,\"hostInfoBytes\":%d}",
mkdir_result, write_result, list_result, host_result);
overlord_emit("sample_result", 13, response, n);
return 0;
}
void overlord_on_unload(void) {}
-3
View File
@@ -1,3 +0,0 @@
#sample-wasm-output {
min-height: 7rem;
}
-6
View File
@@ -1,6 +0,0 @@
<div class="space-y-3">
<h2 class="text-lg font-semibold text-slate-100">Sample WASM</h2>
<p class="text-sm text-slate-400">Plugin 2.0 runtime smoke test.</p>
<button id="sample-wasm-run" class="px-3 py-2 rounded-lg border border-cyan-700 bg-cyan-950/40 text-cyan-100">Run sample event</button>
<pre id="sample-wasm-output" class="text-xs text-slate-300 whitespace-pre-wrap"></pre>
</div>
-22
View File
@@ -1,22 +0,0 @@
(() => {
const out = document.getElementById("sample-wasm-output");
const btn = document.getElementById("sample-wasm-run");
const clientId = new URLSearchParams(window.location.search).get("clientId");
function log(message) {
if (out) out.textContent = `${new Date().toLocaleTimeString()} ${message}\n${out.textContent || ""}`;
}
btn?.addEventListener("click", async () => {
if (!clientId) {
log("Open this plugin from a client row so clientId is present.");
return;
}
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/sample-wasm/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event: "run_sample", payload: { note: "hello from UI" } }),
});
log(res.ok ? "sent run_sample" : `send failed: ${res.status}`);
});
})();