Skip to main content

zeroclaw_runtime/rpc/
fs.rs

1//! Filesystem RPC methods for remote directory browsing (WSS ACP CWD picker).
2//!
3//! These methods are only available to authenticated WSS sessions and are
4//! subject to daemon-side path policy.
5
6use std::path::Path;
7use zeroclaw_api::jsonrpc::error_codes::*;
8use zeroclaw_api::jsonrpc::{FsEntry, FsListDirRequest, FsListDirResponse};
9
10/// Handle `fs/list_dir`.
11pub async fn handle_fs_list_dir(
12    params: &serde_json::Value,
13) -> Result<serde_json::Value, zeroclaw_api::jsonrpc::JsonRpcError> {
14    let req: FsListDirRequest = serde_json::from_value(params.clone())
15        .map_err(|e| rpc_err(INVALID_PARAMS, e.to_string()))?;
16
17    let path = Path::new(&req.path);
18
19    // Basic traversal guard (more sophisticated policy can be added later)
20    if path.components().any(|c| c.as_os_str() == "..") {
21        return Err(rpc_err(FS_INVALID_PATH, "Path traversal not allowed"));
22    }
23
24    if !path.is_dir() {
25        return Err(rpc_err(
26            FS_NOT_FOUND,
27            format!("Not a directory: {}", req.path),
28        ));
29    }
30
31    let mut entries = Vec::new();
32    let read_dir = match std::fs::read_dir(path) {
33        Ok(rd) => rd,
34        Err(e) => {
35            return Err(rpc_err(
36                FS_NOT_FOUND,
37                format!("Cannot read {}: {e}", req.path),
38            ));
39        }
40    };
41
42    for entry in read_dir {
43        let entry = match entry {
44            Ok(e) => e,
45            Err(_) => continue,
46        };
47        let meta = match entry.metadata() {
48            Ok(m) => m,
49            Err(_) => continue,
50        };
51        let name = entry.file_name().to_string_lossy().to_string();
52        let is_hidden = name.starts_with('.');
53        if is_hidden && !req.show_hidden {
54            continue;
55        }
56
57        let full_path = entry.path().to_string_lossy().to_string();
58        entries.push(FsEntry {
59            name,
60            is_dir: meta.is_dir(),
61            size: meta.len(),
62            is_hidden,
63            full_path,
64            mtime: meta.modified().ok().and_then(|t| {
65                t.duration_since(std::time::UNIX_EPOCH)
66                    .ok()
67                    .map(|d| d.as_secs())
68            }),
69        });
70    }
71
72    // Sort: directories first, then files, case-insensitive
73    entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
74        (true, false) => std::cmp::Ordering::Less,
75        (false, true) => std::cmp::Ordering::Greater,
76        _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
77    });
78
79    let cwd = path.to_string_lossy().to_string();
80    let resp = FsListDirResponse { entries, cwd };
81    serde_json::to_value(resp).map_err(|e| rpc_err(INTERNAL_ERROR, e.to_string()))
82}
83
84fn rpc_err(code: i32, msg: impl Into<String>) -> zeroclaw_api::jsonrpc::JsonRpcError {
85    zeroclaw_api::jsonrpc::JsonRpcError {
86        code,
87        message: msg.into(),
88        data: None,
89    }
90}