From efce63f75ff132c3ced76465530897b0bc5e8aa0 Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:26:27 -0400 Subject: [PATCH] Engine: macro recorder applies the macro policy to the steps of a recorded script run An Automate script run while recording was replaced by all of its steps except automate.* ones, without the policy check that top-level commands get. For example, a script that runs window.activate or macro.list produced a recorded macro that the runner refused on playback. The steps of a recorded script run, and of an inlined macro run, now go through the same policy as top-level commands; refused ones are left out and listed as skipped. --- crates/engine/src/macros/recorder.rs | 16 ++++++++++++---- crates/engine/src/tests/macros.rs | 17 +++++++++++++++++ docs/macros.md | 7 ++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/macros/recorder.rs b/crates/engine/src/macros/recorder.rs index bb2e30f74..210872475 100644 --- a/crates/engine/src/macros/recorder.rs +++ b/crates/engine/src/macros/recorder.rs @@ -6,7 +6,7 @@ //! - commands a macro may not run (saving, opening files…) and other macros' commands are left //! out; a macro run while recording is replaced by its steps when it has no inputs, no output //! and only plain commands (otherwise it is left out), and an Automate script run while -//! recording by the script's steps; +//! recording by the script's steps, leaving out the steps a macro may not run; //! - a `sheet.activate` or `selection.set` step is added where the sheet or selection changed //! between commands without one, and runs of `selection.set` collapse to the last; //! - with relative references (the default), the selection when recording started is the example @@ -49,6 +49,7 @@ pub fn convert(name: &str, start: &RecordingStart, steps: &[RecordedStep], wb: & // Commands as they'll be played back: (id, params, sheet before, selection before, whether // they came from another macro and stay as written). let mut cmds: Vec<(String, Json, usize, String, Selection, bool)> = Vec::new(); + let refused = |id: &str, params: &Json| matches!(macro_policy(id, params, crate::Trust::Local), Policy::Deny(_) | Policy::NeedsPermission(_)); for st in steps.iter().filter(|st| st.doc == start.doc) { let id = st.id.as_str(); if id == "macro.run" { @@ -58,9 +59,12 @@ pub fn convert(name: &str, start: &RecordingStart, steps: &[RecordedStep], wb: & match inner { Some(m) if m.inputs.is_empty() && m.output.anchor.is_none() && m.steps.iter().all(|x| matches!(x.kind, StepKind::Command { .. })) => { for x in m.steps.iter().filter(|x| x.enabled) { - if let StepKind::Command { id: cid, params } = &x.kind { - cmds.push((cid.clone(), params.clone(), st.sheet, st.sheet_name.clone(), st.selection.clone(), true)); + let StepKind::Command { id: cid, params } = &x.kind else { continue }; + if refused(cid, params) { + skipped.push(cid.clone()); + continue; } + cmds.push((cid.clone(), params.clone(), st.sheet, st.sheet_name.clone(), st.selection.clone(), true)); } } _ => skipped.push(id.to_string()), @@ -72,6 +76,10 @@ pub fn convert(name: &str, start: &RecordingStart, steps: &[RecordedStep], wb: & match st.params.get("name").and_then(Json::as_str).and_then(|n| crate::cmd::extra::script_steps(wb, n)) { Some(steps) => { for (cid, params) in steps { + if refused(&cid, ¶ms) { + skipped.push(cid); + continue; + } cmds.push((cid, params, st.sheet, st.sheet_name.clone(), st.selection.clone(), true)); } } @@ -79,7 +87,7 @@ pub fn convert(name: &str, start: &RecordingStart, steps: &[RecordedStep], wb: & } continue; } - if matches!(macro_policy(id, &st.params, crate::Trust::Local), Policy::Deny(_) | Policy::NeedsPermission(_)) { + if refused(id, &st.params) { skipped.push(id.to_string()); continue; } diff --git a/crates/engine/src/tests/macros.rs b/crates/engine/src/tests/macros.rs index f3640f08a..e16e8be3e 100644 --- a/crates/engine/src/tests/macros.rs +++ b/crates/engine/src/tests/macros.rs @@ -1078,3 +1078,20 @@ fn a_recorded_macro_run_is_inlined_only_when_its_steps_stand_alone() { assert_eq!(r["skipped"], json!(["macro.run", "macro.run"])); assert_eq!(recorded(&mut s, "Rec"), ["cell.set {\"cell\":\"C1\",\"input\":\"plain\"}"]); } + +#[test] +fn a_recorded_script_run_leaves_out_the_steps_a_macro_may_not_run() { + let mut s = s(); + let steps = json!([ + {"command": "cell.set", "params": {"cell": "B1", "input": "kept"}}, + {"command": "window.activate", "params": {"index": 0}}, + {"command": "macro.list", "params": {}} + ]); + s.execute("automate.newScript", json!({"name": "Mixed", "commands": steps})).unwrap(); + s.execute("macro.record", json!({"relative": false})).unwrap(); + s.execute("automate.runScript", json!({"name": "Mixed"})).unwrap(); + let r = s.execute("macro.record", json!({"stop": true, "name": "Rec"})).unwrap(); + assert_eq!(r["skipped"], json!(["window.activate", "macro.list"])); + assert_eq!(recorded(&mut s, "Rec"), ["cell.set {\"cell\":\"B1\",\"input\":\"kept\"}"]); + s.execute("macro.run", json!({"name": "Rec", "onConflict": "overwrite"})).unwrap(); +} diff --git a/docs/macros.md b/docs/macros.md index 674a35673..fab64144f 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -125,9 +125,10 @@ references the selection when recording starts is the example input: references on its sheet become tokens (on another sheet `C4` stays that sheet's `C4`), and the input's shape, header and column types are inferred. Toggles such as Bold are recorded with the state they set (`"on": true`), so playing them back sets them rather than flipping them. Commands a macro may not -run are left out and listed in the result; a macro run while recording is replaced by its steps -when it has no inputs, no output and only plain commands (otherwise it is left out and listed), and -an Automate script run by the script's steps. +run are left out and listed in the result, also from the steps of a macro or script run while +recording; a macro run while recording is replaced by its steps when it has no inputs, no output +and only plain commands (otherwise it is left out and listed), and an Automate script run by the +script's steps. ## Other commands and storage