mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 12:37:08 -06:00
MCP: --automation-read-root / --automation-write-root confine the agent's file access (#500)
`effectcraft-cli mcp` had no way to keep an agent's file access to a folder; the flags were unknown options. Both now take a folder (repeatable, as in PhotoCraft): with either given, files are read only under a read or write root and written only under a write root (none: no writes). Defaults are unchanged. automation::confine::Confinement checks every engine command the backend runs (execute_command, command_run, open/save_project, each engine.batch step), in both headless and bridge mode, before it runs: the file parameters of the commands that read or write files must be absolute paths inside the roots (symbolic links resolved through the longest existing ancestor, `..` refused), as must any other absolute path in a command's parameters. Commands that write where they aren't told are checked against the target they would write: Save / Increment and Save the open project's file, renderQueue.render every queued output (renderQueue.list). Scripts, Execute File, Reveal in Finder, Open Recent by index, `$N` file-path references in a batch, render_frame/screenshot paths outside a write root and the bridge's UI-driving tools are refused while confined. docs/mcp.md describes it. Test: a confined headless server opens and saves inside its roots and is refused outside them, for a relative path, `..`, a batch step, a `$N` path, Save to a read-only root's project, the Render Queue's default output there, a symbolic link out of the write root, and scripts; the unconfined server still writes anywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
@@ -117,6 +117,9 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
|
||||
(app.project, comps, layers, properties…); prints writeLn
|
||||
output and the result; errors exit 1 with file:line:col
|
||||
mcp [--autosave | --bridge PORT] MCP server (JSON-RPC over stdio); --autosave preserves unsaved headless work
|
||||
[--automation-read-root DIR] [--automation-write-root DIR]
|
||||
confine the agent's file access: reads under a read or write root,
|
||||
writes under a write root (each repeatable); scripts are refused
|
||||
options: --project F.ecproj | --demo | --empty --save | --save-as F --bridge PORT --json --gpu
|
||||
<comp>: id or name, '-' = active comp; <layer>: id, '#n' or name; <value>: JSON or bare string";
|
||||
|
||||
@@ -163,6 +166,9 @@ const VALUED: &[&str] = &[
|
||||
"--webm-codec",
|
||||
"--audio-bitrate",
|
||||
"--opus-app",
|
||||
// mcp
|
||||
"--automation-read-root",
|
||||
"--automation-write-root",
|
||||
];
|
||||
|
||||
/// Options without a value. Any other `--option` is a usage error.
|
||||
@@ -232,6 +238,10 @@ impl Args {
|
||||
fn flag(&self, k: &str) -> bool {
|
||||
self.opts.iter().any(|(o, _)| o == k)
|
||||
}
|
||||
/// Every value of a repeatable option, in order.
|
||||
fn opts(&self, k: &str) -> Vec<&str> {
|
||||
self.opts.iter().filter(|(o, _)| o == k).filter_map(|(_, v)| v.as_deref()).collect()
|
||||
}
|
||||
fn opt(&self, k: &str) -> Option<&str> {
|
||||
self.opts.iter().rev().find(|(o, _)| o == k).and_then(|(_, v)| v.as_deref())
|
||||
}
|
||||
@@ -595,7 +605,12 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> {
|
||||
if args.flag("--autosave") && args.opt("--bridge").is_some() {
|
||||
return usage_err("--autosave is for headless MCP; the bridged app owns its auto-saves");
|
||||
}
|
||||
let b = backend(args, false)?;
|
||||
let mut b = backend(args, false)?;
|
||||
let (read, write) = (args.opts("--automation-read-root"), args.opts("--automation-write-root"));
|
||||
if !read.is_empty() || !write.is_empty() {
|
||||
let roots = |r: Vec<&str>| r.into_iter().map(std::path::PathBuf::from).collect::<Vec<_>>();
|
||||
b.confine(effectcraft_automation::confine::Confinement::new(&roots(read), &roots(write))?);
|
||||
}
|
||||
let server = McpServer::new(b);
|
||||
let mut server = if args.flag("--autosave") {
|
||||
let root =
|
||||
|
||||
Reference in New Issue
Block a user