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:
bflatastic
2026-10-22 21:36:15 +07:00
parent 49d69d48e5
commit 1b7cfc8bee
39 changed files with 301 additions and 111 deletions
+11 -12
View File
@@ -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()))
+53 -1
View File
@@ -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");
+36 -23
View File
@@ -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> {
+9 -5
View File
@@ -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")]
+9 -3
View File
@@ -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"));
}
}
+103 -45
View File
@@ -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() {
+1 -1
View File
@@ -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");
+2 -2
View File
@@ -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");
+1 -1
View File
@@ -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"]);
+3 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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})));
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+1 -1
View File
@@ -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)
+5 -5
View File
@@ -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
View File
@@ -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