Skip to main content

ai_agent/tools/
config.rs

1// Source: ~/claudecode/openclaudecode/src/tools/ConfigTool/ConfigTool.ts
2//! Config tool - dynamic configuration.
3//!
4//! Provides tool for reading and updating configuration settings.
5
6use crate::error::AgentError;
7use crate::types::*;
8use std::collections::HashMap;
9use std::sync::{Mutex, OnceLock};
10
11pub const CONFIG_TOOL_NAME: &str = "Config";
12
13/// Global config store
14static CONFIG: OnceLock<Mutex<HashMap<String, serde_json::Value>>> = OnceLock::new();
15
16fn get_config_map() -> &'static Mutex<HashMap<String, serde_json::Value>> {
17    CONFIG.get_or_init(|| Mutex::new(HashMap::new()))
18}
19
20/// Nested config key resolution
21fn get_nested_config(
22    config: &HashMap<String, serde_json::Value>,
23    key: &str,
24) -> Option<serde_json::Value> {
25    // Simple key lookup (supports dot-notation like "settings.theme")
26    if key.contains('.') {
27        let parts: Vec<&str> = key.split('.').collect();
28        let mut current: Option<&serde_json::Value> = None;
29        for part in &parts {
30            if current.is_none() {
31                current = config.get(*part);
32            } else {
33                current = current
34                    .and_then(|v| v.as_object())
35                    .and_then(|obj| obj.get(*part));
36            }
37        }
38        current.cloned()
39    } else {
40        config.get(key).cloned()
41    }
42}
43
44/// Config tool - read and update dynamic configuration
45pub struct ConfigTool;
46
47impl ConfigTool {
48    pub fn new() -> Self {
49        Self
50    }
51
52    pub fn name(&self) -> &str {
53        CONFIG_TOOL_NAME
54    }
55
56    pub fn description(&self) -> &str {
57        "Read or update dynamic configuration settings. Use 'get' to read a setting, 'set' to update a setting, or 'list' to see all settings."
58    }
59
60    pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
61        "Config".to_string()
62    }
63
64    pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
65        input.and_then(|inp| inp["action"].as_str().map(String::from))
66    }
67
68    pub fn render_tool_result_message(
69        &self,
70        content: &serde_json::Value,
71    ) -> Option<String> {
72        content["content"].as_str().map(|s| s.to_string())
73    }
74
75    pub fn input_schema(&self) -> ToolInputSchema {
76        ToolInputSchema {
77            schema_type: "object".to_string(),
78            properties: serde_json::json!({
79                "action": {
80                    "type": "string",
81                    "enum": ["get", "set", "list"],
82                    "description": "Action to perform: get (read a setting), set (update a setting), or list (show all settings)"
83                },
84                "key": {
85                    "type": "string",
86                    "description": "Configuration key (for get/set actions). Supports dot notation for nested keys (e.g., 'settings.theme')"
87                },
88                "value": {
89                    "type": "string",
90                    "description": "Configuration value (for set action). Will be parsed as JSON if possible, otherwise treated as string"
91                }
92            }),
93            required: Some(vec!["action".to_string()]),
94        }
95    }
96
97    pub async fn execute(
98        &self,
99        input: serde_json::Value,
100        _context: &ToolContext,
101    ) -> Result<ToolResult, AgentError> {
102        let action = input["action"].as_str().unwrap_or("list");
103        let key = input["key"].as_str().unwrap_or("");
104        let value_str = input["value"].as_str().unwrap_or("");
105
106        match action {
107            "get" => {
108                if key.is_empty() {
109                    return Ok(ToolResult {
110                        result_type: "text".to_string(),
111                        tool_use_id: "".to_string(),
112                        content: "Error: key is required for 'get' action".to_string(),
113                        is_error: Some(true),
114                        was_persisted: None,
115                    });
116                }
117                let mut guard = get_config_map().lock().unwrap();
118                if let Some(val) = get_nested_config(&guard, key) {
119                    Ok(ToolResult {
120                        result_type: "text".to_string(),
121                        tool_use_id: "".to_string(),
122                        content: format!("Config '{}': {}", key, val),
123                        is_error: Some(false),
124                        was_persisted: None,
125                    })
126                } else {
127                    Ok(ToolResult {
128                        result_type: "text".to_string(),
129                        tool_use_id: "".to_string(),
130                        content: format!("Config '{}' is not set", key),
131                        is_error: None,
132                        was_persisted: None,
133                    })
134                }
135            }
136            "set" => {
137                if key.is_empty() {
138                    return Ok(ToolResult {
139                        result_type: "text".to_string(),
140                        tool_use_id: "".to_string(),
141                        content: "Error: key is required for 'set' action".to_string(),
142                        is_error: Some(true),
143                        was_persisted: None,
144                    });
145                }
146                if value_str.is_empty() {
147                    return Ok(ToolResult {
148                        result_type: "text".to_string(),
149                        tool_use_id: "".to_string(),
150                        content: "Error: value is required for 'set' action".to_string(),
151                        is_error: Some(true),
152                        was_persisted: None,
153                    });
154                }
155                // Parse value as JSON if possible, otherwise treat as string
156                let value: serde_json::Value =
157                    serde_json::from_str(value_str).unwrap_or(serde_json::json!(value_str));
158
159                let mut guard = get_config_map().lock().unwrap();
160                guard.insert(key.to_string(), value);
161                drop(guard);
162
163                Ok(ToolResult {
164                    result_type: "text".to_string(),
165                    tool_use_id: "".to_string(),
166                    content: format!("Config '{}' has been updated", key),
167                    is_error: Some(false),
168                    was_persisted: None,
169                })
170            }
171            "list" => {
172                let mut guard = get_config_map().lock().unwrap();
173                if guard.is_empty() {
174                    Ok(ToolResult {
175                        result_type: "text".to_string(),
176                        tool_use_id: "".to_string(),
177                        content: "No configuration settings set.".to_string(),
178                        is_error: None,
179                        was_persisted: None,
180                    })
181                } else {
182                    let items: Vec<String> = guard
183                        .iter()
184                        .map(|(k, v)| format!("  {}: {}", k, v))
185                        .collect();
186                    Ok(ToolResult {
187                        result_type: "text".to_string(),
188                        tool_use_id: "".to_string(),
189                        content: format!("Configuration settings:\n{}", items.join("\n")),
190                        is_error: Some(false),
191                        was_persisted: None,
192                    })
193                }
194            }
195            _ => Ok(ToolResult {
196                result_type: "text".to_string(),
197                tool_use_id: "".to_string(),
198                content: format!(
199                    "Invalid action: '{}'. Must be 'get', 'set', or 'list'.",
200                    action
201                ),
202                is_error: Some(true),
203                was_persisted: None,
204            }),
205        }
206    }
207}
208
209impl Default for ConfigTool {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215/// Reset the global config store for test isolation.
216pub fn reset_config_for_testing() {
217    let mut guard = get_config_map().lock().unwrap();
218    guard.clear();
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    use crate::tests::common::clear_all_test_state;
226
227    #[test]
228    fn test_config_tool_name() {
229        clear_all_test_state();
230        let tool = ConfigTool::new();
231        assert_eq!(tool.name(), CONFIG_TOOL_NAME);
232    }
233
234    #[test]
235    fn test_config_tool_schema() {
236        clear_all_test_state();
237        let tool = ConfigTool::new();
238        let schema = tool.input_schema();
239        assert_eq!(schema.schema_type, "object");
240        assert!(schema.properties.get("action").is_some());
241    }
242
243    #[tokio::test]
244    async fn test_config_tool_list_empty() {
245        clear_all_test_state();
246        let tool = ConfigTool::new();
247        let input = serde_json::json!({ "action": "list" });
248        let context = ToolContext::default();
249        let result = tool.execute(input, &context).await;
250        assert!(result.is_ok());
251    }
252
253    #[tokio::test]
254    async fn test_config_tool_set_and_get() {
255        clear_all_test_state();
256        let tool = ConfigTool::new();
257        let context = ToolContext::default();
258
259        // Set a value
260        let set_result = tool
261            .execute(
262                serde_json::json!({ "action": "set", "key": "test_key", "value": "\"hello\"" }),
263                &context,
264            )
265            .await;
266        assert!(set_result.is_ok());
267
268        // Get the value
269        let get_result = tool
270            .execute(
271                serde_json::json!({ "action": "get", "key": "test_key" }),
272                &context,
273            )
274            .await;
275        assert!(get_result.is_ok());
276        assert!(get_result.unwrap().content.contains("hello"));
277    }
278}