From 9ae57b03c0590cc264b62e0730d9f44be4ff8e73 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 21:10:26 -0800 Subject: [PATCH] Plug-ins: the texts say a grant accepted under the flag stays recorded A grant accepted under --allow-plugin-grants is written to grants.json like a person's approval and counts as approved in later sessions and in the desktop app. The CLI usage text, the plugin.install params text and docs/plugins.md and docs/mcp.md said only a person's approval in the desktop app records one. --- apps/vectorcraft-cli/src/main.rs | 6 ++++-- crates/engine/src/cmd/plugin.rs | 6 +++--- docs/mcp.md | 5 +++-- docs/plugins.md | 8 +++++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 7e66b0500..3c53c9d31 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -88,8 +88,10 @@ USAGE: enforces its own (vectorcraft --control PORT --automation-read-root DIR …). --allow-plugin-grants lets plugin.install's grant and allowHeavy take effect in this session, so whoever sends the commands can give a plug-in the permissions it lists, and a heavy budget, - with nobody approving them. Without it they are refused, and a plug-in installs only when a - person already approved its hash in the desktop app. It implies --headless. + with nobody approving them. Without it they are refused, and a plug-in that lists permissions + installs only when its hash is already recorded in grants.json, by a person's approval in the + desktop app or by a grant accepted under this flag. A grant accepted under this flag stays + recorded and counts as approved in later sessions. It implies --headless. vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N] [--ppi N] [--allow-plugin-grants] Headless batch: open FILE (any readable format) or start a new document, run commands in diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index 44375d685..1692e3bef 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -665,8 +665,8 @@ fn install(s: &mut Session, p: &Value) -> Result { ), )); } - // A plug-in that lists permissions installs once a person grants them, unless they already - // granted this very module all of them. + // A plug-in that lists permissions installs once they are granted, unless a grant recorded for + // this very module already covers all of them. let m = plugin.manifest(); let wanted = m.permissions(); if !s.plugin_approved(&plugin) && !grant.as_ref().is_some_and(|g| wanted.iter().all(|w| g.contains(w))) { @@ -976,7 +976,7 @@ pub fn specs() -> Vec { "Install Plug-in", [], None, - "{path: a .wasm file (desktop) | dataBase64: the module (+ name?: shown as its source), replace?: bool (true once the user agreed: a module with other bytes than the plug-in installed with its id, or with none installed, one whose id has settings or a cache in the plug-in store, installs only with it, takes that plug-in's place and gets its settings and cache; false: one whose id is installed is refused; left out, the same module installs again), name?: file name (with dataBase64), allowHeavy?: bool (a filter whose manifest asks for a heavy budget, budget \"heavy\" in plugin.inspect, installs only with it), grant?: [permission] (a plug-in whose manifest lists permissions, needsApproval in plugin.inspect, installs only when grant holds every one of them; the grant is recorded with the module's hash and covers only that module, so a module with other bytes needs a new grant even when it lists the same permissions; with a plug-in store attached, only the grants kept there count; a request's allowHeavy and grant are refused unless the session was started with vectorcraft-cli --allow-plugin-grants: in the desktop app a person approves the install, see ui.installPlugin, and a module whose hash a person already approved needs no grant), hash?: string (the module's SHA-256 as plugin.inspect gave it: a module with other bytes is refused)} validate and install a WebAssembly plug-in for this session → its plugin.list entry", + "{path: a .wasm file (desktop) | dataBase64: the module (+ name?: shown as its source), replace?: bool (true once the user agreed: a module with other bytes than the plug-in installed with its id, or with none installed, one whose id has settings or a cache in the plug-in store, installs only with it, takes that plug-in's place and gets its settings and cache; false: one whose id is installed is refused; left out, the same module installs again), name?: file name (with dataBase64), allowHeavy?: bool (a filter whose manifest asks for a heavy budget, budget \"heavy\" in plugin.inspect, installs only with it), grant?: [permission] (a plug-in whose manifest lists permissions, needsApproval in plugin.inspect, installs only when grant holds every one of them; the grant is recorded with the module's hash and covers only that module, so a module with other bytes needs a new grant even when it lists the same permissions; with a plug-in store attached, only the grants kept there count; a request's allowHeavy and grant are refused unless the session was started with vectorcraft-cli --allow-plugin-grants: in the desktop app a person approves the install, see ui.installPlugin, and a module whose hash is already recorded, by a person's approval or by a grant accepted under that flag, needs no grant), hash?: string (the module's SHA-256 as plugin.inspect gave it: a module with other bytes is refused)} validate and install a WebAssembly plug-in for this session → its plugin.list entry", always, install ), diff --git a/docs/mcp.md b/docs/mcp.md index fa154e43a..4f3dc0c77 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6709,8 +6709,9 @@ needs a person's approval too, and `plugin.install` refuses `grant: [permission] app connected, `ui.installPlugin` opens the approval dialog and returns `{pending: "pluginInstall"}`; `answer_dialog`, `ui.dialog.confirm` and injected input can't confirm it. A `vectorcraft-cli mcp` started with `--allow-plugin-grants` accepts `grant` and `allowHeavy` from any request, in that session only (headless; it can't be combined with -`--connect`). Without `--allow-plugin-grants` and without a desktop approval, `plugin.install` returns -an error naming the permissions needed. The approval is kept with the module's hash and covers only that module: the +`--connect`). Without `--allow-plugin-grants` and without a recorded grant for the module, `plugin.install` returns +an error naming the permissions needed. The approval, or a `grant` accepted under the flag, is kept with the module's +hash, counts as approved in later sessions and in the desktop app, and covers only that module: the same bytes install again without one, also from a headless session that reads the same `grants.json`, and a module with other bytes has `needsApproval: true` and needs a new one even when it lists the same permissions. `plugin.remove` forgets the approval. A diff --git a/docs/plugins.md b/docs/plugins.md index a0332ae38..dd0cbcb16 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -30,7 +30,7 @@ This page is the contract for plug-in authors and the reference for the host (`c | `plugin.list` | `{}` | `{"plugins": [{id, name, version, kind, author, description, params, paramsSchema, effect?, format?, extensions?, budget?, commands?, size, source, api, paintOutput?, permissions, hash, cacheBytes}], "folder": report}` (`api` is the manifest's [plug-in API level](#permissions); `budget: "heavy"` for a [heavy filter](#heavy-filters); `commands` lists its [commands](#commands); `paintOutput: true` marks a [painted live effect](#painted-live-effects); `permissions` lists what it [may do](#permissions), `hash` is the module's SHA-256; `cacheBytes` is the size of its [cache](#settings-and-cache)) | | `plugin.info` | `{"id"}` | One plug-in: the `plugin.list` entry plus `manifest`, `defaults` and `lastError` (a live effect's last failure). | | `plugin.run` | `{"id", "params"?: {…}, "ids"?: [...]}` (plug-in params may also be top-level keys) | Runs an object filter on the selection (or `ids`; an `ids` that is not a list of object ids is an error). One undo step; the result is selected, except the objects it locked or made guides. | -| `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace"?: bool`, `"allowHeavy": bool = false`, `"grant"?: [permission]`, `"hash"?: "sha256 hex"` | Validates and installs a module; returns its `plugin.list` entry. The desktop app and `vectorcraft-cli` keep it in the `Plug-ins` folder next to the preferences, so it is installed again after a restart (#2541). A module with other bytes than the plug-in installed with its id, or, with none installed, one whose id has [settings and a cache](#settings-and-cache) in the `Plug-ins` folder, installs only with `replace: true`, once the user agreed; it takes that plug-in's place and gets its settings and cache. The same module installs again without `replace`, and `replace: false` refuses a module whose id is installed. A [heavy filter](#heavy-filters) installs only with `allowHeavy: true`. A plug-in whose manifest lists [permissions](#permissions) installs only when `grant` lists every one of them, unless a person already granted a module with the same SHA-256. A request's `grant` and `allowHeavy` are refused except in a session started with `vectorcraft-cli --allow-plugin-grants`. With `hash`, a module whose SHA-256 differs is refused. | +| `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace"?: bool`, `"allowHeavy": bool = false`, `"grant"?: [permission]`, `"hash"?: "sha256 hex"` | Validates and installs a module; returns its `plugin.list` entry. The desktop app and `vectorcraft-cli` keep it in the `Plug-ins` folder next to the preferences, so it is installed again after a restart (#2541). A module with other bytes than the plug-in installed with its id, or, with none installed, one whose id has [settings and a cache](#settings-and-cache) in the `Plug-ins` folder, installs only with `replace: true`, once the user agreed; it takes that plug-in's place and gets its settings and cache. The same module installs again without `replace`, and `replace: false` refuses a module whose id is installed. A [heavy filter](#heavy-filters) installs only with `allowHeavy: true`. A plug-in whose manifest lists [permissions](#permissions) installs only when `grant` lists every one of them, unless a grant is already recorded for a module with the same SHA-256. A request's `grant` and `allowHeavy` are refused except in a session started with `vectorcraft-cli --allow-plugin-grants`. With `hash`, a module whose SHA-256 differs is refused. | | `plugin.inspect` | as `plugin.install`: `{"path"}` or `{"dataBase64", "name"?}` | Validates a module without installing it; returns the `plugin.list` entry it would have, plus `needsApproval` (true when installing it needs a person's approval) and `needsReplace` (true when `plugin.install` needs `replace: true`). | | `plugin.remove` | `{"id"}` | Uninstalls, and deletes it from the `Plug-ins` folder with its [settings and cache](#settings-and-cache). | | `plugin.clearCache` | `{"id"?}` | Deletes every entry in the [cache](#settings-and-cache) of plug-in `id`, or of every installed plug-in; settings stay. Returns `{"cleared": [id], "bytesFreed"}`. | @@ -198,12 +198,14 @@ capability model proposed for PhotoCraft plug-ins Only a person can press its OK: `ui.dialog.confirm`, MCP's `answer_dialog` and injected input (`ui.key`, `ui.click`) are refused and the dialog stays open. Enter confirms it only when OK has the keyboard focus. `ui.dialog.set` can't change what it installs. -- `plugin.install` refuses the module, naming the permissions it needs, unless a person already granted this module +- `plugin.install` refuses the module, naming the permissions it needs, unless this module already has a recorded grant (`plugin.inspect`'s `needsApproval` tells it beforehand). A request's `grant` is refused too: `plugin.install` takes it only with the approval the dialog gives. A `vectorcraft-cli mcp` or `run` started with `--allow-plugin-grants` takes `grant` and `allowHeavy` from any request; only the person starting the process can set that flag, and it doesn't apply to a running app. Without it, MCP in headless mode and the CLI install a module that lists - permissions only when a person approved its hash in the desktop app, which they read from the shared `grants.json`. + permissions only when its hash is recorded in the shared `grants.json`. A person's approval in the desktop app + records it, and so does a `grant` accepted under `--allow-plugin-grants`. A recorded grant counts as approved in + later sessions without the flag and in the desktop app. With `--allow-plugin-grants`, pass `hash` set to the `plugin.inspect` entry's `hash` so that the module installed is the one the user saw. - **Replacing asks first.** When the file is another module than the plug-in installed with its id, or, with none