registry::remove released the plug-in set before it moved the revision, as install does not. A removal that finished just before another thread took the set could still move the revision while that thread held it, which made an_install_changes_the_revision_only_while_it_holds_the_set fail intermittently.
Installing a module that lists no permissions under a granted id now
forgets the grant, in the process and in grants.json, so the formerly
approved bytes need a new approval. A refused install leaves the grant.
Attaching a plug-in store forgets the grant of an id whose stored module
lists no permissions. A plug-in folder load forgets the process's grant
for such an id and leaves grants.json alone, because the store keeps the
module its grant covers.
A cache keyed by registry::revision could read the new revision before
the plug-in was in the set and keep the old list under it, so File > Open
and document.formats left out a newly installed importer until the next
install or removal. The revision now changes while install holds the
write lock, and a reader that sees it waits for the set.
A grant records the module's SHA-256, but only its permissions were
compared, so a module with other bytes under a granted id inherited the
grant when it listed no new permission.
A grant now covers a module only when the id, the SHA-256 and every
listed permission match. plugin.inspect reports needsApproval for a
changed module, plugin.install refuses it without a new grant and leaves
the installed module and its grant as they are, Install Plug-in and
File > Open ask again, and the plug-in store and plug-in folders skip it.
The same bytes install again without asking, and a module with no
permissions list or an empty one needs no grant. A grant whose hash is
not 64 lower-case hexadecimal digits covers nothing.
plugin.install still trusts the client that sends grant; the docs say so.
A manifest may list `permissions` (doc.read, doc.write, state,
effect.render, export.write) and an `api` level. doc.read, doc.write
and state follow the capability model proposed in
storytold/photocraft#662; the other names are VectorCraft's own. An
unknown, reserved, duplicate or wrong-kind name is refused by name. A
manifest without the key gets the permissions its kind implies and
installs as before.
- Install Plug-in... and File > Open show the name, id, version, file
SHA-256 and each permission in plain words, with control and
bidirectional characters escaped and each string cut to 64
characters. OK installs the module only if it still has the SHA-256
shown; Cancel installs nothing. The heavy-budget prompt and the
status line show the escaped name too, and OK keeps the `replace` it
was given.
- `plugin.install` needs `grant` listing every permission, and
`plugin.inspect` reports `needsApproval`. `hash` pins the module:
one with another SHA-256 is refused. The grant is kept with the
module's hash in the store's `grants.json`, and only that store's
grants count while it is attached; the same set does not ask again,
more permissions do, and `plugin.remove` forgets it.
- The Additional Plug-ins Folder and `plugin.reload` install only
plug-ins the attached store has granted and report the others as
needing approval. At startup the store skips an ungranted plug-in.
- A filter run without doc.read is given no objects; without state it
neither reads nor writes `documentData`; without doc.write an output
that changes the document is refused and the document is unchanged.
Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
A manifest may list `permissions` (doc.read, doc.write, state,
effect.render, export.write) and an `api` level. doc.read, doc.write
and state follow the capability model proposed in
storytold/photocraft#662; the other names are VectorCraft's own. An
unknown, reserved, duplicate or wrong-kind name is refused by name. A
manifest without the key gets the permissions its kind implies and
installs as before.
- Install Plug-in... and File > Open show the name, id, version, file
SHA-256 and each permission in plain words, with control and
bidirectional characters escaped and each string cut to 64
characters. OK installs the module only if it still has the SHA-256
shown; Cancel installs nothing. The heavy-budget prompt and the
status line show the escaped name too, and OK keeps the `replace` it
was given.
- `plugin.install` needs `grant` listing every permission, and
`plugin.inspect` reports `needsApproval`. `hash` pins the module:
one with another SHA-256 is refused. The grant is kept with the
module's hash in the store's `grants.json`, and only that store's
grants count while it is attached; the same set does not ask again,
more permissions do, and `plugin.remove` forgets it.
- The Additional Plug-ins Folder and `plugin.reload` install only
plug-ins the attached store has granted and report the others as
needing approval. At startup the store skips an ungranted plug-in.
- A filter run without doc.read is given no objects; without state it
neither reads nor writes `documentData`; without doc.write an output
that changes the document is refused and the document is unchanged.
Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
A filter plug-in gets 50 M instructions and a minute for each run.
Filters that order a plotter's paths, pack shapes or run a simulation
need minutes, and the app shouldn't freeze while they compute.
A filter's manifest may now ask for "budget": "heavy" (Budget): each run
gets HEAVY_FUEL (1 G instructions) and HEAVY_WALL_TIME_MS (300 s). Only a
filter may ask. plugin.install refuses such a filter without allowHeavy:
true, and plugin.inspect returns the entry a module would have without
installing it. Install Plug-in… and File › Open ask the user and install
on OK; the plug-in store keeps the answer, and folder loads skip heavy
filters with a note, since they ask nobody.
The app runs a heavy filter on a thread of its own (plugin_runs), not in
Background Save's queue, with the status bar and a progress window
(Cancel, Continue in Background) that it shares with Background Export
(export_progress::window). FilterRun splits plugin.run: prepared on the
session, run on any thread, its output handed in, and the next
plugin.run with the same params applies that output instead of running
the plug-in again, so the edit stays one guarded, journaled command. A
result whose document or plug-in changed meanwhile is dropped, and the
status bar says to run it again. A cancelled plug-in run is now
EngineError::Cancelled.
What a filter keeps with the document (#3720) goes with the run: it is
given _context.documentData when the run is prepared, and the
documentData of its output is applied with its objects, in the same undo
step, when the run is handed in (FilterRun::finish).
Tests: plugins/tests/abi.rs (100 M loop steps out of a light budget,
within a heavy one; folders skip heavy filters), manifest.rs, engine
tests_plugins.rs (consent, inspect, the store, hand-in, a handed-in
run's document data, cancel) and
ui-egui tests_plugin_runs.rs (installing asks, no Preview, a background
run as one undo step, a stale result kept out, the progress window).
CRAFT_UI_HEAVY_FILTER_WASM=<folder> writes a test filter to try in the
app. Its strings and messages have rows in each complete catalog
(COMPLETE_MESSAGES).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
vectorcraft-cli mcp and vectorcraft --control take the flags PhotoCraft
takes. While roots are in force, every path a command reads (open, place,
relink, SVG image links, library and profile loads, plug-ins, preset
imports, font searches) must lie inside the read root and every path it
writes (save, export, package, Export for Screens and for Web, print to
file, captures, library saves) inside the write root. Paths are made
absolute, links are followed (a missing file's nearest folder's), and the
result is compared by whole components, ignoring case on Windows; `..`
below a missing folder, dangling links and device names are refused. A
root left out grants none of its access; without the flags nothing
changes.
The checks live in one place, vectorcraft_engine::file_access, consulted
by the engine's file helpers and the app's file services. The headless
server confines its whole process; the app confines control requests,
the frames carrying input they inject and the background saves they
start, leaving the person at the keyboard and the app's own folders
alone. The screenshot tool now has the backend write its file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VectorCraft had an Additional Plug-ins Folder preference but nothing loaded from it. Port
PhotoCraft's wasmi plug-in host (same sandbox, manifest and parameter schema) as the new
vectorcraft-plugins crate (L2), extended for vector documents:
- Object filters read the selected paths and compound paths as JSON (native PathData and
Paint forms: geometry, fills, strokes, gradients, opacity) and return the objects to keep,
change, add or delete; the output is validated in full and applied as one undo step
(plugin.run). A failing plug-in leaves the document untouched.
- Live-effect plug-ins are geometry effects in the appearance stack (effect id
plugin.<id>), re-evaluated (and cached) when the object or parameters change, editable,
exported and baked by Expand Appearance; a missing plug-in leaves the geometry as is.
- plugin.list / info / run / install / remove / reload; the preference folder installs
every *.wasm at startup and when it changes; File > Open of a .wasm installs it (web).
- Object > Plug-ins and Effect > Plug-ins menus with dialogs generated from the parameter
schema (live preview), the Appearance panel's fx menu, docs/plugins.md, an original
example plug-in (Desaturate, a Rust crate outside the workspace) and fuzzing of the
plug-in boundary (engine/tests/plugin_fuzz.rs).
Tool plug-ins and paint-changing live effects are left for later (docs/plugins.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>