mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:10:01 -06:00
Merge pull request #6285 from dotCooCoo/fix/plugin-grant-app-approval
Plug-ins: permissions and a heavy budget need a person's approval or a start-up flag
This commit is contained in:
@@ -78,7 +78,7 @@ USAGE:
|
||||
Safe Mode is on. Plug-ins stay installed. `--safe-mode mcp` serves a headless session and
|
||||
does not take --connect.
|
||||
|
||||
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
|
||||
@@ -86,8 +86,18 @@ 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 that lists permissions
|
||||
installs only when its hash is already recorded in grants.json, by a person's approval in the
|
||||
desktop app or by a grant accepted under this flag. A grant accepted under this flag stays
|
||||
recorded and counts as approved in later sessions. It implies --headless. It can't be used
|
||||
with --automation-read-root or --automation-write-root: a server confined to folders has no
|
||||
plug-in store, so it reads no grants.json, and a plug-in that lists permissions or asks for a
|
||||
heavy budget does not install there. With VECTORCRAFT_NO_PREFS set there is no store either,
|
||||
and a grant accepted under this flag lasts for that session only.
|
||||
|
||||
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
|
||||
@@ -97,6 +107,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.
|
||||
@@ -233,6 +245,14 @@ 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
|
||||
}
|
||||
|
||||
/// `mcp`. With `--safe-mode` (`safe_mode`) it serves a headless session: a running app loads its own
|
||||
/// plug-ins, so `--connect` is refused and the default does not connect.
|
||||
fn mcp(args: &[String], safe_mode: bool) -> Result<(), String> {
|
||||
@@ -241,6 +261,7 @@ fn mcp(args: &[String], safe_mode: bool) -> Result<(), String> {
|
||||
}
|
||||
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() {
|
||||
@@ -252,6 +273,7 @@ fn mcp(args: &[String], safe_mode: bool) -> 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) }
|
||||
@@ -262,6 +284,12 @@ fn mcp(args: &[String], safe_mode: bool) -> 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());
|
||||
}
|
||||
if allow_grants && !(read_roots.is_empty() && write_roots.is_empty()) {
|
||||
return Err("--allow-plugin-grants can't be used with --automation-read-root or --automation-write-root: a server confined to folders has no plug-in store to record a grant in".into());
|
||||
}
|
||||
if safe_mode && connect.is_some() {
|
||||
return Err("--safe-mode runs a headless session without plug-ins; a running app loads its own: start it with `vectorcraft --safe-mode --control <port>`".into());
|
||||
}
|
||||
@@ -276,9 +304,9 @@ fn mcp(args: &[String], safe_mode: bool) -> 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 || safe_mode {
|
||||
Box::new(with_stores(Headless::with_document()))
|
||||
Box::new(accepting_grants(Headless::with_document().with_automation_roots(Some(roots)), allow_grants))
|
||||
} else if headless || safe_mode || 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}"))?)
|
||||
@@ -561,6 +589,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}"));
|
||||
@@ -595,6 +624,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"))?);
|
||||
@@ -612,7 +642,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.
|
||||
|
||||
@@ -19,6 +19,77 @@ fn empty_dir(dir: std::path::PathBuf) -> std::path::PathBuf {
|
||||
dir
|
||||
}
|
||||
|
||||
/// `run` and `mcp --headless` refuse `plugin.install` with this param unless the process was started
|
||||
/// with `--allow-plugin-grants`.
|
||||
fn approval_param_needs_the_start_up_flag(key: &str, value: Value) {
|
||||
let module = vectorcraft_format::base64_encode(include_bytes!("../../../../crates/plugins/tests/fixtures/desaturate.wasm"));
|
||||
let steps = tmp(&format!("{key}.json"));
|
||||
std::fs::write(&steps, json!([{"command": "plugin.install", "params": {"dataBase64": module, key: value}}]).to_string()).unwrap();
|
||||
let config = empty_dir(std::env::temp_dir().join(format!("vectorcraft-cli-test-{key}-{}", 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 mcp_config = empty_dir(std::env::temp_dir().join(format!("vectorcraft-cli-test-mcp-{key}-{}", std::process::id())));
|
||||
let mcp_install = |extra: &[&str]| -> Value {
|
||||
let mut child = Command::new(BIN)
|
||||
.env("VECTORCRAFT_CONFIG_DIR", &mcp_config)
|
||||
.args(["mcp", "--headless"])
|
||||
.args(extra)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
{
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
let params = json!({"dataBase64": module, key: value});
|
||||
for m in [
|
||||
json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0"}}}),
|
||||
json!({"jsonrpc":"2.0","method":"notifications/initialized"}),
|
||||
json!({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"plugin.install","params":params}}}),
|
||||
] {
|
||||
writeln!(stdin, "{m}").unwrap();
|
||||
}
|
||||
}
|
||||
let out = child.wait_with_output().unwrap();
|
||||
assert!(out.status.success());
|
||||
let replies: Vec<Value> = String::from_utf8(out.stdout).unwrap().lines().map(|l| serde_json::from_str(l).unwrap()).collect();
|
||||
replies.into_iter().find(|r| r["id"] == 2).unwrap()["result"].take()
|
||||
};
|
||||
let refused = mcp_install(&[]);
|
||||
assert_eq!(refused["isError"], true, "without the flag: {refused}");
|
||||
assert!(refused["content"][0]["text"].as_str().unwrap().contains("--allow-plugin-grants"), "{refused}");
|
||||
let accepted = mcp_install(&["--allow-plugin-grants"]);
|
||||
assert_eq!(accepted["isError"], false, "with the flag: {accepted}");
|
||||
}
|
||||
|
||||
/// `run` and `mcp --headless` refuse `plugin.install`'s `grant` and `allowHeavy` unless the process was
|
||||
/// started with `--allow-plugin-grants`, which `mcp` won't take with `--connect` or the automation roots.
|
||||
#[test]
|
||||
fn plugin_grants_need_the_start_up_flag() {
|
||||
approval_param_needs_the_start_up_flag("grant", json!(["doc.read"]));
|
||||
approval_param_needs_the_start_up_flag("allowHeavy", json!(true));
|
||||
|
||||
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 root = std::env::temp_dir();
|
||||
for flag in ["--automation-read-root", "--automation-write-root"] {
|
||||
let out = Command::new(BIN).args(["mcp", "--allow-plugin-grants", flag, root.to_str().unwrap()]).stdin(Stdio::null()).output().unwrap();
|
||||
assert!(!out.status.success(), "{flag}");
|
||||
assert!(String::from_utf8_lossy(&out.stderr).contains("--allow-plugin-grants can't be used with"), "{flag}");
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
//! `document.open` reads (`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 filter or a live effect may add menu commands of its own (its manifest's `commands`). Each
|
||||
//! is the command `plugin.<plug-in id>.<command id>` while the plug-in is installed
|
||||
@@ -555,6 +558,45 @@ fn grant_param(c: &str, p: &Value) -> Result<Option<Vec<Permission>>> {
|
||||
Ok(Some(out))
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Would installing `plugin` take another module's place: one installed with its id and other
|
||||
/// bytes, or, with none installed, the settings and cache the plug-in store keeps for its id?
|
||||
fn takes_over(s: &Session, plugin: &Plugin) -> bool {
|
||||
@@ -564,14 +606,17 @@ fn takes_over(s: &Session, plugin: &Plugin) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// `plugin.install`: loads and checks the module, refuses one that asks for a heavy budget unless
|
||||
/// `allowHeavy` says the user agreed, one that lists permissions the user hasn't granted this very
|
||||
/// module unless `grant` lists them all, one that would take another module's place ([`takes_over`])
|
||||
/// unless `replace` is true, 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. A
|
||||
/// module that lists no permissions ends any grant held for its id.
|
||||
/// `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, one that would take another module's place
|
||||
/// ([`takes_over`]) unless `replace` is true, 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. A module that lists no permissions ends any grant held for its id.
|
||||
fn install(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "plugin.install";
|
||||
// One use: spent whatever this install does.
|
||||
let approval = s.plugin_install_approval.take();
|
||||
let replace = match p.get("replace") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(_) => Some(switch_or(p, "replace", false, C)?),
|
||||
@@ -600,25 +645,38 @@ fn install(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
),
|
||||
));
|
||||
}
|
||||
// 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 they are granted, unless a grant recorded for
|
||||
// this very module already covers all of them.
|
||||
let m = plugin.manifest();
|
||||
let wanted = m.permissions();
|
||||
if !s.plugin_approved(&plugin) && !grant.as_ref().is_some_and(|g| wanted.iter().all(|w| g.contains(w))) {
|
||||
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"
|
||||
),
|
||||
));
|
||||
}
|
||||
// Installed plug-ins stay installed after a restart (#2541). The module is written first and its
|
||||
// grant only once it is there; when the grant can't be written, the store gets the module it
|
||||
@@ -939,7 +997,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 once the user agreed: a module with other bytes than the plug-in installed with its id, or with none installed, one whose id has settings or a cache in the plug-in store, installs only with it, takes that plug-in's place and gets its settings and cache; false: one whose id is installed is refused; left out, the same module installs again), name?: file name (with dataBase64), allowHeavy?: bool (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; installing a module that lists no permissions forgets the grant held for its id; a refused install keeps the grant, a failed write of the module to the plug-in store leaves the grant in the store, and a stored module that is disabled or not read in Safe Mode keeps the grant for its id; 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 once the user agreed: a module with other bytes than the plug-in installed with its id, or with none installed, one whose id has settings or a cache in the plug-in store, installs only with it, takes that plug-in's place and gets its settings and cache; false: one whose id is installed is refused; left out, the same module installs again), name?: file name (with dataBase64), allowHeavy?: bool (a filter whose manifest asks for a heavy budget, budget \"heavy\" in plugin.inspect, installs only with it), grant?: [permission] (a plug-in whose manifest lists permissions, needsApproval in plugin.inspect, installs only when grant holds every one of them; the grant is recorded with the module's hash and covers only that module, so a module with other bytes needs a new grant even when it lists the same permissions; installing a module that lists no permissions forgets the grant held for its id; a refused install keeps the grant, a failed write of the module to the plug-in store leaves the grant in the store, and a stored module that is disabled or not read in Safe Mode keeps the grant for its id; with a plug-in store attached, only the grants kept there count; a request's allowHeavy and grant are refused unless the session was started with vectorcraft-cli --allow-plugin-grants: in the desktop app a person approves the install, see ui.installPlugin, and a module whose hash is already recorded, by a person's approval or by a grant accepted under that flag, needs no grant), hash?: string (the module's SHA-256 as plugin.inspect gave it: a module with other bytes is refused)} validate and install a WebAssembly plug-in for this session → its plugin.list entry",
|
||||
always,
|
||||
install
|
||||
),
|
||||
@@ -948,7 +1006,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) and needsReplace: bool (true when it would take the place of another module with its id: plugin.install then needs replace: true)",
|
||||
"{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) and needsReplace: bool (true when it would take the place of another module with its id: plugin.install then needs replace: true)",
|
||||
always,
|
||||
inspect
|
||||
),
|
||||
|
||||
@@ -161,6 +161,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`]).
|
||||
|
||||
@@ -84,6 +84,8 @@ impl Session {
|
||||
plugin_dir: None,
|
||||
ignored_plugin_grants: Default::default(),
|
||||
plugin_output: None,
|
||||
plugin_install_approval: None,
|
||||
plugin_grants_from_requests: false,
|
||||
recent_urls: vec![],
|
||||
trace_preset_changes: Default::default(),
|
||||
ui_language: None,
|
||||
|
||||
@@ -26,7 +26,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
|
||||
@@ -2542,6 +2550,132 @@ 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 e = s.execute("command.batch", &batch).unwrap_err().to_string();
|
||||
assert!(e.contains(refused), "a batch can't grant: {e}");
|
||||
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}");
|
||||
// So does one refused for its params.
|
||||
s.approve_plugin_install(approval(id, &wasm, &["doc.read"], false));
|
||||
s.execute("plugin.install", &json!({"dataBase64": data, "grant": "doc.read"})).unwrap_err();
|
||||
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 module a person approved in the desktop app, recorded in the store's `grants.json`, installs
|
||||
/// from another session with that store and no flag and no `grant`. Other bytes under its id are
|
||||
/// refused there, with or without `grant`.
|
||||
#[test]
|
||||
fn a_module_approved_in_the_app_installs_from_a_strict_session_with_its_store() {
|
||||
let folder = crate::tests_links::Folder::new("plugin-approval-store");
|
||||
let id = "test.engine.approval.store";
|
||||
let wasm = asking(id, r#"["doc.read"]"#, &json!({"objects": []}));
|
||||
let data = vectorcraft_format::base64_encode(&wasm);
|
||||
let mut desktop = strict_session();
|
||||
desktop.attach_plugin_store(folder.0.clone());
|
||||
desktop.approve_plugin_install(approval(id, &wasm, &["doc.read"], false));
|
||||
run(&mut desktop, "plugin.install", json!({"dataBase64": data, "grant": ["doc.read"]}));
|
||||
assert!(std::fs::read_to_string(folder.0.join("grants.json")).unwrap().contains(id), "recorded in the store");
|
||||
|
||||
// Another process starts with an empty registry and the store's grants.
|
||||
let forget = || {
|
||||
vectorcraft_plugins::registry::remove(id);
|
||||
vectorcraft_plugins::registry::revoke(id);
|
||||
};
|
||||
forget();
|
||||
let mut headless = strict_session();
|
||||
headless.attach_plugin_store(folder.0.clone());
|
||||
forget();
|
||||
run(&mut headless, "plugin.install", json!({"dataBase64": data}));
|
||||
assert_eq!(vectorcraft_plugins::registry::get(id).map(|p| p.hash().to_string()), Some(vectorcraft_plugins::permissions::sha256_hex(&wasm)));
|
||||
|
||||
let other = asking(id, r#"["doc.read"]"#, &json!({"objects": [{"name": "x"}]}));
|
||||
let other_data = vectorcraft_format::base64_encode(&other);
|
||||
let e = headless.execute("plugin.install", &json!({"dataBase64": other_data, "replace": true})).unwrap_err().to_string();
|
||||
assert!(e.contains("approve them in the desktop app"), "{e}");
|
||||
let e = headless.execute("plugin.install", &json!({"dataBase64": other_data, "replace": true, "grant": ["doc.read"]})).unwrap_err().to_string();
|
||||
assert!(e.contains("take effect only in a session started with --allow-plugin-grants"), "{e}");
|
||||
assert_eq!(vectorcraft_plugins::registry::get(id).map(|p| p.hash().to_string()), Some(vectorcraft_plugins::permissions::sha256_hex(&wasm)));
|
||||
run(&mut headless, "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]
|
||||
|
||||
@@ -40,6 +40,7 @@ fn clearing_every_cache_empties_each_plugins_cache_and_keeps_its_settings() {
|
||||
std::fs::create_dir_all(&folder).unwrap();
|
||||
let ids = ["test.clearcache.a", "test.clearcache.b"];
|
||||
let mut s = Session::new();
|
||||
s.allow_plugin_grants_from_requests();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 200})).unwrap();
|
||||
s.attach_plugin_store(folder.clone());
|
||||
for id in ids {
|
||||
|
||||
@@ -695,6 +695,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(|| {
|
||||
@@ -862,6 +863,7 @@ proptest! {
|
||||
prop_assert!(kinds.is_empty() || (man.asks_permissions() && reads), "{}", text);
|
||||
}
|
||||
let mut s = rich_session();
|
||||
s.allow_plugin_grants_from_requests();
|
||||
let module = wat::parse_str(tpl::module(&text, tpl::ECHO, "", 1)).unwrap();
|
||||
let data = vectorcraft_format::base64_encode(&module);
|
||||
let grant = json!(["doc.read", "doc.write", "state", "effect.render", "export.write"]);
|
||||
@@ -915,6 +917,7 @@ proptest! {
|
||||
let _ = std::fs::remove_dir_all(&store.0);
|
||||
let folder = store.0.clone();
|
||||
let mut s = Session::new();
|
||||
s.allow_plugin_grants_from_requests();
|
||||
s.attach_plugin_store(folder.clone());
|
||||
let storing = |id: &str| format!(r#"{{"id":"{id}","name":"F","kind":"filter","api":2,"permissions":["settings","cache"]}}"#);
|
||||
let grant = json!(["settings", "cache"]);
|
||||
|
||||
@@ -204,6 +204,7 @@ fn a_disabled_plug_in_whose_new_grant_cant_be_stored_stays_disabled() {
|
||||
let _f = fresh();
|
||||
let dir = tmp_dir("reinstall-no-grant");
|
||||
let mut s = launch(&dir);
|
||||
s.allow_plugin_grants_from_requests();
|
||||
let manifest = r#"{"id":"test.sm.g","name":"Test","version":"1","kind":"filter","permissions":["doc.read"]}"#;
|
||||
let module = |out: &str| vectorcraft_format::base64_encode(&wat::parse_str(tpl::returning(manifest, out)).unwrap());
|
||||
s.execute("plugin.install", &json!({"dataBase64": module(r#"{"objects":[]}"#), "grant": ["doc.read"]})).unwrap();
|
||||
|
||||
@@ -718,6 +718,11 @@ fn errors_are_tool_results_not_crashes() {
|
||||
|
||||
/// A fake control server: answers `document.inspect`, echoes `engine.execute`, errors otherwise.
|
||||
fn fake_app() -> (String, std::thread::JoinHandle<Vec<Value>>) {
|
||||
fake_app_refusing_confirm(None)
|
||||
}
|
||||
|
||||
/// [`fake_app`], answering `ui.dialog.confirm` with the error `refusal` when one is given.
|
||||
fn fake_app_refusing_confirm(refusal: Option<&'static str>) -> (String, std::thread::JoinHandle<Vec<Value>>) {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap().to_string();
|
||||
let h = std::thread::spawn(move || {
|
||||
@@ -733,6 +738,7 @@ fn fake_app() -> (String, std::thread::JoinHandle<Vec<Value>>) {
|
||||
"engine.execute" => json!({"ok": true, "result": {"echo": msg["params"]}}),
|
||||
"ui.inspect" => json!({"ok": true, "result": {"tool": "selection"}}),
|
||||
"ui.dialog.set" if !dialog_open => json!({"ok": false, "error": "no dialog open"}),
|
||||
"ui.dialog.confirm" if refusal.is_some() => json!({"ok": false, "error": refusal}),
|
||||
m if m.starts_with("ui.dialog.") => {
|
||||
dialog_open &= m != "ui.dialog.cancel";
|
||||
json!({"ok": true, "result": {"echo": msg["params"]}})
|
||||
@@ -823,6 +829,23 @@ fn answer_dialog_does_not_confirm_when_the_app_refuses_the_set() {
|
||||
assert_eq!(seen[1]["params"], json!({"fields": set}));
|
||||
}
|
||||
|
||||
/// The app refuses to confirm a plug-in's install approval for automation: the call is an error
|
||||
/// with the app's reason, and nothing else is sent that could install the module.
|
||||
#[test]
|
||||
fn answer_dialog_reports_that_the_app_refuses_to_approve_a_plugin_install() {
|
||||
let refusal = "Only the person at the keyboard can approve installing a plug-in";
|
||||
let (addr, h) = fake_app_refusing_confirm(Some(refusal));
|
||||
let mut s = Server::new(Box::new(Remote::connect(&addr).unwrap()));
|
||||
let set = json!([{"field": "__params", "value": {"dataBase64": "AAAA"}}]);
|
||||
let r = call(&mut s, 1, "answer_dialog", json!({"set": set, "action": "confirm"}));
|
||||
assert_eq!(r["isError"], true, "{r}");
|
||||
assert!(text_of(&r).contains(refusal), "{r}");
|
||||
drop(s);
|
||||
let seen = h.join().unwrap();
|
||||
let methods: Vec<&str> = seen.iter().map(|m| m["method"].as_str().unwrap()).collect();
|
||||
assert_eq!(methods, ["ui.dialog.set", "ui.dialog.confirm"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn answer_dialog_needs_the_desktop_app() {
|
||||
let mut s = server();
|
||||
|
||||
@@ -168,6 +168,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)]
|
||||
|
||||
@@ -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!({}))
|
||||
|
||||
@@ -402,6 +402,7 @@ registry! {
|
||||
Plugin: [plugin::KIND] => plugin::SPEC,
|
||||
PluginExport: [plugin::EXPORT_KIND] => plugin::EXPORT_SPEC,
|
||||
PluginImport: [plugin::IMPORT_KIND] => plugin::IMPORT_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,
|
||||
@@ -563,6 +564,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;
|
||||
}
|
||||
if d.kind == safe_mode::STOPPED {
|
||||
safe_mode::cancel_stopped(app);
|
||||
}
|
||||
|
||||
@@ -13,16 +13,22 @@
|
||||
//! Export button writes the file. Importers open the files File › Open finds no built-in format
|
||||
//! for, and File › Open With lists them, to open a file with one of them by name; one with
|
||||
//! parameters opens dialog `pluginImport` once the file is picked, and its Open button opens it.
|
||||
//!
|
||||
//! 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. Enter confirms it only when OK has the keyboard focus.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_engine::cmd::plugin::InstallApproval;
|
||||
use vectorcraft_plugins::{Budget, Kind, Manifest, Plugin, permissions, 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";
|
||||
@@ -310,3 +316,71 @@ fn import_confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String>
|
||||
let picked = p["picked"] == true;
|
||||
crate::io::open_with_on(app, &p, picked && crate::opening::available())
|
||||
}
|
||||
|
||||
/// 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: the OK in its body is a `Sense::click` button, which Enter clicks
|
||||
/// only when it has the keyboard focus.
|
||||
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,13 @@ use vectorcraft_plugins::wat as tpl;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The install approval has no OK in the shared footer, so Enter presses OK only when OK has the
|
||||
/// keyboard focus.
|
||||
#[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!(
|
||||
|
||||
@@ -5792,7 +5792,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}
|
||||
@@ -5823,7 +5823,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.
|
@@ -5927,7 +5927,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}
|
||||
@@ -5967,7 +5967,9 @@ digits Malayalam Malaiàlam
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) pressupost d’objectes (més de {MAX_OBJECTS} objectes de només lectura)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` un filtre pesat s’executa en segon pla, de manera que no pot declarar `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` com a màxim {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5919,7 +5919,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}
|
||||
@@ -5959,7 +5959,9 @@ axis Y Axis osa Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) rozpočet objektů (více než {MAX_OBJECTS} objektů jen pro čtení)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` těžký filtr běží na pozadí, takže nemůže deklarovat `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` nejvýše {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5502,7 +5502,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}
|
||||
@@ -5542,7 +5542,9 @@ axis Y Axis Y-Achse
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) Objektbudget (mehr als {MAX_OBJECTS} schreibgeschützte Objekte)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` ein schwerer Filter läuft im Hintergrund und kann daher keine `commands` deklarieren
|
||||
@msg at most {MAX_COMMANDS} `commands` höchstens {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5870,7 +5870,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}
|
||||
@@ -5910,7 +5910,9 @@ axis Y Axis Άξονας Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) προϋπολογισμός αντικειμένων (περισσότερα από {MAX_OBJECTS} αντικείμενα μόνο για ανάγνωση)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` ένα βαρύ φίλτρο εκτελείται στο παρασκήνιο, οπότε δεν μπορεί να δηλώνει `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` το πολύ {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5861,7 +5861,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}
|
||||
@@ -5901,7 +5901,9 @@ axis Y Axis Eje Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) presupuesto de objetos (más de {MAX_OBJECTS} objetos de solo lectura)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` un filtro pesado se ejecuta en segundo plano, así que no puede declarar `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` como máximo {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5949,7 +5949,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}
|
||||
@@ -5989,7 +5989,9 @@ axis Y Axis Y-akseli
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) objektibudjetti (yli {MAX_OBJECTS} vain luku -objektia)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` raskas suodin suoritetaan taustalla, joten se ei voi määritellä `commands`-kohtaa
|
||||
@msg at most {MAX_COMMANDS} `commands` enintään {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5914,7 +5914,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}
|
||||
@@ -5954,7 +5954,9 @@ axis Y Axis Axe Y
|
||||
@msg memory budget (object {_1} needs {size} bytes of input, {left} left) budget mémoire (l’objet {_1} nécessite {size} octets d’entrée, il en reste {left})
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) budget d’objets (plus de {MAX_OBJECTS} objets en lecture seule)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` un filtre lourd s’exécute en arrière-plan, il ne peut donc pas déclarer `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` au plus {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5254,7 +5254,7 @@ antiAlias Sharp शार्प
|
||||
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}" स्थापित करें, जो ये अनुमतियां मांगता है?
|
||||
@@ -5295,7 +5295,9 @@ antiAlias Sharp शार्प
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) ऑब्जेक्ट बजट ({MAX_OBJECTS} से अधिक केवल-पठन ऑब्जेक्ट)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` भारी फ़िल्टर पृष्ठभूमि में चलता है, इसलिए वह `commands` घोषित नहीं कर सकता
|
||||
@msg at most {MAX_COMMANDS} `commands` अधिकतम {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5902,7 +5902,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}
|
||||
@@ -5942,7 +5942,9 @@ axis Y Axis Y tengely
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) objektumkeret (több mint {MAX_OBJECTS} csak olvasható objektum)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` a nehéz szűrő a háttérben fut, ezért nem deklarálhat `commands` értéket
|
||||
@msg at most {MAX_COMMANDS} `commands` legfeljebb {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5536,7 +5536,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}
|
||||
@@ -5576,7 +5576,9 @@ axis Y Axis Asse Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) budget di oggetti (più di {MAX_OBJECTS} oggetti di sola lettura)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` un filtro pesante viene eseguito in background, quindi non può dichiarare `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` al massimo {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5601,7 +5601,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}
|
||||
@@ -5641,7 +5641,9 @@ axis Y Axis Y 軸
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) オブジェクト予算 ({MAX_OBJECTS} 個を超える読み取り専用オブジェクト)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` 重いフィルターはバックグラウンドで実行されるため、`commands` を宣言できません
|
||||
@msg at most {MAX_COMMANDS} `commands` `commands` は最大 {MAX_COMMANDS} 個です
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5842,7 +5842,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}
|
||||
@@ -5882,7 +5882,9 @@ axis Y Axis Y ღერძი
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) ობიექტების ბიუჯეტი ({MAX_OBJECTS}-ზე მეტი მხოლოდ წასაკითხი ობიექტი)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` მძიმე ფილტრი ფონურ რეჟიმში მუშაობს, ამიტომ მას არ შეუძლია `commands`-ის გამოცხადება
|
||||
@msg at most {MAX_COMMANDS} `commands` მაქსიმუმ {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5497,7 +5497,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}
|
||||
@@ -5537,7 +5537,9 @@ axis Y Axis Y축
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) 개체 예산(읽기 전용 개체 {MAX_OBJECTS}개 초과)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` 무거운 필터는 백그라운드에서 실행되므로 `commands`를 선언할 수 없습니다
|
||||
@msg at most {MAX_COMMANDS} `commands` `commands`는 최대 {MAX_COMMANDS}개입니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5773,7 +5773,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}
|
||||
@@ -5813,7 +5813,9 @@ axis Y Axis Oś Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) budżet obiektów (ponad {MAX_OBJECTS} obiektów tylko do odczytu)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` ciężki filtr działa w tle, więc nie może deklarować `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` najwyżej {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5834,7 +5834,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}
|
||||
@@ -5874,7 +5874,9 @@ axis Y Axis Eixo Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) orçamento de objetos (mais de {MAX_OBJECTS} objetos somente leitura)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` um filtro pesado é executado em segundo plano, então não pode declarar `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` no máximo {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -5530,7 +5530,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}
|
||||
@@ -5570,7 +5570,9 @@ axis Y Axis Ось Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) бюджет объектов (более {MAX_OBJECTS} объектов только для чтения)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` тяжёлый фильтр выполняется в фоне, поэтому не может объявлять `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` не более {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5942,7 +5942,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}
|
||||
@@ -5982,7 +5982,9 @@ axis Y Axis Y-axel
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) objektbudget (fler än {MAX_OBJECTS} skrivskyddade objekt)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` ett tungt filter körs i bakgrunden, så det kan inte deklarera `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` högst {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5875,7 +5875,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}
|
||||
@@ -5915,7 +5915,9 @@ axis Y Axis Y ekseni
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) nesne bütçesi ({MAX_OBJECTS} adetten fazla salt okunur nesne)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` ağır bir filtre arka planda çalışır, bu yüzden `commands` bildiremez
|
||||
@msg at most {MAX_COMMANDS} `commands` en fazla {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5516,7 +5516,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}
|
||||
@@ -5556,7 +5556,9 @@ axis Y Axis Вісь Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) бюджет об’єктів (понад {MAX_OBJECTS} об’єктів лише для читання)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` важкий фільтр виконується у фоні, тому не може оголошувати `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` щонайбільше {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -5517,7 +5517,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}
|
||||
@@ -5557,7 +5557,9 @@ axis Y Axis Trục Y
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) ngân sách đối tượng (hơn {MAX_OBJECTS} đối tượng chỉ đọc)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` bộ lọc nặng chạy ở chế độ nền nên không thể khai báo `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` tối đa {MAX_COMMANDS} `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -5757,7 +5757,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}
|
||||
@@ -5797,7 +5797,9 @@ axis Y Axis Y 轴
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) 对象预算(超过 {MAX_OBJECTS} 个只读对象)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` 重型滤镜在后台运行,因此不能声明 `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` 最多 {MAX_COMMANDS} 个 `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5554,7 +5554,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}
|
||||
@@ -5594,7 +5594,9 @@ axis Y Axis Y 軸
|
||||
@msg object budget (more than {MAX_OBJECTS} read-only objects) 物件預算(超過 {MAX_OBJECTS} 個唯讀物件)
|
||||
@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 a heavy filter runs in the background, so it can't declare `commands` 重型濾鏡在背景執行,因此不能宣告 `commands`
|
||||
@msg at most {MAX_COMMANDS} `commands` 最多 {MAX_COMMANDS} 個 `commands`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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, open_with_on, 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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use vectorcraft_engine::cmd::plugin::InstallApproval;
|
||||
use vectorcraft_plugins::permissions;
|
||||
|
||||
use super::save::suggested;
|
||||
@@ -190,79 +191,94 @@ 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, so does a filter that asks for a heavy budget the user hasn't allowed
|
||||
/// yet (`allowHeavy`), and so does a module that would take the place of another one with its id
|
||||
/// and get its settings and cache (`replace`); 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, so does a filter that asks for a heavy budget, and so does a module that would take the
|
||||
/// place of another one with its id and get its settings and cache (`replace`); 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());
|
||||
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");
|
||||
// `replace: false` asks for no replacing, so only a module without `replace` asks.
|
||||
let replace_given = module.get("replace").is_some_and(|r| !r.is_null());
|
||||
if !heavy_allowed || !granted || !replace_given {
|
||||
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", ×.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 }));
|
||||
}
|
||||
if !replace_given && info["needsReplace"] == true {
|
||||
let message = crate::i18n::fmt(tl!("Install “{name}” in place of the plug-in with the same ID?"), &[("name", &name)]);
|
||||
let lines = [
|
||||
crate::i18n::fmt(tl!("ID: {id}"), &[("id", &shown("id", permissions::MAX_SHOWN_FIELD))]),
|
||||
crate::i18n::fmt(tl!("File SHA-256: {hash}"), &[("hash", &shown("hash", 64))]),
|
||||
String::new(),
|
||||
tl!("This file is not the plug-in installed before with this ID. It gets the settings and cache kept for that ID.").to_string(),
|
||||
];
|
||||
let mut replaced = answered;
|
||||
replaced["replace"] = json!(true);
|
||||
dialogs::confirm::ask(app, &message, &lines.join("\n"), "ui.installPlugin", replaced);
|
||||
return Ok(json!({ "pending": dialogs::confirm::KIND }));
|
||||
}
|
||||
let asks_replace = module.get("replace").is_none_or(Value::is_null) && info["needsReplace"] == true;
|
||||
if !needs_grant && !heavy && !asks_replace {
|
||||
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 identity = [
|
||||
crate::i18n::fmt(tl!("ID: {id}"), &[("id", &shown("id", permissions::MAX_SHOWN_FIELD))]),
|
||||
crate::i18n::fmt(tl!("File SHA-256: {hash}"), &[("hash", &shown("hash", 64))]),
|
||||
]
|
||||
.join("\n");
|
||||
let takes_place = tl!("This file is not the plug-in installed before with this ID. It gets the settings and cache kept for that ID.");
|
||||
if asks_replace {
|
||||
install["replace"] = json!(true);
|
||||
}
|
||||
// A client may pass `replace` itself, so taking another module's place alone needs no approval
|
||||
// that only a person can give.
|
||||
if !needs_grant && !heavy {
|
||||
let message = crate::i18n::fmt(tl!("Install “{name}” in place of the plug-in with the same ID?"), &[("name", &name)]);
|
||||
dialogs::confirm::ask(app, &message, &format!("{identity}\n\n{takes_place}"), "ui.installPlugin", install);
|
||||
return Ok(json!({ "pending": dialogs::confirm::KIND }));
|
||||
}
|
||||
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", ×.to_string())],
|
||||
));
|
||||
install["allowHeavy"] = json!(true);
|
||||
}
|
||||
if asks_replace {
|
||||
paragraphs.push(if needs_grant { takes_place.to_string() } else { format!("{identity}\n\n{takes_place}") });
|
||||
}
|
||||
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
|
||||
|
||||
@@ -309,6 +309,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,
|
||||
|
||||
@@ -1488,7 +1488,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 (true: the user agreed it takes the place of the plug-in with its id, as plugin.install), 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, so does a filter that asks for a heavy budget, and so does one without replace that would take the place of another module with its id and get its settings and cache (needsReplace in plugin.inspect), showing its id and file SHA-256: 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 (true: the user agreed it takes the place of the plug-in with its id, as plugin.install), 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, so does a filter that asks for a heavy budget, and so does one without replace that would take the place of another module with its id and get its settings and cache (needsReplace in plugin.inspect), showing its id and file SHA-256: OK installs it if the file still has the SHA-256 shown, Cancel installs nothing. Only a person can press OK when it asks about permissions or a heavy budget: ui.dialog.confirm, answer_dialog and synthetic input are refused → plugin.install's result | {pending: \"pluginInstall\"} while it asks about permissions or a heavy budget | {pending: \"confirm\"} while it asks only about replacing",
|
||||
),
|
||||
(
|
||||
"plugin.cancelRuns",
|
||||
|
||||
@@ -2647,6 +2647,7 @@ fn a_filter_plugin_keeps_its_shortcut_when_replaced_or_reloaded() {
|
||||
fn a_heavy_filter_plugin_runs_from_its_shortcut_as_from_its_menu_item() {
|
||||
use vectorcraft_plugins::wat as tpl;
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.allow_plugin_grants_from_requests();
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let (at_once, asks) = ("test.UI.heavy_filter-at-once", "test.UI.heavy_filter-asks");
|
||||
for (id, params, chord) in [(at_once, "{}", "Cmd+Alt+Shift+F7"), (asks, r#"{"wiggle":{"type":"bool"}}"#, "Cmd+Alt+Shift+F8")] {
|
||||
|
||||
@@ -372,6 +372,147 @@ 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}");
|
||||
// What MCP's `answer_dialog` sends: `ui.dialog.set {fields}`, then `ui.dialog.confirm`.
|
||||
let r = request(&mut app, &tx, "ui.dialog.set", json!({"fields": [{"field": "__params", "value": {"dataBase64": "AAAA"}}]}));
|
||||
assert!(r.get("error").is_none(), "{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() && vectorcraft_plugins::registry::get(id).is_none());
|
||||
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();
|
||||
}
|
||||
|
||||
type DialogHarness = egui_kittest::Harness<'static, VectorcraftApp>;
|
||||
|
||||
/// Runs one UI pass of `h` for each batch of synthetic input the control path queued, each taken
|
||||
/// through the host's raw-input hook, until none is left and the last batch is acknowledged.
|
||||
fn take_injected(h: &mut DialogHarness) {
|
||||
let ctx = h.ctx.clone();
|
||||
loop {
|
||||
let mut raw = egui::RawInput::default();
|
||||
h.state_mut().raw_input_hook(&ctx, &mut raw);
|
||||
if raw.events.is_empty() && h.state().synthetic.is_empty() {
|
||||
break;
|
||||
}
|
||||
h.input_mut().events.extend(raw.events);
|
||||
h.step();
|
||||
}
|
||||
}
|
||||
|
||||
/// The open install approval, with nothing installed or granted for `id`.
|
||||
fn still_waiting(h: &DialogHarness, id: &str) -> bool {
|
||||
h.state().ui.dialog.as_ref().is_some_and(|d| d.kind == crate::dialogs::plugin::INSTALL_KIND)
|
||||
&& vectorcraft_plugins::registry::get(id).is_none()
|
||||
&& vectorcraft_plugins::registry::granted(id).is_none()
|
||||
}
|
||||
|
||||
/// Keys and pointer input sent through the control path approve nothing at the open install
|
||||
/// approval: Enter and Space press nothing with no button focused, and with OK focused they and a
|
||||
/// `ui.click` on it press it and are refused. `ui.pointer` drives the canvas. A person's click on
|
||||
/// the same button approves.
|
||||
#[test]
|
||||
fn injected_keys_and_pointer_input_cannot_approve_a_plugin_install() {
|
||||
use egui_kittest::kittest::{NodeT, Queryable};
|
||||
let refusal = "Only the person at the keyboard";
|
||||
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.injected";
|
||||
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 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 mut fonts = false;
|
||||
let mut h: DialogHarness = egui_kittest::Harness::builder().with_size(egui::vec2(900.0, 800.0)).build_ui_state(
|
||||
move |ui, app: &mut VectorcraftApp| {
|
||||
if !std::mem::replace(&mut fonts, true) {
|
||||
crate::theme::install_fonts(ui.ctx());
|
||||
return;
|
||||
}
|
||||
crate::dialogs::show(app, ui.ctx());
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(4);
|
||||
|
||||
for key in ["Enter", "Space"] {
|
||||
assert!(!h.get_by_label("OK").accesskit_node().is_focused(), "no button has the focus yet");
|
||||
h.state_mut().ui.status.clear();
|
||||
let r = request(h.state_mut(), &tx, "ui.key", json!({"key": key}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
take_injected(&mut h);
|
||||
assert!(still_waiting(&h, id), "{key}");
|
||||
assert_eq!(h.state().ui.status, "", "{key} presses nothing");
|
||||
}
|
||||
|
||||
for key in ["Enter", "Space"] {
|
||||
h.get_by_label("OK").focus();
|
||||
h.run_steps(2);
|
||||
assert!(h.get_by_label("OK").accesskit_node().is_focused());
|
||||
h.state_mut().ui.status.clear();
|
||||
let r = request(h.state_mut(), &tx, "ui.key", json!({"key": key}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
take_injected(&mut h);
|
||||
assert!(still_waiting(&h, id), "{key} on a focused OK");
|
||||
assert!(h.state().ui.status.contains(refusal), "{key} pressed OK and was refused: {:?}", h.state().ui.status);
|
||||
}
|
||||
|
||||
let at = h.get_by_label("OK").rect().center();
|
||||
h.state_mut().ui.status.clear();
|
||||
let r = request(h.state_mut(), &tx, "ui.click", json!({"x": at.x, "y": at.y}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
take_injected(&mut h);
|
||||
assert!(still_waiting(&h, id), "ui.click");
|
||||
assert!(h.state().ui.status.contains(refusal), "ui.click pressed OK and was refused: {:?}", h.state().ui.status);
|
||||
|
||||
let events = json!([{"kind": "down", "x": at.x, "y": at.y}, {"kind": "up", "x": at.x, "y": at.y}]);
|
||||
let r = request(h.state_mut(), &tx, "ui.pointer", json!({"events": events}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
take_injected(&mut h);
|
||||
assert!(still_waiting(&h, id), "ui.pointer");
|
||||
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().ui.dialog.is_none(), "{:?}", h.state().ui.status);
|
||||
assert_eq!(vectorcraft_plugins::registry::granted(id).map(|g| g.hash), Some(vectorcraft_plugins::permissions::sha256_hex(&bytes)));
|
||||
h.state_mut().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]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -131,8 +131,8 @@ fn asking(id: &str, name: &str, permissions: &str) -> Vec<u8> {
|
||||
/// Installing a plug-in that lists permissions asks first, showing its name, id, version, file hash
|
||||
/// and each permission in plain words, with hidden characters from the manifest escaped. Cancel
|
||||
/// installs nothing, OK installs it; installing the same module again doesn't ask, one with other
|
||||
/// bytes asks again, even when it lists the same permissions, and then whether to take the
|
||||
/// installed one's place, as does one that asks for another permission.
|
||||
/// bytes asks again, even when it lists the same permissions, and in the same dialog whether to
|
||||
/// take the installed one's place, as does one that asks for another permission.
|
||||
#[test]
|
||||
fn installing_a_plugin_that_lists_permissions_asks_first() {
|
||||
let mut app = app();
|
||||
@@ -143,25 +143,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());
|
||||
@@ -173,39 +174,37 @@ fn installing_a_plugin_that_lists_permissions_asks_first() {
|
||||
// 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!(app.ui.dialog.as_ref().unwrap().str("message"), "Install “Tidy 2”, which asks for these permissions?");
|
||||
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND, "other bytes ask again");
|
||||
assert_eq!(app.pending_plugin_install.as_ref().unwrap().message, "Install “Tidy 2”, which asks for these permissions?");
|
||||
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");
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
// OK on the permissions goes on to ask before it takes the installed one's place, showing its
|
||||
// id and file hash; Cancel there keeps the one installed and records no grant.
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.str("message"), "Install “Tidy 2” in place of the plug-in with the same ID?");
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(dialogs::plugin::INSTALL_KIND), "File › Open asks too");
|
||||
// The same dialog says it takes the installed one's place, and OK replaces it; Cancel keeps the
|
||||
// one installed and records no grant.
|
||||
let pending = app.pending_plugin_install.as_ref().unwrap();
|
||||
let again_hash = vectorcraft_plugins::permissions::sha256_hex(&again);
|
||||
assert_eq!(
|
||||
d.str("detail"),
|
||||
format!(
|
||||
"ID: {id}\nFile SHA-256: {again_hash}\n\nThis file is not the plug-in installed before with this ID. It gets the settings and cache kept for that ID."
|
||||
)
|
||||
assert!(pending.detail.contains(&format!("File SHA-256: {again_hash}")), "{}", pending.detail);
|
||||
assert!(
|
||||
pending.detail.ends_with("\n\nThis file is not the plug-in installed before with this ID. It gets the settings and cache kept for that ID."),
|
||||
"{}",
|
||||
pending.detail
|
||||
);
|
||||
assert_eq!(pending.params["replace"], json!(true), "OK replaces the one installed");
|
||||
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");
|
||||
assert_ne!(vectorcraft_plugins::registry::granted(id).map(|g| g.hash), Some(again_hash), "Cancel records no grant");
|
||||
app.run("ui.installPlugin", json!({"dataBase64": vectorcraft_format::base64_encode(&again)})).unwrap();
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "both questions are answered");
|
||||
assert!(app.ui.dialog.is_none(), "one question");
|
||||
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);
|
||||
assert_eq!(vectorcraft_plugins::registry::granted(id).map(|g| g.hash), Some(new_hash), "OK records the new module's hash");
|
||||
// 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.contains("• 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.
|
||||
@@ -215,6 +214,43 @@ fn installing_a_plugin_that_lists_permissions_asks_first() {
|
||||
app.run("plugin.remove", json!({"id": id})).unwrap();
|
||||
}
|
||||
|
||||
/// A module with other bytes than the one installed with its id asks before taking its place: in
|
||||
/// the ordinary confirm when it needs nothing that only a person can approve, and in the approval
|
||||
/// dialog, with its id and file hash, when it also asks for a heavy budget.
|
||||
#[test]
|
||||
fn installing_a_plugin_over_another_module_asks_first() {
|
||||
let mut app = app();
|
||||
let id = "test.ui.replaces";
|
||||
let plain = |output: &str| wat::parse_str(tpl::returning(&format!(r#"{{"id":"{id}","name":"Plain","kind":"filter"}}"#), output)).unwrap();
|
||||
let module = |wasm: &[u8]| json!({"dataBase64": vectorcraft_format::base64_encode(wasm)});
|
||||
let takes_place = "This file is not the plug-in installed before with this ID. It gets the settings and cache kept for that ID.";
|
||||
app.run("ui.installPlugin", module(&plain(r#"{"objects":[]}"#))).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "nothing to take the place of");
|
||||
|
||||
let other = plain(r#"{"objects":[],"error":null}"#);
|
||||
let other_hash = vectorcraft_plugins::permissions::sha256_hex(&other);
|
||||
let r = app.run("ui.installPlugin", module(&other)).unwrap();
|
||||
assert_eq!(r["pending"], dialogs::confirm::KIND);
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.str("message"), "Install “Plain” in place of the plug-in with the same ID?");
|
||||
assert_eq!(d.str("detail"), format!("ID: {id}\nFile SHA-256: {other_hash}\n\n{takes_place}"));
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(vectorcraft_plugins::registry::get(id).map(|p| p.hash().to_string()), Some(other_hash));
|
||||
|
||||
let manifest = format!(r#"{{"id":"{id}","name":"Heavy","kind":"filter","budget":"heavy"}}"#);
|
||||
let heavy = wat::parse_str(tpl::spinning(&manifest, 1, r#"{"objects":[]}"#)).unwrap();
|
||||
let heavy_hash = vectorcraft_plugins::permissions::sha256_hex(&heavy);
|
||||
let r = app.run("ui.installPlugin", module(&heavy)).unwrap();
|
||||
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND);
|
||||
let pending = app.pending_plugin_install.as_ref().unwrap();
|
||||
assert!(pending.detail.ends_with(&format!("\n\nID: {id}\nFile SHA-256: {heavy_hash}\n\n{takes_place}")), "{}", pending.detail);
|
||||
assert_eq!(pending.params["replace"], json!(true), "OK replaces the one installed");
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(vectorcraft_plugins::registry::get(id).map(|p| p.hash().to_string()), Some(heavy_hash));
|
||||
app.run("plugin.remove", json!({"id": id})).unwrap();
|
||||
}
|
||||
|
||||
/// A plug-in with two commands: Grow, which adds a triangle (`_context.command` is `grow`), and
|
||||
/// Size, with a parameter, whose `menu` names another menu.
|
||||
fn commands_plugin(id: &str) -> Vec<u8> {
|
||||
@@ -300,6 +336,63 @@ fn a_shortcut_runs_a_plugin_command() {
|
||||
app.run("plugin.remove", json!({"id": id})).unwrap();
|
||||
}
|
||||
|
||||
/// The `grant` and `allowHeavy` that `ui.installPlugin` is given approve nothing: it asks all the
|
||||
/// same, and what OK would approve comes from the manifest. A module with nothing to approve
|
||||
/// installs without either.
|
||||
#[test]
|
||||
fn ui_install_plugin_takes_no_grant_or_heavy_budget_from_its_params() {
|
||||
let mut app = app();
|
||||
let id = "test.ui.approval.params";
|
||||
let manifest = format!(r#"{{"id":"{id}","name":"Asks","kind":"filter","permissions":["doc.read"]}}"#);
|
||||
let asks = wat::parse_str(tpl::returning(&manifest, r#"{"objects":[]}"#)).unwrap();
|
||||
let given = |wasm: &[u8]| json!({"dataBase64": vectorcraft_format::base64_encode(wasm), "grant": ["doc.read", "doc.write"], "allowHeavy": true});
|
||||
let r = app.run("ui.installPlugin", given(&asks)).unwrap();
|
||||
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND);
|
||||
assert!(vectorcraft_plugins::registry::get(id).is_none() && vectorcraft_plugins::registry::granted(id).is_none());
|
||||
let pending = app.pending_plugin_install.as_ref().unwrap();
|
||||
assert_eq!(pending.approval.permissions, ["doc.read"], "the permissions the manifest lists");
|
||||
assert_eq!(pending.params["grant"], json!(["doc.read"]));
|
||||
assert!(!pending.approval.heavy && pending.params.get("allowHeavy").is_none(), "no heavy budget was asked about");
|
||||
dialogs::cancel(&mut app);
|
||||
assert!(vectorcraft_plugins::registry::get(id).is_none());
|
||||
|
||||
let heavy_id = "test.ui.approval.params.heavy";
|
||||
let manifest = format!(r#"{{"id":"{heavy_id}","name":"Heavy","kind":"filter","budget":"heavy"}}"#);
|
||||
let heavy = wat::parse_str(tpl::spinning(&manifest, 1, r#"{"objects":[]}"#)).unwrap();
|
||||
let r = app.run("ui.installPlugin", given(&heavy)).unwrap();
|
||||
assert_eq!(r["pending"], dialogs::plugin::INSTALL_KIND, "allowHeavy skips no question");
|
||||
assert!(vectorcraft_plugins::registry::get(heavy_id).is_none());
|
||||
dialogs::cancel(&mut app);
|
||||
|
||||
let plain_id = "test.ui.approval.params.plain";
|
||||
let manifest = format!(r#"{{"id":"{plain_id}","name":"Plain","kind":"filter"}}"#);
|
||||
let plain = wat::parse_str(tpl::returning(&manifest, r#"{"objects":[]}"#)).unwrap();
|
||||
let r = app.run("ui.installPlugin", given(&plain)).unwrap();
|
||||
assert!(r.get("pending").is_none() && app.ui.dialog.is_none(), "{r}");
|
||||
assert!(vectorcraft_plugins::registry::get(plain_id).is_some());
|
||||
assert!(vectorcraft_plugins::registry::granted(plain_id).is_none(), "the grant it was given isn't recorded");
|
||||
app.run("plugin.remove", json!({"id": plain_id})).unwrap();
|
||||
}
|
||||
|
||||
/// An action's step can't grant what only a person approves: a played `plugin.install` with `grant`
|
||||
/// is refused at Accelerated and at Step by Step, and nothing is installed.
|
||||
#[test]
|
||||
fn an_action_step_cannot_grant_a_plugin_its_permissions() {
|
||||
use crate::panels::actions::{Action, ActionSet, PlaybackSpeed, play};
|
||||
let mut app = app();
|
||||
let id = "test.ui.approval.action";
|
||||
let manifest = format!(r#"{{"id":"{id}","name":"Asks","kind":"filter","permissions":["doc.read"]}}"#);
|
||||
let wasm = wat::parse_str(tpl::returning(&manifest, r#"{"objects":[]}"#)).unwrap();
|
||||
let step = json!({"dataBase64": vectorcraft_format::base64_encode(&wasm), "grant": ["doc.read"]});
|
||||
app.ui.action_sets = vec![ActionSet::new("Test", vec![Action::new("Install", vec![("plugin.install".into(), step)])])];
|
||||
for speed in [PlaybackSpeed::Accelerated, PlaybackSpeed::Step] {
|
||||
app.ui.action_playback.speed = speed;
|
||||
let e = play(&mut app, 0, 0).unwrap_err();
|
||||
assert!(e.contains("take effect only in a session started with --allow-plugin-grants"), "{e}");
|
||||
assert!(vectorcraft_plugins::registry::get(id).is_none() && vectorcraft_plugins::registry::granted(id).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// A plug-in of API level 2 that lists `progress` names it in plain words when it asks.
|
||||
#[test]
|
||||
fn installing_a_plugin_that_reports_progress_says_so() {
|
||||
@@ -308,8 +401,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));
|
||||
|
||||
@@ -216,7 +216,8 @@ the `effect` dialog (`effect.dialog {effect: "plugin.<id>"}`). An exporter plug-
|
||||
export's own parameters); `ui.dialog.confirm` writes the file. An importer plug-in with parameters opens the
|
||||
`pluginImport` dialog when `file.openWith` is given no `params`: one field per parameter, `__plugin`, and `__open`
|
||||
(`path` and `importer`); `ui.dialog.confirm` opens 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`,
|
||||
|
||||
+19
-12
@@ -6690,7 +6690,7 @@ them again at startup (`plugin.remove` deletes them there):
|
||||
schema, defaults). An object filter runs on the selected paths and compound paths with `plugin.run {id, params}` (one
|
||||
undo step; it may change, delete or add objects, and the result is selected). A filter whose manifest lists `inputs`
|
||||
(`type`, `image`, `symbolInstance`; `inputs` in its `plugin.list` entry) also lists `doc.read` in its `permissions`, so
|
||||
`plugin.install` needs `grant`. It is also given the selected objects of those kinds to read, in paint order from the
|
||||
installing it needs a person's approval. It is also given the selected objects of those kinds to read, in paint order from the
|
||||
bottom, with at most 16 MiB of pixels and 16 MiB of type runs over one run (an object that doesn't fit what the objects
|
||||
before it left comes with `pixelsOmitted` or `truncated`; an image that reaches the decoder counts toward the pixels
|
||||
even when decoding fails). Every object counts toward the input's half of the plug-in's memory cap, ids, bounds,
|
||||
@@ -6721,19 +6721,26 @@ on the desktop a picked file is read off the UI thread and the reply is `{backgr
|
||||
`pluginImport` dialog. `params` holds the importer's parameters (their defaults when
|
||||
omitted). A failing plug-in returns an error and leaves the document as it was (an export writes
|
||||
no file, and an import opens no document). 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, and so does installing a module that lists no
|
||||
permissions under that id: the earlier bytes then need a new `grant`. A refused install keeps the grant. A failed write
|
||||
of the module to the plug-in store leaves the grant in the store. A stored module that is disabled, or not read in Safe
|
||||
Mode, keeps the grant for its id. When `plugin.remove` can't remove the grant from
|
||||
the plug-in store, it returns an error and the plug-in stays installed. 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`, `--automation-read-root` or `--automation-write-root`). A server confined to folders has no plug-in
|
||||
store, so it reads no `grants.json`, and a module that lists permissions or asks for a heavy budget does not install
|
||||
there. Without `--allow-plugin-grants` and without a recorded grant for the module, `plugin.install` returns an error
|
||||
naming the permissions needed. With `VECTORCRAFT_NO_PREFS` set there is no store, and a `grant` accepted under the
|
||||
flag lasts for that session only. In other sessions the approval, or a `grant` accepted under the flag, is kept with the module's
|
||||
hash, counts as approved in later sessions and in the desktop app, and covers only that module: the
|
||||
same bytes install again without one, also from a headless session that reads the same `grants.json`, and a module
|
||||
with other bytes has `needsApproval: true` and needs a new one even when it lists the same permissions. `plugin.remove`
|
||||
forgets the approval, and so does installing a module that lists no permissions under that id: the earlier bytes then
|
||||
need a new approval. A refused install keeps the grant. A failed write of the module to the plug-in store leaves the
|
||||
grant in the store. A stored module that is disabled, or not read in Safe Mode, keeps the grant for its id. When
|
||||
`plugin.remove` can't remove the grant from the plug-in store, it returns an error and the plug-in stays installed. 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
|
||||
|
||||
+25
-11
@@ -30,8 +30,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?, commands?, size, source, api, paintOutput?, permissions, hash, cacheBytes}], "folder": report}` (`api` is the manifest's [plug-in API level](#permissions); `budget: "heavy"` for a [heavy filter](#heavy-filters); `commands` lists its [commands](#commands); `paintOutput: true` marks a [painted live effect](#painted-live-effects); `permissions` lists what it [may do](#permissions), `hash` is the module's SHA-256; `cacheBytes` is the size of its [cache](#settings-and-cache)) |
|
||||
| `plugin.info` | `{"id"}` | One plug-in: the `plugin.list` entry plus `manifest`, `defaults` and `lastError` (a live effect's last failure). |
|
||||
| `plugin.run` | `{"id", "params"?: {…}, "ids"?: [...]}` (plug-in params may also be top-level keys) | Runs an object filter on the selection (or `ids`; an `ids` that is not a list of object ids is an error). One undo step; the result is selected, except the objects it locked or made guides. |
|
||||
| `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace"?: bool`, `"allowHeavy": bool = false`, `"grant"?: [permission]`, `"hash"?: "sha256 hex"` | Validates and installs a module; returns its `plugin.list` entry. The desktop app and `vectorcraft-cli` keep it in the `Plug-ins` folder next to the preferences, so it is installed again after a restart (#2541). A module with other bytes than the plug-in installed with its id, or, with none installed, one whose id has [settings and a cache](#settings-and-cache) in the `Plug-ins` folder, installs only with `replace: true`, once the user agreed; it takes that plug-in's place and gets its settings and cache. The same module installs again without `replace`, and `replace: false` refuses a module whose id is installed. A [heavy filter](#heavy-filters) installs only with `allowHeavy: true`, 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`) and `needsReplace` (true when `plugin.install` needs `replace: true`). |
|
||||
| `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace"?: bool`, `"allowHeavy": bool = false`, `"grant"?: [permission]`, `"hash"?: "sha256 hex"` | Validates and installs a module; returns its `plugin.list` entry. The desktop app and `vectorcraft-cli` keep it in the `Plug-ins` folder next to the preferences, so it is installed again after a restart (#2541). A module with other bytes than the plug-in installed with its id, or, with none installed, one whose id has [settings and a cache](#settings-and-cache) in the `Plug-ins` folder, installs only with `replace: true`, once the user agreed; it takes that plug-in's place and gets its settings and cache. The same module installs again without `replace`, and `replace: false` refuses a module whose id is installed. A [heavy filter](#heavy-filters) installs only with `allowHeavy: true`. A plug-in whose manifest lists [permissions](#permissions) installs only when `grant` lists every one of them, unless a grant is already recorded for a module with the same SHA-256. A request's `grant` and `allowHeavy` are refused except in a session started with `vectorcraft-cli --allow-plugin-grants`. With `hash`, a module whose SHA-256 differs is refused. |
|
||||
| `plugin.inspect` | as `plugin.install`: `{"path"}` or `{"dataBase64", "name"?}` | Validates a module without installing it; returns the `plugin.list` entry it would have, plus `needsApproval` (true when installing it needs a person's approval) and `needsReplace` (true when `plugin.install` needs `replace: true`). |
|
||||
| `plugin.remove` | `{"id"}` | Uninstalls, and deletes it from the `Plug-ins` folder with its [settings and cache](#settings-and-cache). |
|
||||
| `plugin.clearCache` | `{"id"?}` | Deletes every entry in the [cache](#settings-and-cache) of plug-in `id`, or of every installed plug-in; settings stay. Returns `{"cleared": [id], "bytesFreed"}`. |
|
||||
| `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). |
|
||||
@@ -147,8 +147,9 @@ an importer an import's). Each of its runs gets 1 G instructions (20 times a lig
|
||||
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
|
||||
@@ -194,16 +195,29 @@ 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. Enter confirms it only when OK has the keyboard focus. `ui.dialog.set` can't
|
||||
change what it installs.
|
||||
- `plugin.install` refuses the module, naming the permissions it needs, unless this module already has a recorded grant
|
||||
(`plugin.inspect`'s `needsApproval` tells it beforehand). A request's `grant` is refused too: `plugin.install` takes
|
||||
it only with the approval the dialog gives. A `vectorcraft-cli mcp` or `run` started with `--allow-plugin-grants`
|
||||
takes `grant` and `allowHeavy` from any request; only the person starting the process can set that flag, and it
|
||||
doesn't apply to a running app. `mcp` refuses the flag together with `--automation-read-root` or
|
||||
`--automation-write-root`: a server confined to folders has no plug-in store, so it reads no `grants.json`, and a
|
||||
module that lists permissions or asks for a heavy budget does not install there. Without the flag, MCP in headless
|
||||
mode and the CLI install a module that lists permissions only when its hash is recorded in the shared `grants.json`.
|
||||
A person's approval in the desktop app records it, and so does a `grant` accepted under `--allow-plugin-grants`. A
|
||||
recorded grant counts as approved in later sessions without the flag and in the desktop app. With
|
||||
`VECTORCRAFT_NO_PREFS` set there is no store, and a `grant` accepted under the flag lasts for that session only.
|
||||
With `--allow-plugin-grants`, pass `hash` set to the `plugin.inspect` entry's `hash` so that the module installed is
|
||||
the one the user saw.
|
||||
- **Replacing asks first.** When the file is another module than the plug-in installed with its id, or, with none
|
||||
installed, its id has settings and a cache in the `Plug-ins` folder, Install Plug-in… and File › Open show its id
|
||||
and SHA-256 and ask whether to install it in place of that plug-in, which gives it the settings and cache kept for
|
||||
the id. `plugin.install` refuses such a module, naming it, unless `replace` is `true`, which an agent passes once
|
||||
the user agreed (`plugin.inspect`'s `needsReplace` tells it beforehand). The Additional Plug-ins Folder and
|
||||
`plugin.reload` do not ask.
|
||||
the id. When the same module also needs its permissions or a heavy budget approved, the one approval dialog asks
|
||||
about all of them, and only a person can press its OK. `plugin.install` refuses such a module, naming it, unless
|
||||
`replace` is `true`, which an agent passes once the user agreed (`plugin.inspect`'s `needsReplace` tells it
|
||||
beforehand). The Additional Plug-ins Folder and `plugin.reload` do not ask.
|
||||
- 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, and does not get the id's settings and cache until
|
||||
|
||||
@@ -1585,6 +1585,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **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; for a plug-in that lists permissions, `plugin.install` needs `grant` and folders install it only when it is granted. 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.
|
||||
- **Filter plug-ins read type, images and symbol instances (G17, #4333):** a filter at plug-in API level 2 lists `inputs` (`type`, `image`, `symbolInstance`) with `doc.read` in its `permissions`, so installing it asks for approval, and is also given the selected objects of those kinds, read only: type with its runs, character and paragraph attributes (no text past 256 KiB or 4,096 runs or paragraphs, or 16 MiB of runs over the input), images with their size, color mode and, when embedded and at most 1 MiB in base64 (16 MiB over the input), their RGBA pixels (a CMYK image's through the working CMYK profile; never a linked file's), and symbol instances with their symbol and transform. The objects come in paint order from the bottom, and an image that reaches the decoder counts toward the 16 MiB of pixels even when decoding fails. Every object counts toward the input's half of the memory cap; one whose fields besides its pixels and runs don't fit fails the run naming it and its size, a path or compound path that doesn't fit fails it too, and so do more than 100,000 of these objects. Its output returns each as `{id}` or exactly as given; any change or omission is refused before the document changes. Filters without `inputs` get the same input as before, except that compound paths that clip a clip group and paths inside a group that clips one are left out for every filter. Regressions: `crates/plugins/src/objects/tests.rs`, `crates/plugins/src/manifest.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.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 that lists permissions and has 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 module with a missing or empty permissions list needs no grant; without the list, it gets its kind's implied permissions. 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.
|
||||
- **A plug-in with no permissions ends its id's grant (G17):** installing a module that lists no permissions under a granted id forgets the grant, in the process and in `grants.json`, so the formerly approved bytes need a new approval. Attaching a `Plug-ins` folder whose module for that id lists no permissions forgets it too, and plug-in folders forget the process's grant. A refused install leaves the grant. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_safe_mode.rs`. Scores and hours unchanged.
|
||||
- **Plug-in commands (G17):** a filter or live effect may declare up to 32 `commands` (`{id, label, params?}`); each is the command `plugin.<id>.<command>` while the plug-in is installed, in a submenu named after the plug-in under Object › Plug-ins (Effect › Plug-ins for a live effect) whatever menu the manifest names, with its label escaped. Commands are recorded in actions, listed in Keyboard Shortcuts for the user to give a shortcut, and run through `run` and MCP with their parameters checked first (a command with parameters opens a dialog from its menu item or shortcut). A run uses the plug-in's permissions (a live effect with commands may also hold `doc.write` and `state`, and gets them without a `permissions` list), is one undo step named after the command, and on failure (a trap or an exhausted instruction budget) leaves the document and the undo history as they were with an error naming the plug-in and the command. For a command and for `plugin.run`, an `ids` that is not a list of object ids is an error rather than the selection. A malformed entry is refused by name when the module is installed. Regressions: `crates/plugins/src/manifest.rs`, `crates/plugins/src/permissions.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 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.
|
||||
|
||||
Reference in New Issue
Block a user