Files
notescraft/scripts/waves/wave2/plugins.md
T
2026-10-13 19:00:33 -03:00

12 lines
2.7 KiB
Markdown

Crate: crates/plugins (notescraft-plugins, layer L3). SPEC section 7 (plugin contract) in full. Allowed new dependencies: wasmi, thiserror, serde, serde_json; dev-dependency: wat (all `{ workspace = true }`). Reference implementation to read for style (do not copy blindly): /Volumes/Storage/Projects/photocraft/crates/plugins/src. Do NOT depend on other notescraft crates; host access to the vault, search, editor etc. goes through a trait.
Implement the sandboxed WASM host:
1. `Manifest` (serde): id, name, version, min_app_version, description, author, permissions: Vec<Permission>, commands: Vec<CommandDecl{id,name}>; `Manifest::from_json(&str) -> Result<Manifest>` validates id charset `[a-z0-9-]{1,64}`, unknown permission = Err, caps on sizes. `Permission` enum: VaultRead, VaultWrite, Editor.
2. `trait HostApi` (implemented by the app later): `log`, `vault_read(path)`, `vault_write(path, content)`, `vault_list(glob)`, `search(query)`, `links(path)`, `backlinks(path)`, `active_note()`, `insert_text(text)`, `notice(text)`, `now()`, each returning `Result<String /* json */, HostError>`. The host checks the plugin's permissions BEFORE calling the trait (`Error::PermissionDenied`).
3. `Plugin::load(manifest, wasm_bytes, Arc<dyn HostApi + Send + Sync>, Limits) -> Result<Plugin>` using wasmi: validate module, require exports `ns_abi_version` (must return 1), `ns_alloc`, `ns_free`, `ns_on_load`, `ns_on_unload`, `ns_run_command`, `ns_on_event` and a memory; link ONLY the `ns` host imports from SPEC 7.2 (any other import = Err). Strings pass as (ptr,len) in guest memory, results are packed i64 `(ptr << 32) | len`; negative i32 returns are error codes. All pointer/length reads from guest memory are bounds-checked with `get`.
4. `Limits` (defaults from SPEC 7.3): fuel 50_000_000 per call, memory 64 MiB, 2 s wall clock (check via fuel metering and an epoch/deadline check), result size 4 MiB, 8 host calls per ms. Exceeding = `Error::PluginLimit`, traps = `Error::PluginTrap`; after 3 consecutive failures `Plugin::is_disabled()` is true and calls return `Error::Disabled`.
5. `plugin.run_command(cmd_id, args_json) -> Result<String>`, `plugin.send_event(event_json)`, `on_load`/`on_unload` lifecycle.
6. A `PluginSet` that holds loaded plugins by id, lists commands as `plugin.<id>.<command>` and dispatches by that full id.
Tests with `wat` fixtures (tests/fixtures as .wat text compiled in tests): a good plugin that echoes args, calls `ns.log` and `ns.vault_read`; hostile modules: infinite loop, huge memory.grow, out-of-bounds pointer returned, result length over cap, missing export, wrong ABI version, extra import, trap (unreachable), recursion, returns negative error code. Permission-denied test with a stub HostApi. Auto-disable after 3 failures. Nothing may panic the host.