返回 CodeWhale
run_tool.rs
根目录 / crates / tui / src / tools / run_tool.rs
1 //! Canonical action-based wrapper for run/test/verifier tools.
2 //!
3 //! The model sees one tool: `Run` with an `action` parameter
4 //! (tests | verifiers). The per-action legacy execution aliases were removed
5 //! in v0.9.3.
6
7 use async_trait::async_trait;
8 use serde_json::{Value, json};
9
10 use super::canonical_action::required_action;
11 use super::spec::{
12 ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
13 };
14 use super::test_runner::RunTestsTool;
15 use super::verifier::RunVerifiersTool;
16
17 pub struct RunTool {
18 name: &'static str,
19 forced_action: Option<&'static str>,
20 }
21
22 impl RunTool {
23 pub const fn new(name: &'static str) -> Self {
24 Self {
25 name,
26 forced_action: None,
27 }
28 }
29
30 const ACTIONS: &'static [&'static str] = &["tests", "verifiers"];
31
32 /// Policy-side resolution: approval and parallel-safety predicates cannot
33 /// fail, so a missing action resolves to the most conservative answer.
34 /// Execution does not share this fallback — see `required_action`.
35 fn resolve_action<'a>(&self, input: &'a Value) -> &'a str {
36 self.forced_action.unwrap_or_else(|| {
37 input
38 .get("action")
39 .and_then(Value::as_str)
40 .unwrap_or("tests")
41 })
42 }
43
44 fn required_action(&self, input: &Value) -> Result<String, ToolError> {
45 if let Some(forced) = self.forced_action {
46 return Ok(forced.to_string());
47 }
48 required_action(input, self.name, Self::ACTIONS)
49 }
50
51 fn strip_action(&self, input: Value) -> Result<Value, ToolError> {
52 let mut input = input;
53 if let Some(obj) = input.as_object_mut() {
54 obj.remove("action");
55 Ok(input)
56 } else {
57 Err(ToolError::invalid_input("Run tool input must be an object"))
58 }
59 }
60 }
61
62 #[async_trait]
63 impl ToolSpec for RunTool {
64 fn name(&self) -> &'static str {
65 self.name
66 }
67
68 fn model_visible(&self) -> bool {
69 self.name == "Run"
70 }
71
72 fn description(&self) -> &'static str {
73 "Run Cargo tests or repository verifier gates. Use tests for focused Rust test runs; use verifiers for cross-language build, test, lint, and syntax gates. Set background=true for verifier suites expected to take more than a few seconds."
74 }
75
76 fn input_schema(&self) -> Value {
77 json!({
78 "type": "object",
79 "properties": {
80 "action": {
81 "type": "string",
82 "enum": ["tests", "verifiers"],
83 "description": "Action to perform"
84 },
85 "args": {
86 "type": "string",
87 "description": "Extra arguments for cargo test (action=tests)"
88 },
89 "all_features": {
90 "type": "boolean",
91 "description": "Include --all-features for cargo test (action=tests)"
92 },
93 "cwd": {
94 "type": "string",
95 "description": "Optional working directory, relative to the workspace, to run the tests or gates in. Must exist inside the workspace."
96 },
97 "profile": {
98 "type": "string",
99 "enum": ["auto", "rust", "node", "python", "go"],
100 "description": "Verifier profile (action=verifiers)"
101 },
102 "level": {
103 "type": "string",
104 "enum": ["quick", "full"],
105 "description": "Verifier level (action=verifiers)"
106 },
107 "max_python_files": {
108 "type": "integer",
109 "description": "Maximum Python files for the verifier syntax gate (action=verifiers)"
110 },
111 "commands": {
112 "type": "array",
113 "description": "Optional explicit verifier gates (action=verifiers)",
114 "items": {
115 "type": "object",
116 "properties": {
117 "name": { "type": "string" },
118 "program": { "type": "string" },
119 "args": { "type": "array", "items": { "type": "string" } },
120 "cwd": { "type": "string" }
121 },
122 "required": ["name", "program"]
123 }
124 },
125 "background": {
126 "type": "boolean",
127 "description": "Start verifier gates as background jobs (action=verifiers)"
128 }
129 },
130 "required": ["action"]
131 })
132 }
133
134 fn capabilities(&self) -> Vec<ToolCapability> {
135 vec![ToolCapability::ExecutesCode, ToolCapability::Sandboxable]
136 }
137
138 fn approval_requirement_for(&self, _input: &Value) -> ApprovalRequirement {
139 ApprovalRequirement::Required
140 }
141
142 fn is_read_only_for(&self, _input: &Value) -> bool {
143 false
144 }
145
146 fn supports_parallel_for(&self, _input: &Value) -> bool {
147 false
148 }
149
150 fn starts_detached_for(&self, input: &Value) -> bool {
151 self.resolve_action(input) == "verifiers"
152 && input.get("background").and_then(Value::as_bool) == Some(true)
153 }
154
155 async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
156 crate::core::engine::tool_catalog::enforce_tool_denial(context, self.name(), &input)?;
157 let action = self.required_action(&input)?;
158 let input = self.strip_action(input)?;
159
160 match action.as_str() {
161 "tests" => RunTestsTool.execute(input, context).await,
162 "verifiers" => RunVerifiersTool.execute(input, context).await,
163 other => Err(ToolError::invalid_input(format!(
164 "Unknown Run action \"{other}\"; nothing was run. Pass one of: {}.",
165 Self::ACTIONS.join(", ")
166 ))),
167 }
168 }
169 }
170
171 #[cfg(test)]
172 mod tests {
173 use super::*;
174 use serde_json::json;
175 use tempfile::tempdir;
176
177 async fn err(input: Value) -> String {
178 let tmp = tempdir().expect("tempdir");
179 let ctx = ToolContext::new(tmp.path().to_path_buf());
180 RunTool::new("Run")
181 .execute(input, &ctx)
182 .await
183 .expect_err("call must be refused")
184 .to_string()
185 }
186
187 /// Defaulting here ran `cargo test` for a model that meant `verifiers`.
188 #[tokio::test]
189 async fn missing_action_does_not_silently_run_tests() {
190 let message = err(json!({"background": true})).await;
191 assert!(message.contains("requires an `action`"), "{message}");
192 assert!(message.contains("nothing was run"), "{message}");
193 assert!(message.contains("tests, verifiers"), "{message}");
194 }
195
196 #[tokio::test]
197 async fn unknown_action_names_the_actions_that_dispatch() {
198 let message = err(json!({"action": "lint"})).await;
199 assert!(message.contains("lint"), "{message}");
200 assert!(message.contains("tests, verifiers"), "{message}");
201 }
202
203 #[test]
204 fn advertised_actions_match_the_actions_that_dispatch() {
205 let schema = RunTool::new("Run").input_schema();
206 let advertised: Vec<&str> = schema["properties"]["action"]["enum"]
207 .as_array()
208 .expect("action enum")
209 .iter()
210 .map(|value| value.as_str().expect("string"))
211 .collect();
212 assert_eq!(advertised, RunTool::ACTIONS);
213 }
214 }
215
215 lines RUST