mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Automation: --automation-read-root and --automation-write-root may each be given more than once (#3546)
Squashed from #3631 onto main.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! `vectorcraft-cli`: VectorCraft from the command line.
|
||||
//!
|
||||
//! ```text
|
||||
//! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] [--automation-read-root DIR] [--automation-write-root DIR]
|
||||
//! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]...
|
||||
//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2]
|
||||
//! vectorcraft-cli commands
|
||||
//! vectorcraft-cli convert IN OUT [--scale 2] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
|
||||
@@ -68,13 +68,14 @@ const USAGE: &str = "\
|
||||
vectorcraft-cli — VectorCraft automation
|
||||
|
||||
USAGE:
|
||||
vectorcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR] [--automation-write-root DIR]
|
||||
vectorcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]...
|
||||
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
|
||||
(open, place, relink, library loads…) and write (save, export, package, library saves…) to
|
||||
DIR, links followed; a root left out grants none of its access. They imply --headless: a
|
||||
running app enforces its own (vectorcraft --control PORT --automation-read-root DIR …).
|
||||
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 …).
|
||||
|
||||
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N]
|
||||
Headless batch: open FILE (any readable format) or start a new document, run commands in
|
||||
@@ -181,7 +182,7 @@ fn with_stores(mut h: Headless) -> Headless {
|
||||
fn mcp(args: &[String]) -> Result<(), String> {
|
||||
let mut connect: Option<String> = None;
|
||||
let mut headless = false;
|
||||
let (mut read_root, mut write_root): (Option<String>, Option<String>) = (None, None);
|
||||
let (mut read_roots, mut write_roots): (Vec<String>, Vec<String>) = (vec![], vec![]);
|
||||
let mut it = args.iter();
|
||||
while let Some(a) = it.next() {
|
||||
// The roots also come as `--flag=DIR`, as the other Craft apps take them.
|
||||
@@ -194,10 +195,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
|
||||
"--headless" => headless = true,
|
||||
"--automation-read-root" | "--automation-write-root" => {
|
||||
let dir = inline.or_else(|| it.next().cloned()).ok_or_else(|| format!("{flag} needs a folder"))?;
|
||||
let slot = if flag == "--automation-read-root" { &mut read_root } else { &mut write_root };
|
||||
if slot.replace(dir).is_some() {
|
||||
return Err(format!("{flag} is given twice: it takes one folder"));
|
||||
}
|
||||
if flag == "--automation-read-root" { read_roots.push(dir) } else { write_roots.push(dir) }
|
||||
}
|
||||
other => return Err(format!("unknown mcp option `{other}`")),
|
||||
}
|
||||
@@ -205,7 +203,9 @@ fn mcp(args: &[String]) -> Result<(), String> {
|
||||
if headless && connect.is_some() {
|
||||
return Err("use either --connect or --headless".into());
|
||||
}
|
||||
let roots = AutomationRoots::new(read_root.as_deref().map(Path::new), write_root.as_deref().map(Path::new))?;
|
||||
let read: Vec<&Path> = read_roots.iter().map(Path::new).collect();
|
||||
let write: Vec<&Path> = write_roots.iter().map(Path::new).collect();
|
||||
let roots = AutomationRoots::new(&read, &write)?;
|
||||
if roots.is_some() && connect.is_some() {
|
||||
return Err("--automation-read-root and --automation-write-root confine the headless server; a running app enforces its own roots: start it with `vectorcraft --control <port> --automation-read-root <dir> --automation-write-root <dir>`"
|
||||
.into());
|
||||
@@ -213,8 +213,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
|
||||
let backend: Box<dyn Backend> = if let Some(roots) = roots {
|
||||
// Every thread of this process, not only the server's: nothing reaches past the roots.
|
||||
file_access::confine_process(roots.clone());
|
||||
let show = |root: Option<&Path>| root.map_or_else(|| "none".to_string(), |r| r.display().to_string());
|
||||
eprintln!("vectorcraft-cli: files confined: read root {}, write root {}", show(roots.read_root()), show(roots.write_root()));
|
||||
eprintln!("vectorcraft-cli: files confined: {}", roots.describe());
|
||||
Box::new(Headless::with_document().with_automation_roots(Some(roots)))
|
||||
} else if headless {
|
||||
Box::new(with_stores(Headless::with_document()))
|
||||
|
||||
@@ -244,6 +244,58 @@ fn mcp_automation_roots_in_a_folder_an_earlier_run_left() {
|
||||
automation_roots(&empty_dir(dir).join("roots"));
|
||||
}
|
||||
|
||||
/// #3546: each root flag may be given more than once: a path inside any of the read roots opens,
|
||||
/// one inside any of the write roots is written, and everything else, a write root's sibling read
|
||||
/// root included, is refused.
|
||||
#[test]
|
||||
fn mcp_automation_roots_take_several_folders() {
|
||||
let base = tmp("several-roots");
|
||||
for d in ["in", "in2", "out", "out2", "outside"] {
|
||||
std::fs::create_dir_all(base.join(d)).unwrap();
|
||||
}
|
||||
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="40" height="30"><rect width="20" height="10"/></svg>"#;
|
||||
for f in ["in/a.svg", "in2/b.svg", "outside/c.svg"] {
|
||||
std::fs::write(base.join(f), svg).unwrap();
|
||||
}
|
||||
let dir = |d: &str| base.join(d).to_string_lossy().into_owned();
|
||||
let file = |d: &str, f: &str| base.join(d).join(f).to_string_lossy().into_owned();
|
||||
let mut child = Command::new(BIN)
|
||||
.args(["mcp", "--automation-read-root", &dir("in"), &format!("--automation-read-root={}", dir("in2"))])
|
||||
.args(["--automation-write-root", &dir("out"), "--automation-write-root", &dir("out2")])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let call = |id: u64, name: &str, args: Value| json!({"jsonrpc":"2.0","id":id,"method":"tools/call","params":{"name":name,"arguments":args}});
|
||||
let calls = [
|
||||
(call(1, "open_file", json!({"path": file("in", "a.svg")})), false),
|
||||
(call(2, "open_file", json!({"path": file("in2", "b.svg")})), false),
|
||||
(call(3, "open_file", json!({"path": file("outside", "c.svg")})), true),
|
||||
(call(4, "export", json!({"path": file("out", "a.png")})), false),
|
||||
(call(5, "export", json!({"path": file("out2", "b.png")})), false),
|
||||
(call(6, "export", json!({"path": file("outside", "c.png")})), true),
|
||||
(call(7, "export", json!({"path": file("in2", "d.png")})), true),
|
||||
(call(8, "open_file", json!({"path": file("out2", "b.png")})), true),
|
||||
];
|
||||
{
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
for (m, _) in &calls {
|
||||
writeln!(stdin, "{m}").unwrap();
|
||||
}
|
||||
}
|
||||
let out = child.wait_with_output().unwrap();
|
||||
assert!(out.status.success());
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("files confined: read ") && stderr.contains("; write "), "{stderr}");
|
||||
let replies: Vec<Value> = String::from_utf8(out.stdout).unwrap().lines().map(|l| serde_json::from_str(l).unwrap()).collect();
|
||||
let errors: Vec<bool> = replies.iter().map(|r| r["result"]["isError"] == true).collect();
|
||||
assert_eq!(errors, calls.iter().map(|(_, e)| *e).collect::<Vec<_>>(), "{replies:?}");
|
||||
assert!(replies[2]["result"]["content"][0]["text"].as_str().unwrap().contains("outside the read roots"), "{}", replies[2]);
|
||||
assert!(base.join("out/a.png").is_file() && base.join("out2/b.png").is_file());
|
||||
assert!(!base.join("outside/c.png").exists() && !base.join("in2/d.png").exists());
|
||||
}
|
||||
|
||||
fn automation_roots(base: &std::path::Path) {
|
||||
for d in ["in", "out", "outside"] {
|
||||
std::fs::create_dir_all(base.join(d)).unwrap();
|
||||
@@ -267,7 +319,7 @@ fn automation_roots(base: &std::path::Path) {
|
||||
assert!(!out.status.success() && err.contains(why), "{args:?}: {err}");
|
||||
};
|
||||
fails(&["mcp", "--automation-read-root", &dir("missing")], "cannot open the automation read root");
|
||||
fails(&["mcp", "--automation-read-root", &dir("in"), "--automation-read-root", &dir("out")], "given twice");
|
||||
fails(&["mcp", "--automation-read-root", &dir("in"), "--automation-read-root", &dir("missing")], "cannot open the automation read root");
|
||||
fails(&["mcp", "--automation-write-root"], "needs a folder");
|
||||
fails(&["mcp", "--connect", "127.0.0.1:1", &format!("--automation-write-root={}", dir("out"))], "a running app enforces its own roots");
|
||||
|
||||
|
||||
@@ -6,29 +6,29 @@ use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct AutomationArgs {
|
||||
pub read_root: Option<PathBuf>,
|
||||
pub write_root: Option<PathBuf>,
|
||||
pub read_roots: Vec<PathBuf>,
|
||||
pub write_roots: Vec<PathBuf>,
|
||||
read_given: bool,
|
||||
write_given: bool,
|
||||
}
|
||||
|
||||
impl AutomationArgs {
|
||||
/// The roots the environment gives (`VECTORCRAFT_AUTOMATION_READ_ROOT` / `_WRITE_ROOT`), until
|
||||
/// the command line gives its own.
|
||||
pub fn new(read_root: Option<PathBuf>, write_root: Option<PathBuf>) -> Self {
|
||||
Self { read_root, write_root, read_given: false, write_given: false }
|
||||
Self { read_roots: read_root.into_iter().collect(), write_roots: write_root.into_iter().collect(), read_given: false, write_given: false }
|
||||
}
|
||||
|
||||
/// Returns whether this argument is a root flag. CLI values override environment defaults
|
||||
/// once; a second occurrence is an error, including when the two spellings are mixed.
|
||||
/// Returns whether this argument is a root flag. Each flag may be given more than once (#3546),
|
||||
/// in either spelling: the first replaces the environment's root of its kind, the others add
|
||||
/// folders.
|
||||
pub fn consume(&mut self, arg: &str, args: &mut impl Iterator<Item = String>) -> Result<bool, String> {
|
||||
let (flag, inline) = arg.split_once('=').map_or((arg, None), |(flag, value)| (flag, Some(value)));
|
||||
let (root, given) = match flag {
|
||||
"--automation-read-root" => (&mut self.read_root, &mut self.read_given),
|
||||
"--automation-write-root" => (&mut self.write_root, &mut self.write_given),
|
||||
let (roots, given) = match flag {
|
||||
"--automation-read-root" => (&mut self.read_roots, &mut self.read_given),
|
||||
"--automation-write-root" => (&mut self.write_roots, &mut self.write_given),
|
||||
_ => return Ok(false),
|
||||
};
|
||||
if *given {
|
||||
return Err(format!("{flag} given twice"));
|
||||
}
|
||||
let value = match inline {
|
||||
Some(value) => value.to_owned(),
|
||||
None => {
|
||||
@@ -42,8 +42,10 @@ impl AutomationArgs {
|
||||
if value.is_empty() {
|
||||
return Err(format!("{flag} needs a non-empty folder"));
|
||||
}
|
||||
*root = Some(value.into());
|
||||
*given = true;
|
||||
if !std::mem::replace(given, true) {
|
||||
roots.clear();
|
||||
}
|
||||
roots.push(value.into());
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@@ -97,17 +99,24 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roots_override_environment_once_and_leave_other_arguments() {
|
||||
let mut roots = AutomationArgs::new(Some("environment".into()), None);
|
||||
fn roots_override_environment_and_add_up() {
|
||||
let mut roots = AutomationArgs::new(Some("environment".into()), Some("environment out".into()));
|
||||
let mut args = ["a folder".to_owned(), "drawing.svg".to_owned()].into_iter();
|
||||
assert!(roots.consume("--automation-read-root", &mut args).unwrap());
|
||||
assert!(roots.consume("--automation-write-root=out", &mut args).unwrap());
|
||||
assert!(!roots.consume("--in-window-menus", &mut args).unwrap());
|
||||
assert_eq!(roots.read_root, Some("a folder".into()));
|
||||
assert_eq!(roots.write_root, Some("out".into()));
|
||||
assert_eq!(roots.read_roots, [PathBuf::from("a folder")]);
|
||||
assert_eq!(roots.write_roots, [PathBuf::from("out")]);
|
||||
assert_eq!(args.next().as_deref(), Some("drawing.svg"));
|
||||
assert!(roots.consume("--automation-read-root=other", &mut args).is_err());
|
||||
assert_eq!(roots.read_root, Some("a folder".into()));
|
||||
// #3546: a flag given again adds a folder, in either spelling.
|
||||
assert!(roots.consume("--automation-read-root=other", &mut args).unwrap());
|
||||
assert!(roots.consume("--automation-write-root", &mut ["out2".to_owned()].into_iter()).unwrap());
|
||||
assert_eq!(roots.read_roots, [PathBuf::from("a folder"), PathBuf::from("other")]);
|
||||
assert_eq!(roots.write_roots, [PathBuf::from("out"), PathBuf::from("out2")]);
|
||||
// A kind the command line leaves alone keeps the environment's root.
|
||||
let mut roots = AutomationArgs::new(Some("environment".into()), Some("environment out".into()));
|
||||
assert!(roots.consume("--automation-read-root=a", &mut std::iter::empty()).unwrap());
|
||||
assert_eq!(roots.write_roots, [PathBuf::from("environment out")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -117,11 +126,15 @@ mod tests {
|
||||
let mut roots = AutomationArgs::new(Some("read".into()), Some("write".into()));
|
||||
let mut args = operand.map(str::to_owned).into_iter();
|
||||
assert!(roots.consume(flag, &mut args).is_err(), "{flag} {operand:?}");
|
||||
assert_eq!(roots.read_root, Some("read".into()));
|
||||
assert_eq!(roots.write_root, Some("write".into()));
|
||||
assert_eq!(roots.read_roots, [PathBuf::from("read")]);
|
||||
assert_eq!(roots.write_roots, [PathBuf::from("write")]);
|
||||
}
|
||||
let mut roots = AutomationArgs::new(None, None);
|
||||
assert!(roots.consume(&format!("{flag}="), &mut std::iter::empty()).is_err());
|
||||
// A bad second folder is an error too, and keeps the first.
|
||||
assert!(roots.consume(&format!("{flag}=first"), &mut std::iter::empty()).unwrap());
|
||||
assert!(roots.consume(flag, &mut std::iter::empty()).is_err());
|
||||
assert_eq!([roots.read_roots.len(), roots.write_roots.len()].iter().sum::<usize>(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +143,8 @@ mod tests {
|
||||
let mut roots = AutomationArgs::new(None, None);
|
||||
assert!(roots.consume("--automation-read-root=-folder", &mut std::iter::empty()).unwrap());
|
||||
assert!(roots.consume("--automation-write-root", &mut ["./-folder".to_owned()].into_iter()).unwrap());
|
||||
assert_eq!(roots.read_root, Some("-folder".into()));
|
||||
assert_eq!(roots.write_root, Some("./-folder".into()));
|
||||
assert_eq!(roots.read_roots, [PathBuf::from("-folder")]);
|
||||
assert_eq!(roots.write_roots, [PathBuf::from("./-folder")]);
|
||||
}
|
||||
|
||||
fn control(args: &[&str]) -> Result<Option<u16>, String> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! VectorCraft desktop app.
|
||||
//!
|
||||
//! Usage: `vectorcraft [--control <port> [--automation-read-root <dir>] [--automation-write-root <dir>]]
|
||||
//! Usage: `vectorcraft [--control <port> [--automation-read-root <dir>]... [--automation-write-root <dir>]...]
|
||||
//! [--in-window-menus] [files…]`, `vectorcraft --version`, `vectorcraft --help` (or `-h`)
|
||||
//!
|
||||
//! `--in-window-menus` (or `VECTORCRAFT_IN_WINDOW_MENUS=1`) keeps the menus inside the window on
|
||||
@@ -725,7 +725,9 @@ Options:
|
||||
--control <port> Start the localhost JSON-lines control server on <port>
|
||||
(or VECTORCRAFT_CONTROL_PORT)
|
||||
--automation-read-root <dir> Confine the files control requests read to <dir>
|
||||
(may be given more than once)
|
||||
--automation-write-root <dir> Confine the files control requests write to <dir>
|
||||
(may be given more than once)
|
||||
--in-window-menus Keep the menus inside the window on macOS
|
||||
(or VECTORCRAFT_IN_WINDOW_MENUS=1)
|
||||
--version Print the version and exit
|
||||
@@ -774,7 +776,7 @@ fn main() -> std::process::ExitCode {
|
||||
_ => files.push(a),
|
||||
}
|
||||
}
|
||||
let automation_args::AutomationArgs { read_root, write_root, .. } = roots_args;
|
||||
let automation_args::AutomationArgs { read_roots, write_roots, .. } = roots_args;
|
||||
// After the arguments: `--control` and `--version` win over a bad environment value.
|
||||
let control_port = match control.port {
|
||||
Some(port) => Some(port),
|
||||
@@ -813,12 +815,14 @@ fn main() -> std::process::ExitCode {
|
||||
// The roots confine the control channel: a bad one keeps the app from starting rather than
|
||||
// leaving agents unconfined. Without a control port there is nothing to confine.
|
||||
let roots = if control_port.is_none() {
|
||||
if read_root.is_some() || write_root.is_some() {
|
||||
if !read_roots.is_empty() || !write_roots.is_empty() {
|
||||
log::warn!("--automation-read-root and --automation-write-root confine the control channel: ignored without --control");
|
||||
}
|
||||
None
|
||||
} else {
|
||||
match AutomationRoots::new(read_root.as_deref(), write_root.as_deref()) {
|
||||
let read: Vec<&std::path::Path> = read_roots.iter().map(std::path::PathBuf::as_path).collect();
|
||||
let write: Vec<&std::path::Path> = write_roots.iter().map(std::path::PathBuf::as_path).collect();
|
||||
match AutomationRoots::new(&read, &write) {
|
||||
Ok(roots) => roots,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
@@ -1015,7 +1019,7 @@ fn main() -> std::process::ExitCode {
|
||||
// Everywhere the app doesn't say whose work it is (worker threads), the
|
||||
// roots hold: unconfined is only what the person at the keyboard does.
|
||||
file_access::confine_process(roots.clone());
|
||||
log::info!("control channel confined to the automation roots: read {:?}, write {:?}", roots.read_root(), roots.write_root());
|
||||
log::info!("control channel confined to the automation roots: {}", roots.describe());
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -60,11 +60,17 @@ fn missing_empty_or_option_operands_fail_before_startup() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #3546: a root flag may be given more than once, in either spelling; a bad later operand still
|
||||
/// stops the app before it starts.
|
||||
#[test]
|
||||
fn duplicate_roots_fail_for_both_spellings() {
|
||||
fn repeated_roots_are_accepted_for_both_spellings() {
|
||||
for flag in ["--automation-read-root", "--automation-write-root"] {
|
||||
rejects(&[flag, ".", &format!("{flag}=.")], &format!("{flag} given twice"));
|
||||
rejects(&[&format!("{flag}=."), flag, "."], &format!("{flag} given twice"));
|
||||
let out = Command::new(BIN).args([flag, ".", format!("{flag}=.").as_str(), flag, "./", "--version"]).output().unwrap();
|
||||
assert!(out.status.success(), "{flag}: {}", String::from_utf8_lossy(&out.stderr));
|
||||
assert_eq!(String::from_utf8(out.stdout).unwrap(), format!("vectorcraft {}\n", env!("CARGO_PKG_VERSION")));
|
||||
assert!(out.stderr.is_empty());
|
||||
rejects(&[flag, ".", flag, ""], &format!("{flag} needs a non-empty folder"));
|
||||
rejects(&[&format!("{flag}=."), flag], &format!("{flag} needs a folder"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
//!
|
||||
//! `vectorcraft-cli mcp --automation-read-root <dir> --automation-write-root <dir>`, and
|
||||
//! `vectorcraft --control <port>` with the same flags, confine the files an agent's commands
|
||||
//! reach. [`AutomationRoots`] holds the two folders. While they are in force, every path a command
|
||||
//! reads goes through [`check_read`] and every path it writes through [`check_write`]:
|
||||
//! reach; each flag may be given more than once (#3546). [`AutomationRoots`] holds the folders of
|
||||
//! each kind. While they are in force, every path a command reads goes through [`check_read`] and
|
||||
//! every path it writes through [`check_write`]:
|
||||
//!
|
||||
//! - The path is made absolute against the working directory, as the file system reads it. Its
|
||||
//! links are followed: for a file that doesn't exist yet, the links of its nearest existing
|
||||
//! folder, the rest of the path being plain names (`..` below a missing folder is refused, and so
|
||||
//! is a link that leads nowhere).
|
||||
//! - The result must lie inside the root, compared by whole components (`/work2` isn't inside
|
||||
//! `/work`), without regard to case on Windows.
|
||||
//! - The result must lie inside one of the roots of its kind, compared by whole components
|
||||
//! (`/work2` isn't inside `/work`), without regard to case on Windows.
|
||||
//! - Read and write access are separate: a root given alone grants only its own kind of access.
|
||||
//!
|
||||
//! Roots are in force on a thread inside [`confine`], and in the whole process after
|
||||
@@ -35,11 +36,11 @@ pub enum Access {
|
||||
Write,
|
||||
}
|
||||
|
||||
/// The folders automation may read and write; a root left out grants no access of its kind.
|
||||
/// The folders automation may read and write; a kind with no root grants no access of its kind.
|
||||
#[derive(Debug)]
|
||||
pub struct AutomationRoots {
|
||||
read: Option<Root>,
|
||||
write: Option<Root>,
|
||||
read: Vec<Root>,
|
||||
write: Vec<Root>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -65,48 +66,59 @@ impl Root {
|
||||
}
|
||||
|
||||
impl AutomationRoots {
|
||||
/// The roots `read` and `write`, which must be existing folders; `None` when neither is given
|
||||
/// (nothing is confined then).
|
||||
pub fn new(read: Option<&Path>, write: Option<&Path>) -> Result<Option<Arc<Self>>, String> {
|
||||
if read.is_none() && write.is_none() {
|
||||
/// The roots `read` and `write`, which must be existing folders (one given twice counts once);
|
||||
/// `None` when neither kind has any (nothing is confined then).
|
||||
pub fn new(read: &[&Path], write: &[&Path]) -> Result<Option<Arc<Self>>, String> {
|
||||
if read.is_empty() && write.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let read = read.map(|p| Root::open(p, Access::Read)).transpose()?;
|
||||
let write = write.map(|p| Root::open(p, Access::Write)).transpose()?;
|
||||
Ok(Some(Arc::new(Self { read, write })))
|
||||
}
|
||||
|
||||
/// The read root, absolute with its links followed.
|
||||
pub fn read_root(&self) -> Option<&Path> {
|
||||
self.read.as_ref().map(|r| r.shown.as_path())
|
||||
}
|
||||
|
||||
/// The write root, absolute with its links followed.
|
||||
pub fn write_root(&self) -> Option<&Path> {
|
||||
self.write.as_ref().map(|r| r.shown.as_path())
|
||||
}
|
||||
|
||||
/// May the file or folder at `path` be read (or written)? `Err` says why not.
|
||||
pub fn check(&self, path: &str, access: Access) -> Result<(), String> {
|
||||
let root = match access {
|
||||
Access::Read => self.read.as_ref(),
|
||||
Access::Write => self.write.as_ref(),
|
||||
let open = |paths: &[&Path], access: Access| -> Result<Vec<Root>, String> {
|
||||
let mut roots: Vec<Root> = Vec::with_capacity(paths.len());
|
||||
for path in paths {
|
||||
let root = Root::open(path, access)?;
|
||||
if !roots.iter().any(|r| r.resolved == root.resolved) {
|
||||
roots.push(root);
|
||||
}
|
||||
}
|
||||
Ok(roots)
|
||||
};
|
||||
let Some(Root { resolved: folder, shown }) = root else {
|
||||
Ok(Some(Arc::new(Self { read: open(read, Access::Read)?, write: open(write, Access::Write)? })))
|
||||
}
|
||||
|
||||
/// May the file or folder at `path` be read (or written)? It must lie inside one of the roots
|
||||
/// of that kind. `Err` says why not.
|
||||
pub fn check(&self, path: &str, access: Access) -> Result<(), String> {
|
||||
let roots = match access {
|
||||
Access::Read => &self.read,
|
||||
Access::Write => &self.write,
|
||||
};
|
||||
// The messages are spelled out whole, as the translation catalogs match them.
|
||||
if roots.is_empty() {
|
||||
return match access {
|
||||
Access::Read => Err(format!("automation filesystem access is not granted: read authority is absent: {path}")),
|
||||
Access::Write => Err(format!("automation filesystem access is not granted: write authority is absent: {path}")),
|
||||
};
|
||||
};
|
||||
if inside(&resolve(path)?, folder) {
|
||||
}
|
||||
let resolved = resolve(path)?;
|
||||
if roots.iter().any(|r| inside(&resolved, &r.resolved)) {
|
||||
return Ok(());
|
||||
}
|
||||
let root = shown.display();
|
||||
match access {
|
||||
Access::Read => Err(format!("automation path rejected: outside the read root {root}: {path}")),
|
||||
Access::Write => Err(format!("automation path rejected: outside the write root {root}: {path}")),
|
||||
let shown: Vec<String> = roots.iter().map(|r| r.shown.display().to_string()).collect();
|
||||
match (shown.as_slice(), access) {
|
||||
([root], Access::Read) => Err(format!("automation path rejected: outside the read root {root}: {path}")),
|
||||
([root], Access::Write) => Err(format!("automation path rejected: outside the write root {root}: {path}")),
|
||||
(_, Access::Read) => Err(format!("automation path rejected: outside the read roots {roots}: {path}", roots = shown.join(", "))),
|
||||
(_, Access::Write) => Err(format!("automation path rejected: outside the write roots {roots}: {path}", roots = shown.join(", "))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The roots for people: `read A, B; write C` (`none` for a kind without any).
|
||||
pub fn describe(&self) -> String {
|
||||
let list = |roots: &[Root]| {
|
||||
if roots.is_empty() { "none".to_string() } else { roots.iter().map(|r| r.shown.display().to_string()).collect::<Vec<_>>().join(", ") }
|
||||
};
|
||||
format!("read {}; write {}", list(&self.read), list(&self.write))
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the roots are in force on a thread ([`confine`], [`unconfined`]).
|
||||
@@ -280,7 +292,7 @@ mod tests {
|
||||
}
|
||||
std::fs::write(base.join("in/a.svg"), "<svg/>").unwrap();
|
||||
std::fs::write(base.join("outside/secret.svg"), "<svg/>").unwrap();
|
||||
let roots = AutomationRoots::new(Some(&base.join("in")), Some(&base.join("out"))).unwrap().unwrap();
|
||||
let roots = AutomationRoots::new(&[&base.join("in")], &[&base.join("out")]).unwrap().unwrap();
|
||||
(base, roots)
|
||||
}
|
||||
|
||||
@@ -340,18 +352,20 @@ mod tests {
|
||||
#[test]
|
||||
fn a_missing_root_is_an_error_and_none_is_no_confinement() {
|
||||
let (base, _) = folders("missing");
|
||||
let e = AutomationRoots::new(Some(&base.join("nope")), None).unwrap_err();
|
||||
let e = AutomationRoots::new(&[&base.join("nope")], &[]).unwrap_err();
|
||||
assert!(e.starts_with("cannot open the automation read root"), "{e}");
|
||||
let e = AutomationRoots::new(None, Some(&base.join("in/a.svg"))).unwrap_err();
|
||||
let e = AutomationRoots::new(&[], &[&base.join("in/a.svg")]).unwrap_err();
|
||||
assert!(
|
||||
e.starts_with("cannot open the automation write root")
|
||||
&& e.ends_with(&std::io::Error::from(std::io::ErrorKind::NotADirectory).to_string()),
|
||||
"{e}"
|
||||
);
|
||||
assert!(AutomationRoots::new(Some(Path::new("")), None).unwrap_err().starts_with("cannot open the automation read root ``"));
|
||||
assert!(AutomationRoots::new(None, None).unwrap().is_none());
|
||||
assert!(AutomationRoots::new(&[Path::new("")], &[]).unwrap_err().starts_with("cannot open the automation read root ``"));
|
||||
assert!(AutomationRoots::new(&[], &[]).unwrap().is_none());
|
||||
// A missing folder among several is an error too.
|
||||
assert!(AutomationRoots::new(&[&base.join("in"), &base.join("nope")], &[]).unwrap_err().starts_with("cannot open the automation read root"));
|
||||
// One root alone grants only its own access.
|
||||
let read_only = AutomationRoots::new(Some(&base.join("in")), None).unwrap().unwrap();
|
||||
let read_only = AutomationRoots::new(&[&base.join("in")], &[]).unwrap().unwrap();
|
||||
let e = read_only.check(&s(base.join("in/x.svg")), Access::Write).unwrap_err();
|
||||
assert!(e.contains("write authority is absent"), "{e}");
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
@@ -388,11 +402,55 @@ mod tests {
|
||||
assert_eq!(roots.check(&s(base.join("in/alias/x.svg")), Access::Read), Ok(()));
|
||||
// A root given through a link is the folder it leads to.
|
||||
assert!(made(&base.join("in"), &base.join("in-link")));
|
||||
let linked = AutomationRoots::new(Some(&base.join("in-link")), None).unwrap().unwrap();
|
||||
let linked = AutomationRoots::new(&[&base.join("in-link")], &[]).unwrap().unwrap();
|
||||
assert_eq!(linked.check(&s(base.join("in/a.svg")), Access::Read), Ok(()));
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// #3546: several roots of a kind: a path inside any of them passes, one inside none is
|
||||
/// refused naming them all, and read and write stay separate.
|
||||
#[test]
|
||||
fn several_roots_of_a_kind_each_grant_their_access() {
|
||||
let (base, _) = folders("several");
|
||||
std::fs::create_dir_all(base.join("out2")).unwrap();
|
||||
std::fs::write(base.join("in2/b.svg"), "<svg/>").unwrap();
|
||||
let roots = AutomationRoots::new(&[&base.join("in"), &base.join("in2")], &[&base.join("out"), &base.join("out2")]).unwrap().unwrap();
|
||||
assert_eq!(roots.check(&s(base.join("in/a.svg")), Access::Read), Ok(()));
|
||||
assert_eq!(roots.check(&s(base.join("in2/b.svg")), Access::Read), Ok(()));
|
||||
let e = roots.check(&s(base.join("outside/secret.svg")), Access::Read).unwrap_err();
|
||||
assert!(e.starts_with("automation path rejected: outside the read roots ") && e.contains(", "), "{e}");
|
||||
assert_eq!(roots.check(&s(base.join("out/x.png")), Access::Write), Ok(()));
|
||||
assert_eq!(roots.check(&s(base.join("out2/new/x.png")), Access::Write), Ok(()));
|
||||
assert!(roots.check(&s(base.join("outside/x.png")), Access::Write).unwrap_err().contains("outside the write roots"));
|
||||
// Read and write access stay separate with several roots.
|
||||
assert!(roots.check(&s(base.join("in2/b.svg")), Access::Write).is_err());
|
||||
assert!(roots.check(&s(base.join("out2/x.png")), Access::Read).is_err());
|
||||
// `..` out of one root into another root's sibling is still outside.
|
||||
let sep = std::path::MAIN_SEPARATOR;
|
||||
let up = format!("{}{sep}..{sep}outside{sep}secret.svg", s(base.join("in2")));
|
||||
assert!(roots.check(&up, Access::Read).is_err());
|
||||
// A folder given twice (here once through `..`) counts once.
|
||||
let twice = AutomationRoots::new(&[&base.join("in"), &base.join("in/sub/..")], &[]).unwrap().unwrap();
|
||||
assert!(!twice.describe().contains(", "), "{}", twice.describe());
|
||||
assert!(twice.describe().ends_with("; write none"), "{}", twice.describe());
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// A link inside one root that leads into another root of the same kind stays inside; one that
|
||||
/// leads out of all of them is outside.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn links_between_several_roots() {
|
||||
let (base, _) = folders("several-links");
|
||||
std::fs::write(base.join("in2/b.svg"), "<svg/>").unwrap();
|
||||
std::os::unix::fs::symlink(base.join("in2/b.svg"), base.join("in/to-in2.svg")).unwrap();
|
||||
std::os::unix::fs::symlink(base.join("outside/secret.svg"), base.join("in2/leak.svg")).unwrap();
|
||||
let roots = AutomationRoots::new(&[&base.join("in"), &base.join("in2")], &[]).unwrap().unwrap();
|
||||
assert_eq!(roots.check(&s(base.join("in/to-in2.svg")), Access::Read), Ok(()));
|
||||
assert!(roots.check(&s(base.join("in2/leak.svg")), Access::Read).unwrap_err().contains("outside the read roots"));
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_ignores_case_and_refuses_devices() {
|
||||
|
||||
@@ -3145,7 +3145,7 @@ fn file_list_lists_a_folder_within_the_read_root() {
|
||||
json!([{"name": "a.pdf", "kind": "file", "size": 4}, {"name": "b.svg", "kind": "file", "size": 6}, {"name": "sub", "kind": "folder"}])
|
||||
);
|
||||
assert_eq!(r["truncated"], false);
|
||||
let roots = crate::file_access::AutomationRoots::new(Some(&base.join("in")), None).unwrap().unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(&[&base.join("in")], &[]).unwrap().unwrap();
|
||||
crate::file_access::confine(Some(&roots), || {
|
||||
assert!(s.execute("file.list", &json!({"path": path("in/sub")})).is_ok());
|
||||
assert!(s.execute("file.list", &json!({"path": path("outside")})).is_err(), "outside the read root");
|
||||
|
||||
@@ -306,7 +306,7 @@ fn a_copy_stays_inside_the_write_root() {
|
||||
let allowed = root.join("allowed");
|
||||
write(&allowed.join("photo.png"), b"kept as it is");
|
||||
std::os::unix::fs::symlink(allowed.join("photo.png"), root.join("doc/photo.png")).unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(Some(&root), Some(&allowed)).unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(&[&root], &[&allowed]).unwrap();
|
||||
let r = crate::file_access::confine(roots.as_ref(), || s.execute("links.relink", &json!({"ids": [id], "path": found, "copy": true})));
|
||||
assert!(matches!(&r, Err(EngineError::Other(m)) if m.contains("outside the write root")), "{r:?}");
|
||||
assert!(!root.join("doc/photo 2.png").exists());
|
||||
@@ -332,7 +332,7 @@ fn an_svg_reads_its_links_within_the_read_root() {
|
||||
};
|
||||
let (inside, outside) = (import("in"), import("out"));
|
||||
assert_eq!(inside, outside, "both are read with no roots in force");
|
||||
let roots = crate::file_access::AutomationRoots::new(Some(&root.join("in")), None).unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(&[&root.join("in")], &[]).unwrap();
|
||||
crate::file_access::confine(roots.as_ref(), || {
|
||||
assert_eq!(import("in"), inside, "inside the read root: read as before");
|
||||
assert_ne!(import("out"), outside, "outside the read root: not read");
|
||||
|
||||
@@ -113,7 +113,7 @@ fn links_out_of_the_folder_and_the_automation_read_root_keep_reads_inside() {
|
||||
assert_eq!(statuses(&r), ["notFollowed", "checked"]);
|
||||
assert_eq!(r["outside"], json!([]));
|
||||
// Under a read root, the script and its includes are read inside it, and a script outside isn't.
|
||||
let roots = crate::file_access::AutomationRoots::new(Some(&dir.join("scripts")), None).unwrap().unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(&[&dir.join("scripts")], &[]).unwrap().unwrap();
|
||||
let mut s = Session::new();
|
||||
let r = crate::file_access::confine(Some(&roots), || s.execute("script.check", &json!({"path": main, "includes": true}))).unwrap();
|
||||
assert_eq!(statuses(&r), ["notFollowed", "checked"]);
|
||||
|
||||
@@ -25,7 +25,9 @@ fn confined(tag: &str, read: bool, write: bool) -> (Server, PathBuf) {
|
||||
std::fs::write(base.join("in2/c.svg"), SVG).unwrap();
|
||||
let in_dir = base.join("in");
|
||||
let out_dir = base.join("out");
|
||||
let roots = AutomationRoots::new(read.then_some(in_dir.as_path()), write.then_some(out_dir.as_path())).unwrap();
|
||||
let (read, write): (Vec<&Path>, Vec<&Path>) =
|
||||
(read.then_some(in_dir.as_path()).into_iter().collect(), write.then_some(out_dir.as_path()).into_iter().collect());
|
||||
let roots = AutomationRoots::new(&read, &write).unwrap();
|
||||
(Server::new(Box::new(Headless::with_document().with_automation_roots(roots))), base)
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ proptest! {
|
||||
let root = base.join("a/b/c/root");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
std::fs::create_dir_all(base.join("outside")).unwrap();
|
||||
let roots = vectorcraft_engine::file_access::AutomationRoots::new(Some(root.as_path()), Some(root.as_path())).unwrap();
|
||||
let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[root.as_path()], &[root.as_path()]).unwrap();
|
||||
let mut s = Server::new(Box::new(Headless::with_document().with_automation_roots(roots)));
|
||||
let path = format!("{}{}{tail}", root.display(), std::path::MAIN_SEPARATOR);
|
||||
let r = catch_quiet(|| call(&mut s, tool, json!({"path": path})));
|
||||
|
||||
@@ -4375,7 +4375,9 @@ axis Both И двете
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} достъпът на автоматизацията до файловата система не е разрешен: липсва право за четене: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} достъпът на автоматизацията до файловата система не е разрешен: липсва право за запис: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} пътят за автоматизация е отхвърлен: извън корена за четене {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} пътят за автоматизация е отхвърлен: извън корените за четене {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} пътят за автоматизация е отхвърлен: извън корена за запис {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} пътят за автоматизация е отхвърлен: извън корените за запис {roots}: {path}
|
||||
@msg automation path rejected: path is empty пътят за автоматизация е отхвърлен: пътят е празен
|
||||
@msg automation path rejected: a link that leads nowhere: {path} пътят за автоматизация е отхвърлен: връзка, която не води доникъде: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} пътят за автоматизация е отхвърлен: не може да се разреши {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4737,7 +4737,9 @@ imageTrace Outline Obrys
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} přístup automatizace k systému souborů není udělen: chybí oprávnění ke čtení: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} přístup automatizace k systému souborů není udělen: chybí oprávnění k zápisu: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} cesta automatizace odmítnuta: mimo kořen pro čtení {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} cesta automatizace odmítnuta: mimo kořeny pro čtení {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} cesta automatizace odmítnuta: mimo kořen pro zápis {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} cesta automatizace odmítnuta: mimo kořeny pro zápis {roots}: {path}
|
||||
@msg automation path rejected: path is empty cesta automatizace odmítnuta: cesta je prázdná
|
||||
@msg automation path rejected: a link that leads nowhere: {path} cesta automatizace odmítnuta: odkaz, který nikam nevede: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} cesta automatizace odmítnuta: {path} nelze vyhodnotit: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4435,7 +4435,9 @@ imageTrace Outline Kontur
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} automatiseringens adgang til filsystemet er ikke tildelt: læserettighed mangler: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} automatiseringens adgang til filsystemet er ikke tildelt: skriverettighed mangler: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} automatiseringssti afvist: uden for læseroden {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} automatiseringssti afvist: uden for læserødderne {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} automatiseringssti afvist: uden for skriveroden {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} automatiseringssti afvist: uden for skriverødderne {roots}: {path}
|
||||
@msg automation path rejected: path is empty automatiseringssti afvist: stien er tom
|
||||
@msg automation path rejected: a link that leads nowhere: {path} automatiseringssti afvist: et link, der ikke fører nogen steder hen: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} automatiseringssti afvist: kan ikke finde frem til {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4396,7 +4396,9 @@ theme Appearance Erscheinungsbild
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} Dateisystemzugriff für die Automatisierung ist nicht gewährt: Die Leseberechtigung fehlt: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} Dateisystemzugriff für die Automatisierung ist nicht gewährt: Die Schreibberechtigung fehlt: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} Automatisierungspfad abgelehnt: außerhalb des Lesestammordners {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} Automatisierungspfad abgelehnt: außerhalb der Lesestammordner {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} Automatisierungspfad abgelehnt: außerhalb des Schreibstammordners {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} Automatisierungspfad abgelehnt: außerhalb der Schreibstammordner {roots}: {path}
|
||||
@msg automation path rejected: path is empty Automatisierungspfad abgelehnt: Der Pfad ist leer
|
||||
@msg automation path rejected: a link that leads nowhere: {path} Automatisierungspfad abgelehnt: eine Verknüpfung, die nirgendwohin führt: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} Automatisierungspfad abgelehnt: {path} kann nicht aufgelöst werden: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4439,7 +4439,9 @@ theme Appearance Εμφάνιση
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} δεν έχει επιτραπεί πρόσβαση αυτοματισμού στο σύστημα αρχείων: δεν υπάρχει δικαίωμα ανάγνωσης: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} δεν έχει επιτραπεί πρόσβαση αυτοματισμού στο σύστημα αρχείων: δεν υπάρχει δικαίωμα εγγραφής: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} απορρίφθηκε η διαδρομή αυτοματισμού: εκτός ρίζας ανάγνωσης {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} απορρίφθηκε η διαδρομή αυτοματισμού: εκτός των ριζών ανάγνωσης {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} απορρίφθηκε η διαδρομή αυτοματισμού: εκτός ρίζας εγγραφής {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} απορρίφθηκε η διαδρομή αυτοματισμού: εκτός των ριζών εγγραφής {roots}: {path}
|
||||
@msg automation path rejected: path is empty απορρίφθηκε η διαδρομή αυτοματισμού: η διαδρομή είναι κενή
|
||||
@msg automation path rejected: a link that leads nowhere: {path} απορρίφθηκε η διαδρομή αυτοματισμού: σύνδεσμος χωρίς προορισμό: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} απορρίφθηκε η διαδρομή αυτοματισμού: δεν ήταν δυνατή η επίλυση του {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2045 and column 19.
|
@@ -4458,7 +4458,9 @@ axis Both Ambos
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} la automatización no tiene acceso al sistema de archivos: falta el permiso de lectura: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} la automatización no tiene acceso al sistema de archivos: falta el permiso de escritura: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} ruta de automatización rechazada: fuera de la carpeta raíz de lectura {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} ruta de automatización rechazada: fuera de las carpetas raíz de lectura {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} ruta de automatización rechazada: fuera de la carpeta raíz de escritura {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} ruta de automatización rechazada: fuera de las carpetas raíz de escritura {roots}: {path}
|
||||
@msg automation path rejected: path is empty ruta de automatización rechazada: la ruta está vacía
|
||||
@msg automation path rejected: a link that leads nowhere: {path} ruta de automatización rechazada: un vínculo que no lleva a ninguna parte: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} ruta de automatización rechazada: no se puede resolver {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4646,7 +4646,9 @@ imageTrace Outline Ääriviiva
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} automaation tiedostojärjestelmäkäyttöä ei ole myönnetty: lukuoikeus puuttuu: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} automaation tiedostojärjestelmäkäyttöä ei ole myönnetty: kirjoitusoikeus puuttuu: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} automaation polku hylättiin: lukujuuren {root} ulkopuolella: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} automaation polku hylättiin: lukujuurten {roots} ulkopuolella: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} automaation polku hylättiin: kirjoitusjuuren {root} ulkopuolella: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} automaation polku hylättiin: kirjoitusjuurten {roots} ulkopuolella: {path}
|
||||
@msg automation path rejected: path is empty automaation polku hylättiin: polku on tyhjä
|
||||
@msg automation path rejected: a link that leads nowhere: {path} automaation polku hylättiin: linkki, joka ei johda mihinkään: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} automaation polku hylättiin: polkua {path} ei voi selvittää: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4469,7 +4469,9 @@ axis Both Les deux
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} l’automatisation n’a pas accès au système de fichiers : aucune autorisation de lecture : {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} l’automatisation n’a pas accès au système de fichiers : aucune autorisation d’écriture : {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} chemin d’automatisation refusé : hors du dossier racine de lecture {root} : {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} chemin d’automatisation refusé : hors des dossiers racines de lecture {roots} : {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} chemin d’automatisation refusé : hors du dossier racine d’écriture {root} : {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} chemin d’automatisation refusé : hors des dossiers racines d’écriture {roots} : {path}
|
||||
@msg automation path rejected: path is empty chemin d’automatisation refusé : le chemin est vide
|
||||
@msg automation path rejected: a link that leads nowhere: {path} chemin d’automatisation refusé : un lien qui ne mène nulle part : {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} chemin d’automatisation refusé : impossible de résoudre {path} : {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4493,7 +4493,9 @@ imageTrace Outline Körvonal
|
||||
@msg automation path rejected: a link that leads nowhere: {path} az automatizálás útvonala elutasítva: a hivatkozás nem vezet sehova: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} az automatizálás útvonala elutasítva: nem oldható fel: {path}: {e}
|
||||
@msg automation path rejected: outside the read root {root}: {path} az automatizálás útvonala elutasítva: kívül esik az olvasási gyökéren ({root}): {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} az automatizálás útvonala elutasítva: kívül esik az olvasási gyökereken ({roots}): {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} az automatizálás útvonala elutasítva: kívül esik az írási gyökéren ({root}): {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} az automatizálás útvonala elutasítva: kívül esik az írási gyökereken ({roots}): {path}
|
||||
@msg automation path rejected: parent traversal below a folder that doesn't exist: {path} az automatizálás útvonala elutasítva: szülőmappára lépés egy nem létező mappa alatt: {path}
|
||||
@msg automation path rejected: path is empty az automatizálás útvonala elutasítva: az útvonal üres
|
||||
@msg automation path rejected: reserved device names are not allowed: {path} az automatizálás útvonala elutasítva: fenntartott eszköznév nem használható: {path}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4435,7 +4435,9 @@ imageTrace Outline Kontur
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} akses sistem berkas otomatisasi tidak diberikan: otoritas baca tidak ada: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} akses sistem berkas otomatisasi tidak diberikan: otoritas tulis tidak ada: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} jalur otomatisasi ditolak: di luar akar baca {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} jalur otomatisasi ditolak: di luar akar-akar baca {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} jalur otomatisasi ditolak: di luar akar tulis {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} jalur otomatisasi ditolak: di luar akar-akar tulis {roots}: {path}
|
||||
@msg automation path rejected: path is empty jalur otomatisasi ditolak: jalur kosong
|
||||
@msg automation path rejected: a link that leads nowhere: {path} jalur otomatisasi ditolak: tautan yang tidak mengarah ke mana pun: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} jalur otomatisasi ditolak: tidak dapat menyelesaikan {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3816 and column 19.
|
@@ -4498,7 +4498,9 @@ axis Both Entrambi
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} l'automazione non ha accesso al file system: manca l'autorizzazione di lettura: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} l'automazione non ha accesso al file system: manca l'autorizzazione di scrittura: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} percorso di automazione rifiutato: fuori dalla cartella radice di lettura {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} percorso di automazione rifiutato: fuori dalle cartelle radice di lettura {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} percorso di automazione rifiutato: fuori dalla cartella radice di scrittura {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} percorso di automazione rifiutato: fuori dalle cartelle radice di scrittura {roots}: {path}
|
||||
@msg automation path rejected: path is empty percorso di automazione rifiutato: il percorso è vuoto
|
||||
@msg automation path rejected: a link that leads nowhere: {path} percorso di automazione rifiutato: un collegamento che non porta da nessuna parte: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} percorso di automazione rifiutato: impossibile risolvere {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4536,7 +4536,9 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} 自動化にはファイルシステムへのアクセスが許可されていません: 読み込み権限がありません: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} 自動化にはファイルシステムへのアクセスが許可されていません: 書き込み権限がありません: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} 自動化のパスを拒否しました: 読み込みルート {root} の外です: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} 自動化のパスを拒否しました: 読み込みルート {roots} のいずれの中でもありません: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} 自動化のパスを拒否しました: 書き込みルート {root} の外です: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} 自動化のパスを拒否しました: 書き込みルート {roots} のいずれの中でもありません: {path}
|
||||
@msg automation path rejected: path is empty 自動化のパスを拒否しました: パスが空です
|
||||
@msg automation path rejected: a link that leads nowhere: {path} 自動化のパスを拒否しました: リンク先が存在しません: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} 自動化のパスを拒否しました: {path} を解決できません: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4187,7 +4187,9 @@ effect Regular ჩვეულებრივი
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} ავტომატიზაციას ფაილურ სისტემაზე წვდომა არ აქვს: წაკითხვის უფლება არ არის: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} ავტომატიზაციას ფაილურ სისტემაზე წვდომა არ აქვს: ჩაწერის უფლება არ არის: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} ავტომატიზაციის გზა უარყოფილია: წაკითხვის ძირის {root} გარეთაა: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} ავტომატიზაციის გზა უარყოფილია: წაკითხვის ძირების {roots} გარეთაა: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} ავტომატიზაციის გზა უარყოფილია: ჩაწერის ძირის {root} გარეთაა: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} ავტომატიზაციის გზა უარყოფილია: ჩაწერის ძირების {roots} გარეთაა: {path}
|
||||
@msg automation path rejected: path is empty ავტომატიზაციის გზა უარყოფილია: გზა ცარიელია
|
||||
@msg automation path rejected: a link that leads nowhere: {path} ავტომატიზაციის გზა უარყოფილია: ბმული არსად მიდის: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} ავტომატიზაციის გზა უარყოფილია: {path} ვერ გაირკვა: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4468,7 +4468,9 @@ theme Appearance 외관
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} 자동화 파일 시스템 접근이 허용되지 않았습니다: 읽기 권한이 없습니다: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} 자동화 파일 시스템 접근이 허용되지 않았습니다: 쓰기 권한이 없습니다: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} 자동화 경로가 거부되었습니다: 읽기 루트 {root} 밖입니다: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} 자동화 경로가 거부되었습니다: 읽기 루트 {roots} 밖입니다: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} 자동화 경로가 거부되었습니다: 쓰기 루트 {root} 밖입니다: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} 자동화 경로가 거부되었습니다: 쓰기 루트 {roots} 밖입니다: {path}
|
||||
@msg automation path rejected: path is empty 자동화 경로가 거부되었습니다: 경로가 비어 있습니다
|
||||
@msg automation path rejected: a link that leads nowhere: {path} 자동화 경로가 거부되었습니다: 어디로도 이어지지 않는 링크입니다: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} 자동화 경로가 거부되었습니다: {path}을(를) 해석할 수 없습니다: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4447,7 +4447,9 @@ imageTrace Outline Contour
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} toegang tot het bestandssysteem voor automatisering is niet verleend: de leesbevoegdheid ontbreekt: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} toegang tot het bestandssysteem voor automatisering is niet verleend: de schrijfbevoegdheid ontbreekt: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} automatiseringspad afgewezen: buiten de leesroot {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} automatiseringspad afgewezen: buiten de leesroots {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} automatiseringspad afgewezen: buiten de schrijfroot {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} automatiseringspad afgewezen: buiten de schrijfroots {roots}: {path}
|
||||
@msg automation path rejected: path is empty automatiseringspad afgewezen: het pad is leeg
|
||||
@msg automation path rejected: a link that leads nowhere: {path} automatiseringspad afgewezen: een koppeling die nergens heen leidt: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} automatiseringspad afgewezen: kan {path} niet oplossen: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4562,7 +4562,9 @@ imageTrace Outline Kontur
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} nie przyznano dostępu automatyzacji do systemu plików: brak uprawnień do odczytu: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} nie przyznano dostępu automatyzacji do systemu plików: brak uprawnień do zapisu: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} odrzucono ścieżkę automatyzacji: poza katalogiem głównym odczytu {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} odrzucono ścieżkę automatyzacji: poza katalogami głównymi odczytu {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} odrzucono ścieżkę automatyzacji: poza katalogiem głównym zapisu {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} odrzucono ścieżkę automatyzacji: poza katalogami głównymi zapisu {roots}: {path}
|
||||
@msg automation path rejected: path is empty odrzucono ścieżkę automatyzacji: ścieżka jest pusta
|
||||
@msg automation path rejected: a link that leads nowhere: {path} odrzucono ścieżkę automatyzacji: łącze prowadzi donikąd: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} odrzucono ścieżkę automatyzacji: nie można rozwiązać {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4003,7 +4003,9 @@ axis Both Ambos
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} o acesso da automação ao sistema de arquivos não foi concedido: falta autorização de leitura: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} o acesso da automação ao sistema de arquivos não foi concedido: falta autorização de gravação: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} caminho da automação rejeitado: fora da raiz de leitura {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} caminho da automação rejeitado: fora das raízes de leitura {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} caminho da automação rejeitado: fora da raiz de gravação {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} caminho da automação rejeitado: fora das raízes de gravação {roots}: {path}
|
||||
@msg automation path rejected: path is empty caminho da automação rejeitado: o caminho está vazio
|
||||
@msg automation path rejected: a link that leads nowhere: {path} caminho da automação rejeitado: um link que não leva a lugar nenhum: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} caminho da automação rejeitado: não foi possível resolver {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4496,7 +4496,9 @@ axis Both Оба
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} у автоматизации нет доступа к файловой системе: нет права на чтение: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} у автоматизации нет доступа к файловой системе: нет права на запись: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} путь автоматизации отклонён: вне корневой папки для чтения {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} путь автоматизации отклонён: вне корневых папок для чтения {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} путь автоматизации отклонён: вне корневой папки для записи {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} путь автоматизации отклонён: вне корневых папок для записи {roots}: {path}
|
||||
@msg automation path rejected: path is empty путь автоматизации отклонён: путь пуст
|
||||
@msg automation path rejected: a link that leads nowhere: {path} путь автоматизации отклонён: ссылка, которая никуда не ведёт: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} путь автоматизации отклонён: не удаётся определить {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4381,7 +4381,9 @@ axis Both Båda
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} automatiseringen har inte fått åtkomst till filsystemet: läsbehörighet saknas: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} automatiseringen har inte fått åtkomst till filsystemet: skrivbehörighet saknas: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} automatiseringssökvägen avvisades: utanför läsroten {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} automatiseringssökvägen avvisades: utanför läsrötterna {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} automatiseringssökvägen avvisades: utanför skrivroten {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} automatiseringssökvägen avvisades: utanför skrivrötterna {roots}: {path}
|
||||
@msg automation path rejected: path is empty automatiseringssökvägen avvisades: sökvägen är tom
|
||||
@msg automation path rejected: a link that leads nowhere: {path} automatiseringssökvägen avvisades: en länk som inte leder någonstans: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} automatiseringssökvägen avvisades: kan inte lösa upp {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4224,7 +4224,9 @@ axis Both Her İkisi
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} otomasyon dosya sistemine erişim izni verilmedi: okuma yetkisi yok: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} otomasyon dosya sistemine erişim izni verilmedi: yazma yetkisi yok: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} otomasyon yolu reddedildi: okuma kökünün dışında {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} otomasyon yolu reddedildi: okuma köklerinin dışında {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} otomasyon yolu reddedildi: yazma kökünün dışında {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} otomasyon yolu reddedildi: yazma köklerinin dışında {roots}: {path}
|
||||
@msg automation path rejected: path is empty otomasyon yolu reddedildi: yol boş
|
||||
@msg automation path rejected: a link that leads nowhere: {path} otomasyon yolu reddedildi: hiçbir yere gitmeyen bağlantı: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} otomasyon yolu reddedildi: {path} çözülemiyor: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4482,7 +4482,9 @@ axis Both Обидві
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} автоматизація не має доступу до файлової системи: немає права на читання: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} автоматизація не має доступу до файлової системи: немає права на запис: {path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} шлях автоматизації відхилено: поза кореневою папкою для читання {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} шлях автоматизації відхилено: поза кореневими папками для читання {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} шлях автоматизації відхилено: поза кореневою папкою для запису {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} шлях автоматизації відхилено: поза кореневими папками для запису {roots}: {path}
|
||||
@msg automation path rejected: path is empty шлях автоматизації відхилено: шлях порожній
|
||||
@msg automation path rejected: a link that leads nowhere: {path} шлях автоматизації відхилено: посилання, яке нікуди не веде: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} шлях автоматизації відхилено: не вдається визначити {path}: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 733 and column 40.
|
@@ -4142,7 +4142,9 @@ theme Appearance Giao diện
|
||||
@msg automation path rejected: a link that leads nowhere: {path} đường dẫn tự động bị từ chối: liên kết không dẫn đến đâu: {path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} đường dẫn tự động bị từ chối: không thể phân giải {path}: {e}
|
||||
@msg automation path rejected: outside the read root {root}: {path} đường dẫn tự động bị từ chối: ngoài thư mục gốc đọc {root}: {path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} đường dẫn tự động bị từ chối: ngoài các thư mục gốc đọc {roots}: {path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} đường dẫn tự động bị từ chối: ngoài thư mục gốc ghi {root}: {path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} đường dẫn tự động bị từ chối: ngoài các thư mục gốc ghi {roots}: {path}
|
||||
@msg automation path rejected: parent traversal below a folder that doesn't exist: {path} đường dẫn tự động bị từ chối: duyệt cha bên dưới thư mục không tồn tại: {path}
|
||||
@msg automation path rejected: path is empty đường dẫn tự động bị từ chối: đường dẫn trống
|
||||
@msg automation path rejected: reserved device names are not allowed: {path} đường dẫn tự động bị từ chối: tên thiết bị đặt trước không được phép: {path}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4028 and column 10.
|
@@ -4257,7 +4257,9 @@ imageTrace Outline 轮廓
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} 未授予自动化文件系统访问权限:缺少读取权限:{path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} 未授予自动化文件系统访问权限:缺少写入权限:{path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} 已拒绝自动化路径:位于读取根目录 {root} 之外:{path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} 已拒绝自动化路径:位于所有读取根目录 {roots} 之外:{path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} 已拒绝自动化路径:位于写入根目录 {root} 之外:{path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} 已拒绝自动化路径:位于所有写入根目录 {roots} 之外:{path}
|
||||
@msg automation path rejected: path is empty 已拒绝自动化路径:路径为空
|
||||
@msg automation path rejected: a link that leads nowhere: {path} 已拒绝自动化路径:链接指向不存在的位置:{path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} 已拒绝自动化路径:无法解析 {path}:{e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2240,7 +2240,9 @@ file-filter Templates 範本
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} 未授予自動化檔案系統存取權:沒有讀取權限:{path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} 未授予自動化檔案系統存取權:沒有寫入權限:{path}
|
||||
@msg automation path rejected: outside the read root {root}: {path} 已拒絕自動化路徑:位於讀取根目錄 {root} 之外:{path}
|
||||
@msg automation path rejected: outside the read roots {roots}: {path} 已拒絕自動化路徑:位於所有讀取根目錄 {roots} 之外:{path}
|
||||
@msg automation path rejected: outside the write root {root}: {path} 已拒絕自動化路徑:位於寫入根目錄 {root} 之外:{path}
|
||||
@msg automation path rejected: outside the write roots {roots}: {path} 已拒絕自動化路徑:位於所有寫入根目錄 {roots} 之外:{path}
|
||||
@msg automation path rejected: path is empty 已拒絕自動化路徑:路徑是空的
|
||||
@msg automation path rejected: a link that leads nowhere: {path} 已拒絕自動化路徑:連結指向不存在的位置:{path}
|
||||
@msg automation path rejected: cannot resolve {path}: {e} 已拒絕自動化路徑:無法解析 {path}:{e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -27,7 +27,7 @@ fn confined(tag: &str) -> (VectorcraftApp, std::sync::mpsc::Sender<ControlReques
|
||||
write: Some(Box::new(|p: &str, b: &[u8]| vectorcraft_format::write_atomic(Path::new(p), b).map_err(|e| e.to_string()))),
|
||||
..Default::default()
|
||||
};
|
||||
let roots = AutomationRoots::new(Some(&base.join("in")), Some(&base.join("out"))).unwrap();
|
||||
let roots = AutomationRoots::new(&[&base.join("in")], &[&base.join("out")]).unwrap();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let app = VectorcraftApp::new(Session::new(), services).with_control(rx).with_automation_roots(roots);
|
||||
(app, tx, base)
|
||||
|
||||
@@ -62,8 +62,8 @@ vectorcraft --control 7979 --automation-read-root /work/project --automation-wri
|
||||
`--automation-read-root <dir>` and `--automation-write-root <dir>` (or `VECTORCRAFT_AUTOMATION_READ_ROOT` /
|
||||
`VECTORCRAFT_AUTOMATION_WRITE_ROOT`; `--flag=<dir>` works too) confine what the control channel reads and
|
||||
writes, with the same flags as `vectorcraft-cli mcp` and the other Craft apps. Every path a request reads
|
||||
(open, place, relink, library and profile loads, plug-ins…) must lie inside the read root, every path it
|
||||
writes (save, export, package, captures, library saves…) inside the write root; anything else is an
|
||||
(open, place, relink, library and profile loads, plug-ins…) must lie inside a read root, every path it
|
||||
writes (save, export, package, captures, library saves…) inside a write root; anything else is an
|
||||
ordinary error reply. Paths are made absolute against the app's working directory, links are followed
|
||||
(for a file that doesn't exist yet, its folder's), and the comparison is by whole folder names (`/work2` is
|
||||
not inside `/work`), ignoring case on Windows; `..` below a folder that doesn't exist, a link that leads
|
||||
@@ -71,9 +71,9 @@ nowhere and Windows device names are refused. A root left out grants none of its
|
||||
existing folders, or the app doesn't start; without `--control` they are ignored. See `docs/mcp.md`
|
||||
› Confining file access for the details.
|
||||
|
||||
Each root flag may be given once; a command-line root overrides its environment default. Missing or
|
||||
empty values, another option used as a value, and duplicate flags stop the desktop before it starts
|
||||
any services, even without `--control`. For a folder name starting with `-`, use `--flag=-folder`
|
||||
Each root flag may be given more than once, a path inside any of its folders being allowed (#3546); the
|
||||
command line's folders of a kind replace its environment default. Missing or empty values and another
|
||||
option used as a value stop the desktop before it starts any services, even without `--control`. For a folder name starting with `-`, use `--flag=-folder`
|
||||
or the relative path `./-folder`.
|
||||
|
||||
What is confined: the requests themselves, the frames that carry input they inject (`ui.click`, `ui.key`,
|
||||
|
||||
+15
-8
@@ -73,6 +73,8 @@ claude mcp add vectorcraft -- vectorcraft-cli mcp
|
||||
|
||||
```sh
|
||||
vectorcraft-cli mcp --automation-read-root /work/project --automation-write-root /work/project/out
|
||||
vectorcraft-cli mcp --automation-read-root /work/project --automation-read-root /work/shared-assets \
|
||||
--automation-write-root /work/project/out --automation-write-root /tmp/scratch
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -83,15 +85,19 @@ vectorcraft-cli mcp --automation-read-root /work/project --automation-write-root
|
||||
The two flags (PhotoCraft and the other Craft apps take the same ones, so one client config works across
|
||||
the suite) keep the agent to the folders you give it:
|
||||
|
||||
- Every file a command reads must lie inside the **read root**: `open_file`, `file.place`, relinking and
|
||||
- Every file a command reads must lie inside a **read root**: `open_file`, `file.place`, relinking and
|
||||
updating links, the images an SVG links to, swatch library, graphic style library and colour-profile loads, plug-ins,
|
||||
`file.newFromTemplate`, preset imports, shortcut sets (`shortcutSet.read`), the folders `text.findFontFiles` and `links.findFiles` search, the
|
||||
scripts `script.check` reads and the files they include.
|
||||
- Every file a command writes must lie inside the **write root**: `save_file`, `export`, the save and
|
||||
- Every file a command writes must lie inside a **write root**: `save_file`, `export`, the save and
|
||||
export commands, Export for Screens, Package, Export for Web, print to file, `screenshot {path}`,
|
||||
library and preset saves to a path, the copies `links.relink {copy}` makes.
|
||||
- `file.list {path}` (through `run_command`) lists a folder's files and folders, held to the read root like any read, so an agent can see what it was given (#1163).
|
||||
- Read and write access are separate. Give the same folder twice for both; a root left out grants none
|
||||
- `file.list {path}` (through `run_command`) lists a folder's files and folders, held to the read roots like any read, so an agent can see what it was given (#1163).
|
||||
- Each flag may be given more than once (#3546): a path inside any of the read roots may be read, one inside any
|
||||
of the write roots written, so one session can read a project folder and a shared assets folder, or write to an
|
||||
output folder and a scratch folder, without widening a root to their common parent. A folder given twice
|
||||
counts once.
|
||||
- Read and write access are separate. Give the same folder to both flags for both; a kind left out grants none
|
||||
of its access (with only `--automation-read-root`, nothing can be written to disk, though `export` and
|
||||
the save commands without a path still return the bytes). Without either flag nothing changes.
|
||||
- A path is made absolute against the server's working directory, then its links are followed: a file
|
||||
@@ -102,15 +108,16 @@ the suite) keep the agent to the folders you give it:
|
||||
Windows case is ignored, UNC paths (`\\server\share\…`) are resolved like drive-letter ones, and
|
||||
device names (`CON`, `NUL`, `COM1`…) are refused.
|
||||
- A refused path is an ordinary tool error the agent can read and correct, for example
|
||||
`automation path rejected: outside the write root /work/project/out: /etc/x.png` or
|
||||
`automation path rejected: outside the write root /work/project/out: /etc/x.png` (with several,
|
||||
`outside the write roots /work/project/out, /tmp/scratch: …`) or
|
||||
`automation filesystem access is not granted: write authority is absent: /work/project/out/x.png`.
|
||||
Links in an opened document that point outside the read root show as missing.
|
||||
Links in an opened document that point outside the read roots show as missing.
|
||||
- The preferences that move folders the app reads or writes on its own (`fontsFolder`,
|
||||
`pluginsFolder`, `recoveryFolder`, `templatesFolder`) can only be set inside the roots. The app's own folders
|
||||
(preferences, Data Recovery, the User Defined library folders, VectorCraft's Fonts folder) are not
|
||||
confined: no agent names a path there.
|
||||
- The roots must be existing folders; a missing one stops the server with an error. Each flag is
|
||||
given once (`--flag <dir>` or `--flag=<dir>`).
|
||||
- The roots must be existing folders; a missing one stops the server with an error. Each flag takes one
|
||||
folder per use (`--flag <dir>` or `--flag=<dir>`) and may be used again for more.
|
||||
|
||||
The flags confine the headless server and imply `--headless`; `--connect` with them is an error, since
|
||||
the server would otherwise talk to an app that isn't confined. A running app confines itself: start it
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user