Remove WASM plugin support
This commit is contained in:
+5
-11
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
@@ -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%
|
||||
|
||||
@@ -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`.
|
||||
@@ -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%
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
@@ -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.
|
||||
@@ -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%
|
||||
@@ -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) {}
|
||||
@@ -1,3 +0,0 @@
|
||||
#sample-wasm-output {
|
||||
min-height: 7rem;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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}`);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user