Effect ▸ Install Effect Plug-in… and plug-in bundles (wasm + ScriptUI panel in one go) (#2749)

* plugins: install effect plug-in bundles (wasm + ScriptUI panels) in one go

* plugins: address review - keep listing errors, refuse replacing an installed plug-in

* plugins: report unreadable bundle entries when their metadata cannot be read

* plugins: surface partial install errors in the menu bar, cover the install flow with tests

* plugins: surface partial install errors for async dialog answers too

* plugins: show result errors as compact status text

* plugins: harden installs - confined automation, no panel overwrite, size cap, cleanup errors

* plugins: bound install reads from one handle, detect unreadable panels, clarify install docs

* plugins: drop an unneeded mut in the bounded read

* plugins: saturate the bounded read limit

* plugins: install into the session's Plug-ins folder, refuse hidden single installs and case-variant panels

- Install .wasm files into Session::plugins_dir() (the folder the Plug-in Library
  from #2524 uses), not a path rebuilt from the settings folder; the host tests
  now point both at their temp folder so they never touch the real settings.
- A single install of a hidden file (".x.wasm") was skipped and reported as a
  success that installed nothing: it is now an error.
- Installed names must be bare file names (and not Windows device names on
  Windows), as file.installScript requires, so every installed panel is listed
  and can be uninstalled.
- A panel whose name differs from an installed one only in case replaced it on
  Windows and macOS: compare ignoring case and check the file on disk too.
- The loaded plug-in's source is the installed copy, not the file it came from.
- Document the `bundle` menu parameter, so checked execution accepts it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* plugins: drop needless to_vec in the bundle install test (clippy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* plugins: keep the loader test's byte vectors, drop to_vec in the install test (clippy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: STAR Web Studio <hello.starwebstudio@gmail.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
just_anshh
2026-11-05 11:35:54 +01:00
committed by GitHub
co-authored by Claude Opus 5.5 STAR Web Studio FredApps
parent 611a282bea
commit a439b1bb19
13 changed files with 509 additions and 10 deletions
+50 -2
View File
@@ -163,7 +163,7 @@ There are two ways to write one:
| | WebAssembly plug-in | Rust plug-in |
|---|---|---|
| What | a `.wasm` (or `.wat`) module | a type implementing `effectcraft_effects::plugin::EffectPlugin` |
| Loaded by | Effect ▸ Load Effect Plug-in…, `effect.plugins.load {path \| folder}`, or the `Plug-ins` folder next to the settings at start-up | `register_plugin(Arc::new(MyEffect))` in an app that links EffectCraft's crates |
| Loaded by | Effect ▸ Install Effect Plug-in… (copied into the `Plug-ins` folder next to the settings and loaded on every start-up), Effect ▸ Load Effect Plug-in… / `effect.plugins.load {path \| folder}` (loads in this session only), the `Plug-ins` folder next to the settings at start-up, or `effect.plugins.install {path \| folder}` | `register_plugin(Arc::new(MyEffect))` in an app that links EffectCraft's crates |
| Sandbox | yes: no imports (no files, clock or network), fuel-limited, at most 1 GiB of memory, deterministic floats | none (it is your code) |
| Platforms | desktop and CLI (the wasmi interpreter, crate feature `effectcraft-plugin/wasm`); not the web build | everywhere |
@@ -241,11 +241,59 @@ plain Rust (no dependencies):
```sh
cd examples/plugins/posterize-bands
cargo build --release --target wasm32-unknown-unknown
# then, in EffectCraft: Effect ▸ Load Effect Plug-in… → target/wasm32-unknown-unknown/release/posterize_bands.wasm
# then, in EffectCraft: Effect ▸ Install Effect Plug-in…
# → target/wasm32-unknown-unknown/release/posterize_bands.wasm (copied into Plug-ins, loads on start-up)
# or in-session only: Effect ▸ Load Effect Plug-in…
# or headless:
effectcraft-cli exec effect.plugins.load '{"path": "target/wasm32-unknown-unknown/release/posterize_bands.wasm"}'
```
**Bundles.** `effect.plugins.install {"folder": ...}` installs a whole plug-in folder in one
go: every `.wasm` at its top level is copied into the `Plug-ins` folder (and loads at once),
and every `.jsx` at its top level is installed as a ScriptUI panel — so a third-party plug-in
ships as one folder the user installs once. A `.wasm` that does not load is not copied (the
install rolls it back), and per-file problems in a folder install come back in `errors`
instead of failing the whole bundle.
**What an install does and does not do.** It is a security boundary: it copies code into a
folder the app runs at every start, so:
* Only `.wasm` (`path` / a bundle's `.wasm`) and `.jsx` (a bundle's panel) are installed; other
files in a bundle (readmes, manifests, sources) are skipped. Files are named by their own
entry name, so a bundle cannot carry a path, a `..` component or a name that escapes the
destination folder.
* Nothing is overwritten. An existing plug-in or panel with the same file name makes that file
fail with "already installed": delete it from `Plug-ins` / `Scripts/ScriptUI Panels` first to
update it (versioning the file name is the other option). A bundle is installed file by file,
so one collision does not undo the rest.
* Files over 64 MiB are refused before they are read, rather than copied into a startup folder.
* A `.wasm` is loaded once, in the same sandbox it runs in later (see below), so a module that
cannot load is reported instead of failing every start. That load runs the module's start-up
code and its metadata exports; `ec_render` is never called, and a panel's script is not run at
install time (it runs when the panel opens, as with any installed panel). The copy made before
the load is then removed again — best effort: if removing it fails, the command says so and
names the file, and you delete it from `Plug-ins` yourself.
* A `.wasm` is uninstalled by deleting it from `Plug-ins` (the next start no longer loads it):
there is no `effect.plugins.uninstall`. A panel installed here is an installed script, so
`file.uninstallScript {name}` removes it. Replacing either means removing it first.
* Hidden files (names starting with `.`, such as macOS `._x.wasm` copies) are skipped in a bundle
and refused as a single install. A panel name that differs from an installed panel's only in
letter case counts as the same name.
* Installed plug-ins share the `Plug-ins` folder with the [Plug-in Library](#plug-in-library)
(which keeps its packages in `Plug-ins/Library`). An effect id can be registered only once: a
plug-in whose id is already loaded, from either kind of install, is refused. At start-up the
files installed here load before the library's packages, so if both carry the same id (for
example after files were copied by hand), the file here wins and the library package reports
the duplicate.
**The sandbox.** A plug-in runs in the [wasmi](https://github.com/wasmi-labs/wasmi) interpreter
with no imports (no file, network or clock access), deterministic floats, a fuel budget per
frame and a 1 GiB memory cap, so a plug-in cannot escape the sandbox, hang the app or render
differently on another machine. A ScriptUI panel is different: it is a script with the app's
scripting object model, so it can read and write project data through commands; file and
network access for scripts follow Settings ▸ Scripting & Expressions, exactly as for a script
installed with File ▸ Scripts ▸ Install ScriptUI Panel….
Any language that compiles to WebAssembly works (C, Zig, AssemblyScript…) as long as the module
has no imports. `crates/plugin/src/tests.rs` has a plug-in written directly in WebAssembly text.