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.
This commit is contained in:
prxnvt
2026-10-22 02:26:32 -04:00
parent 3f9223c85f
commit efce63f75f
3 changed files with 33 additions and 7 deletions
+12 -4
View File
@@ -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, &params) {
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;
}
+17
View File
@@ -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();
}