Skip to main content

a3s_common/
tools.rs

1//! Core types and traits for A3S tool system
2
3use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5
6/// Tool definition
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Tool {
9    pub name: String,
10    pub description: String,
11    pub parameters: serde_json::Value,
12}
13
14/// Tool execution result
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ToolResult {
17    pub success: bool,
18    pub output: String,
19    pub error: Option<String>,
20}
21
22/// Resolve a path relative to workspace, ensuring it's within bounds
23pub fn resolve_path(workspace: &Path, path: &str) -> Result<PathBuf, String> {
24    let resolved = workspace.join(path);
25
26    // Canonicalize to resolve .. and symlinks
27    let canonical = resolved
28        .canonicalize()
29        .map_err(|e| format!("Failed to resolve path: {}", e))?;
30
31    // Ensure the path is within workspace
32    if !canonical.starts_with(workspace) {
33        return Err(format!("Path escapes workspace: {}", path));
34    }
35
36    Ok(canonical)
37}
38
39/// Resolve a path for writing (allows non-existent files)
40pub fn resolve_path_for_write(workspace: &Path, path: &str) -> Result<PathBuf, String> {
41    let resolved = workspace.join(path);
42
43    // Check parent directory exists and is within workspace
44    if let Some(parent) = resolved.parent() {
45        if parent.exists() {
46            let canonical = parent
47                .canonicalize()
48                .map_err(|e| format!("Failed to resolve parent: {}", e))?;
49
50            if !canonical.starts_with(workspace) {
51                return Err(format!("Path escapes workspace: {}", path));
52            }
53
54            // Return the resolved path with the filename
55            if let Some(filename) = resolved.file_name() {
56                return Ok(canonical.join(filename));
57            }
58        }
59    }
60
61    // If parent doesn't exist or path is invalid, return error
62    Err(format!(
63        "Invalid path or parent directory doesn't exist: {}",
64        path
65    ))
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use std::fs;
72
73    #[test]
74    fn test_tool_creation() {
75        let tool = Tool {
76            name: "test".to_string(),
77            description: "Test tool".to_string(),
78            parameters: serde_json::json!({}),
79        };
80        assert_eq!(tool.name, "test");
81    }
82
83    #[test]
84    fn test_resolve_path() {
85        let temp = std::env::temp_dir()
86            .canonicalize()
87            .expect("temp dir should be canonicalizable");
88        let workspace = temp.join("test_workspace_common");
89        fs::create_dir_all(&workspace).unwrap();
90
91        let result = resolve_path(&workspace, ".");
92        assert!(result.is_ok());
93
94        fs::remove_dir_all(&workspace).ok();
95    }
96}