Plug-ins: only a person can approve a plug-in's permissions and heavy budget

`plugin.install` accepted `grant` and `allowHeavy` from any client, so
the program installing a plug-in could answer its own permission
prompt.

The desktop app now shows an approval dialog that names the module,
its hash and what it asks for. Confirming it gives the engine a
one-use approval tied to that module id, hash and permission list. A
control or MCP request and a frame of synthetic input cannot confirm
the dialog, and Enter does not confirm it.

Without such an approval, `grant` and `allowHeavy` are refused. The
new `--allow-plugin-grants` flag on `mcp` and `run` lets a person who
starts a headless session accept them from requests. The flag cannot
be combined with `--connect`. A module already approved in the desktop
app still installs through its stored grant.
This commit is contained in:
dotCooCoo
2026-11-08 11:03:38 -08:00
parent 65a1f364f5
commit a4a516e82f
46 changed files with 565 additions and 159 deletions
+27 -6
View File
@@ -72,7 +72,7 @@ const USAGE: &str = "\
vectorcraft-cli — VectorCraft automation
USAGE:
vectorcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]...
vectorcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]... [--allow-plugin-grants]
Run the MCP server on stdio. Default: connect to a running app at 127.0.0.1:7979
(vectorcraft --control 7979), falling back to a headless in-process session.
--automation-read-root / --automation-write-root confine the files the agent's commands read
@@ -80,8 +80,12 @@ USAGE:
DIR, links followed; each may be given more than once, a path inside any of its folders being
allowed, and a kind left out grants none of its access. They imply --headless: a running app
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.
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N] [--ppi N]
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
order, export each FILE in the format its extension picks (see Writable formats; a .ait FILE
is a template, written as file.saveAsTemplate writes it). Raster exports use the document's
@@ -91,6 +95,8 @@ USAGE:
--cmd and --export steps: a JSON array of {\"command\": ID, \"params\": {...}} (or \"id\" for
\"command\"), or one such object per line (JSON Lines), at most 64 MiB and 100000 steps.
Errors name the step, counting from 0 as \"$N\" references do.
--allow-plugin-grants lets plugin.install's grant and allowHeavy take effect in this run, as
for mcp.
vectorcraft-cli commands
Print the command catalogue as JSON.
@@ -213,12 +219,21 @@ fn with_stores(mut h: Headless) -> Headless {
h
}
/// Makes `h`'s `plugin.install` accept `grant` and `allowHeavy` when `allow` (`--allow-plugin-grants`).
fn accepting_grants(mut h: Headless, allow: bool) -> Headless {
if allow {
h.session.allow_plugin_grants_from_requests();
}
h
}
fn mcp(args: &[String]) -> Result<(), String> {
if help_requested(args, &["--connect", "--automation-read-root", "--automation-write-root"]) {
return help();
}
let mut connect: Option<String> = None;
let mut headless = false;
let mut allow_grants = false;
let (mut read_roots, mut write_roots): (Vec<String>, Vec<String>) = (vec![], vec![]);
let mut it = args.iter();
while let Some(a) = it.next() {
@@ -230,6 +245,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
match flag {
"--connect" => connect = Some(it.next().cloned().ok_or("--connect needs an address")?),
"--headless" => headless = true,
"--allow-plugin-grants" => allow_grants = true,
"--automation-read-root" | "--automation-write-root" => {
let dir = inline.or_else(|| it.next().cloned()).ok_or_else(|| format!("{flag} needs a folder"))?;
if flag == "--automation-read-root" { read_roots.push(dir) } else { write_roots.push(dir) }
@@ -240,6 +256,9 @@ fn mcp(args: &[String]) -> Result<(), String> {
if headless && connect.is_some() {
return Err("use either --connect or --headless".into());
}
if allow_grants && connect.is_some() {
return Err("--allow-plugin-grants applies to the headless server; in a running app a person approves each plug-in".into());
}
let read: Vec<&Path> = read_roots.iter().map(Path::new).collect();
let write: Vec<&Path> = write_roots.iter().map(Path::new).collect();
let roots = AutomationRoots::new(&read, &write)?;
@@ -251,9 +270,9 @@ fn mcp(args: &[String]) -> Result<(), String> {
// Every thread of this process, not only the server's: nothing reaches past the roots.
file_access::confine_process(roots.clone());
eprintln!("vectorcraft-cli: files confined: {}", roots.describe());
Box::new(Headless::with_document().with_automation_roots(Some(roots)))
} else if headless {
Box::new(with_stores(Headless::with_document()))
Box::new(accepting_grants(Headless::with_document().with_automation_roots(Some(roots)), allow_grants))
} else if headless || allow_grants {
Box::new(accepting_grants(with_stores(Headless::with_document()), allow_grants))
} else if let Some(addr) = connect {
// Explicit address: fail loudly if the app isn't there.
Box::new(Remote::connect(&addr).map_err(|e| format!("cannot connect to {addr}: {e}"))?)
@@ -512,6 +531,7 @@ fn run(args: &[String]) -> Result<(), String> {
let mut stdin_read = false;
let mut scale = None::<f64>;
let mut ppi = None::<f64>;
let mut allow_grants = false;
let mut it = args.iter();
while let Some(a) = it.next() {
let mut val = |what: &str| it.next().cloned().ok_or_else(|| format!("{a} needs {what}"));
@@ -546,6 +566,7 @@ fn run(args: &[String]) -> Result<(), String> {
steps.push(Step::Export(val("a file")?));
params_for = None;
}
"--allow-plugin-grants" => allow_grants = true,
"--scale" | "-s" => {
let raw = val("a number")?;
scale = Some(raw.parse::<f64>().map_err(|_| format!("--scale {raw}: not a number"))?);
@@ -563,7 +584,7 @@ fn run(args: &[String]) -> Result<(), String> {
}
}
let mut h = with_stores(Headless::new());
let mut h = accepting_grants(with_stores(Headless::new()), allow_grants);
let mut out = std::io::stdout().lock();
// A reader that stopped early (a closed pipe) isn't an error, and the batch goes on: its later
// steps (an export…) still run, unprinted.
+25
View File
@@ -19,6 +19,31 @@ fn empty_dir(dir: std::path::PathBuf) -> std::path::PathBuf {
dir
}
/// `run` refuses `plugin.install`'s `grant` unless the process was started with
/// `--allow-plugin-grants`, which `mcp` won't take with `--connect`.
#[test]
fn plugin_grants_need_the_start_up_flag() {
let module = vectorcraft_format::base64_encode(include_bytes!("../../../../crates/plugins/tests/fixtures/desaturate.wasm"));
let steps = tmp("grants.json");
std::fs::write(&steps, json!([{"command": "plugin.install", "params": {"dataBase64": module, "grant": ["doc.read"]}}]).to_string()).unwrap();
let config = empty_dir(std::env::temp_dir().join(format!("vectorcraft-cli-test-grants-{}", std::process::id())));
let run = |extra: &[&str]| {
Command::new(BIN).env("VECTORCRAFT_CONFIG_DIR", &config).args(["run", "--commands", steps.to_str().unwrap()]).args(extra).output().unwrap()
};
let out = run(&[]);
assert!(!out.status.success());
let shown = String::from_utf8_lossy(&out.stderr).into_owned() + &String::from_utf8_lossy(&out.stdout);
assert!(shown.contains("--allow-plugin-grants"), "{shown}");
let out = run(&["--allow-plugin-grants"]);
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
let out = Command::new(BIN).args(["mcp", "--allow-plugin-grants", "--connect", "127.0.0.1:1"]).output().unwrap();
assert!(!out.status.success());
assert!(String::from_utf8_lossy(&out.stderr).contains("--allow-plugin-grants applies to the headless server"));
let usage = Command::new(BIN).arg("help").output().unwrap();
assert!(String::from_utf8_lossy(&usage.stdout).contains("--allow-plugin-grants lets plugin.install's grant and allowHeavy take effect"));
}
#[test]
fn commands_prints_catalogue() {
let out = Command::new(BIN).arg("commands").output().unwrap();
+75 -17
View File
@@ -10,15 +10,18 @@
//! exporters add a file format that `document.export` writes (`fileio::plugin_formats`).
//!
//! A filter may ask for a heavy budget (`Budget::Heavy`: minutes rather than a minute, for routing,
//! packing or simulation). It installs only once the user agrees (`plugin.install {allowHeavy}`),
//! which loading a folder doesn't ask, so folders skip it; the app runs it in the background with
//! Cancel ([`FilterRun`]).
//! packing or simulation). It installs only once a person agrees, which loading a folder doesn't
//! ask, so folders skip it; the app runs it in the background with Cancel ([`FilterRun`]).
//!
//! A plug-in whose manifest lists permissions installs once the user grants them
//! (`plugin.install {grant}`, recorded with the module's hash and kept in the plug-in store), and
//! a filter does only what they allow: without `doc.read` it is given no objects, without
//! `doc.write` an output that changes the document is refused, and without `state` it neither
//! reads nor writes the data it keeps with the document.
//! A plug-in whose manifest lists permissions installs once a person grants them (recorded with
//! the module's hash and kept in the plug-in store), and a filter does only what they allow:
//! without `doc.read` it is given no objects, without `doc.write` an output that changes the
//! document is refused, and without `state` it neither reads nor writes the data it keeps with
//! the document.
//!
//! `plugin.install {grant, allowHeavy}` takes effect only with an [`InstallApproval`], which no
//! command can set, or in a session started to accept them from requests
//! ([`Session::allow_plugin_grants_from_requests`]).
//!
//! A plug-in of API level 2 that lists `progress` may import the host's `vc_progress`,
//! `vc_cancelled` and `vc_log` ([`vectorcraft_plugins::host`]): run inside a
@@ -332,8 +335,48 @@ fn grant_param(c: &str, p: &Value) -> Result<Option<Vec<Permission>>> {
Ok(Some(out))
}
/// `plugin.install`: loads and checks the module, refuses one that asks for a heavy budget unless
/// `allowHeavy` says the user agreed, and one that lists permissions the user hasn't granted this
/// What a person approved for one install: the module they were shown (its id, SHA-256 and the
/// permissions it lists) and whether they agreed to its heavy budget. Only the code that asked the
/// person sets it ([`Session::approve_plugin_install`]); no command can.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InstallApproval {
pub id: String,
pub hash: String,
pub permissions: Vec<String>,
pub heavy: bool,
}
impl InstallApproval {
/// Is `plugin` the module this approval was given for?
fn covers(&self, plugin: &Plugin) -> bool {
let m = plugin.manifest();
self.id == m.id && self.hash == plugin.hash() && self.permissions.iter().map(String::as_str).eq(permissions::names(&m.permissions()))
}
}
impl Session {
/// Lets the next `plugin.install` take its `grant` and `allowHeavy`, for the module `approval`
/// describes. Call it only with what a person approved, just before that install.
pub fn approve_plugin_install(&mut self, approval: InstallApproval) {
self.plugin_install_approval = Some(approval);
}
/// Drops an approval that no install has spent.
pub fn clear_plugin_install_approval(&mut self) {
self.plugin_install_approval = None;
}
/// Makes `plugin.install` accept `grant` and `allowHeavy` from any request in this session.
/// For the person starting the process (`vectorcraft-cli --allow-plugin-grants`), never for
/// a request.
pub fn allow_plugin_grants_from_requests(&mut self) {
self.plugin_grants_from_requests = true;
}
}
/// `plugin.install`: loads and checks the module, refuses `grant` and `allowHeavy` that neither an
/// [`InstallApproval`] for this module nor the session allows, one that asks for a heavy budget
/// unless `allowHeavy` says a person agreed, one that lists permissions no person has granted this
/// very module unless `grant` lists them all, and one whose SHA-256 isn't the `hash` given; records
/// the grant with the module's hash, keeps it for the next start and installs it → its
/// `plugin.list` entry.
@@ -341,6 +384,8 @@ fn install(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "plugin.install";
let (replace, allow_heavy) = (switch_or(p, "replace", true, C)?, switch_or(p, "allowHeavy", false, C)?);
let grant = grant_param(C, p)?;
// One use: spent whatever this install does.
let approval = s.plugin_install_approval.take();
let (plugin, bytes) = load_given(C, p)?;
// `hash` pins the module the user was shown: one with other bytes is refused.
if let Some(h) = p.get("hash").filter(|v| !v.is_null())
@@ -351,25 +396,38 @@ fn install(s: &mut Session, p: &Value) -> Result<Value> {
if !replace && registry::get(plugin.id()).is_some() {
return Err(bad(C, format!("a plug-in with id {:?} is already installed", plugin.id())));
}
// A heavy plug-in is installed once the user agrees: whoever installs it asks them first.
// `grant` and `allowHeavy` say that a person agreed, and a request alone can't say that.
let approval = approval.filter(|a| a.covers(&plugin));
if !s.plugin_grants_from_requests && ((grant.is_some() && approval.is_none()) || (allow_heavy && !approval.is_some_and(|a| a.heavy))) {
return Err(bad(
C,
"`grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app",
));
}
// A heavy plug-in is installed once a person agrees.
if plugin.manifest().budget == Budget::Heavy && !allow_heavy {
let (minutes, times) = vectorcraft_plugins::heavy_terms();
return Err(bad(
C,
format!(
"“{}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees",
"“{}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants",
plugin.manifest().name
),
));
}
// A plug-in that lists permissions installs once the user grants them: whoever installs it
// asks them first, unless they already granted this very module all of them.
// A plug-in that lists permissions installs once a person grants them, unless they already
// granted this very module 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))) {
let name = permissions::display_text(&m.name, permissions::MAX_SHOWN_NAME);
let asked = permissions::names(&wanted).join(", ");
return Err(bad(C, format!("“{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees")));
return Err(bad(
C,
format!(
"“{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants"
),
));
}
if m.asks_permissions() {
let g = Grant { id: m.id.clone(), hash: plugin.hash().to_string(), permissions: wanted };
@@ -527,7 +585,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Install Plug-in",
[],
None,
"{path: a .wasm file (desktop) | dataBase64: the module (+ name?: shown as its source), replace?: bool (true: a plug-in with the same id is replaced), name?: file name (with dataBase64), allowHeavy?: bool (true once the user agreed to a heavy budget: a filter whose manifest asks for one, budget \"heavy\" in plugin.inspect, installs only with it), grant?: [permission] (the permissions the user granted: 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), 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: a plug-in with the same id is replaced), 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",
always,
install
),
@@ -536,7 +594,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Inspect Plug-in",
[],
None,
"{path: a .wasm file (desktop) | dataBase64: the module, name?: the file name shown as its source (with dataBase64)} validate a WebAssembly plug-in without installing it → the plugin.list entry it would have, plus needsApproval: bool (true when it lists permissions the user hasn't granted this very module, its bytes included: plugin.install then needs grant)",
"{path: a .wasm file (desktop) | dataBase64: the module, name?: the file name shown as its source (with dataBase64)} validate a WebAssembly plug-in without installing it → the plugin.list entry it would have, plus needsApproval: bool (true when it lists permissions no person has granted this very module, its bytes included: it then needs a person's approval, see plugin.install)",
always,
inspect
),
+6
View File
@@ -132,6 +132,12 @@ pub struct Session {
/// A filter's output that a run made in the background handed in for `plugin.run` to apply
/// ([`cmd::plugin::FilterRun::hand_in`]); the next `plugin.run` takes it.
plugin_output: Option<cmd::plugin::HandedIn>,
/// What a person approved for the next `plugin.install` ([`Session::approve_plugin_install`]);
/// that install spends it, whatever it does.
plugin_install_approval: Option<cmd::plugin::InstallApproval>,
/// `plugin.install` takes `grant` and `allowHeavy` from any request
/// ([`Session::allow_plugin_grants_from_requests`]).
plugin_grants_from_requests: bool,
/// URLs recently given in the Attributes panel (`attributes.set {url}`), newest first; not saved.
pub recent_urls: Vec<String>,
/// The saved tracing presets' revision and latest changes ([`Session::trace_presets_revision`]).
+2
View File
@@ -71,6 +71,8 @@ impl Session {
profile_dir: None,
plugin_dir: None,
plugin_output: None,
plugin_install_approval: None,
plugin_grants_from_requests: false,
recent_urls: vec![],
trace_preset_changes: Default::default(),
ui_language: None,
+92
View File
@@ -25,7 +25,15 @@ fn install_wat(s: &mut Session, src: &str) -> Value {
install(s, &wat::parse_str(src).unwrap())
}
/// A session started to take `grant` and `allowHeavy` from requests, as most tests here install.
fn session() -> Session {
let mut s = strict_session();
s.allow_plugin_grants_from_requests();
s
}
/// A session as MCP and the CLI have by default: `grant` and `allowHeavy` need an approval.
fn strict_session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 200})).unwrap();
s
@@ -1073,6 +1081,90 @@ fn a_level_2_filter_installs_with_progress_granted_and_reports_to_the_caller() {
run(&mut s, "plugin.remove", json!({"id": id}));
}
/// An approval of the module `wasm` with id `id`, its `permissions` and, if `heavy`, its budget.
fn approval(id: &str, wasm: &[u8], permissions: &[&str], heavy: bool) -> crate::cmd::plugin::InstallApproval {
crate::cmd::plugin::InstallApproval {
id: id.into(),
hash: vectorcraft_plugins::permissions::sha256_hex(wasm),
permissions: permissions.iter().map(|p| p.to_string()).collect(),
heavy,
}
}
/// In a session that wasn't started to accept them, `grant` and `allowHeavy` are refused whichever
/// way the command runs, a batch included, until an approval covers the module, its id, SHA-256 and
/// permissions. An approval is spent by the first install, whatever it does. A hash already approved
/// still installs.
#[test]
fn a_request_cannot_grant_what_only_a_person_approves() {
let mut s = strict_session();
let id = "test.engine.approval";
let wasm = asking(id, r#"["doc.read"]"#, &json!({"objects": []}));
let data = vectorcraft_format::base64_encode(&wasm);
let refused = "take effect only in a session started with --allow-plugin-grants";
let e = s.execute("plugin.install", &json!({"dataBase64": data, "grant": ["doc.read"]})).unwrap_err().to_string();
assert!(e.contains(refused), "{e}");
let e = s.execute("plugin.install", &json!({"dataBase64": data})).unwrap_err().to_string();
assert!(e.contains("approve them in the desktop app") && e.contains("--allow-plugin-grants"), "{e}");
let batch = json!({"commands": [{"command": "plugin.install", "params": {"dataBase64": data, "grant": ["doc.read"]}}]});
let r = s.execute("command.batch", &batch);
assert!(r.as_ref().map_or(true, |v| v.to_string().contains(refused)), "a batch can't grant: {r:?}");
assert!(vectorcraft_plugins::registry::get(id).is_none());
// Approved for other bytes, for another id or for other permissions: refused, and spent.
let other = asking(id, r#"["doc.read"]"#, &json!({"objects": [{"name": "x"}]}));
for wrong in [
approval(id, &other, &["doc.read"], false),
approval("test.engine.approval.other", &wasm, &["doc.read"], false),
approval(id, &wasm, &["doc.read", "state"], false),
] {
s.approve_plugin_install(wrong);
let e = s.execute("plugin.install", &json!({"dataBase64": data, "grant": ["doc.read"]})).unwrap_err().to_string();
assert!(e.contains(refused), "{e}");
}
assert!(vectorcraft_plugins::registry::get(id).is_none());
// The approval for this module installs it once.
s.approve_plugin_install(approval(id, &wasm, &["doc.read"], false));
run(&mut s, "plugin.install", json!({"dataBase64": data, "grant": ["doc.read"]}));
assert_eq!(vectorcraft_plugins::registry::granted(id).map(|g| g.hash), Some(vectorcraft_plugins::permissions::sha256_hex(&wasm)));
let e = s.execute("plugin.install", &json!({"dataBase64": data, "grant": ["doc.read"]})).unwrap_err().to_string();
assert!(e.contains(refused), "the approval was spent: {e}");
// A hash already approved installs without one.
run(&mut s, "plugin.install", json!({"dataBase64": data}));
run(&mut s, "plugin.remove", json!({"id": id}));
// A refused install spends the approval too.
s.approve_plugin_install(approval(id, &wasm, &["doc.read"], false));
let e = s.execute("plugin.install", &json!({"dataBase64": data, "grant": ["doc.read"], "hash": "0"})).unwrap_err().to_string();
assert!(e.contains("SHA-256"), "{e}");
let e = s.execute("plugin.install", &json!({"dataBase64": data, "grant": ["doc.read"]})).unwrap_err().to_string();
assert!(e.contains(refused), "{e}");
// The heavy budget needs its own agreement.
let heavy_id = "test.engine.approval.heavy";
let heavy_wasm = heavy(heavy_id, 1);
let heavy_data = vectorcraft_format::base64_encode(&heavy_wasm);
let e = s.execute("plugin.install", &json!({"dataBase64": heavy_data, "allowHeavy": true})).unwrap_err().to_string();
assert!(e.contains(refused), "{e}");
// Without a `permissions` list it has the ones its kind implies.
let inspected = run(&mut s, "plugin.inspect", json!({"dataBase64": heavy_data}));
let implied: Vec<&str> = inspected["permissions"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
s.approve_plugin_install(approval(heavy_id, &heavy_wasm, &implied, false));
let e = s.execute("plugin.install", &json!({"dataBase64": heavy_data, "allowHeavy": true})).unwrap_err().to_string();
assert!(e.contains(refused), "an approval without the heavy budget doesn't cover it: {e}");
let e = s.execute("plugin.install", &json!({"dataBase64": heavy_data})).unwrap_err().to_string();
assert!(e.contains("approve it in the desktop app"), "{e}");
s.approve_plugin_install(approval(heavy_id, &heavy_wasm, &implied, true));
run(&mut s, "plugin.install", json!({"dataBase64": heavy_data, "allowHeavy": true}));
run(&mut s, "plugin.remove", json!({"id": heavy_id}));
// A session started to accept them takes them from requests.
s.allow_plugin_grants_from_requests();
run(&mut s, "plugin.install", json!({"dataBase64": data, "grant": ["doc.read"]}));
run(&mut s, "plugin.remove", json!({"id": id}));
}
/// A filter run the caller cancels (MCP's `notifications/cancelled`, the app's Cancel) stops between
/// two slices of its budget and changes nothing.
#[test]
+1
View File
@@ -308,6 +308,7 @@ proptest! {
prop_assert!(shown.chars().count() <= vectorcraft_plugins::permissions::MAX_SHOWN_NAME);
}
let mut s = rich_session();
s.allow_plugin_grants_from_requests();
let module = wat::parse_str(tpl::returning(&text, r#"{"objects":[]}"#)).unwrap();
let data = vectorcraft_format::base64_encode(&module);
let r = catch_quiet(|| {
+1
View File
@@ -165,6 +165,7 @@ impl VectorcraftApp {
scrub_scope: None,
automation_roots: None,
synthetic_frame: false,
pending_plugin_install: None,
pen_end: Default::default(),
host_iteration: 0,
#[cfg(test)]
+7 -1
View File
@@ -24,12 +24,18 @@ impl VectorcraftApp {
/// control or MCP request, or a frame of synthetic input): the automation roots don't confine
/// what that app does.
pub(crate) fn refuse_automation(&self, id: &str) -> Result<(), String> {
if self.ui.answering_control || self.synthetic_frame {
if self.automation_is_acting() {
return Err(format!("{id} opens another app: only the person at the keyboard can run it"));
}
Ok(())
}
/// Did automation send what is being handled now: a control or MCP request, or a frame of
/// synthetic input?
pub(crate) fn automation_is_acting(&self) -> bool {
self.ui.answering_control || self.synthetic_frame
}
/// Run a Help link command and open the URL it returns.
pub fn open_link(&mut self, id: &str) {
if let Ok(v) = self.run(id, serde_json::json!({}))
+4
View File
@@ -390,6 +390,7 @@ registry! {
PrintPresets: [print_presets::KIND] => print_presets::SPEC,
Plugin: [plugin::KIND] => plugin::SPEC,
PluginExport: [plugin::EXPORT_KIND] => plugin::EXPORT_SPEC,
PluginInstall: [plugin::INSTALL_KIND] => plugin::INSTALL_SPEC,
ScriptCheck: [script_check::KIND] => script_check::SPEC,
VectorHalftone: [halftone::KIND] => halftone::SPEC,
SimplifySlider: [simplify_slider::KIND] => simplify_slider::SPEC,
@@ -546,6 +547,9 @@ pub fn cancel(app: &mut VectorcraftApp) {
if d.kind == swatch_conflict::KIND {
crate::placing::cancel_swatch_paste(app);
}
if d.kind == plugin::INSTALL_KIND {
app.pending_plugin_install = None;
}
// Toolbar-only Flare options start no preview and must not cancel a gesture
// that was already active before opening them. The generic parameter dialog has a spec
// without a preview, yet previews some commands (Grid Repeat Options) when asked to.
+74 -1
View File
@@ -9,16 +9,22 @@
//! it runs in the background ([`crate::plugin_runs`]). Exporters are formats of File › Export ›
//! Export As; one with parameters opens dialog `pluginExport` once the file is picked, and its
//! Export button writes the file.
//!
//! Dialog `pluginInstall` asks a person to approve a plug-in's permissions or heavy budget. What it
//! shows and installs is held by the app, not in its fields; a control request or synthetic input
//! can't confirm it, and Enter doesn't.
use std::sync::Arc;
use serde_json::{Map, Value, json};
use vectorcraft_engine::cmd::plugin::InstallApproval;
use vectorcraft_plugins::{Budget, Kind, Manifest, Plugin, registry};
use super::{DialogSpec, form};
use super::{DialogAction, DialogSpec, form};
use crate::VectorcraftApp;
use crate::menus::Item;
use crate::state::Dialog;
use crate::widgets;
/// The dialog kind of plug-in filters.
pub const KIND: &str = "plugin";
@@ -197,3 +203,70 @@ fn export_confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String>
app.ui.dialog = None;
crate::io::export(app, format.as_deref(), path, &p)
}
/// The dialog kind of the approval of a plug-in install.
pub const INSTALL_KIND: &str = "pluginInstall";
/// A plug-in install waiting for a person's answer.
pub(crate) struct PendingInstall {
/// The question and the lines under it, in the UI language.
pub message: String,
pub detail: String,
/// `plugin.install`'s params for OK.
pub params: Value,
/// What OK approves: the module the question is about.
pub approval: InstallApproval,
}
/// No OK in the shared footer, so Enter does nothing.
pub(super) const INSTALL_SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Install Plug-in…").trim_end_matches('…').to_string(),
body_with_actions: Some(install_body),
confirm: install_confirm,
ok: None,
max_width: Some(420.0),
..DialogSpec::FORM
};
/// Asks a person whether to install what `pending` describes. → `{pending}`.
pub(crate) fn ask_install(app: &mut VectorcraftApp, pending: PendingInstall) -> Value {
app.pending_plugin_install = Some(pending);
app.ui.dialog = Some(Dialog::new(INSTALL_KIND, json!({})));
json!({ "pending": INSTALL_KIND })
}
fn install_body(app: &mut VectorcraftApp, ui: &mut egui::Ui, _: &mut Dialog) -> DialogAction {
let Some(pending) = app.pending_plugin_install.as_ref() else { return DialogAction::Cancel };
ui.label(egui::RichText::new(&pending.message).strong());
ui.add_space(8.0);
ui.label(egui::RichText::new(&pending.detail).color(crate::theme::Tokens::get(ui.ctx()).text_dim));
ui.add_space(16.0);
let mut action = DialogAction::None;
let row = egui::vec2(ui.min_rect().width(), ui.spacing().interact_size.y);
ui.allocate_ui_with_layout(row, egui::Layout::right_to_left(egui::Align::Center), |ui| {
if widgets::primary_button_enabled(ui, "OK", true).clicked() {
action = DialogAction::Confirm;
}
ui.add_space(8.0);
if widgets::secondary_button(ui, "Cancel").clicked() {
action = DialogAction::Cancel;
}
});
action
}
/// OK: installs the module the dialog showed, if a person pressed it; automation is refused and the
/// dialog stays open.
fn install_confirm(app: &mut VectorcraftApp, _: &Dialog) -> Result<Value, String> {
if app.automation_is_acting() {
return Err("Only the person at the keyboard can approve installing a plug-in".into());
}
app.ui.dialog = None;
let Some(pending) = app.pending_plugin_install.take() else { return Ok(Value::Null) };
app.session.approve_plugin_install(pending.approval);
let r = app.run("plugin.install", pending.params);
app.session.clear_plugin_install_approval();
let r = r?;
crate::io::report_installed(app, &r);
Ok(r)
}
@@ -13,6 +13,12 @@ use vectorcraft_plugins::wat as tpl;
use super::*;
/// The install approval has no OK in the shared footer: Enter, real or injected, doesn't press it.
#[test]
fn enter_does_not_answer_the_install_approval() {
assert!(plugin::INSTALL_SPEC.ok.is_none() && plugin::INSTALL_SPEC.body_with_actions.is_some());
}
/// A filter whose parameters use every presentation key.
fn manifest(id: &str) -> String {
format!(
+4 -2
View File
@@ -5737,7 +5737,7 @@ axis Y Axis Y osa
@msg Running “{name}”… Pokretanje „{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` živi efekat radi dok se dokument iscrtava, pa ne može biti `heavy`
@msg an exporter always gets an export's budget: leave `budget` out izvoznik uvijek dobija budžet izvoza: izostavite `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}” traži veliki budžet (svako pokretanje do {minutes} minuta i {times} puta više uobičajenih instrukcija): instalirajte ga s `allowHeavy: true` kada korisnik pristane
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants „{_1}” traži veliki budžet (svako pokretanje do {minutes} minuta i {times} puta više uobičajenih instrukcija): osoba ga mora odobriti u desktop aplikaciji, ili proslijedite `allowHeavy: true` u sesiji pokrenutoj s --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Instalirati „{name}”, koji traži ove dozvole?
ID: {id} ID: {id}
@@ -5762,7 +5762,9 @@ axis Y Axis Y osa
@msg permission `{shown}` is listed twice dozvola `{shown}` navedena je dvaput
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it dozvola `{shown}` rezervisana je za kasniji API dodataka; ovaj domaćin je ne dodjeljuje
@msg unknown permission `{shown}` nepoznata dozvola `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees „{name}” traži dozvole {asked}: instalirajte ga s `grant` koji ih navodi kada korisnik pristane
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants „{name}” traži dozvole {asked}: osoba ih mora odobriti u desktop aplikaciji, ili proslijedite `grant` u sesiji pokrenutoj s --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` i `allowHeavy` važe samo u sesiji pokrenutoj s --allow-plugin-grants: odobrite dodatak u desktop aplikaciji
@msg Only the person at the keyboard can approve installing a plug-in Instalaciju dodatka može odobriti samo osoba za tastaturom
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed „{name}” nije dobio `{p}`, što je potrebno njegovom izlazu: ništa nije promijenjeno
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed modul nema SHA-256 naveden u `hash`: ništa nije instalirano
New Set… Novi skup…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5855,7 +5855,7 @@ digits Malayalam Malaiàlam
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg {tool} isn't in a toolbar group of several tools {tool} no forma part d’un grup de diverses eines a la barra d’eines
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees «{_1}» demana un pressupost elevat (cada execució fins a {minutes} minuts i {times} vegades les instruccions habituals): instal·la’l amb `allowHeavy: true` quan l’usuari hi estigui d’acord
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants «{_1}» demana un pressupost elevat (cada execució fins a {minutes} minuts i {times} vegades les instruccions habituals): una persona l’ha d’aprovar a l’aplicació d’escriptori, o passa `allowHeavy: true` en una sessió iniciada amb --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Instal·la «{name}», que demana aquests permisos?
ID: {id} ID: {id}
@@ -5880,7 +5880,9 @@ digits Malayalam Malaiàlam
@msg permission `{shown}` is listed twice el permís `{shown}` apareix dues vegades
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it el permís `{shown}` està reservat per a una API de complements posterior; aquest amfitrió no el concedeix
@msg unknown permission `{shown}` permís desconegut `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees «{name}» demana els permisos {asked}: instal·la’l amb un `grant` que els enumeri quan l’usuari hi estigui d’acord
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants «{name}» demana els permisos {asked}: una persona els ha d’aprovar a l’aplicació d’escriptori, o passa `grant` en una sessió iniciada amb --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` i `allowHeavy` només tenen efecte en una sessió iniciada amb --allow-plugin-grants: aprova el complement a l’aplicació d’escriptori
@msg Only the person at the keyboard can approve installing a plug-in Només la persona al teclat pot aprovar la instal·lació d’un complement
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed a «{name}» no se li ha concedit `{p}`, que la seva sortida necessita: no s’ha canviat res
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed el mòdul no té el SHA-256 que indica `hash`: no s’ha instal·lat res
@msg “{name}” is not a file «{name}» no és un fitxer
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5841,7 +5841,7 @@ axis Y Axis osa Y
@msg Running “{name}”… Spouští se „{name}“…
@msg a live effect runs while the document is drawn, so it can't be `heavy` živý efekt běží během vykreslování dokumentu, takže nemůže být `heavy`
@msg an exporter always gets an export's budget: leave `budget` out exportní zásuvný modul dostává vždy rozpočet exportu: klíč `budget` vynechte
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}“ žádá o velký výpočetní rozpočet (každé spuštění až {minutes} minut a {times}krát víc instrukcí než obvykle): nainstalujte ho s `allowHeavy: true`, jakmile uživatel souhlasí
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants „{_1}“ žádá o velký výpočetní rozpočet (každé spuštění až {minutes} minut a {times}krát víc instrukcí než obvykle): musí ho schválit člověk v desktopové aplikaci, nebo předejte `allowHeavy: true` v relaci spuštěné s --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Nainstalovat „{name}“, který žádá o tato oprávnění?
ID: {id} ID: {id}
@@ -5866,7 +5866,9 @@ axis Y Axis osa Y
@msg permission `{shown}` is listed twice oprávnění `{shown}` je uvedeno dvakrát
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it oprávnění `{shown}` je vyhrazeno pro pozdější API zásuvných modulů; tento hostitel ho neuděluje
@msg unknown permission `{shown}` neznámé oprávnění `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees „{name}“ žádá o oprávnění {asked}: nainstalujte ho s `grant`, který je uvádí, jakmile uživatel souhlasí
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants „{name}“ žádá o oprávnění {asked}: musí je schválit člověk v desktopové aplikaci, nebo předejte `grant` v relaci spuštěné s --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` a `allowHeavy` platí jen v relaci spuštěné s --allow-plugin-grants: schvalte zásuvný modul v desktopové aplikaci
@msg Only the person at the keyboard can approve installing a plug-in Instalaci zásuvného modulu může schválit jen člověk u klávesnice
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed „{name}“ nedostal `{p}`, které jeho výstup potřebuje: nic se nezměnilo
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed modul nemá SHA-256 uvedený v `hash`: nic nebylo nainstalováno
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5423,7 +5423,7 @@ axis Y Axis Y-Achse
@msg Running “{name}”… „{name}“ wird ausgeführt…
@msg a live effect runs while the document is drawn, so it can't be `heavy` ein Live-Effekt läuft, während das Dokument gezeichnet wird, daher kann er nicht `heavy` sein
@msg an exporter always gets an export's budget: leave `budget` out ein Export-Plug-in erhält immer das Budget eines Exports: Lassen Sie `budget` weg
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}“ verlangt ein großes Rechenbudget (jede Ausführung bis zu {minutes} Minuten und {times}-mal so viele Anweisungen wie üblich): Installieren Sie es mit `allowHeavy: true`, sobald der Benutzer zustimmt
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants „{_1}“ verlangt ein großes Rechenbudget (jede Ausführung bis zu {minutes} Minuten und {times}-mal so viele Anweisungen wie üblich): Eine Person muss es in der Desktop-App genehmigen, oder übergeben Sie `allowHeavy: true` in einer mit --allow-plugin-grants gestarteten Sitzung
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? „{name}“ installieren, das diese Berechtigungen verlangt?
ID: {id} ID: {id}
@@ -5448,7 +5448,9 @@ axis Y Axis Y-Achse
@msg permission `{shown}` is listed twice die Berechtigung `{shown}` ist doppelt aufgeführt
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it die Berechtigung `{shown}` ist für eine spätere Plug-in-API reserviert; dieser Host gewährt sie nicht
@msg unknown permission `{shown}` unbekannte Berechtigung `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees „{name}“ verlangt die Berechtigungen {asked}: Installieren Sie es mit `grant`, das sie aufführt, sobald der Benutzer zustimmt
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants „{name}“ verlangt die Berechtigungen {asked}: Eine Person muss sie in der Desktop-App genehmigen, oder übergeben Sie `grant` in einer mit --allow-plugin-grants gestarteten Sitzung
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` und `allowHeavy` wirken nur in einer mit --allow-plugin-grants gestarteten Sitzung: Genehmigen Sie das Plug-in in der Desktop-App
@msg Only the person at the keyboard can approve installing a plug-in Nur die Person an der Tastatur kann die Installation eines Plug-ins genehmigen
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed „{name}“ wurde `{p}` nicht gewährt, das seine Ausgabe braucht: Es wurde nichts geändert
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed das Modul hat nicht den SHA-256-Wert aus `hash`: Es wurde nichts installiert
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5792,7 +5792,7 @@ axis Y Axis Άξονας Y
@msg Running “{name}”… Εκτέλεση του «{name}»…
@msg a live effect runs while the document is drawn, so it can't be `heavy` ένα δυναμικό εφέ εκτελείται κατά τη σχεδίαση του εγγράφου, οπότε δεν μπορεί να είναι `heavy`
@msg an exporter always gets an export's budget: leave `budget` out ένα πρόσθετο εξαγωγής παίρνει πάντα τον προϋπολογισμό μιας εξαγωγής: παραλείψτε το `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees το «{_1}» ζητά μεγάλο υπολογιστικό προϋπολογισμό (κάθε εκτέλεση έως {minutes} λεπτά και {times} φορές τις συνήθεις εντολές): εγκαταστήστε το με `allowHeavy: true` μόλις συμφωνήσει ο χρήστης
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants το «{_1}» ζητά μεγάλο υπολογιστικό προϋπολογισμό (κάθε εκτέλεση έως {minutes} λεπτά και {times} φορές τις συνήθεις εντολές): πρέπει να το εγκρίνει ένα άτομο στην εφαρμογή επιφάνειας εργασίας ή περάστε `allowHeavy: true` σε μια συνεδρία που ξεκίνησε με --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Να εγκατασταθεί το «{name}», που ζητά αυτά τα δικαιώματα;
ID: {id} Αναγνωριστικό: {id}
@@ -5817,7 +5817,9 @@ axis Y Axis Άξονας Y
@msg permission `{shown}` is listed twice το δικαίωμα `{shown}` αναφέρεται δύο φορές
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it το δικαίωμα `{shown}` είναι δεσμευμένο για μεταγενέστερο API προσθέτων· αυτός ο κεντρικός υπολογιστής δεν το παραχωρεί
@msg unknown permission `{shown}` άγνωστο δικαίωμα `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees το «{name}» ζητά τα δικαιώματα {asked}: εγκαταστήστε το με ένα `grant` που τα απαριθμεί μόλις συμφωνήσει ο χρήστης
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants το «{name}» ζητά τα δικαιώματα {asked}: πρέπει να τα εγκρίνει ένα άτομο στην εφαρμογή επιφάνειας εργασίας ή περάστε `grant` σε μια συνεδρία που ξεκίνησε με --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app τα `grant` και `allowHeavy` ισχύουν μόνο σε συνεδρία που ξεκίνησε με --allow-plugin-grants: εγκρίνετε το πρόσθετο στην εφαρμογή επιφάνειας εργασίας
@msg Only the person at the keyboard can approve installing a plug-in Μόνο το άτομο στο πληκτρολόγιο μπορεί να εγκρίνει την εγκατάσταση ενός πρόσθετου
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed στο «{name}» δεν παραχωρήθηκε το `{p}`, που χρειάζεται η έξοδός του: δεν άλλαξε τίποτα
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed η λειτουργική μονάδα δεν έχει το SHA-256 που δίνει το `hash`: δεν εγκαταστάθηκε τίποτα
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 168 and column 5.
+4 -2
View File
@@ -5782,7 +5782,7 @@ axis Y Axis Eje Y
@msg Running “{name}”… Ejecutando “{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` un efecto interactivo se ejecuta mientras se dibuja el documento, así que no puede ser `heavy`
@msg an exporter always gets an export's budget: leave `budget` out un plugin de exportación siempre recibe el presupuesto de una exportación: omite `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” pide un presupuesto de cálculo grande (cada ejecución hasta {minutes} minutos y {times} veces las instrucciones habituales): instálalo con `allowHeavy: true` cuando el usuario lo acepte
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants “{_1}” pide un presupuesto de cálculo grande (cada ejecución hasta {minutes} minutos y {times} veces las instrucciones habituales): una persona debe aprobarlo en la aplicación de escritorio, o pasa `allowHeavy: true` en una sesión iniciada con --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? ¿Instalar “{name}”, que pide estos permisos?
ID: {id} ID: {id}
@@ -5807,7 +5807,9 @@ axis Y Axis Eje Y
@msg permission `{shown}` is listed twice el permiso `{shown}` aparece dos veces
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it el permiso `{shown}` está reservado para una API de plugins posterior; este host no lo concede
@msg unknown permission `{shown}` permiso desconocido `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees “{name}” pide los permisos {asked}: instálalo con un `grant` que los incluya cuando el usuario lo acepte
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants “{name}” pide los permisos {asked}: una persona debe aprobarlos en la aplicación de escritorio, o pasa `grant` en una sesión iniciada con --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` y `allowHeavy` solo tienen efecto en una sesión iniciada con --allow-plugin-grants: aprueba el complemento en la aplicación de escritorio
@msg Only the person at the keyboard can approve installing a plug-in Solo la persona que está en el teclado puede aprobar la instalación de un complemento
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed a “{name}” no se le concedió `{p}`, que su resultado necesita: no se cambió nada
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed el módulo no tiene el SHA-256 que indica `hash`: no se instaló nada
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5871,7 +5871,7 @@ axis Y Axis Y-akseli
@msg Running “{name}”… Suoritetaan ”{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` live-tehoste suoritetaan asiakirjaa piirrettäessä, joten se ei voi olla `heavy`
@msg an exporter always gets an export's budget: leave `budget` out vientilaajennus saa aina viennin budjetin: jätä `budget` pois
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees ”{_1}” pyytää suurta laskentabudjettia (jokainen ajo enintään {minutes} minuuttia ja {times} kertaa tavallinen määrä käskyjä): asenna se asetuksella `allowHeavy: true`, kun käyttäjä hyväksyy
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants ”{_1}” pyytää suurta laskentabudjettia (jokainen ajo enintään {minutes} minuuttia ja {times} kertaa tavallinen määrä käskyjä): ihmisen on hyväksyttävä se työpöytäsovelluksessa, tai anna `allowHeavy: true` istunnossa, joka on käynnistetty valitsimella --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Asennetaanko ”{name}”, joka pyytää näitä käyttöoikeuksia?
ID: {id} Tunnus: {id}
@@ -5896,7 +5896,9 @@ axis Y Axis Y-akseli
@msg permission `{shown}` is listed twice käyttöoikeus `{shown}` on lueteltu kahdesti
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it käyttöoikeus `{shown}` on varattu myöhemmälle laajennus-API:lle; tämä isäntä ei myönnä sitä
@msg unknown permission `{shown}` tuntematon käyttöoikeus `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees ”{name}” pyytää käyttöoikeuksia {asked}: asenna se `grant`-luettelolla, jossa ne ovat, kun käyttäjä suostuu
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants ”{name}” pyytää käyttöoikeuksia {asked}: ihmisen on hyväksyttävä ne työpöytäsovelluksessa, tai anna `grant` istunnossa, joka on käynnistetty valitsimella --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` ja `allowHeavy` toimivat vain istunnossa, joka on käynnistetty valitsimella --allow-plugin-grants: hyväksy laajennus työpöytäsovelluksessa
@msg Only the person at the keyboard can approve installing a plug-in Vain näppäimistön ääressä oleva ihminen voi hyväksyä laajennuksen asennuksen
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed ”{name}” ei saanut käyttöoikeutta `{p}`, jota sen tulos tarvitsee: mitään ei muutettu
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed moduulilla ei ole `hash`-kentän SHA-256-arvoa: mitään ei asennettu
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5836,7 +5836,7 @@ axis Y Axis Axe Y
@msg Running “{name}”… Exécution de « {name} »…
@msg a live effect runs while the document is drawn, so it can't be `heavy` un effet dynamique s’exécute pendant le tracé du document, il ne peut donc pas être `heavy`
@msg an exporter always gets an export's budget: leave `budget` out un module externe d’exportation reçoit toujours le budget d’une exportation : omettez `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees « {_1} » demande un budget de calcul important (chaque exécution jusqu’à {minutes} minutes et {times} fois les instructions habituelles) : installez-le avec `allowHeavy: true` une fois que l’utilisateur l’accepte
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants « {_1} » demande un budget de calcul important (chaque exécution jusqu’à {minutes} minutes et {times} fois les instructions habituelles) : une personne doit l’approuver dans l’application de bureau, ou passez `allowHeavy: true` dans une session démarrée avec --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Installer « {name} », qui demande ces autorisations ?
ID: {id} ID : {id}
@@ -5861,7 +5861,9 @@ axis Y Axis Axe Y
@msg permission `{shown}` is listed twice l’autorisation `{shown}` figure deux fois
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it l’autorisation `{shown}` est réservée à une future API de module externe ; cet hôte ne l’accorde pas
@msg unknown permission `{shown}` autorisation inconnue `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees « {name} » demande les autorisations {asked} : installez-le avec `grant` qui les liste une fois que l’utilisateur l’accepte
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants « {name} » demande les autorisations {asked} : une personne doit les approuver dans l’application de bureau, ou passez `grant` dans une session démarrée avec --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` et `allowHeavy` ne prennent effet que dans une session démarrée avec --allow-plugin-grants : approuvez le module externe dans l’application de bureau
@msg Only the person at the keyboard can approve installing a plug-in Seule la personne au clavier peut approuver l’installation d’un module externe
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed « {name} » n’a pas reçu `{p}`, dont sa sortie a besoin : rien n’a été modifié
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed le module n’a pas le SHA-256 indiqué par `hash` : rien n’a été installé
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5212,7 +5212,7 @@
Zoom with Mouse Wheel माउस व्हील के साथ ज़ूम
@id file.close बंद करें
@id pathfinder.add मिलाएँ
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” अधिक संसाधन माँगता है (हर रन में {minutes} मिनट और सामान्य से {times} गुना निर्देश): उपयोगकर्ता की सहमति मिलने पर इसे `allowHeavy: true` से इंस्टॉल करें
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants “{_1}” अधिक संसाधन माँगता है (हर रन में {minutes} मिनट और सामान्य से {times} गुना निर्देश): इसे डेस्कटॉप ऐप में किसी व्यक्ति को स्वीकृत करना होगा, या --allow-plugin-grants से शुरू किए गए सत्र में `allowHeavy: true` दें
@msg "{0}" is not a colour: write it as #rrggbb "{0}" एक रंग नहीं है: इसे #rrggbb के रूप में लिखें
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? "{name}" स्थापित करें, जो ये अनुमतियां मांगता है?
@@ -5238,7 +5238,9 @@
@msg permission `{shown}` is listed twice अनुमति `{shown}` दो बार सूचीबद्ध है
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it अनुमति `{shown}` बाद के प्लग-इन API के लिए आरक्षित है; यह होस्ट इसे नहीं देता
@msg unknown permission `{shown}` अज्ञात अनुमति `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees “{name}” अनुमतियां {asked} मांगता है: उपयोगकर्ता की सहमति मिलने पर इसे उन्हें सूचीबद्ध करने वाले `grant` के साथ इंस्टॉल करें
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants “{name}” अनुमतियां {asked} मांगता है: इन्हें डेस्कटॉप ऐप में किसी व्यक्ति को स्वीकृत करना होगा, या --allow-plugin-grants से शुरू किए गए सत्र में `grant` दें
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` और `allowHeavy` केवल --allow-plugin-grants से शुरू किए गए सत्र में प्रभावी होते हैं: डेस्कटॉप ऐप में प्लग-इन स्वीकृत करें
@msg Only the person at the keyboard can approve installing a plug-in प्लग-इन इंस्टॉल करने की स्वीकृति केवल कीबोर्ड पर मौजूद व्यक्ति दे सकता है
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed “{name}” को `{p}` नहीं दी गई, जो उसके आउटपुट को चाहिए: कुछ नहीं बदला
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed मॉड्यूल का SHA-256 वह नहीं है जो `hash` बताता है: कुछ भी इंस्टॉल नहीं हुआ
@msg “{name}” is not a file "{name}" एक फ़ाइल नहीं है
Can't render this file because it is too large.
+4 -2
View File
@@ -5824,7 +5824,7 @@ axis Y Axis Y tengely
@msg Running “{name}”… Futtatás: „{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` az élő effektus a dokumentum rajzolása közben fut, ezért nem lehet `heavy`
@msg an exporter always gets an export's budget: leave `budget` out az exportáló bővítmény mindig az exportálás keretét kapja: hagyd ki a `budget` kulcsot
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees a(z) „{_1}” nagy számítási keretet kér (futásonként legfeljebb {minutes} perc és a szokásos utasításszám {times}-szorosa): telepítsd az `allowHeavy: true` megadásával, miután a felhasználó beleegyezett
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants a(z) „{_1}” nagy számítási keretet kér (futásonként legfeljebb {minutes} perc és a szokásos utasításszám {times}-szorosa): egy személynek jóvá kell hagynia az asztali alkalmazásban, vagy add meg az `allowHeavy: true` értéket egy --allow-plugin-grants kapcsolóval indított munkamenetben
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Telepíted a(z) „{name}” bővítményt, amely ezeket az engedélyeket kéri?
ID: {id} Azonosító: {id}
@@ -5849,7 +5849,9 @@ axis Y Axis Y tengely
@msg permission `{shown}` is listed twice a(z) `{shown}` engedély kétszer szerepel
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it a(z) `{shown}` engedély egy későbbi bővítmény-API számára van fenntartva; ez a gazdaprogram nem adja meg
@msg unknown permission `{shown}` ismeretlen engedély: `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees a(z) „{name}” a következő engedélyeket kéri: {asked}; telepítsd egy őket felsoroló `grant` értékkel, ha a felhasználó beleegyezik
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants a(z) „{name}” a következő engedélyeket kéri: {asked}; egy személynek jóvá kell hagynia őket az asztali alkalmazásban, vagy add meg a `grant` értéket egy --allow-plugin-grants kapcsolóval indított munkamenetben
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app a `grant` és az `allowHeavy` csak --allow-plugin-grants kapcsolóval indított munkamenetben érvényes: hagyd jóvá a bővítményt az asztali alkalmazásban
@msg Only the person at the keyboard can approve installing a plug-in Bővítmény telepítését csak a billentyűzetnél ülő személy hagyhatja jóvá
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed a(z) „{name}” nem kapta meg a(z) `{p}` engedélyt, amelyre a kimenetének szüksége van: semmi sem változott
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed a modul SHA-256 értéke nem az, amit a `hash` megad: semmi sem lett telepítve
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5457,7 +5457,7 @@ axis Y Axis Asse Y
@msg Running “{name}”… Esecuzione di “{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` un effetto dinamico viene eseguito mentre il documento viene disegnato, quindi non può essere `heavy`
@msg an exporter always gets an export's budget: leave `budget` out un plug-in di esportazione riceve sempre il budget di un’esportazione: ometti `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” chiede un budget di calcolo elevato (ogni esecuzione fino a {minutes} minuti e {times} volte le istruzioni consuete): installalo con `allowHeavy: true` quando l’utente acconsente
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants “{_1}” chiede un budget di calcolo elevato (ogni esecuzione fino a {minutes} minuti e {times} volte le istruzioni consuete): una persona deve approvarlo nell’app desktop, oppure passa `allowHeavy: true` in una sessione avviata con --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Installare “{name}”, che chiede queste autorizzazioni?
ID: {id} ID: {id}
@@ -5482,7 +5482,9 @@ axis Y Axis Asse Y
@msg permission `{shown}` is listed twice l’autorizzazione `{shown}` è elencata due volte
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it l’autorizzazione `{shown}` è riservata a un’API dei plug-in successiva; questo host non la concede
@msg unknown permission `{shown}` autorizzazione sconosciuta `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees “{name}” chiede le autorizzazioni {asked}: installalo con un `grant` che le elenca quando l’utente acconsente
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants “{name}” chiede le autorizzazioni {asked}: una persona deve approvarle nell’app desktop, oppure passa `grant` in una sessione avviata con --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` e `allowHeavy` hanno effetto solo in una sessione avviata con --allow-plugin-grants: approva il plug-in nell’app desktop
@msg Only the person at the keyboard can approve installing a plug-in Solo la persona alla tastiera può approvare l’installazione di un plug-in
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed a “{name}” non è stata concessa `{p}`, che serve al suo risultato: non è stato modificato nulla
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed il modulo non ha lo SHA-256 indicato da `hash`: non è stato installato nulla
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5522,7 +5522,7 @@ axis Y Axis Y 軸
@msg Running “{name}”… 「{name}」を実行中…
@msg a live effect runs while the document is drawn, so it can't be `heavy` ライブ効果はドキュメントの描画中に実行されるため、`heavy` にはできません
@msg an exporter always gets an export's budget: leave `budget` out 書き出しプラグインには常に書き出しの予算が割り当てられます。`budget` は省略してください
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees 「{_1}」は大きな計算予算を求めています (1 回の実行につき最大 {minutes} 分、通常の {times} 倍の命令)。ユーザーが同意したら `allowHeavy: true` を指定してインストールしてください
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants 「{_1}」は大きな計算予算を求めています (1 回の実行につき最大 {minutes} 分、通常の {times} 倍の命令)。デスクトップ アプリで人が承認するか、--allow-plugin-grants を付けて開始したセッションで `allowHeavy: true` を指定してください
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? これらの権限を求める「{name}」をインストールしますか?
ID: {id} ID: {id}
@@ -5547,7 +5547,9 @@ axis Y Axis Y 軸
@msg permission `{shown}` is listed twice 権限 `{shown}` が 2 回指定されています
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it 権限 `{shown}` は今後のプラグイン API 用に予約されています。このホストはこの権限を付与しません
@msg unknown permission `{shown}` 不明な権限 `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees 「{name}」は権限 {asked} を求めています。ユーザーが同意したら、それらを列挙した `grant` を指定してインストールしてください
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants 「{name}」は権限 {asked} を求めています。デスクトップ アプリで人が承認するか、--allow-plugin-grants を付けて開始したセッションで `grant` を指定してください
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` と `allowHeavy` は --allow-plugin-grants を付けて開始したセッションでのみ有効です。デスクトップ アプリでプラグインを承認してください
@msg Only the person at the keyboard can approve installing a plug-in プラグインのインストールを承認できるのはキーボードの前にいる人だけです
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed 「{name}」には出力に必要な `{p}` が付与されていません。何も変更されませんでした
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed モジュールの SHA-256 が `hash` の値と一致しません。何もインストールされませんでした
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5764,7 +5764,7 @@ axis Y Axis Y ღერძი
@msg Running “{name}”… მიმდინარეობს „{name}“-ის გაშვება…
@msg a live effect runs while the document is drawn, so it can't be `heavy` ცოცხალი ეფექტი დოკუმენტის დახატვისას ეშვება, ამიტომ ის ვერ იქნება `heavy`
@msg an exporter always gets an export's budget: leave `budget` out ექსპორტის პლაგინი ყოველთვის ექსპორტის ბიუჯეტს იღებს: გამოტოვეთ `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}“ დიდ გამოთვლით ბიუჯეტს ითხოვს (თითო გაშვება {minutes} წუთამდე და ჩვეულებრივზე {times}-ჯერ მეტი ინსტრუქცია): მომხმარებლის თანხმობის შემდეგ დააყენეთ ის `allowHeavy: true` პარამეტრით
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants „{_1}“ დიდ გამოთვლით ბიუჯეტს ითხოვს (თითო გაშვება {minutes} წუთამდე და ჩვეულებრივზე {times}-ჯერ მეტი ინსტრუქცია): ის დესკტოპის აპში ადამიანმა უნდა დაამტკიცოს, ან გადაეცით `allowHeavy: true` --allow-plugin-grants-ით გაშვებულ სესიაში
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? დავაყენოთ „{name}“, რომელიც ამ ნებართვებს ითხოვს?
ID: {id} ID: {id}
@@ -5789,7 +5789,9 @@ axis Y Axis Y ღერძი
@msg permission `{shown}` is listed twice ნებართვა `{shown}` ორჯერაა ჩამოთვლილი
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it ნებართვა `{shown}` დაჯავშნილია პლაგინების მომავალი API-სთვის; ეს ჰოსტი მას არ გასცემს
@msg unknown permission `{shown}` უცნობი ნებართვა `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees „{name}“ ითხოვს ნებართვებს {asked}: მომხმარებლის თანხმობის შემდეგ დააყენეთ ის `grant` პარამეტრით, რომელშიც ისინი ჩამოთვლილია
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants „{name}“ ითხოვს ნებართვებს {asked}: ისინი დესკტოპის აპში ადამიანმა უნდა დაამტკიცოს, ან გადაეცით `grant` --allow-plugin-grants-ით გაშვებულ სესიაში
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` და `allowHeavy` მოქმედებს მხოლოდ --allow-plugin-grants-ით გაშვებულ სესიაში: დაამტკიცეთ დამატება დესკტოპის აპში
@msg Only the person at the keyboard can approve installing a plug-in დამატების დაყენებას მხოლოდ კლავიატურასთან მყოფი ადამიანი ამტკიცებს
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed „{name}“-ს არ მიეცა `{p}`, რომელიც მის შედეგს სჭირდება: არაფერი შეცვლილა
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed მოდულს არ აქვს `hash`-ში მითითებული SHA-256: არაფერი დაყენებულა
# Actions panel: action sets (#3712).
Can't render this file because it is too large.
+4 -2
View File
@@ -5418,7 +5418,7 @@ axis Y Axis Y축
@msg Running “{name}”… “{name}” 실행 중…
@msg a live effect runs while the document is drawn, so it can't be `heavy` 라이브 효과는 문서를 그리는 동안 실행되므로 `heavy`일 수 없습니다
@msg an exporter always gets an export's budget: leave `budget` out 내보내기 플러그인은 항상 내보내기 예산을 받습니다. `budget`은 생략하세요
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}”은(는) 큰 계산 예산을 요청합니다(실행당 최대 {minutes}분, 평소의 {times}배 명령어). 사용자가 동의하면 `allowHeavy: true`로 설치하세요
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants “{_1}”은(는) 큰 계산 예산을 요청합니다(실행당 최대 {minutes}분, 평소의 {times}배 명령어). 데스크톱 앱에서 사람이 승인하거나 --allow-plugin-grants로 시작한 세션에서 `allowHeavy: true`를 전달하세요
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? 이 권한을 요청하는 “{name}”을(를) 설치하시겠습니까?
ID: {id} ID: {id}
@@ -5443,7 +5443,9 @@ axis Y Axis Y축
@msg permission `{shown}` is listed twice `{shown}` 권한이 두 번 나열되어 있습니다
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it `{shown}` 권한은 이후 플러그인 API용으로 예약되어 있습니다. 이 호스트는 이 권한을 부여하지 않습니다
@msg unknown permission `{shown}` 알 수 없는 권한 `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees “{name}”은(는) {asked} 권한을 요청합니다. 사용자가 동의하면 이를 나열한 `grant`로 설치하세요
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants “{name}”은(는) {asked} 권한을 요청합니다. 데스크톱 앱에서 사람이 승인하거나 --allow-plugin-grants로 시작한 세션에서 `grant`를 전달하세요
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant`와 `allowHeavy`는 --allow-plugin-grants로 시작한 세션에서만 적용됩니다. 데스크톱 앱에서 플러그인을 승인하세요
@msg Only the person at the keyboard can approve installing a plug-in 플러그인 설치는 키보드 앞에 있는 사람만 승인할 수 있습니다
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed “{name}”에 출력에 필요한 `{p}` 권한이 부여되지 않았습니다. 아무것도 변경되지 않았습니다
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed 모듈의 SHA-256이 `hash`에 지정된 값과 다릅니다. 아무것도 설치되지 않았습니다
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5694,7 +5694,7 @@ axis Y Axis Oś Y
@msg Running “{name}”… Uruchamianie „{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` efekt na żywo działa podczas rysowania dokumentu, więc nie może być `heavy`
@msg an exporter always gets an export's budget: leave `budget` out wtyczka eksportu zawsze dostaje budżet eksportu: pomiń `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}” prosi o duży budżet obliczeń (każde uruchomienie do {minutes} minut i {times} razy więcej instrukcji niż zwykle): zainstaluj wtyczkę z `allowHeavy: true`, gdy użytkownik się zgodzi
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants „{_1}” prosi o duży budżet obliczeń (każde uruchomienie do {minutes} minut i {times} razy więcej instrukcji niż zwykle): musi ją zatwierdzić osoba w aplikacji desktopowej albo przekaż `allowHeavy: true` w sesji uruchomionej z --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Zainstalować „{name}”, wtyczkę, która prosi o te uprawnienia?
ID: {id} ID: {id}
@@ -5719,7 +5719,9 @@ axis Y Axis Oś Y
@msg permission `{shown}` is listed twice uprawnienie `{shown}` występuje dwa razy
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it uprawnienie `{shown}` jest zarezerwowane dla późniejszego API wtyczek; ten host go nie przyznaje
@msg unknown permission `{shown}` nieznane uprawnienie `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees „{name}” prosi o uprawnienia {asked}: zainstaluj ją z `grant`, który je wymienia, gdy użytkownik się zgodzi
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants „{name}” prosi o uprawnienia {asked}: musi je zatwierdzić osoba w aplikacji desktopowej albo przekaż `grant` w sesji uruchomionej z --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` i `allowHeavy` działają tylko w sesji uruchomionej z --allow-plugin-grants: zatwierdź wtyczkę w aplikacji desktopowej
@msg Only the person at the keyboard can approve installing a plug-in Instalację wtyczki może zatwierdzić tylko osoba przy klawiaturze
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed „{name}” nie otrzymała `{p}`, którego potrzebuje jej wynik: nic nie zmieniono
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed moduł nie ma skrótu SHA-256 podanego w `hash`: nic nie zainstalowano
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5756,7 +5756,7 @@ axis Y Axis Eixo Y
@msg Running “{name}”… Executando “{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` um efeito dinâmico é executado enquanto o documento é desenhado, então não pode ser `heavy`
@msg an exporter always gets an export's budget: leave `budget` out um plug-in de exportação sempre recebe o orçamento de uma exportação: omita `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” pede um orçamento de processamento grande (cada execução até {minutes} minutos e {times} vezes as instruções habituais): instale-o com `allowHeavy: true` depois que o usuário concordar
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants “{_1}” pede um orçamento de processamento grande (cada execução até {minutes} minutos e {times} vezes as instruções habituais): uma pessoa precisa aprová-lo no aplicativo de desktop, ou passe `allowHeavy: true` em uma sessão iniciada com --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Instalar “{name}”, que pede estas permissões?
ID: {id} ID: {id}
@@ -5781,7 +5781,9 @@ axis Y Axis Eixo Y
@msg permission `{shown}` is listed twice a permissão `{shown}` aparece duas vezes
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it a permissão `{shown}` está reservada para uma API de plug-in futura; este host não a concede
@msg unknown permission `{shown}` permissão desconhecida `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees “{name}” pede as permissões {asked}: instale-o com `grant` listando-as depois que o usuário concordar
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants “{name}” pede as permissões {asked}: uma pessoa precisa aprová-las no aplicativo de desktop, ou passe `grant` em uma sessão iniciada com --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` e `allowHeavy` só têm efeito em uma sessão iniciada com --allow-plugin-grants: aprove o plug-in no aplicativo de desktop
@msg Only the person at the keyboard can approve installing a plug-in Somente a pessoa ao teclado pode aprovar a instalação de um plug-in
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed “{name}” não recebeu `{p}`, de que sua saída precisa: nada foi alterado
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed o módulo não tem o SHA-256 indicado em `hash`: nada foi instalado
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4 -2
View File
@@ -5452,7 +5452,7 @@ axis Y Axis Ось Y
@msg Running “{name}”… Выполняется «{name}»…
@msg a live effect runs while the document is drawn, so it can't be `heavy` динамический эффект выполняется во время отрисовки документа, поэтому он не может быть `heavy`
@msg an exporter always gets an export's budget: leave `budget` out модуль экспорта всегда получает бюджет экспорта: не указывайте `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees «{_1}» запрашивает большой вычислительный бюджет (каждый запуск до {minutes} минут и в {times} раз больше инструкций, чем обычно): установите его с `allowHeavy: true`, когда пользователь согласится
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants «{_1}» запрашивает большой вычислительный бюджет (каждый запуск до {minutes} минут и в {times} раз больше инструкций, чем обычно): его должен одобрить человек в настольном приложении, либо передайте `allowHeavy: true` в сеансе, запущенном с --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Установить «{name}», которому нужны эти разрешения?
ID: {id} ID: {id}
@@ -5477,7 +5477,9 @@ axis Y Axis Ось Y
@msg permission `{shown}` is listed twice разрешение `{shown}` указано дважды
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it разрешение `{shown}` зарезервировано для будущего API модулей; этот хост его не выдаёт
@msg unknown permission `{shown}` неизвестное разрешение `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees «{name}» запрашивает разрешения {asked}: установите его с `grant`, где они перечислены, когда пользователь согласится
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants «{name}» запрашивает разрешения {asked}: их должен одобрить человек в настольном приложении, либо передайте `grant` в сеансе, запущенном с --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` и `allowHeavy` действуют только в сеансе, запущенном с --allow-plugin-grants: одобрите плагин в настольном приложении
@msg Only the person at the keyboard can approve installing a plug-in Одобрить установку плагина может только человек за клавиатурой
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed «{name}» не получил разрешение `{p}`, нужное для его результата: ничего не изменено
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed у модуля не тот SHA-256, что указан в `hash`: ничего не установлено
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5864,7 +5864,7 @@ axis Y Axis Y-axel
@msg Running “{name}”… Kör ”{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` en live-effekt körs medan dokumentet ritas, så den kan inte vara `heavy`
@msg an exporter always gets an export's budget: leave `budget` out ett exportinsticksprogram får alltid en exports budget: utelämna `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees ”{_1}” begär en stor beräkningsbudget (varje körning upp till {minutes} minuter och {times} gånger de vanliga instruktionerna): installera det med `allowHeavy: true` när användaren har godkänt
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants ”{_1}” begär en stor beräkningsbudget (varje körning upp till {minutes} minuter och {times} gånger de vanliga instruktionerna): en person måste godkänna det i skrivbordsappen, eller skicka `allowHeavy: true` i en session som startats med --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Installera ”{name}”, som begär dessa behörigheter?
ID: {id} ID: {id}
@@ -5889,7 +5889,9 @@ axis Y Axis Y-axel
@msg permission `{shown}` is listed twice behörigheten `{shown}` står två gånger
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it behörigheten `{shown}` är reserverad för ett senare plugin-API; den här värden beviljar den inte
@msg unknown permission `{shown}` okänd behörighet `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees ”{name}” begär behörigheterna {asked}: installera det med en `grant` som listar dem när användaren har godkänt
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants ”{name}” begär behörigheterna {asked}: en person måste godkänna dem i skrivbordsappen, eller skicka `grant` i en session som startats med --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` och `allowHeavy` gäller bara i en session som startats med --allow-plugin-grants: godkänn insticksprogrammet i skrivbordsappen
@msg Only the person at the keyboard can approve installing a plug-in Bara personen vid tangentbordet kan godkänna installationen av ett insticksprogram
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed ”{name}” beviljades inte `{p}`, som dess utdata behöver: ingenting ändrades
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed modulen har inte den SHA-256 som `hash` anger: ingenting installerades
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4 -2
View File
@@ -5798,7 +5798,7 @@ axis Y Axis Y ekseni
@msg Running “{name}”… “{name}” çalıştırılıyor…
@msg a live effect runs while the document is drawn, so it can't be `heavy` canlı efekt belge çizilirken çalışır, bu yüzden `heavy` olamaz
@msg an exporter always gets an export's budget: leave `budget` out dışa aktarma eklentisi her zaman bir dışa aktarmanın bütçesini alır: `budget` anahtarını çıkarın
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” büyük bir hesaplama bütçesi istiyor (her çalıştırma en fazla {minutes} dakika ve olağan komutların {times} katı): kullanıcı onayladıktan sonra `allowHeavy: true` ile yükleyin
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants “{_1}” büyük bir hesaplama bütçesi istiyor (her çalıştırma en fazla {minutes} dakika ve olağan komutların {times} katı): masaüstü uygulamasında bir kişinin onaylaması gerekir veya --allow-plugin-grants ile başlatılan bir oturumda `allowHeavy: true` geçirin
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Bu izinleri isteyen “{name}” yüklensin mi?
ID: {id} Kimlik: {id}
@@ -5823,7 +5823,9 @@ axis Y Axis Y ekseni
@msg permission `{shown}` is listed twice `{shown}` izni iki kez listelenmiş
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it `{shown}` izni sonraki bir eklenti API’si için ayrılmıştır; bu ana bilgisayar bu izni vermez
@msg unknown permission `{shown}` bilinmeyen izin `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees “{name}” {asked} izinlerini istiyor: kullanıcı onayladıktan sonra bunları listeleyen bir `grant` ile yükleyin
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants “{name}” {asked} izinlerini istiyor: masaüstü uygulamasında bir kişinin onaylaması gerekir veya --allow-plugin-grants ile başlatılan bir oturumda `grant` geçirin
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` ve `allowHeavy` yalnızca --allow-plugin-grants ile başlatılan bir oturumda etkili olur: eklentiyi masaüstü uygulamasında onaylayın
@msg Only the person at the keyboard can approve installing a plug-in Bir eklentinin yüklenmesini yalnızca klavye başındaki kişi onaylayabilir
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed “{name}” öğesine çıktısının ihtiyaç duyduğu `{p}` izni verilmedi: hiçbir şey değiştirilmedi
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed modül `hash` içinde verilen SHA-256 değerine sahip değil: hiçbir şey yüklenmedi
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4 -2
View File
@@ -5437,7 +5437,7 @@ axis Y Axis Вісь Y
@msg Running “{name}”… Виконується «{name}»…
@msg a live effect runs while the document is drawn, so it can't be `heavy` динамічний ефект виконується під час відмальовування документа, тому він не може бути `heavy`
@msg an exporter always gets an export's budget: leave `budget` out модуль експорту завжди отримує бюджет експорту: не вказуйте `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees «{_1}» запитує великий обчислювальний бюджет (кожен запуск до {minutes} хвилин і в {times} разів більше інструкцій, ніж зазвичай): встановіть його з `allowHeavy: true`, коли користувач погодиться
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants «{_1}» запитує великий обчислювальний бюджет (кожен запуск до {minutes} хвилин і в {times} разів більше інструкцій, ніж зазвичай): його має схвалити людина в настільному застосунку, або передайте `allowHeavy: true` у сеансі, запущеному з --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Встановити «{name}», якому потрібні ці дозволи?
ID: {id} ID: {id}
@@ -5462,7 +5462,9 @@ axis Y Axis Вісь Y
@msg permission `{shown}` is listed twice дозвіл `{shown}` указано двічі
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it дозвіл `{shown}` зарезервовано для пізнішого API модулів; цей хост його не надає
@msg unknown permission `{shown}` невідомий дозвіл `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees «{name}» запитує дозволи {asked}: установіть його з `grant`, де їх перелічено, коли користувач погодиться
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants «{name}» запитує дозволи {asked}: їх має схвалити людина в настільному застосунку, або передайте `grant` у сеансі, запущеному з --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` і `allowHeavy` діють лише в сеансі, запущеному з --allow-plugin-grants: схваліть плагін у настільному застосунку
@msg Only the person at the keyboard can approve installing a plug-in Схвалити встановлення плагіна може лише людина за клавіатурою
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed «{name}» не отримав дозволу `{p}`, потрібного для його результату: нічого не змінено
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed модуль має не той SHA-256, що вказаний у `hash`: нічого не встановлено
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 182 and column 5.
+4 -2
View File
@@ -5438,7 +5438,7 @@ axis Y Axis Trục Y
@msg Running “{name}”… Đang chạy “{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` hiệu ứng trực tiếp chạy trong lúc tài liệu được vẽ nên không thể là `heavy`
@msg an exporter always gets an export's budget: leave `budget` out plug-in xuất luôn nhận ngân sách của một lần xuất: hãy bỏ `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” yêu cầu ngân sách tính toán lớn (mỗi lần chạy tới {minutes} phút và gấp {times} lần số lệnh thông thường): hãy cài đặt với `allowHeavy: true` khi người dùng đồng ý
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants “{_1}” yêu cầu ngân sách tính toán lớn (mỗi lần chạy tới {minutes} phút và gấp {times} lần số lệnh thông thường): cần một người phê duyệt trong ứng dụng máy tính, hoặc truyền `allowHeavy: true` trong phiên khởi động bằng --allow-plugin-grants
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? Cài đặt “{name}”, plug-in yêu cầu các quyền này?
ID: {id} ID: {id}
@@ -5463,7 +5463,9 @@ axis Y Axis Trục Y
@msg permission `{shown}` is listed twice quyền `{shown}` được liệt kê hai lần
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it quyền `{shown}` được dành cho một API plug-in sau này; máy chủ này không cấp quyền đó
@msg unknown permission `{shown}` quyền không xác định `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees “{name}” yêu cầu các quyền {asked}: hãy cài đặt với `grant` liệt kê chúng khi người dùng đồng ý
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants “{name}” yêu cầu các quyền {asked}: cần một người phê duyệt trong ứng dụng máy tính, hoặc truyền `grant` trong phiên khởi động bằng --allow-plugin-grants
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` và `allowHeavy` chỉ có hiệu lực trong phiên khởi động bằng --allow-plugin-grants: hãy phê duyệt trình cắm trong ứng dụng máy tính
@msg Only the person at the keyboard can approve installing a plug-in Chỉ người ngồi tại bàn phím mới có thể phê duyệt việc cài đặt trình cắm
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed “{name}” chưa được cấp `{p}`, quyền mà đầu ra của nó cần: không có gì thay đổi
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed mô-đun không có SHA-256 mà `hash` cho biết: không có gì được cài đặt
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 204 and column 5.
+4 -2
View File
@@ -5679,7 +5679,7 @@ axis Y Axis Y 轴
@msg Running “{name}”… 正在运行“{name}”…
@msg a live effect runs while the document is drawn, so it can't be `heavy` 实时效果在绘制文档时运行,因此不能为 `heavy`
@msg an exporter always gets an export's budget: leave `budget` out 导出增效工具始终使用导出的预算:请省略 `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}”需要大量计算预算(每次运行最多 {minutes} 分钟,指令数为通常的 {times} 倍):请在用户同意后使用 `allowHeavy: true` 安装
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants “{_1}”需要大量计算预算(每次运行最多 {minutes} 分钟,指令数为通常的 {times} 倍):需要有人在桌面应用中批准,或在以 --allow-plugin-grants 启动的会话中传入 `allowHeavy: true`
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? 要安装需要这些权限的“{name}”吗?
ID: {id} ID:{id}
@@ -5704,7 +5704,9 @@ axis Y Axis Y 轴
@msg permission `{shown}` is listed twice 权限 `{shown}` 列出了两次
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it 权限 `{shown}` 保留给以后的增效工具 API;此主机不授予该权限
@msg unknown permission `{shown}` 未知权限 `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees “{name}”需要权限 {asked}:请在用户同意后使用列出这些权限的 `grant` 安装
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants “{name}”需要权限 {asked}:需要有人在桌面应用中批准,或在以 --allow-plugin-grants 启动的会话中传入 `grant`
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` 和 `allowHeavy` 仅在以 --allow-plugin-grants 启动的会话中生效:请在桌面应用中批准此插件
@msg Only the person at the keyboard can approve installing a plug-in 只有键盘前的人才能批准安装插件
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed “{name}”未获授予其输出所需的 `{p}`:未作任何更改
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed 模块的 SHA-256 与 `hash` 给出的不符:未安装任何内容
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -2
View File
@@ -5475,7 +5475,7 @@ axis Y Axis Y 軸
@msg Running “{name}”… 正在執行「{name}」…
@msg a live effect runs while the document is drawn, so it can't be `heavy` 即時效果會在繪製文件時執行,因此不能是 `heavy`
@msg an exporter always gets an export's budget: leave `budget` out 匯出增效模組一律使用匯出的預算:請移除 `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees 「{_1}」需要大量運算預算(每次執行最多 {minutes} 分鐘,指令數為一般的 {times} 倍):請在使用者同意後以 `allowHeavy: true` 安裝
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): a person has to approve it in the desktop app, or pass `allowHeavy: true` in a session started with --allow-plugin-grants 「{_1}」需要大量運算預算(每次執行最多 {minutes} 分鐘,指令數為一般的 {times} 倍):需要有人在桌面應用程式中核准,或在以 --allow-plugin-grants 啟動的工作階段中傳入 `allowHeavy: true`
# Plug-in permissions (Install Plug-in…, plugin.install grant)
Install “{name}”, which asks for these permissions? 要安裝需要這些權限的「{name}」嗎?
ID: {id} ID:{id}
@@ -5500,7 +5500,9 @@ axis Y Axis Y 軸
@msg permission `{shown}` is listed twice 權限 `{shown}` 列出了兩次
@msg permission `{shown}` is reserved for a later plug-in API; this host doesn't grant it 權限 `{shown}` 保留給日後的增效模組 API;此主機不授予該權限
@msg unknown permission `{shown}` 未知權限 `{shown}`
@msg “{name}” asks for the permissions {asked}: install it with `grant` listing them once the user agrees 「{name}」需要權限 {asked}:請在使用者同意後以列出這些權限的 `grant` 安裝
@msg “{name}” asks for the permissions {asked}: a person has to approve them in the desktop app, or pass `grant` in a session started with --allow-plugin-grants 「{name}」需要權限 {asked}:需要有人在桌面應用程式中核准,或在以 --allow-plugin-grants 啟動的工作階段中傳入 `grant`
@msg `grant` and `allowHeavy` take effect only in a session started with --allow-plugin-grants: approve the plug-in in the desktop app `grant` 和 `allowHeavy` 僅在以 --allow-plugin-grants 啟動的工作階段中生效:請在桌面應用程式中核准此外掛
@msg Only the person at the keyboard can approve installing a plug-in 只有鍵盤前的人才能核准安裝外掛
@msg “{name}” wasn't granted `{p}`, which its output needs: nothing was changed 「{name}」未獲授予其輸出所需的 `{p}`:未做任何變更
@msg the module doesn't have the SHA-256 `hash` gives: nothing was installed 模組的 SHA-256 與 `hash` 提供的不符:未安裝任何內容
# Actions panel: action sets (#3712).
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -21,6 +21,7 @@ pub use export::{EXPORT_CANCELLED, cancel_exports, export, export_for_screens, e
pub(crate) use export::{
export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named,
};
pub(crate) use open::report_installed;
pub(crate) use open::{EXPORT_PRESETS, import_presets, opened};
pub use open::{
MAX_RECENT_FILES, OpensAs, ask_revert, install_module, install_plugin, new_from_template, note_recent, open_bytes, open_dialog, open_document,
+63 -53
View File
@@ -1,3 +1,4 @@
use vectorcraft_engine::cmd::plugin::InstallApproval;
use vectorcraft_plugins::permissions;
use super::save::suggested;
@@ -181,63 +182,72 @@ fn permission_words(name: &str) -> String {
}
/// Installs the plug-in module that `module` names (`plugin.install`'s `path`, or `dataBase64`
/// with `name`) → `plugin.install`'s result. A plug-in that lists permissions the user hasn't
/// granted it yet (`grant`) asks them first, showing its name, id, version, file hash and what each
/// permission lets it do, and so does a filter that asks for a heavy budget the user hasn't allowed
/// yet (`allowHeavy`); OK installs it if the module still has the hash the prompt showed, Cancel
/// installs nothing → `{pending}` while it asks. Every
/// text from the manifest is shown with its hidden characters escaped and its length capped
/// with `name`) → `plugin.install`'s result. A plug-in that lists permissions no person has granted
/// it yet asks first, showing its name, id, version, file hash and what each permission lets it
/// do, and so does a filter that asks for a heavy budget; OK installs it if the module still has
/// the hash the prompt showed, Cancel installs nothing → `{pending}` while it asks. The `grant`
/// and `allowHeavy` of a request are ignored: only the dialog's OK approves. Every text from the
/// manifest is shown with its hidden characters escaped and its length capped
/// ([`permissions::display_text`]).
pub fn install_module(app: &mut VectorcraftApp, module: Value) -> Result<Value, String> {
let heavy_allowed = module.get("allowHeavy").and_then(Value::as_bool) == Some(true);
let granted = module.get("grant").is_some_and(|g| !g.is_null());
if !heavy_allowed || !granted {
let given = |k: &str| module.get(k).map(|v| (k.to_string(), v.clone()));
let source = Value::Object(["path", "dataBase64", "name"].into_iter().filter_map(given).collect());
// What OK installs: the same module, with what the user has agreed to so far, pinned to
// the bytes the prompt shows (`hash`).
let mut answered =
Value::Object(["path", "dataBase64", "name", "replace", "grant", "allowHeavy", "hash"].into_iter().filter_map(given).collect());
let info = app.run("plugin.inspect", source)?;
if answered.get("hash").is_none() {
answered["hash"] = info["hash"].clone();
}
let shown = |k: &str, max: usize| permissions::display_text(info[k].as_str().unwrap_or_default(), max);
let name = shown("name", permissions::MAX_SHOWN_NAME);
if !granted && info["needsApproval"] == true {
let message = crate::i18n::fmt(tl!("Install “{name}”, which asks for these permissions?"), &[("name", &name)]);
let mut lines = vec![crate::i18n::fmt(tl!("ID: {id}"), &[("id", &shown("id", permissions::MAX_SHOWN_FIELD))])];
let version = shown("version", permissions::MAX_SHOWN_FIELD);
if !version.is_empty() {
lines.push(crate::i18n::fmt(tl!("Version: {version}"), &[("version", &version)]));
}
lines.push(crate::i18n::fmt(tl!("File SHA-256: {hash}"), &[("hash", &shown("hash", 64))]));
lines.push(String::new());
let asked = info["permissions"].as_array().cloned().unwrap_or_default();
lines.extend(asked.iter().filter_map(Value::as_str).map(|p| format!("• {}", permission_words(p))));
let mut again = answered;
again["grant"] = Value::Array(asked);
dialogs::confirm::ask(app, &message, &lines.join("\n"), "ui.installPlugin", again);
return Ok(json!({ "pending": dialogs::confirm::KIND }));
}
if !heavy_allowed && info["budget"] == "heavy" {
let (minutes, times) = vectorcraft_plugins::heavy_terms();
let message = crate::i18n::fmt(tl!("Install “{name}”, which asks for a heavy budget?"), &[("name", &name)]);
let detail = crate::i18n::fmt(
tl!(
"Each run may take up to {minutes} minutes and do {times} times the work other plug-ins may. It runs in the background, and you can cancel it."
),
&[("minutes", &minutes.to_string()), ("times", &times.to_string())],
);
let mut allowed = answered;
allowed["allowHeavy"] = json!(true);
dialogs::confirm::ask(app, &message, &detail, "ui.installPlugin", allowed);
return Ok(json!({ "pending": dialogs::confirm::KIND }));
}
let given = |k: &str| module.get(k).map(|v| (k.to_string(), v.clone()));
let source = Value::Object(["path", "dataBase64", "name"].into_iter().filter_map(given).collect());
let mut install = Value::Object(["path", "dataBase64", "name", "replace", "hash"].into_iter().filter_map(given).collect());
let info = app.run("plugin.inspect", source)?;
let (needs_grant, heavy) = (info["needsApproval"] == true, info["budget"] == "heavy");
if !needs_grant && !heavy {
let r = app.run("plugin.install", install)?;
report_installed(app, &r);
return Ok(r);
}
let r = app.run("plugin.install", module)?;
// Pinned to the bytes the prompt shows.
if install.get("hash").is_none() {
install["hash"] = info["hash"].clone();
}
let shown = |k: &str, max: usize| permissions::display_text(info[k].as_str().unwrap_or_default(), max);
let name = shown("name", permissions::MAX_SHOWN_NAME);
let asked = info["permissions"].as_array().cloned().unwrap_or_default();
let mut paragraphs = vec![];
if needs_grant {
let mut lines = vec![crate::i18n::fmt(tl!("ID: {id}"), &[("id", &shown("id", permissions::MAX_SHOWN_FIELD))])];
let version = shown("version", permissions::MAX_SHOWN_FIELD);
if !version.is_empty() {
lines.push(crate::i18n::fmt(tl!("Version: {version}"), &[("version", &version)]));
}
lines.push(crate::i18n::fmt(tl!("File SHA-256: {hash}"), &[("hash", &shown("hash", 64))]));
lines.push(String::new());
lines.extend(asked.iter().filter_map(Value::as_str).map(|p| format!("• {}", permission_words(p))));
paragraphs.push(lines.join("\n"));
install["grant"] = Value::Array(asked.clone());
}
if heavy {
let (minutes, times) = vectorcraft_plugins::heavy_terms();
paragraphs.push(crate::i18n::fmt(
tl!(
"Each run may take up to {minutes} minutes and do {times} times the work other plug-ins may. It runs in the background, and you can cancel it."
),
&[("minutes", &minutes.to_string()), ("times", &times.to_string())],
));
install["allowHeavy"] = json!(true);
}
let message = if needs_grant {
crate::i18n::fmt(tl!("Install “{name}”, which asks for these permissions?"), &[("name", &name)])
} else {
crate::i18n::fmt(tl!("Install “{name}”, which asks for a heavy budget?"), &[("name", &name)])
};
let approval = InstallApproval {
id: info["id"].as_str().unwrap_or_default().to_string(),
hash: info["hash"].as_str().unwrap_or_default().to_string(),
permissions: asked.iter().filter_map(Value::as_str).map(str::to_string).collect(),
heavy,
};
let pending = dialogs::plugin::PendingInstall { message, detail: paragraphs.join("\n\n"), params: install, approval };
Ok(dialogs::plugin::ask_install(app, pending))
}
/// Says in the status bar that `plugin.install`'s result `r` was installed.
pub(crate) fn report_installed(app: &mut VectorcraftApp, r: &Value) {
app.status(format!("Installed plug-in {}", permissions::display_text(r["name"].as_str().unwrap_or_default(), permissions::MAX_SHOWN_NAME)));
Ok(r)
}
/// Where File → Open… starts: the folder of the most recently opened file, else the active
+3
View File
@@ -302,6 +302,9 @@ pub struct VectorcraftApp {
/// This frame carries input the control channel injected ([`Self::raw_input_hook`]): it runs
/// confined to [`Self::automation_roots`].
synthetic_frame: bool,
/// The plug-in install the approval dialog asks about, kept out of the dialog's fields, which
/// `ui.dialog.set` writes.
pub(crate) pending_plugin_install: Option<dialogs::plugin::PendingInstall>,
/// The pen end the canvas saw last and the tool its eraser end stands in for
/// ([`stylus::sync_eraser_tool`]).
pub(crate) pen_end: stylus::PenEnd,
+1 -1
View File
@@ -1457,7 +1457,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"ui.installPlugin",
"Install Plug-in…",
"",
"{path?: a .wasm file | dataBase64?: the module, name?: the file name shown as its source (with dataBase64), allowHeavy?: bool (the user agreed to its heavy budget), grant?: [permission] (the permissions the user granted it), replace?: bool, hash?: the module's SHA-256 (as plugin.install)} install a WebAssembly plug-in (plugin.install); without a path pick one (the web opens the browser's file picker; File › Open of a .wasm installs it too). A plug-in that lists permissions the user hasn't granted it asks the user first, showing its name, id, version, file SHA-256 and what each permission lets it do, and so does a filter that asks for a heavy budget: OK installs it if the file still has the SHA-256 shown, Cancel installs nothing → plugin.install's result | {pending: \"confirm\"} while it asks",
"{path?: a .wasm file | dataBase64?: the module, name?: the file name shown as its source (with dataBase64), replace?: bool, hash?: the module's SHA-256 (as plugin.install)} install a WebAssembly plug-in (plugin.install); without a path pick one (the web opens the browser's file picker; File › Open of a .wasm installs it too). A plug-in that lists permissions the user hasn't granted it asks the user first, showing its name, id, version, file SHA-256 and what each permission lets it do, and so does a filter that asks for a heavy budget: OK installs it if the file still has the SHA-256 shown, Cancel installs nothing. Only a person can press OK: ui.dialog.confirm, answer_dialog and synthetic input are refused → plugin.install's result | {pending: \"pluginInstall\"} while it asks",
),
(
"plugin.cancelRuns",
+38
View File
@@ -294,6 +294,44 @@ fn an_unidentifiable_stale_injected_batch_fails_closed() {
let _ = std::fs::remove_dir_all(base);
}
/// A plug-in's permissions are approved only by a person. A control request (or MCP, which comes
/// through it) can't pass `grant`, can't answer the approval dialog or rewrite what it installs,
/// and a frame of synthetic input can't press its OK; the person at the keyboard can.
#[test]
fn automation_cannot_approve_a_plugin_install() {
let (tx, rx) = std::sync::mpsc::channel();
let mut app = VectorcraftApp::new(Session::new(), Default::default()).with_control(rx);
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
let id = "test.ui.approval.automation";
let manifest = format!(r#"{{"id":"{id}","name":"Approval","version":"1","kind":"filter","permissions":["doc.read"]}}"#);
let bytes = wat::parse_str(vectorcraft_plugins::wat::returning(&manifest, r#"{"objects":[]}"#)).unwrap();
let data = vectorcraft_format::base64_encode(&bytes);
let refused = |r: &Value, says: &str| assert!(r["error"].as_str().is_some_and(|e| e.contains(says)), "{r}");
let r = request(&mut app, &tx, "engine.execute", json!({"command": "plugin.install", "params": {"dataBase64": data, "grant": ["doc.read"]}}));
refused(&r, "--allow-plugin-grants");
assert!(app.ui.dialog.is_none());
let r = request(&mut app, &tx, "ui.menu.invoke", json!({"command": "ui.installPlugin", "params": {"dataBase64": data}}));
assert_eq!(r["result"]["pending"], crate::dialogs::plugin::INSTALL_KIND, "{r}");
let r = request(&mut app, &tx, "ui.dialog.confirm", json!({}));
refused(&r, "Only the person at the keyboard");
assert!(app.ui.dialog.is_some(), "the refused answer leaves it open");
assert!(vectorcraft_plugins::registry::get(id).is_none());
// What the dialog installs isn't in its fields.
let r = request(&mut app, &tx, "ui.dialog.set", json!({"field": "__params", "value": {"dataBase64": "AAAA"}}));
assert!(r.get("error").is_none(), "{r}");
app.synthetic_frame = true;
assert!(crate::dialogs::confirm(&mut app).unwrap_err().contains("Only the person at the keyboard"));
assert!(vectorcraft_plugins::registry::get(id).is_none());
app.synthetic_frame = false;
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(vectorcraft_plugins::registry::granted(id).map(|g| g.hash), Some(vectorcraft_plugins::permissions::sha256_hex(&bytes)));
app.run("plugin.remove", json!({"id": id})).unwrap();
}
/// Edit Original and the Attributes panel's Browser button hand a file or URL to another app: a
/// control request (or MCP, which comes through it) can't run them, roots or not.
#[test]
+6 -5
View File
@@ -13,6 +13,7 @@ use crate::{VectorcraftApp, dialogs, plugin_runs, theme};
/// An app with a new 200 × 200 pt document.
fn app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.allow_plugin_grants_from_requests();
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
app
}
@@ -71,10 +72,10 @@ fn installing_a_heavy_filter_asks_first() {
let id = "test.ui.heavy.install";
let bytes = heavy(id, 1, "{}");
let r = app.run("ui.installPlugin", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "route.wasm"})).unwrap();
assert_eq!(r["pending"], dialogs::confirm::KIND);
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!(d.str("message"), format!("Install “Test {id}”, which asks for a heavy budget?"));
assert!(d.str("detail").contains("up to 5 minutes and do 20 times the work"), "{}", d.str("detail"));
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND);
let pending = app.pending_plugin_install.as_ref().unwrap();
assert_eq!(pending.message, format!("Install “Test {id}”, which asks for a heavy budget?"));
assert!(pending.detail.contains("up to 5 minutes and do 20 times the work"), "{}", pending.detail);
assert!(registry::get(id).is_none(), "not before the user agrees");
dialogs::confirm(&mut app).unwrap();
assert_eq!(registry::get(id).map(|p| p.manifest().name.clone()), Some(format!("Test {id}")));
@@ -82,7 +83,7 @@ fn installing_a_heavy_filter_asks_first() {
// File › Open of the .wasm asks too, and Cancel installs nothing.
registry::remove(id);
crate::io::open_bytes(&mut app, "route.wasm", &bytes, None).unwrap();
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::confirm::KIND));
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::plugin::INSTALL_KIND));
dialogs::cancel(&mut app);
assert!(registry::get(id).is_none());
}
+14 -13
View File
@@ -142,25 +142,26 @@ fn installing_a_plugin_that_lists_permissions_asks_first() {
let bytes = asking(id, name, r#"["doc.read","doc.write"]"#);
let data = vectorcraft_format::base64_encode(&bytes);
let r = app.run("ui.installPlugin", json!({"dataBase64": data, "name": "tidy.wasm"})).unwrap();
assert_eq!(r["pending"], dialogs::confirm::KIND);
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!(d.str("message"), "Install “Tidy\\u{202E}fdp.exe\\u{2066}”, which asks for these permissions?");
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND);
let pending = app.pending_plugin_install.as_ref().unwrap();
assert_eq!(pending.message, "Install “Tidy\\u{202E}fdp.exe\\u{2066}”, which asks for these permissions?");
let hash = vectorcraft_plugins::permissions::sha256_hex(&bytes);
assert_eq!(d.fields.get("__params").map(|p| p["hash"].clone()), Some(json!(hash)), "OK installs the module shown");
let detail = d.str("detail");
assert_eq!(pending.params["hash"], json!(hash), "OK installs the module shown");
let detail = pending.detail.clone();
assert_eq!(
detail,
format!(
"ID: {id}\nVersion: 3.0\\u{{202E}}\\u{{0009}}\\u{{0007}}\nFile SHA-256: {hash}\n\n• Read the objects it is given\n• Change, add and delete objects in the document"
)
);
assert!(!detail.chars().chain(d.str("message").chars()).any(|c| matches!(c, '\u{202E}' | '\u{2066}' | '\t' | '\u{0007}')));
assert!(!detail.chars().chain(pending.message.chars()).any(|c| matches!(c, '\u{202E}' | '\u{2066}' | '\t' | '\u{0007}')));
assert!(vectorcraft_plugins::registry::get(id).is_none(), "not before the user agrees");
dialogs::cancel(&mut app);
assert!(vectorcraft_plugins::registry::get(id).is_none(), "Cancel installs nothing");
assert!(app.pending_plugin_install.is_none(), "Cancel drops what it asked about");
// File › Open asks too, and OK installs it with what it lists granted.
io::open_bytes(&mut app, "tidy.wasm", &bytes, None).unwrap();
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::confirm::KIND));
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::plugin::INSTALL_KIND));
dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert!(vectorcraft_plugins::registry::get(id).is_some());
@@ -171,11 +172,11 @@ fn installing_a_plugin_that_lists_permissions_asks_first() {
// Other bytes with the same permissions ask, from both ways in; Cancel keeps the one installed.
let again = asking(id, "Tidy 2", r#"["doc.write","doc.read"]"#);
let r = app.run("ui.installPlugin", json!({"dataBase64": vectorcraft_format::base64_encode(&again)})).unwrap();
assert_eq!(r["pending"], dialogs::confirm::KIND, "other bytes ask again");
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND, "other bytes ask again");
dialogs::cancel(&mut app);
assert_eq!(vectorcraft_plugins::registry::get(id).map(|p| p.manifest().name.clone()), Some(name.into()), "Cancel keeps the one installed");
io::open_bytes(&mut app, "tidy2.wasm", &again, None).unwrap();
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::confirm::KIND), "File › Open asks too");
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::plugin::INSTALL_KIND), "File › Open asks too");
dialogs::confirm(&mut app).unwrap();
assert_eq!(vectorcraft_plugins::registry::get(id).map(|p| p.manifest().name.clone()), Some("Tidy 2".into()));
let new_hash = vectorcraft_plugins::permissions::sha256_hex(&again);
@@ -183,8 +184,8 @@ fn installing_a_plugin_that_lists_permissions_asks_first() {
// One that asks for another permission asks again.
let more = asking(id, "Tidy 3", r#"["doc.read","doc.write","state"]"#);
let r = app.run("ui.installPlugin", json!({"dataBase64": vectorcraft_format::base64_encode(&more)})).unwrap();
assert_eq!(r["pending"], dialogs::confirm::KIND);
assert!(app.ui.dialog.as_ref().unwrap().str("detail").ends_with("• Keep data of its own with the document"));
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND);
assert!(app.pending_plugin_install.as_ref().unwrap().detail.ends_with("• Keep data of its own with the document"));
dialogs::cancel(&mut app);
assert_eq!(vectorcraft_plugins::registry::get(id).map(|p| p.manifest().name.clone()), Some("Tidy 2".into()), "Cancel keeps the one installed");
// OK keeps the `replace` it was asked with: `false` leaves the installed one in place.
@@ -202,8 +203,8 @@ fn installing_a_plugin_that_reports_progress_says_so() {
let manifest = format!(r#"{{"id":"{id}","name":"Count","kind":"filter","api":2,"permissions":["doc.read","progress"]}}"#);
let bytes = wat::parse_str(tpl::reporting(&manifest, "(call $vc_progress (i64.const 1) (i64.const 2))", r#"{"objects":[]}"#)).unwrap();
let r = app.run("ui.installPlugin", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
assert_eq!(r["pending"], dialogs::confirm::KIND);
let detail = app.ui.dialog.as_ref().unwrap().str("detail");
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND);
let detail = app.pending_plugin_install.as_ref().unwrap().detail.clone();
assert!(detail.ends_with("• Read the objects it is given\n• Show its progress, see Cancel and write log lines"), "{detail}");
dialogs::confirm(&mut app).unwrap();
assert_eq!(vectorcraft_plugins::registry::granted(id).map(|g| g.permissions.len()), Some(2));
+2 -1
View File
@@ -214,7 +214,8 @@ after it), `__plugin` (the plug-in id) and `preview`. It previews `plugin.run` o
the `effect` dialog (`effect.dialog {effect: "plugin.<id>"}`). An exporter plug-in with parameters opens the
`pluginExport` dialog once Export As has picked the file: one field per parameter, `__plugin`, and `__export` (the
export's own parameters); `ui.dialog.confirm` writes the file. `ui.installPlugin {path?}` installs a `.wasm` (without
a path it asks for one); see [plugins.md](plugins.md).
a path it asks for one); one that needs approval opens the `pluginInstall` dialog, which only a person can confirm
(`ui.dialog.confirm` and injected input are refused); see [plugins.md](plugins.md).
Overprint Black: the Edit → Edit Colors → Overprint Black… menu item opens a `command` parameter dialog for
`edit.colors.overprintBlack` (fields `remove`, `percentage`, `fill`, `stroke`, `includeCmyBlacks`,
+11 -7
View File
@@ -6168,15 +6168,19 @@ id `plugin.<id>`: `effect.apply`, `effect.setParams` and `effect.expandAppearanc
`document.export {format: "plugin.<id>", params?, path?}` writes it, as does a `path` ending in one of its extensions
that no built-in format has. A failing plug-in returns an error and leaves the document as it was (an export writes
no file). A filter whose manifest asks for a heavy budget (minutes rather than a minute, `budget: "heavy"` in its
`plugin.inspect {path | dataBase64}` entry) installs only with `allowHeavy: true`: ask the user first. `plugin.run`
`plugin.inspect {path | dataBase64}` entry) needs a person's approval, and `plugin.install` refuses `allowHeavy: true`
from a request. `plugin.run`
runs it where it is called, so the call may take minutes; `notifications/cancelled` stops it.
A plug-in whose manifest lists permissions (`permissions` in its `plugin.inspect` entry, with `needsApproval: true`)
installs only with `grant: [permission]` listing every one of them: show the user the list and ask first, and pass
the entry's `hash` as `hash` so that a module that changed since is refused. Without `grant`, `plugin.install` returns
an error naming the permissions needed. The grant is kept with the module's hash and covers only that module: the
same bytes install again without `grant`, and a module with other bytes has `needsApproval: true` and needs a new
`grant` even when it lists the same permissions. `plugin.install` does not check that a person approved `grant`: it
trusts the client that sends it. `plugin.remove` forgets the grant. A
needs a person's approval too, and `plugin.install` refuses `grant: [permission]` from a request. With a desktop
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
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
filter without `doc.write` or `state` that returns output needing them gets an error and the document is unchanged.
A plug-in of API level 2 (`api: 2` in its entry) that lists `progress` may report its progress, see Cancel and write
log lines: `notifications/cancelled` makes its `vc_cancelled` return 1, and its log lines go to the server's log
+13 -8
View File
@@ -26,8 +26,8 @@ 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?, size, source, api, permissions, hash}], "folder": report}` (`api` is the manifest's [plug-in API level](#permissions); `budget: "heavy"` for a [heavy filter](#heavy-filters); `permissions` lists what it [may do](#permissions), `hash` is the module's SHA-256) |
| `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`). 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 = true`, `"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 [heavy filter](#heavy-filters) installs only with `allowHeavy: true`, once the user agreed. A plug-in whose manifest lists [permissions](#permissions) installs only when `grant` lists every one of them, once the user agreed, unless the user already granted a module with the same SHA-256. 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 `plugin.install` needs `grant`). |
| `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace": bool = true`, `"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 [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.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). |
| `plugin.remove` | `{"id"}` | Uninstalls, and deletes it from the `Plug-ins` folder. |
| `plugin.reload` | `{"path"?: "folder"}` (default: the preference folder) | Installs every `*.wasm` in a folder; a bad module is reported and skipped, and so are a heavy filter and a plug-in whose permissions the user hasn't granted (nobody was asked). |
@@ -89,8 +89,9 @@ budget). Each of its runs gets 1 G instructions (20 times a light run's) plus th
much there is only once the run has ended.
- **Installing asks first.** Install Plug-in… and File › Open show what the filter asks for, and install it once the
user clicks OK; `plugin.install` refuses it without `allowHeavy: true`, which an agent passes once the user agreed
(`plugin.inspect` tells it beforehand). A filter kept in the `Plug-ins` folder was agreed to and is installed again
user clicks OK (see [Permissions](#permissions) for who can). `plugin.install` refuses it without `allowHeavy: true`,
and refuses `allowHeavy` from a request except in a session started with `vectorcraft-cli --allow-plugin-grants`
(`plugin.inspect` shows the budget beforehand). A filter kept in the `Plug-ins` folder was agreed to and is installed again
at startup. The Additional Plug-ins Folder skips heavy filters with a note in its report, since loading a folder
asks nobody.
- **It runs in the background.** Its dialog has no Preview. OK (or the menu item, without parameters) runs it on a
@@ -129,10 +130,14 @@ capability model proposed for PhotoCraft plug-ins
the file and each permission in plain words, and install it once the user clicks OK, if the file still has the
SHA-256 shown; Cancel installs nothing. The
names shown have control and bidirectional characters escaped (`\u{202E}`) and are cut to 64 characters.
`plugin.install` refuses the module, naming the permissions it needs, unless `grant` lists them all, which an agent
passes once the user agreed (`plugin.inspect`'s `needsApproval` tells it beforehand), with `hash` set to the
`plugin.inspect` entry's `hash` so that the module installed is the one the user saw. `plugin.install` does not
check that a person approved `grant`: it trusts the client that sends it.
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, and Enter does not confirm it. `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.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`.
- The grant is recorded with the plug-in's id, the module's hash and the permissions, in `grants.json` in the
`Plug-ins` folder, and covers only that module. Installing the same bytes again needs no new approval. A module with
other bytes asks again, even when it lists the same permissions. Until it is approved, the installed module and its
+1
View File
@@ -1505,6 +1505,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Plug-in instruction budget at a bulk memory instruction:** a run that pauses where one slice of its instruction budget ends keeps the instructions the slice still held. A `memory.fill` or `memory.copy` that the slice can't pay for in full runs on the next slice, so a run whose budget covers its work finishes; a run ends with `Limit("instruction budget")` only when the instructions it has left can't pay for the next instruction. Regressions: `crates/plugins/tests/abi.rs`. Scores and hours unchanged.
- **Plug-in permissions (G17):** a manifest may list `permissions` (`doc.read`, `doc.write`, `state`, `effect.render`, `export.write`; the first three follow storytold/photocraft#662) and an `api` level; without the list a plug-in gets what 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 install nothing on Cancel; `plugin.install` needs `grant`, and folders install only granted plug-ins. The grant is kept with the module's hash, a module asking for more asks again, and a filter run refuses output its permissions don't allow. Regressions: `crates/plugins/src/permissions.rs`, `crates/plugins/src/manifest.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugins.rs`. Scores and hours unchanged.
- **Plug-in grants cover one module (G17):** a grant counts only for the module with the id and SHA-256 it records. A module with other bytes under the same id needs a new approval, even with the same permissions: `plugin.inspect` reports `needsApproval`, `plugin.install` refuses it without `grant`, Install Plug-in… and File › Open ask again, and the plug-in store and plug-in folders skip it. A grant with a malformed hash covers nothing. Regressions: `crates/plugins/src/permissions.rs`, `crates/engine/src/tests/plugins.rs`, `crates/ui-egui/src/tests/plugins.rs`. Scores and hours unchanged.
- **Plug-in approval needs a person (G17):** a plug-in's permissions and heavy budget are approved in a dialog that only a person can confirm: the control channel, MCP and injected input are refused, and `plugin.install` takes `grant` or `allowHeavy` only with that approval. `vectorcraft-cli mcp` and `run` refuse them unless started with `--allow-plugin-grants`. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/ui-egui/src/tests/automation.rs`, `apps/vectorcraft-cli/tests/it/cli.rs`. Scores and hours unchanged.
- **Plug-in progress, Cancel and log lines (G17):** at plug-in API level 2 a plug-in with the `progress` permission imports `env.vc_progress`, `env.vc_cancelled` and `env.vc_log`. A heavy filter's and an export's progress windows show its progress as a percentage bar, their Cancel reaches it through `vc_cancelled`, and its log lines go to the app's log with its id, capped per line and per run. Installing checks every import and names the one it refuses. Regressions: `crates/plugins/src/host.rs`, `crates/plugins/tests/abi.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugin_runs.rs`. Scores and hours unchanged.
- **Plug-in effects after a plug-in changes:** when a plug-in is installed for the first time, installed again or removed (`plugin.install`, `plugin.reload`, `plugin.remove`, File › Open of a `.wasm` file, the Additional Plug-ins Folder preference), the next frame the canvas draws uses the plug-ins then installed for objects with a plug-in effect and their raster effects, for the blends, envelopes and repeats that show such objects, and for pattern tiles and symbol instances whose art holds one.