MCP: a screenshot's reply hides library paths outside the read roots

The reply is sent after the request was handled, and now hides what any other reply hides.
This commit is contained in:
dotCooCoo
2026-11-12 07:17:46 -08:00
parent 13bee0d6ef
commit 61771e4ead
4 changed files with 53 additions and 14 deletions
+6 -3
View File
@@ -157,9 +157,12 @@ impl VectorcraftApp {
let pending = self.pending_screenshots.remove(index);
self.screenshot_reserved = self.screenshot_reserved.saturating_sub(pending.reserved_bytes);
let roots = self.automation_roots.clone();
let _ = pending
.reply
.send(file_access::confine(roots.as_ref(), || control::save_screenshot(self, &image, pending.path.as_deref(), pending.data)));
let reply = file_access::confine(roots.as_ref(), || {
let mut reply = control::save_screenshot(self, &image, pending.path.as_deref(), pending.data);
control::hide_in_deferred_reply(self, &mut reply, &json!({"path": pending.path}));
reply
});
let _ = pending.reply.send(reply);
}
Err(e) => {
let error = format!("the screenshot could not be used: {e}");
+25 -9
View File
@@ -1002,8 +1002,9 @@ const DOCKING_ACTION: &str = "action";
/// errors included, an error from a panic too: the libraries hidden before it ran and after it. The
/// error of a failed request also hides the paths outside the read root that the request sent
/// ([`sent_paths`]). A reply with no `/`, `\` or `%` in it is not searched, since every hidden path
/// holds a separator, written or escaped. A deferred reply (a screenshot, a `ui.pointer` reply sent
/// after the tool's edit) holds no library id or path.
/// holds a separator, written or escaped. A screenshot's reply is hidden when it is sent
/// ([`hide_in_deferred_reply`]); a `ui.pointer` reply sent after the tool's edit holds no library id
/// or path.
pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome {
if vectorcraft_engine::file_access::current().is_none() {
return dispatch(app, ctx, req);
@@ -1031,18 +1032,33 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
}
};
let Outcome::Done(mut reply) = outcome else { return outcome };
if !holds_a_separator(&reply) {
return Outcome::Done(reply);
hide_in_reply(app, &mut reply, &req.params, before);
Outcome::Done(reply)
}
/// Hides in `reply` the libraries hidden now and in `before`, and for a failed request the paths
/// outside the read root that `sent`, the request's params, holds ([`handle`]).
fn hide_in_reply(app: &VectorcraftApp, reply: &mut Value, sent: &Value, before: Hiding) {
if !holds_a_separator(reply) {
return;
}
let mut hiding = unreadable_library_ids(app);
hiding.merge(before);
if reply.get("ok").and_then(Value::as_bool) == Some(false) {
let mut sent = Vec::new();
sent_paths(&req.params, &mut sent);
hiding.set_hidden(sent, cfg!(windows));
let mut paths = Vec::new();
sent_paths(sent, &mut paths);
hiding.set_hidden(paths, cfg!(windows));
}
hide_library_ids(reply, &hiding);
}
/// Hides in a reply sent after its request was handled (a screenshot's) what [`handle`] hides in a
/// reply, with `sent` as the request's params. Without a read root in force it leaves the reply as
/// it is.
pub(crate) fn hide_in_deferred_reply(app: &VectorcraftApp, reply: &mut Value, sent: &Value) {
if vectorcraft_engine::file_access::current().is_some() {
hide_in_reply(app, reply, sent, Hiding::default());
}
hide_library_ids(&mut reply, &hiding);
Outcome::Done(reply)
}
/// The control methods that send synthetic input.
+20
View File
@@ -752,6 +752,26 @@ fn an_error_hides_a_sent_path_whether_or_not_a_library_was_loaded_from_it() {
let _ = std::fs::remove_dir_all(base);
}
/// A screenshot's reply, sent after its request was handled, hides a hidden library's path, and in an
/// error the path outside the read root that the request sent.
#[test]
fn a_screenshot_reply_hides_paths_like_any_reply() {
let (mut app, _tx, base) = confined("deferred-reply");
let path = s(base.join("outside/b.svg"));
app.ui.color_guide_limit = format!("loaded/{path}");
let sent = s(base.join("outside/shot.png"));
let roots = AutomationRoots::new(&[&base.join("in")], &[&base.join("out")]).unwrap();
let mut done = json!({"ok": true, "result": {"note": format!("from {path}")}});
let mut failed = json!({"ok": false, "error": format!("could not write {sent}")});
vectorcraft_engine::file_access::confine(roots.as_ref(), || {
crate::control::hide_in_deferred_reply(&app, &mut done, &json!({"path": sent}));
crate::control::hide_in_deferred_reply(&app, &mut failed, &json!({"path": sent}));
});
assert_eq!(done["result"]["note"], "from b.svg", "{done}");
assert_eq!(failed["error"], "could not write shot.png", "{failed}");
let _ = std::fs::remove_dir_all(base);
}
/// A reply whose only trace of a hidden library's path is a `file:` URL with every separator escaped
/// shows the file name.
#[test]
+2 -2
View File
@@ -166,8 +166,8 @@ the suite) keep the agent to the folders you give it:
reopens its panel), and the reply to such a request shows that id and file name wherever the library's id or path
appears, an error included. Every reply under the read roots shows a hidden library's id as the id shown and its
path as the file name in every string and key, the status text (`ui.status`), dialogs, the command palette query
(`ui.palette_query`), a script's report (`scriptReport`) and the error from an internal fault included, for the
libraries hidden before the request ran and after it. The path is also matched with `%XX` escapes as a `file:`
(`ui.palette_query`), a script's report (`scriptReport`), a screenshot's reply and the error from an internal
fault included, for the libraries hidden before the request ran and after it. The path is also matched with `%XX` escapes as a `file:`
URL writes it, and on Windows in any case of its letters. A hidden id or path stays only where it begins
another loaded library's whole id with no `.` or `..` part, so a readable library's id that begins with a hidden one stays. The error of a failed request also shows a path outside the
read roots that the request sent by its file name, whether or not a library was loaded from it, so that the