返回 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 "profile": {
94 "type": "string",
95 "enum": ["auto", "rust", "node", "python", "go"],
96 "description": "Verifier profile (action=verifiers)"
97 },
98 "level": {
99 "type": "string",
100 "enum": ["quick", "full"],
101 "description": "Verifier level (action=verifiers)"
102 },
103 "max_python_files": {
104 "type": "integer",
105 "description": "Maximum Python files for the verifier syntax gate (action=verifiers)"
106 },
107 "commands": {
108 "type": "array",
109 "description": "Optional explicit verifier gates (action=verifiers)",
110 "items": {
111 "type": "object",
112 "properties": {
113 "name": { "type": "string" },
114 "program": { "type": "string" },
115 "args": { "type": "array", "items": { "type": "string" } },
116 "cwd": { "type": "string" }
117 },
118 "required": ["name", "program"]
119 }
120 },
121 "background": {
122 "type": "boolean",
123 "description": "Start verifier gates as background jobs (action=verifiers)"
124 }
125 },
126 "required": ["action"]
127 })
128 }
129
130 fn capabilities(&self) -> Vec<ToolCapability> {
131 vec![ToolCapability::ExecutesCode, ToolCapability::Sandboxable]
132 }
133
134 fn approval_requirement_for(&self, _input: &Value) -> ApprovalRequirement {
135 ApprovalRequirement::Required
136 }
137
138 fn is_read_only_for(&self, _input: &Value) -> bool {
139 false
140 }
141
142 fn supports_parallel_for(&self, _input: &Value) -> bool {
143 false
144 }
145
146 fn starts_detached_for(&self, input: &Value) -> bool {
147 self.resolve_action(input) == "verifiers"
148 && input.get("background").and_then(Value::as_bool) == Some(true)
149 }
150
151 async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
152 let action = self.required_action(&input)?;
153 let input = self.strip_action(input)?;
154
155 match action.as_str() {
156 "tests" => RunTestsTool.execute(input, context).await,
157 "verifiers" => RunVerifiersTool.execute(input, context).await,
158 other => Err(ToolError::invalid_input(format!(
159 "Unknown Run action \"{other}\"; nothing was run. Pass one of: {}.",
160 Self::ACTIONS.join(", ")
161 ))),
162 }
163 }
164 }
165
166 #[cfg(test)]
167 mod tests {
168 use super::*;
169 use serde_json::json;
170 use tempfile::tempdir;
171
172 async fn err(input: Value) -> String {
173 let tmp = tempdir().expect("tempdir");
174 let ctx = ToolContext::new(tmp.path().to_path_buf());
175 RunTool::new("Run")
176 .execute(input, &ctx)
177 .await
178 .expect_err("call must be refused")
179 .to_string()
180 }
181
182 /// Defaulting here ran `cargo test` for a model that meant `verifiers`.
183 #[tokio::test]
184 async fn missing_action_does_not_silently_run_tests() {
185 let message = err(json!({"background": true})).await;
186 assert!(message.contains("requires an `action`"), "{message}");
187 assert!(message.contains("nothing was run"), "{message}");
188 assert!(message.contains("tests, verifiers"), "{message}");
189 }
190
191 #[tokio::test]
192 async fn unknown_action_names_the_actions_that_dispatch() {
193 let message = err(json!({"action": "lint"})).await;
194 assert!(message.contains("lint"), "{message}");
195 assert!(message.contains("tests, verifiers"), "{message}");
196 }
197
198 #[test]
199 fn advertised_actions_match_the_actions_that_dispatch() {
200 let schema = RunTool::new("Run").input_schema();
201 let advertised: Vec<&str> = schema["properties"]["action"]["enum"]
202 .as_array()
203 .expect("action enum")
204 .iter()
205 .map(|value| value.as_str().expect("string"))
206 .collect();
207 assert_eq!(advertised, RunTool::ACTIONS);
208 }
209 }
210
210 lines RUST