kaish_kernel/backend/mod.rs
1//! KernelBackend trait for abstracting kaish's I/O layer.
2//!
3//! This module defines the `KernelBackend` trait which provides a unified interface
4//! for file operations and tool dispatch. Two implementations are provided:
5//!
6//! - `LocalBackend`: Default implementation wrapping VfsRouter for local filesystem access
7//! - (Future) `KaijutsuBackend`: CRDT-backed blocks when running under kaijutsu
8//!
9//! # Architecture
10//!
11//! ```text
12//! Builtins (cat, ls, echo, etc.)
13//! ↓
14//! ctx.backend: Arc<dyn KernelBackend>
15//! ↓
16//! ┌─────────────────────────────────────────────┐
17//! │ LocalBackend (default) │ KaijutsuBackend │
18//! │ - wraps VfsRouter │ - CRDT blocks │
19//! │ - local ToolRegistry │ - parent tools │
20//! └─────────────────────────────────────────────┘
21//! ```
22
23mod 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 std::path::{Path, PathBuf};
37
38// Data types re-exported from kaish-types.
39pub use kaish_types::backend::{
40 BackendError, BackendResult, ConflictError, PatchOp, ReadRange, ToolInfo, ToolResult, WriteMode,
41};
42
43use crate::tools::{ExecContext, ToolArgs};
44use crate::vfs::{DirEntry, MountInfo};
45
46/// Abstract backend interface for file operations and tool dispatch.
47///
48/// This trait abstracts kaish's I/O layer, enabling different backends:
49/// - `LocalBackend`: Default implementation using VfsRouter
50/// - `KaijutsuBackend`: CRDT-backed implementation for collaborative editing
51#[async_trait]
52pub trait KernelBackend: Send + Sync {
53 // ═══════════════════════════════════════════════════════════════════════════
54 // File Operations
55 // ═══════════════════════════════════════════════════════════════════════════
56
57 /// Read file contents, optionally with a range specification.
58 async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>>;
59
60 /// Write content to a file with the specified mode.
61 async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()>;
62
63 /// Append content to a file.
64 async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()>;
65
66 /// Apply patch operations to a file.
67 ///
68 /// Patch operations support compare-and-set (CAS) for conflict detection.
69 /// If an operation's `expected` field doesn't match the actual content,
70 /// returns `BackendError::Conflict`.
71 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()>;
72
73 // ═══════════════════════════════════════════════════════════════════════════
74 // Directory Operations
75 // ═══════════════════════════════════════════════════════════════════════════
76
77 /// List directory contents.
78 async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>>;
79
80 /// Get file or directory metadata.
81 async fn stat(&self, path: &Path) -> BackendResult<DirEntry>;
82
83 /// Create a directory (and parent directories if needed).
84 async fn mkdir(&self, path: &Path) -> BackendResult<()>;
85
86 /// Remove a file or directory.
87 ///
88 /// If `recursive` is true, removes directories and their contents.
89 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()>;
90
91 /// Rename (move) a file or directory.
92 ///
93 /// This is an atomic operation when source and destination are on the same
94 /// filesystem. Cross-mount renames are not supported.
95 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()>;
96
97 /// Check if a path exists.
98 async fn exists(&self, path: &Path) -> bool;
99
100 // ═══════════════════════════════════════════════════════════════════════════
101 // Symlink Operations
102 // ═══════════════════════════════════════════════════════════════════════════
103
104 /// Get metadata for a path without following symlinks.
105 ///
106 /// Unlike `stat`, this returns metadata about the symlink itself,
107 /// not the target it points to.
108 async fn lstat(&self, path: &Path) -> BackendResult<DirEntry>;
109
110 /// Read the target of a symbolic link.
111 ///
112 /// Returns the path the symlink points to without following it.
113 async fn read_link(&self, path: &Path) -> BackendResult<PathBuf>;
114
115 /// Create a symbolic link.
116 ///
117 /// Creates a symlink at `link` pointing to `target`.
118 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()>;
119
120 // ═══════════════════════════════════════════════════════════════════════════
121 // Tool Dispatch
122 // ═══════════════════════════════════════════════════════════════════════════
123
124 /// Call a tool by name with the given arguments and execution context.
125 ///
126 /// For local backends, this executes the tool directly via ToolRegistry.
127 /// For remote backends (e.g., kaijutsu), this may serialize the call
128 /// and forward it to the parent process.
129 async fn call_tool(
130 &self,
131 name: &str,
132 args: ToolArgs,
133 ctx: &mut ExecContext,
134 ) -> BackendResult<ToolResult>;
135
136 /// List available external tools.
137 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>>;
138
139 /// Get information about a specific tool.
140 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>>;
141
142 // ═══════════════════════════════════════════════════════════════════════════
143 // Backend Information
144 // ═══════════════════════════════════════════════════════════════════════════
145
146 /// Returns true if this backend is read-only.
147 fn read_only(&self) -> bool;
148
149 /// Returns the backend type identifier (e.g., "local", "kaijutsu").
150 fn backend_type(&self) -> &str;
151
152 /// List all mount points.
153 fn mounts(&self) -> Vec<MountInfo>;
154
155 /// Resolve a VFS path to a real filesystem path.
156 ///
157 /// Returns `Some(path)` if the VFS path maps to a real filesystem (like LocalFs),
158 /// or `None` if the path is in a virtual filesystem (like MemoryFs).
159 ///
160 /// This is needed for tools like `git` that must use real paths with external libraries.
161 fn resolve_real_path(&self, path: &Path) -> Option<std::path::PathBuf>;
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use super::testing::MockBackend;
168 use std::sync::atomic::Ordering;
169 use std::sync::Arc;
170
171 #[test]
172 fn test_backend_error_from_io_error() {
173 let not_found = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
174 let backend_err: BackendError = not_found.into();
175 assert!(matches!(backend_err, BackendError::NotFound(_)));
176
177 let permission = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no access");
178 let backend_err: BackendError = permission.into();
179 assert!(matches!(backend_err, BackendError::PermissionDenied(_)));
180 }
181
182 #[test]
183 fn test_dir_entry_constructors() {
184 let dir = DirEntry::directory("mydir");
185 assert!(dir.is_dir());
186 assert_eq!(dir.name, "mydir");
187
188 let file = DirEntry::file("myfile.txt", 1024);
189 assert!(file.is_file());
190 assert_eq!(file.size, 1024);
191 }
192
193 #[test]
194 fn test_tool_result() {
195 let success = ToolResult::success("hello");
196 assert!(success.ok());
197 assert_eq!(success.stdout, "hello");
198
199 let failure = ToolResult::failure(1, "error");
200 assert!(!failure.ok());
201 assert_eq!(failure.code, 1);
202 }
203
204 #[test]
205 fn test_read_range() {
206 let lines = ReadRange::lines(10, 20);
207 assert_eq!(lines.start_line, Some(10));
208 assert_eq!(lines.end_line, Some(20));
209
210 let bytes = ReadRange::bytes(100, 50);
211 assert_eq!(bytes.offset, Some(100));
212 assert_eq!(bytes.limit, Some(50));
213 }
214
215 #[tokio::test]
216 async fn test_mock_backend_call_tool_routing() {
217 let (backend, call_count) = MockBackend::new();
218 let backend: Arc<dyn KernelBackend> = Arc::new(backend);
219 let mut ctx = ExecContext::with_backend(backend.clone());
220
221 // Verify initial count is 0
222 assert_eq!(call_count.load(Ordering::SeqCst), 0);
223
224 // Call tool through backend
225 let args = ToolArgs::new();
226 let result = backend.call_tool("test-tool", args, &mut ctx).await.unwrap();
227
228 // Verify call was routed through backend
229 assert_eq!(call_count.load(Ordering::SeqCst), 1);
230 assert!(result.ok());
231 assert!(result.stdout.contains("mock executed: test-tool"));
232
233 // Call again to verify count increments
234 let args = ToolArgs::new();
235 backend.call_tool("another-tool", args, &mut ctx).await.unwrap();
236 assert_eq!(call_count.load(Ordering::SeqCst), 2);
237 }
238}