Skip to main content

oxios_kernel/tools/builtin/
mount_tool.rs

1//! Mount tool — wraps `MountManager` behind the `AgentTool` interface (RFC-025).
2//!
3//! Provides agents with Mount query and enrichment capabilities. The agent
4//! explores a Mount's filesystem and writes its findings via the `update`
5//! action — this is the agent-driven enrichment path.
6//!
7//! Actions: `list`, `get`, `update` (enrichment).
8
9use async_trait::async_trait;
10use std::sync::Arc;
11
12use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
13use serde_json::{Value, json};
14
15use crate::kernel_handle::KernelHandle;
16use crate::mount::{MountId, MountManager};
17
18/// Agent tool for Mount queries + agent-driven enrichment (RFC-025).
19///
20/// Agents can query Mounts and refine their `auto_description`/`auto_meta`
21/// via the `update` action, but cannot create or remove Mounts (user-level).
22pub struct MountTool {
23    mount_manager: Option<Arc<MountManager>>,
24}
25
26impl MountTool {
27    /// Create from a `KernelHandle`.
28    pub fn from_kernel(kernel: &KernelHandle) -> Self {
29        Self {
30            mount_manager: kernel.mounts.as_ref().map(|m| m.mount_manager.clone()),
31        }
32    }
33}
34
35impl std::fmt::Debug for MountTool {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.debug_struct("MountTool").finish()
38    }
39}
40
41#[async_trait]
42impl AgentTool for MountTool {
43    fn name(&self) -> &str {
44        "mount"
45    }
46
47    fn label(&self) -> &str {
48        "Mount"
49    }
50
51    fn description(&self) -> &'static str {
52        "Query and enrich Mounts (path aliases). The agent explores a Mount's \
53         filesystem and writes its findings to auto_description/auto_meta via \
54         'update'. Actions: list, get, update."
55    }
56
57    fn parameters_schema(&self) -> Value {
58        json!({
59            "type": "object",
60            "properties": {
61                "action": {
62                    "type": "string",
63                    "enum": ["list", "get", "update"],
64                    "description": "Mount operation to perform"
65                },
66                "id": {
67                    "type": "string",
68                    "description": "Mount UUID"
69                },
70                "name": {
71                    "type": "string",
72                    "description": "Mount name (alternative to id for 'get')"
73                },
74                "auto_description": {
75                    "type": "string",
76                    "description": "(update) Agent-written description, ≤500 chars. Cite real files you read."
77                },
78                "auto_meta": {
79                    "type": "object",
80                    "description": "(update) Auto-detected metadata to set",
81                    "properties": {
82                        "languages": { "type": "array", "items": { "type": "string" } },
83                        "stack": { "type": "array", "items": { "type": "string" } },
84                        "markers": { "type": "array", "items": { "type": "string" } },
85                        "summary": { "type": "string" }
86                    }
87                }
88            },
89            "required": ["action"]
90        })
91    }
92
93    async fn execute(
94        &self,
95        _tool_call_id: &str,
96        params: Value,
97        _signal: Option<tokio::sync::oneshot::Receiver<()>>,
98        _ctx: &ToolContext,
99    ) -> Result<AgentToolResult, oxi_sdk::ToolError> {
100        let action = params
101            .get("action")
102            .and_then(|v| v.as_str())
103            .ok_or_else(|| "Missing required parameter: action".to_string())?;
104
105        let mm = self
106            .mount_manager
107            .as_ref()
108            .ok_or_else(|| "Mount system not available (SQLite not enabled)".to_string())?;
109
110        match action {
111            "list" => {
112                let mounts = mm.list_mounts();
113                if mounts.is_empty() {
114                    return Ok(AgentToolResult::success("No Mounts registered."));
115                }
116                let mut out = format!("Found {} Mount(s):\n\n", mounts.len());
117                for m in &mounts {
118                    let paths = m
119                        .paths
120                        .iter()
121                        .map(|p| p.to_string_lossy().to_string())
122                        .collect::<Vec<_>>()
123                        .join(", ");
124                    let langs = m.auto_meta.languages.join(", ");
125                    out.push_str(&format!(
126                        "- {} ({}) [{}]\n  paths: {}\n  summary: {}\n",
127                        m.name,
128                        &m.id.to_string()[..8],
129                        if langs.is_empty() { "unknown" } else { &langs },
130                        paths,
131                        m.summary_line(),
132                    ));
133                }
134                Ok(AgentToolResult::success(out))
135            }
136
137            "get" => {
138                let mount = if let Some(id_str) = params.get("id").and_then(|v| v.as_str()) {
139                    let id =
140                        MountId::parse_str(id_str).map_err(|e| format!("Invalid mount ID: {e}"))?;
141                    mm.get_mount(id)
142                } else if let Some(name) = params.get("name").and_then(|v| v.as_str()) {
143                    mm.get_mount_by_name(name)
144                } else {
145                    return Err("'get' requires 'id' or 'name' parameter".to_string());
146                };
147
148                match mount {
149                    Some(m) => Ok(AgentToolResult::success(
150                        serde_json::to_string_pretty(&json!({
151                            "id": m.id.to_string(),
152                            "name": m.name,
153                            "source": m.source.to_string(),
154                            "paths": m.paths.iter().map(|p| p.to_string_lossy().to_string()).collect::<Vec<_>>(),
155                            "auto_description": m.auto_description,
156                            "auto_meta": {
157                                "languages": m.auto_meta.languages,
158                                "stack": m.auto_meta.stack,
159                                "markers": m.auto_meta.markers,
160                                "summary": m.auto_meta.summary,
161                            },
162                            "enrichment_pending": m.enrichment_pending,
163                            "last_enriched_at": m.last_enriched_at.map(|t| t.to_rfc3339()),
164                            "last_active_at": m.last_active_at.to_rfc3339(),
165                        }))
166                        .unwrap_or_default(),
167                    )),
168                    None => Ok(AgentToolResult::error("Mount not found")),
169                }
170            }
171
172            "update" => {
173                let id_str = params
174                    .get("id")
175                    .and_then(|v| v.as_str())
176                    .ok_or_else(|| "update requires 'id'".to_string())?;
177                let id =
178                    MountId::parse_str(id_str).map_err(|e| format!("Invalid mount ID: {e}"))?;
179
180                let auto_description = params
181                    .get("auto_description")
182                    .and_then(|v| v.as_str())
183                    .map(String::from);
184
185                let auto_meta = params
186                    .get("auto_meta")
187                    .and_then(|v| v.as_object())
188                    .map(|obj| crate::mount::MountMeta {
189                        languages: obj
190                            .get("languages")
191                            .and_then(|v| v.as_array())
192                            .map(|a| {
193                                a.iter()
194                                    .filter_map(|v| v.as_str().map(String::from))
195                                    .collect()
196                            })
197                            .unwrap_or_default(),
198                        stack: obj
199                            .get("stack")
200                            .and_then(|v| v.as_array())
201                            .map(|a| {
202                                a.iter()
203                                    .filter_map(|v| v.as_str().map(String::from))
204                                    .collect()
205                            })
206                            .unwrap_or_default(),
207                        markers: obj
208                            .get("markers")
209                            .and_then(|v| v.as_array())
210                            .map(|a| {
211                                a.iter()
212                                    .filter_map(|v| v.as_str().map(String::from))
213                                    .collect()
214                            })
215                            .unwrap_or_default(),
216                        summary: obj
217                            .get("summary")
218                            .and_then(|v| v.as_str())
219                            .map(String::from)
220                            .unwrap_or_default(),
221                    });
222
223                if auto_description.is_none() && auto_meta.is_none() {
224                    return Err("update requires 'auto_description' or 'auto_meta'".to_string());
225                }
226
227                match mm.update_enrichment(id, auto_description, auto_meta) {
228                    Ok(m) => Ok(AgentToolResult::success(format!(
229                        "Updated Mount '{}' ({}). enrichment_pending cleared.",
230                        m.name,
231                        &id_str[..8.min(id_str.len())]
232                    ))),
233                    Err(e) => Ok(AgentToolResult::error(format!(
234                        "Failed to update mount: {e}"
235                    ))),
236                }
237            }
238
239            other => Err(format!(
240                "Unknown mount action '{other}'. Valid: list, get, update"
241            )),
242        }
243    }
244}