Files
vectorcraft/crates/plugins/src/lib.rs
T
dotCooCoo cde367b889 Plug-ins: only an approval records a grant, and installs need one
registry::grant takes a GrantToken, which the engine makes after a
person's approval and when it loads grants.json. registry::install,
install_bytes and folder loads refuse a module that lists permissions no
recorded grant gives this very module (Error::NeedsApproval), and leave
the plug-in under its id and the revision as they were. A folder load's
approve callback only narrows what installs. A module without
permissions, or with an empty list, installs as before.

A folder load in a session with a plug-in store records the store's
grant for each module it approves, so those modules still load.

docs/plugins.md says what the approval does not protect against: native
code linked into the app, and another program running as the same user,
which can edit the Plug-ins folder and its unsigned grants.json.
2026-11-11 17:35:06 -08:00

101 lines
4.7 KiB
Rust

//! Sandboxed WebAssembly plug-ins for VectorCraft.
//!
//! A plug-in is a WebAssembly module that implements the VectorCraft plug-in ABI (v1, documented
//! in `docs/plugins.md`). It runs in [`wasmi`], a pure-Rust interpreter, with:
//!
//! - **no host imports but those of API level 2** ([`host`]: progress, Cancel and log lines with
//! the `progress` permission, and settings and a cache of its own with the `settings` and `cache`
//! permissions, [`storage`], and the commands its `commandAllowlist` names with the `commands`
//! permission): no WASI, file system, network, clock or randomness. A module that
//! imports anything else is rejected;
//! - an **instruction budget** (fuel) per call, metered in slices so a wall-clock deadline is
//! also enforced, so infinite loops end with an error. A filter may ask for a
//! larger one ([`Budget::Heavy`]), which the host lets the user allow before installing it;
//! - a **linear-memory cap**, a **recursion-depth cap** and wasmi's strict module limits;
//! - a **fresh instance per run**, so no state leaks between calls; what a plug-in keeps is only
//! what it writes to its settings and cache.
//!
//! Every failure (malformed module, trap, exhausted budget, bad output) is an [`Error`]; nothing
//! a module does can panic or hang the host.
//!
//! Plug-ins see the document as JSON ([`objects`]): the selected paths and compound paths with
//! their geometry, fills, strokes (gradients included) and opacity, and, for a filter that asks
//! for them ([`InputKind`]), type, images and symbol instances to read. An **object filter** returns
//! the objects to keep, change or add, applied to the document as one edit
//! ([`objects::apply_output`]); a **live effect** ([`effect`]) rewrites one object's geometry each
//! time it is drawn, from an appearance stack, and at `api` 2 may paint what it returns
//! ([`painted`]); an **exporter** ([`export`]) writes the whole
//! document in a file format of its own; an **importer** ([`import`]) reads a file in a format of
//! its own into a new document. The process-wide [`registry`] holds the installed
//! plug-ins; in [`safe_mode`] none of them loads or runs. A plug-in's parameters ([`manifest`]) come with what the host's dialogs need to show
//! them well: labels, units, groups, help and conditions ([`dialog`]). What a plug-in may do is
//! the set of [`permissions`] its manifest lists, which the user approves when it is installed. A
//! filter or a live effect may also add menu commands of its own ([`Command`]), which the host
//! runs through `vc_run` with the command's id in `_context`.
#![forbid(unsafe_code)]
pub mod deferred;
pub mod dialog;
pub mod effect;
pub mod export;
pub mod host;
pub mod import;
pub mod manifest;
pub mod objects;
pub mod painted;
pub mod permissions;
pub mod registry;
mod runtime;
pub mod safe_mode;
pub mod storage;
pub mod wat;
pub use dialog::ParamUi;
pub use host::{LogLevel, LogLine, Report};
pub use manifest::{Budget, Command, InputKind, Kind, Manifest, Param, ParamSpec};
pub use runtime::{ABI_VERSION, HEAVY_FUEL, HEAVY_WALL_TIME_MS, Limits, Plugin, heavy_terms};
#[doc(hidden)]
pub use runtime::{BYTES_COPIED_PER_FUEL, FUEL_SLICE};
/// What the host puts before a plug-in's id to name what it adds: its live effect
/// (`plugin.<id>`, [`effect`]) or its file format ([`export`], [`import`]).
pub const PREFIX: &str = "plugin.";
/// A plug-in error. Every way a module can misbehave ends here, never in a panic.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("not a valid plug-in module: {0}")]
Module(String),
#[error("plug-in ABI: {0}")]
Abi(String),
#[error("plug-in manifest: {0}")]
Manifest(String),
#[error("plug-in trapped: {0}")]
Trap(String),
#[error("plug-in exceeded its {0}")]
Limit(String),
#[error("plug-in parameters: {0}")]
Params(String),
#[error("plug-in output: {0}")]
Output(String),
#[error("plug-in failed: {0}")]
Failed(String),
#[error("no plug-in {0:?} is installed")]
NotFound(String),
/// The caller asked the run to stop ([`vectorcraft_doc::cancel`]), between two slices of
/// its instruction budget.
#[error("cancelled")]
Cancelled,
/// Safe Mode is on ([`safe_mode`]): no plug-in loads or runs.
#[error("Safe Mode is on: plug-ins don't load or run until VectorCraft starts normally")]
SafeMode,
/// The module lists permissions that no recorded grant gives this very module
/// ([`registry::approved`]): it names them, comma-separated.
#[error("needs approval: it asks for {0}, which the user grants when installing it on its own")]
NeedsApproval(String),
#[error("{0}")]
Io(String),
}
pub type Result<T> = std::result::Result<T, Error>;