opendev_repl/handlers/
process_handler.rs1use std::collections::HashMap;
12
13use regex::Regex;
14use serde_json::Value;
15use tracing::debug;
16
17use opendev_runtime::approval::ApprovalRulesManager;
18
19use super::traits::{HandlerMeta, HandlerResult, PreCheckResult, ToolHandler};
20
21const MAX_OUTPUT_LINES: usize = 200;
23
24const SERVER_PATTERNS: &[&str] = &[
26 r"flask\s+run",
27 r"python.*app\.py",
28 r"python.*manage\.py\s+runserver",
29 r"django.*runserver",
30 r"uvicorn",
31 r"gunicorn",
32 r"python.*-m\s+http\.server",
33 r"npm\s+(run\s+)?(start|dev|serve)",
34 r"yarn\s+(run\s+)?(start|dev|serve)",
35 r"node.*server",
36 r"nodemon",
37 r"next\s+(dev|start)",
38 r"rails\s+server",
39 r"php.*artisan\s+serve",
40 r"hugo\s+server",
41 r"jekyll\s+serve",
42 r"cargo\s+run",
43 r"go\s+run",
44];
45
46pub struct ProcessHandler {
48 approval_manager: Option<ApprovalRulesManager>,
49 server_re: Vec<Regex>,
50}
51
52impl ProcessHandler {
53 pub fn new(approval_manager: Option<ApprovalRulesManager>) -> Self {
55 let server_re = SERVER_PATTERNS
56 .iter()
57 .filter_map(|p| Regex::new(p).ok())
58 .collect();
59 Self {
60 approval_manager,
61 server_re,
62 }
63 }
64
65 fn is_server_command(&self, command: &str) -> bool {
67 self.server_re.iter().any(|re| re.is_match(command))
68 }
69
70 fn truncate_output(output: &str) -> String {
72 let lines: Vec<&str> = output.lines().collect();
73 if lines.len() <= MAX_OUTPUT_LINES {
74 return output.to_string();
75 }
76
77 let head = MAX_OUTPUT_LINES * 2 / 3;
78 let tail = MAX_OUTPUT_LINES - head - 1;
79 let omitted = lines.len() - head - tail;
80
81 let mut result = lines[..head].join("\n");
82 result.push_str(&format!("\n\n... ({omitted} lines omitted) ...\n\n"));
83 result.push_str(&lines[lines.len() - tail..].join("\n"));
84 result
85 }
86}
87
88impl ToolHandler for ProcessHandler {
89 fn handles(&self) -> &[&str] {
90 &["Bash", "bash"]
91 }
92
93 fn pre_check(&self, _tool_name: &str, args: &HashMap<String, Value>) -> PreCheckResult {
94 let command = match args.get("command").and_then(|v| v.as_str()) {
95 Some(cmd) => cmd,
96 None => return PreCheckResult::Deny("Missing 'command' argument".to_string()),
97 };
98
99 if let Some(ref mgr) = self.approval_manager
101 && let Some(rule) = mgr.evaluate_command(command)
102 {
103 use opendev_runtime::approval::RuleAction;
104 match rule.action {
105 RuleAction::AutoDeny => {
106 return PreCheckResult::Deny(format!(
107 "Command denied by rule: {}",
108 &rule.description,
109 ));
110 }
111 RuleAction::AutoApprove => {
112 debug!(command, "Command auto-approved by rule");
113 }
114 _ => {} }
116 }
117
118 let background = args
120 .get("background")
121 .and_then(|v| v.as_bool())
122 .unwrap_or(false);
123 if !background && self.is_server_command(command) {
124 debug!(command, "Auto-promoting server command to background");
125 let mut new_args = args.clone();
126 new_args.insert("background".to_string(), Value::Bool(true));
127 return PreCheckResult::ModifyArgs(new_args);
128 }
129
130 PreCheckResult::Allow
131 }
132
133 fn post_process(
134 &self,
135 _tool_name: &str,
136 args: &HashMap<String, Value>,
137 output: Option<&str>,
138 error: Option<&str>,
139 success: bool,
140 ) -> HandlerResult {
141 let is_background = args
142 .get("background")
143 .and_then(|v| v.as_bool())
144 .unwrap_or(false);
145
146 let truncated_output = output.map(Self::truncate_output);
147
148 HandlerResult {
149 output: truncated_output,
150 error: error.map(|s| s.to_string()),
151 success,
152 meta: HandlerMeta {
153 is_background,
154 operation_id: Some(format!(
155 "bash_{}",
156 std::time::SystemTime::now()
157 .duration_since(std::time::UNIX_EPOCH)
158 .map(|d| d.as_millis())
159 .unwrap_or(0)
160 )),
161 ..Default::default()
162 },
163 }
164 }
165}
166
167#[cfg(test)]
168#[path = "process_handler_tests.rs"]
169mod tests;