cc_toolgate/commands/tools/
cargo.rs1use super::super::CommandSpec;
7use crate::config::CargoConfig;
8use crate::eval::{CommandContext, Decision, RuleMatch};
9use agent_shell_parser::parse::Word;
10use std::collections::HashMap;
11
12pub struct CargoSpec {
20 safe_subcommands: Vec<String>,
22 allowed_with_config: Vec<String>,
24 config_env: HashMap<String, String>,
26}
27
28impl CargoSpec {
29 pub fn from_config(config: &CargoConfig) -> Self {
31 Self {
32 safe_subcommands: config.safe_subcommands.clone(),
33 allowed_with_config: config.allowed_with_config.clone(),
34 config_env: config.config_env.clone(),
35 }
36 }
37
38 fn subcommand(ctx: &CommandContext) -> Option<&Word> {
41 let mut iter = ctx.words.iter();
42 for word in iter.by_ref() {
43 if word == "cargo" {
44 return iter.find(|w| !w.is_flag());
45 }
46 }
47 None
48 }
49
50 fn env_keys_display(&self) -> String {
52 let mut keys: Vec<&str> = self.config_env.keys().map(|k| k.as_str()).collect();
53 keys.sort();
54 keys.join(", ")
55 }
56}
57
58impl CommandSpec for CargoSpec {
59 fn evaluate(&self, ctx: &CommandContext) -> RuleMatch {
60 let sub_str: &str = Self::subcommand(ctx).map(|w| w.as_str()).unwrap_or("?");
61
62 if self.safe_subcommands.iter().any(|s| s == sub_str) {
63 if let Some(ref r) = ctx.redirection {
64 return RuleMatch {
65 decision: Decision::Ask,
66 reason: format!("cargo {sub_str} with {}", r),
67 };
68 }
69 return RuleMatch {
70 decision: Decision::Allow,
71 reason: format!("cargo {sub_str}"),
72 };
73 }
74
75 if self.allowed_with_config.iter().any(|s| s == sub_str) {
77 if !self.config_env.is_empty() && ctx.env_satisfies(&self.config_env) {
78 if let Some(ref r) = ctx.redirection {
79 return RuleMatch {
80 decision: Decision::Ask,
81 reason: format!("cargo {sub_str} with {}", r),
82 };
83 }
84 return RuleMatch {
85 decision: Decision::Allow,
86 reason: format!("cargo {sub_str} with {}", self.env_keys_display()),
87 };
88 }
89 return RuleMatch {
90 decision: Decision::Ask,
91 reason: format!("cargo {sub_str} requires confirmation"),
92 };
93 }
94
95 if ctx.has_any_flag(&["--version", "-V"]) {
97 return RuleMatch {
98 decision: Decision::Allow,
99 reason: "cargo --version".into(),
100 };
101 }
102
103 RuleMatch {
104 decision: Decision::Ask,
105 reason: format!("cargo {sub_str} requires confirmation"),
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::config::Config;
114
115 fn spec() -> CargoSpec {
116 CargoSpec::from_config(&Config::default_config().cargo)
117 }
118
119 fn eval(cmd: &str) -> Decision {
120 let s = spec();
121 let ctx = CommandContext::from_command(cmd);
122 s.evaluate(&ctx).decision
123 }
124
125 #[test]
126 fn allow_build() {
127 assert_eq!(eval("cargo build --release"), Decision::Allow);
128 }
129
130 #[test]
131 fn allow_test() {
132 assert_eq!(eval("cargo test"), Decision::Allow);
133 }
134
135 #[test]
136 fn allow_clippy() {
137 assert_eq!(eval("cargo clippy"), Decision::Allow);
138 }
139
140 #[test]
141 fn allow_version() {
142 assert_eq!(eval("cargo --version"), Decision::Allow);
143 }
144
145 #[test]
146 fn allow_version_short() {
147 assert_eq!(eval("cargo -V"), Decision::Allow);
148 }
149
150 #[test]
151 fn ask_install() {
152 assert_eq!(eval("cargo install ripgrep"), Decision::Ask);
153 }
154
155 #[test]
156 fn ask_publish() {
157 assert_eq!(eval("cargo publish"), Decision::Ask);
158 }
159
160 #[test]
161 fn redir_build() {
162 assert_eq!(eval("cargo build --release > /tmp/log"), Decision::Ask);
163 }
164
165 fn spec_with_env_gate() -> CargoSpec {
168 CargoSpec::from_config(&CargoConfig {
169 safe_subcommands: vec!["build".into(), "check".into(), "test".into()],
170 allowed_with_config: vec!["install".into(), "publish".into()],
171 config_env: HashMap::from([("CARGO_INSTALL_ROOT".into(), "/tmp/bin".into())]),
172 })
173 }
174
175 fn eval_with_env_gate(cmd: &str) -> Decision {
176 let s = spec_with_env_gate();
177 let ctx = CommandContext::from_command(cmd);
178 s.evaluate(&ctx).decision
179 }
180
181 #[test]
182 fn env_gate_install_with_matching_value() {
183 assert_eq!(
184 eval_with_env_gate("CARGO_INSTALL_ROOT=/tmp/bin cargo install ripgrep"),
185 Decision::Allow
186 );
187 }
188
189 #[test]
190 fn env_gate_install_with_wrong_value() {
191 assert_eq!(
192 eval_with_env_gate("CARGO_INSTALL_ROOT=/usr/local cargo install ripgrep"),
193 Decision::Ask
194 );
195 }
196
197 #[test]
198 fn env_gate_install_no_config() {
199 assert_eq!(eval_with_env_gate("cargo install ripgrep"), Decision::Ask);
200 }
201
202 #[test]
203 fn env_gate_publish_with_config() {
204 assert_eq!(
205 eval_with_env_gate("CARGO_INSTALL_ROOT=/tmp/bin cargo publish"),
206 Decision::Allow
207 );
208 }
209
210 #[test]
211 fn env_gate_build_still_safe_no_env() {
212 assert_eq!(eval_with_env_gate("cargo build"), Decision::Allow);
214 }
215}