plugins 2.0 update
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,60 @@
|
||||
@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%
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
#sample-wasm-output {
|
||||
min-height: 7rem;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<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>
|
||||
@@ -0,0 +1,22 @@
|
||||
(() => {
|
||||
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