1mod local;
24mod overlay;
25
26pub use local::LocalBackend;
27pub use overlay::VirtualOverlayBackend;
28
29#[cfg(test)]
30pub mod testing;
31
32#[cfg(test)]
33pub use testing::MockBackend;
34
35use async_trait::async_trait;
36use serde_json::Value as JsonValue;
37use std::path::{Path, PathBuf};
38use thiserror::Error;
39
40use crate::interpreter::{value_to_json, ExecResult, OutputData};
41use crate::tools::{ExecContext, ToolArgs, ToolSchema};
42use crate::vfs::MountInfo;
43
44pub type BackendResult<T> = Result<T, BackendError>;
46
47#[derive(Debug, Clone, Error)]
49pub enum BackendError {
50 #[error("not found: {0}")]
51 NotFound(String),
52 #[error("already exists: {0}")]
53 AlreadyExists(String),
54 #[error("permission denied: {0}")]
55 PermissionDenied(String),
56 #[error("is a directory: {0}")]
57 IsDirectory(String),
58 #[error("not a directory: {0}")]
59 NotDirectory(String),
60 #[error("read-only filesystem")]
61 ReadOnly,
62 #[error("conflict: {0}")]
63 Conflict(ConflictError),
64 #[error("tool not found: {0}")]
65 ToolNotFound(String),
66 #[error("io error: {0}")]
67 Io(String),
68 #[error("invalid operation: {0}")]
69 InvalidOperation(String),
70}
71
72impl From<std::io::Error> for BackendError {
73 fn from(err: std::io::Error) -> Self {
74 use std::io::ErrorKind;
75 match err.kind() {
76 ErrorKind::NotFound => BackendError::NotFound(err.to_string()),
77 ErrorKind::AlreadyExists => BackendError::AlreadyExists(err.to_string()),
78 ErrorKind::PermissionDenied => BackendError::PermissionDenied(err.to_string()),
79 ErrorKind::IsADirectory => BackendError::IsDirectory(err.to_string()),
80 ErrorKind::NotADirectory => BackendError::NotDirectory(err.to_string()),
81 ErrorKind::ReadOnlyFilesystem => BackendError::ReadOnly,
82 _ => BackendError::Io(err.to_string()),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Error)]
89#[error("conflict at {location}: expected {expected:?}, found {actual:?}")]
90pub struct ConflictError {
91 pub location: String,
93 pub expected: String,
95 pub actual: String,
97}
98
99#[derive(Debug, Clone)]
114pub enum PatchOp {
115 Insert { offset: usize, content: String },
117
118 Delete {
121 offset: usize,
122 len: usize,
123 expected: Option<String>,
124 },
125
126 Replace {
129 offset: usize,
130 len: usize,
131 content: String,
132 expected: Option<String>,
133 },
134
135 InsertLine { line: usize, content: String },
137
138 DeleteLine { line: usize, expected: Option<String> },
141
142 ReplaceLine {
145 line: usize,
146 content: String,
147 expected: Option<String>,
148 },
149
150 Append { content: String },
152}
153
154#[derive(Debug, Clone, Default)]
156pub struct ReadRange {
157 pub start_line: Option<usize>,
159 pub end_line: Option<usize>,
161 pub offset: Option<u64>,
163 pub limit: Option<u64>,
165}
166
167impl ReadRange {
168 pub fn lines(start: usize, end: usize) -> Self {
170 Self {
171 start_line: Some(start),
172 end_line: Some(end),
173 ..Default::default()
174 }
175 }
176
177 pub fn bytes(offset: u64, limit: u64) -> Self {
179 Self {
180 offset: Some(offset),
181 limit: Some(limit),
182 ..Default::default()
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy, Default)]
189pub enum WriteMode {
190 CreateNew,
192 #[default]
194 Overwrite,
195 UpdateOnly,
197 Truncate,
199}
200
201#[derive(Debug, Clone)]
203pub struct EntryInfo {
204 pub name: String,
206 pub is_dir: bool,
208 pub is_file: bool,
210 pub is_symlink: bool,
212 pub size: u64,
214 pub modified: Option<u64>,
216 pub permissions: Option<u32>,
218 pub symlink_target: Option<std::path::PathBuf>,
220}
221
222impl EntryInfo {
223 pub fn directory(name: impl Into<String>) -> Self {
225 Self {
226 name: name.into(),
227 is_dir: true,
228 is_file: false,
229 is_symlink: false,
230 size: 0,
231 modified: None,
232 permissions: None,
233 symlink_target: None,
234 }
235 }
236
237 pub fn file(name: impl Into<String>, size: u64) -> Self {
239 Self {
240 name: name.into(),
241 is_dir: false,
242 is_file: true,
243 is_symlink: false,
244 size,
245 modified: None,
246 permissions: None,
247 symlink_target: None,
248 }
249 }
250
251 pub fn symlink(name: impl Into<String>, target: impl Into<std::path::PathBuf>) -> Self {
253 Self {
254 name: name.into(),
255 is_dir: false,
256 is_file: false,
257 is_symlink: true,
258 size: 0,
259 modified: None,
260 permissions: None,
261 symlink_target: Some(target.into()),
262 }
263 }
264}
265
266#[derive(Debug, Clone)]
268pub struct ToolResult {
269 pub code: i32,
271 pub stdout: String,
273 pub stderr: String,
275 pub data: Option<JsonValue>,
277 pub output: Option<OutputData>,
279}
280
281impl ToolResult {
282 pub fn success(stdout: impl Into<String>) -> Self {
284 Self {
285 code: 0,
286 stdout: stdout.into(),
287 stderr: String::new(),
288 data: None,
289 output: None,
290 }
291 }
292
293 pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
295 Self {
296 code,
297 stdout: String::new(),
298 stderr: stderr.into(),
299 data: None,
300 output: None,
301 }
302 }
303
304 pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
306 Self {
307 code: 0,
308 stdout: stdout.into(),
309 stderr: String::new(),
310 data: Some(data),
311 output: None,
312 }
313 }
314
315 pub fn ok(&self) -> bool {
317 self.code == 0
318 }
319}
320
321impl From<ExecResult> for ToolResult {
322 fn from(exec: ExecResult) -> Self {
323 let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
325
326 let data = exec.data.map(|v| value_to_json(&v));
328
329 Self {
330 code,
331 stdout: exec.out,
332 stderr: exec.err,
333 data,
334 output: exec.output,
335 }
336 }
337}
338
339#[derive(Debug, Clone)]
341pub struct ToolInfo {
342 pub name: String,
344 pub description: String,
346 pub schema: ToolSchema,
348}
349
350#[async_trait]
356pub trait KernelBackend: Send + Sync {
357 async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>>;
363
364 async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()>;
366
367 async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()>;
369
370 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()>;
376
377 async fn list(&self, path: &Path) -> BackendResult<Vec<EntryInfo>>;
383
384 async fn stat(&self, path: &Path) -> BackendResult<EntryInfo>;
386
387 async fn mkdir(&self, path: &Path) -> BackendResult<()>;
389
390 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()>;
394
395 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()>;
400
401 async fn exists(&self, path: &Path) -> bool;
403
404 async fn read_link(&self, path: &Path) -> BackendResult<PathBuf>;
412
413 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()>;
417
418 async fn call_tool(
428 &self,
429 name: &str,
430 args: ToolArgs,
431 ctx: &mut ExecContext,
432 ) -> BackendResult<ToolResult>;
433
434 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>>;
436
437 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>>;
439
440 fn read_only(&self) -> bool;
446
447 fn backend_type(&self) -> &str;
449
450 fn mounts(&self) -> Vec<MountInfo>;
452
453 fn resolve_real_path(&self, path: &Path) -> Option<std::path::PathBuf>;
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use super::testing::MockBackend;
466 use std::sync::atomic::Ordering;
467 use std::sync::Arc;
468
469 #[test]
470 fn test_backend_error_from_io_error() {
471 let not_found = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
472 let backend_err: BackendError = not_found.into();
473 assert!(matches!(backend_err, BackendError::NotFound(_)));
474
475 let permission = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no access");
476 let backend_err: BackendError = permission.into();
477 assert!(matches!(backend_err, BackendError::PermissionDenied(_)));
478 }
479
480 #[test]
481 fn test_entry_info_constructors() {
482 let dir = EntryInfo::directory("mydir");
483 assert!(dir.is_dir);
484 assert!(!dir.is_file);
485 assert_eq!(dir.name, "mydir");
486
487 let file = EntryInfo::file("myfile.txt", 1024);
488 assert!(!file.is_dir);
489 assert!(file.is_file);
490 assert_eq!(file.size, 1024);
491 }
492
493 #[test]
494 fn test_tool_result() {
495 let success = ToolResult::success("hello");
496 assert!(success.ok());
497 assert_eq!(success.stdout, "hello");
498
499 let failure = ToolResult::failure(1, "error");
500 assert!(!failure.ok());
501 assert_eq!(failure.code, 1);
502 }
503
504 #[test]
505 fn test_read_range() {
506 let lines = ReadRange::lines(10, 20);
507 assert_eq!(lines.start_line, Some(10));
508 assert_eq!(lines.end_line, Some(20));
509
510 let bytes = ReadRange::bytes(100, 50);
511 assert_eq!(bytes.offset, Some(100));
512 assert_eq!(bytes.limit, Some(50));
513 }
514
515 #[tokio::test]
516 async fn test_mock_backend_call_tool_routing() {
517 let (backend, call_count) = MockBackend::new();
518 let backend: Arc<dyn KernelBackend> = Arc::new(backend);
519 let mut ctx = ExecContext::with_backend(backend.clone());
520
521 assert_eq!(call_count.load(Ordering::SeqCst), 0);
523
524 let args = ToolArgs::new();
526 let result = backend.call_tool("test-tool", args, &mut ctx).await.unwrap();
527
528 assert_eq!(call_count.load(Ordering::SeqCst), 1);
530 assert!(result.ok());
531 assert!(result.stdout.contains("mock executed: test-tool"));
532
533 let args = ToolArgs::new();
535 backend.call_tool("another-tool", args, &mut ctx).await.unwrap();
536 assert_eq!(call_count.load(Ordering::SeqCst), 2);
537 }
538}