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

2.7 KiB

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, 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.