Skip to content

Latest commit

 

History

History
187 lines (154 loc) · 11.5 KB

plugins.md

File metadata and controls

187 lines (154 loc) · 11.5 KB

Plugins

RFC 0002: Plugins defines the plugin API and phases. Phase 1 splits the host into two app-agnostic crates:

  • crates/craft-plugin owns the generic WIT contract, manifest, permissions, entry input schemas and typed host errors. It has no wasmtime dependency.
  • crates/craft-plugin-wasm runs WebAssembly components against that contract with fuel, memory and stack limits. Its linker exposes no network imports.

An app implements PluginHost: it supplies command schemas, each command's read or edit effect, command execution, transaction begin/commit/rollback, and the folders that file grants may resolve to. One successful plugin call is one undo step; a failed guest call rolls back the transaction. Granted file writes are staged and published only after the guest succeeds and the app commits. A rollback deletes staged files without replacing their targets. A filesystem failure during publication after app commit is reported and may leave some files changed. App commands remain JSON data rather than app-specific WIT types. Both crates are Shared crates; cargo xtask layers forbids direct and indirect SolidCraft dependencies.

SolidCraft's adapter lives in crates/engine and uses the same command surface as MCP. Entries appear as plugin_id.function; plugin.list and engine.describe expose their declared inputs. Selection and target inputs are lists. Entries may declare named menu or ribbon slots; each app decides which slots exist and draws forms from the input schemas. Plugins ship no UI code. SolidCraft retains approval, cancellation and plugin preferences.

Installing

Install plugin… in Preferences > Plugins (desktop only) picks a plugin .zip. One confirmation screen shows its name, version, summary, requested permissions and entry placements. Each command group explains its access in plain words: files, designs, application settings or other plugins. Enter an absolute local path for each requested folder. Each folder shows its requested read/write access. When the plugin asks for more memory than the ceiling in Preferences > Plugins (128 MiB unless changed), the screen says how much it asks for and what the ceiling is. Install approves the displayed command groups, folders and memory, publishes the package and stores the approvals; Cancel changes nothing. Installation never runs plugin code. Approved memory applies to that exact package only: a replaced or changed package asks again. Fuel still has to fit the ceiling.

Programs use the same plugin.install command without opening dialogs:

{
  "path": "gears.zip",
  "approval": "COPY_THE_APPROVAL_FROM_THE_REVIEWED_DRY_RUN",
  "approve": {
    "command_groups": ["file"],
    "folders": [{"name": "exports", "path": "/home/user/exports", "read": false, "write": true}],
    "memory_mib": 1024
  }
}

Approvals may only narrow the manifest's requests. An unrequested group, folder name or access mode returns a typed refusal. memory_mib is either omitted (the plugin runs under the app's ceiling) or exactly the manifest's runtime.limits.memory_mib. Omitting approve grants no groups, folders or memory, including when replacing a plugin. Providing approve replaces that plugin id's stored approvals; grants the new manifest no longer requests cannot survive replacement. plugin.run checks stored grants against the current manifest on every call. A package copied into the plugin directory by hand also starts with no grants.

Set "dry_run": true to check and describe the zip, including approval validation, without writing files or grants. The response includes an approval hash of the exact manifest and component. It also includes archive, a hash of the whole .zip, which covers the plugin's other files too. Send both as approval and archive when installing the reviewed package. A changed package returns a typed invalid_input error for approval or archive before any files or grants are written. The dialog always sends both hashes; programmatic callers may omit it to install directly without a prior review. Replacing an installed plugin with the same id requires "replace": true. The response includes name, version, description, capabilities, permissions, requested limits, the app's host_limits, app commands, namespaced entries, declared entry_points with input schemas and slots, replacement information, the target folder and whether it was installed. A dry run reports installed: false. Dry-run description is the package review path; there is no separate review command or folder installer.

A zip holds one plugin, with plugin.json and its component at the top or inside one top-level folder. The existing solidcraft_io::safe_zip reader refuses absolute paths, .., symlinks and device entries. The installer limits the archive to 256 entries, each file to 16 MiB, and input and unpacked content to 64 MiB. The manifest has a separate 16 KiB cap. Before publication, the host parses the manifest and refuses unknown or newer capabilities. It unpacks into a hidden .install-… folder beside the installed plugins, then renames it to plugins/<id>/. The entries resolve immediately, with no restart.

Removing

Each installed plugin card in Preferences > Plugins has an enable switch and Remove action. Remove asks for confirmation, then deletes the package folder and its stored grants. Cancelling Preferences does not restore a removed package. Removing a plugin preserves its data in saved designs. Approved grants and removal survive closing or resetting Preferences. Grants are host preferences, never design data; preferences retain at most 256 plugin ids, four command groups and 32 folders per id. The browser Plugins page remains desktop-only.

The command surface exposes management without opening dialogs:

Command Parameters Result
plugin.install {path, approval?, archive?, replace?, dry_run?, approve?: {command_groups, folders: [{name, path, read, write}], memory_mib?}} Package description and installation status, including declared entry_points
plugin.list {} {plugins: [{manifest, commands, disabled}]}
plugin.remove {id} {id, removed: true}

All commands expose input and output schemas through engine.describe. Removal validates the id and confines deletion to the host's plugin directory. Installation and removal do not edit the design. On desktop, send plugin.install and plugin.remove as separate engine.execute requests. engine.batch and engine.script refuse these external file operations before any earlier step runs. Direct requests use a worker to keep the window responsive.

Menu and ribbon entries

The Plugins menu in the app bar lists every installed plugin entry, including entries without slots. Disabled entries remain listed but cannot be selected. Discovery examines at most 1,024 package folders and 8,192 host slot items. Only enabled plugins contribute host menu and ribbon items. The host draws each slot item with the plugin's name, a plugin badge and the entry name. Plugins declare placement in their manifests and supply no widget, menu or ribbon code.

Declared slot Placement (names ignore case)
menu named File or Help The existing File or Help menu; each item includes the plugin's name
Any other menu name No host menu item; the entry remains in the Plugins menu
ribbon named for an existing Design ribbon tab A group named for the plugin in that tab
Any other ribbon name One Plugins ribbon tab, with a group per plugin

The Design tab names are Model, Surface, Assemble, Insert, Inspect, Manage, View, Sketch, Sheet Metal and Weldments. Existing tab visibility and workspace rules still apply. For example, {"kind":"menu","name":"File"} places an entry in File; {"kind":"ribbon","name":"Model"} places it in Model.

Clicking a File, Help or ribbon item opens the same input-schema dialog as the Plugins menu. The host finds the entry by its namespaced <plugin-id>.<entry> command. The dialog uses the same enabled state and grant checks from every placement. Missing command-group grants leave Run off with an explanation. Opening the dialog executes no plugin code; Run submits the existing plugin.run transaction.

Integration APIs

Preferences and the Plugins menu share plugin_install::start(app) in crates/ui-egui/src/plugin_install.rs. It selects a zip, calls plugin.install with dry_run: true, draws the confirmation screen and submits plugin.install with the reviewed approval and archive hashes and selected approve object. Successful installation persists grants in desktop preferences; successful removal deletes them. There is one package reader and one installation confirmation screen.

plugin_menu::list in crates/ui-egui/src/plugin_menu.rs and Preferences use the same package inventory as slot discovery. The list includes each plugin's enabled state and requested command groups. File, Help and ribbon clicks resolve their namespaced entry against that list, then call plugin_menu::open_entry(app, p, e), as the Plugins menu does. Unknown menu names need no fallback rows because the Plugins menu already lists every entry.

The shared dialog draws the entry's input schema and supplies the package's content approval hash when invoking plugin.run. An entry without fields receives {}. Slot clicks alone do not execute a plugin. Installing, enabling or discovering a plugin never runs an entry. Installation grants do not replace execution consent or the manifest-and-component identity checked by plugin.run.

A string with a fixed set of values becomes a dropdown. With enum the dropdown shows the values themselves; to show words instead, list the options as oneOf, each {"const": value, "title": shown} (with an optional description). The dropdown shows the titles and the plugin receives the value:

"up_axis": {"type": "string", "title": "Up axis", "default": "as_is", "oneOf": [
  {"const": "as_is", "title": "As in the file"},
  {"const": "stand_up", "title": "Stand up (Y-up to Z-up)"}
]}

Versions

The WIT package is craft:plugin@0.1.0. Today's surface is one capability, commands@0.1: running app commands, logging, progress and cancellation, named files, a clock, and the exported entry. A plugin's plugin.json lists what it needs, for example "capabilities": ["commands@0.1"]; the host refuses an unknown or newer capability when it reads the manifest, with a plain message such as "needs commands 0.2; update SolidCraft or the plugin". ui, viewport, formats and storage arrive later as their own versioned interfaces. While a version is 0.x, a minor version may break the one before it.

Commands and permissions

A plugin can call every command in the MCP catalog that its manifest lists. Commands that reach past the open design are grouped (file, doc, app, plugin) and refused unless the manifest asks for the group in permissions.command_groups and the user grants it. File access requires approved folder grants. The reserved net permission is refused with a typed error. Traps, missing permissions, failed commands, limits and invalid manifest fields return structured errors. Later phases add import/export, author tooling and browser execution after the phase 1 design review.

Plugin data in a design

A design container reserves plugins/<plugin-id>/ for a plugin's own files. They are kept and saved back untouched when the plugin is missing, and opening a design never fails because a plugin is missing. The storage capability will let plugins read and write them.