Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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:
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.Permissionenum: VaultRead, VaultWrite, Editor.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 returningResult<String /* json */, HostError>. The host checks the plugin's permissions BEFORE calling the trait (Error::PermissionDenied).Plugin::load(manifest, wasm_bytes, Arc<dyn HostApi + Send + Sync>, Limits) -> Result<Plugin>using wasmi: validate module, require exportsns_abi_version(must return 1),ns_alloc,ns_free,ns_on_load,ns_on_unload,ns_run_command,ns_on_eventand a memory; link ONLY thenshost 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 withget.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 failuresPlugin::is_disabled()is true and calls returnError::Disabled.plugin.run_command(cmd_id, args_json) -> Result<String>,plugin.send_event(event_json),on_load/on_unloadlifecycle.- A
PluginSetthat holds loaded plugins by id, lists commands asplugin.<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.