Skip to content

Commit 0c463b7

Browse files
fohteclaudefohte-bot[bot]
authored
fix(adapter): evaluate extracted sub-command instead of raw input for simplified compound constructs (#87)
Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: fohte-bot[bot] <139195068+fohte-bot[bot]@users.noreply.github.com>
1 parent 68daac1 commit 0c463b7

1 file changed

Lines changed: 77 additions & 1 deletion

File tree

src/adapter/mod.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ pub fn run_with_options(endpoint: &dyn Endpoint, config: &Config, options: &RunO
181181
}
182182
}
183183

184+
// When extract_commands simplifies a compound shell construct (e.g. a for-loop)
185+
// down to a single sub-command, evaluate that sub-command instead of the
186+
// original input string.
187+
let effective_command = if commands.len() == 1 && commands[0] != command {
188+
if options.verbose {
189+
eprintln!(
190+
"[verbose] Single sub-command extracted: {:?} (from {:?})",
191+
commands[0], command
192+
);
193+
}
194+
&commands[0]
195+
} else {
196+
&command
197+
};
198+
184199
let action_result = if commands.len() > 1 {
185200
match evaluate_compound(config, &command, &context) {
186201
Ok(compound_result) => {
@@ -198,7 +213,7 @@ pub fn run_with_options(endpoint: &dyn Endpoint, config: &Config, options: &RunO
198213
Err(e) => return endpoint.handle_error(e.into()),
199214
}
200215
} else {
201-
match evaluate_command(config, &command, &context) {
216+
match evaluate_command(config, effective_command, &context) {
202217
Ok(result) => {
203218
if options.verbose {
204219
log_matched_rules(&result.matched_rules);
@@ -885,6 +900,67 @@ mod tests {
885900
}
886901
}
887902

903+
// --- single extracted sub-command uses simplified form ---
904+
905+
#[rstest]
906+
#[case::for_loop_with_echo(
907+
"for f in *.yaml; do echo $f; done",
908+
allow_rule("echo *"),
909+
true, // expect handle_action
910+
false, // expect handle_no_match
911+
)]
912+
#[case::for_loop_no_matching_rule(
913+
"for f in *.yaml; do echo $f; done",
914+
allow_rule("git status"),
915+
false,
916+
true
917+
)]
918+
fn single_extracted_subcommand_evaluates_simplified_form(
919+
#[case] command: &str,
920+
#[case] rule: RuleEntry,
921+
#[case] expect_action: bool,
922+
#[case] expect_no_match: bool,
923+
) {
924+
let endpoint = MockEndpoint::new(Ok(Some(command.to_string())));
925+
let config = make_config(vec![rule]);
926+
run(&endpoint, &config);
927+
928+
assert_eq!(*endpoint.called_handle_action.borrow(), expect_action);
929+
assert_eq!(*endpoint.called_handle_no_match.borrow(), expect_no_match);
930+
}
931+
932+
#[rstest]
933+
fn for_loop_with_deny_rule_on_subcommand() {
934+
let endpoint =
935+
MockEndpoint::new(Ok(Some("for f in *.yaml; do rm -rf $f; done".to_string())));
936+
let config = make_config(vec![deny_rule("rm *")]);
937+
run(&endpoint, &config);
938+
939+
assert!(*endpoint.called_handle_action.borrow());
940+
assert!(matches!(
941+
*endpoint.last_action.borrow(),
942+
Some(Action::Deny(_))
943+
));
944+
}
945+
946+
#[rstest]
947+
fn single_extracted_subcommand_verbose_logs_extraction() {
948+
let endpoint = MockEndpoint::new(Ok(Some("for f in *.yaml; do echo $f; done".to_string())));
949+
let config = make_config(vec![allow_rule("echo *")]);
950+
let options = RunOptions {
951+
dry_run: false,
952+
verbose: true,
953+
};
954+
let exit_code = run_with_options(&endpoint, &config, &options);
955+
956+
assert!(*endpoint.called_handle_action.borrow());
957+
assert!(matches!(
958+
*endpoint.last_action.borrow(),
959+
Some(Action::Allow)
960+
));
961+
assert_eq!(exit_code, 0);
962+
}
963+
888964
// --- apply_sandbox_fallback unit tests ---
889965

890966
#[rstest]

0 commit comments

Comments
 (0)