Skip to main content

kaish_kernel/backend/
overlay.rs

1//! VirtualOverlayBackend: Routes /v/* paths to internal VFS while delegating everything else.
2//!
3//! This backend is designed for embedders who provide their own `KernelBackend` but want
4//! kaish's virtual filesystems (like `/v/jobs` for job observability) to work automatically.
5//!
6//! # Usage
7//!
8//! Prefer using `Kernel::with_backend()` which handles overlay setup automatically:
9//!
10//! ```ignore
11//! let kernel = Kernel::with_backend(my_backend, config, |vfs| {
12//!     vfs.mount_arc("/v/docs", docs_fs);
13//! }, |_| {})?;
14//! ```
15//!
16//! # Path Routing
17//!
18//! - `/v/*` → Internal VFS (JobFs, MemoryFs for blobs, etc.)
19//! - Everything else → Custom backend
20
21use async_trait::async_trait;
22use std::path::{Path, PathBuf};
23use std::sync::Arc;
24
25use super::{
26    BackendError, BackendResult, KernelBackend, LocalBackend, PatchOp, ReadRange,
27    ToolInfo, ToolResult, WriteMode,
28};
29use crate::tools::{ExecContext, ToolArgs};
30use crate::vfs::{DirEntry, Filesystem, MountInfo, VfsRouter};
31
32/// Backend that overlays virtual paths (`/v/*`) on top of a custom backend.
33///
34/// This enables embedders to provide their own storage backend while still
35/// getting kaish's virtual filesystem features like `/v/jobs` for job observability.
36pub struct VirtualOverlayBackend {
37    /// Custom backend for most paths (embedder-provided).
38    inner: Arc<dyn KernelBackend>,
39    /// VFS for /v/* paths (internal virtual filesystems).
40    vfs: Arc<VfsRouter>,
41}
42
43impl VirtualOverlayBackend {
44    /// Create a new virtual overlay backend.
45    ///
46    /// # Arguments
47    ///
48    /// * `inner` - The custom backend to delegate non-virtual paths to
49    /// * `vfs` - VFS router containing virtual filesystem mounts (typically at /v/*)
50    ///
51    /// # Example
52    ///
53    /// ```ignore
54    /// let overlay = VirtualOverlayBackend::new(my_backend, vfs);
55    /// ```
56    pub fn new(inner: Arc<dyn KernelBackend>, vfs: Arc<VfsRouter>) -> Self {
57        Self { inner, vfs }
58    }
59
60    /// Check if a path should be handled by the VFS (virtual paths).
61    fn is_virtual_path(path: &Path) -> bool {
62        let path_str = path.to_string_lossy();
63        path_str == "/v" || path_str.starts_with("/v/")
64    }
65
66    /// Get the inner backend.
67    pub fn inner(&self) -> &Arc<dyn KernelBackend> {
68        &self.inner
69    }
70
71    /// Get the VFS router.
72    pub fn vfs(&self) -> &Arc<VfsRouter> {
73        &self.vfs
74    }
75}
76
77impl std::fmt::Debug for VirtualOverlayBackend {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("VirtualOverlayBackend")
80            .field("inner_type", &self.inner.backend_type())
81            .field("vfs", &self.vfs)
82            .finish()
83    }
84}
85
86#[async_trait]
87impl KernelBackend for VirtualOverlayBackend {
88    // ═══════════════════════════════════════════════════════════════════════════
89    // File Operations
90    // ═══════════════════════════════════════════════════════════════════════════
91
92    async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
93        if Self::is_virtual_path(path) {
94            let content = self.vfs.read(path).await?;
95            match range {
96                Some(r) => Ok(LocalBackend::apply_read_range(&content, &r)),
97                None => Ok(content),
98            }
99        } else {
100            self.inner.read(path, range).await
101        }
102    }
103
104    async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
105        if Self::is_virtual_path(path) {
106            match mode {
107                WriteMode::CreateNew => {
108                    if self.vfs.exists(path).await {
109                        return Err(BackendError::AlreadyExists(path.display().to_string()));
110                    }
111                    self.vfs.write(path, content).await?;
112                }
113                WriteMode::Overwrite | WriteMode::Truncate => {
114                    self.vfs.write(path, content).await?;
115                }
116                WriteMode::UpdateOnly => {
117                    if !self.vfs.exists(path).await {
118                        return Err(BackendError::NotFound(path.display().to_string()));
119                    }
120                    self.vfs.write(path, content).await?;
121                }
122                // WriteMode is #[non_exhaustive] — treat unknown modes as Overwrite
123                _ => {
124                    self.vfs.write(path, content).await?;
125                }
126            }
127            Ok(())
128        } else {
129            self.inner.write(path, content, mode).await
130        }
131    }
132
133    async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
134        if Self::is_virtual_path(path) {
135            let mut existing = match self.vfs.read(path).await {
136                Ok(data) => data,
137                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
138                Err(e) => return Err(e.into()),
139            };
140            existing.extend_from_slice(content);
141            self.vfs.write(path, &existing).await?;
142            Ok(())
143        } else {
144            self.inner.append(path, content).await
145        }
146    }
147
148    async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
149        if Self::is_virtual_path(path) {
150            // Read existing content
151            let data = self.vfs.read(path).await?;
152            let mut content = String::from_utf8(data)
153                .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
154
155            // Apply each patch operation
156            for op in ops {
157                LocalBackend::apply_patch_op(&mut content, op)?;
158            }
159
160            // Write back
161            self.vfs.write(path, content.as_bytes()).await?;
162            Ok(())
163        } else {
164            self.inner.patch(path, ops).await
165        }
166    }
167
168    // ═══════════════════════════════════════════════════════════════════════════
169    // Directory Operations
170    // ═══════════════════════════════════════════════════════════════════════════
171
172    async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>> {
173        if Self::is_virtual_path(path) {
174            Ok(self.vfs.list(path).await?)
175        } else if path.to_string_lossy() == "/" || path.to_string_lossy().is_empty() {
176            // Root listing: combine inner backend's root with /v
177            let mut entries = self.inner.list(path).await?;
178            // Add /v if not already present
179            if !entries.iter().any(|e| e.name == "v") {
180                entries.push(DirEntry::directory("v"));
181            }
182            Ok(entries)
183        } else {
184            self.inner.list(path).await
185        }
186    }
187
188    async fn stat(&self, path: &Path) -> BackendResult<DirEntry> {
189        if Self::is_virtual_path(path) {
190            Ok(self.vfs.stat(path).await?)
191        } else {
192            self.inner.stat(path).await
193        }
194    }
195
196    async fn mkdir(&self, path: &Path) -> BackendResult<()> {
197        if Self::is_virtual_path(path) {
198            self.vfs.mkdir(path).await?;
199            Ok(())
200        } else {
201            self.inner.mkdir(path).await
202        }
203    }
204
205    async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
206        if Self::is_virtual_path(path) {
207            if recursive
208                && let Ok(entry) = self.vfs.stat(path).await
209                && entry.is_dir()
210                && let Ok(entries) = self.vfs.list(path).await
211            {
212                for entry in entries {
213                    let child_path = path.join(&entry.name);
214                    Box::pin(self.remove(&child_path, true)).await?;
215                }
216            }
217            self.vfs.remove(path).await?;
218            Ok(())
219        } else {
220            self.inner.remove(path, recursive).await
221        }
222    }
223
224    async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
225        let from_virtual = Self::is_virtual_path(from);
226        let to_virtual = Self::is_virtual_path(to);
227
228        if from_virtual != to_virtual {
229            return Err(BackendError::InvalidOperation(
230                "cannot rename between virtual and non-virtual paths".into(),
231            ));
232        }
233
234        if from_virtual {
235            self.vfs.rename(from, to).await?;
236            Ok(())
237        } else {
238            self.inner.rename(from, to).await
239        }
240    }
241
242    async fn exists(&self, path: &Path) -> bool {
243        if Self::is_virtual_path(path) {
244            self.vfs.exists(path).await
245        } else {
246            self.inner.exists(path).await
247        }
248    }
249
250    // ═══════════════════════════════════════════════════════════════════════════
251    // Symlink Operations
252    // ═══════════════════════════════════════════════════════════════════════════
253
254    async fn lstat(&self, path: &Path) -> BackendResult<DirEntry> {
255        if Self::is_virtual_path(path) {
256            Ok(self.vfs.lstat(path).await?)
257        } else {
258            self.inner.lstat(path).await
259        }
260    }
261
262    async fn read_link(&self, path: &Path) -> BackendResult<PathBuf> {
263        if Self::is_virtual_path(path) {
264            Ok(self.vfs.read_link(path).await?)
265        } else {
266            self.inner.read_link(path).await
267        }
268    }
269
270    async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
271        if Self::is_virtual_path(link) {
272            self.vfs.symlink(target, link).await?;
273            Ok(())
274        } else {
275            self.inner.symlink(target, link).await
276        }
277    }
278
279    // ═══════════════════════════════════════════════════════════════════════════
280    // Tool Dispatch
281    // ═══════════════════════════════════════════════════════════════════════════
282
283    async fn call_tool(
284        &self,
285        name: &str,
286        args: ToolArgs,
287        ctx: &mut ExecContext,
288    ) -> BackendResult<ToolResult> {
289        // Tools are dispatched through the inner backend
290        self.inner.call_tool(name, args, ctx).await
291    }
292
293    async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
294        self.inner.list_tools().await
295    }
296
297    async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
298        self.inner.get_tool(name).await
299    }
300
301    // ═══════════════════════════════════════════════════════════════════════════
302    // Backend Information
303    // ═══════════════════════════════════════════════════════════════════════════
304
305    fn read_only(&self) -> bool {
306        // We're not read-only if either layer is writable
307        self.inner.read_only() && self.vfs.read_only()
308    }
309
310    fn backend_type(&self) -> &str {
311        "virtual-overlay"
312    }
313
314    fn mounts(&self) -> Vec<MountInfo> {
315        let mut mounts = self.inner.mounts();
316        mounts.extend(self.vfs.list_mounts());
317        mounts
318    }
319
320    fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
321        if Self::is_virtual_path(path) {
322            // Virtual paths don't map to real filesystem
323            None
324        } else {
325            self.inner.resolve_real_path(path)
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::backend::testing::MockBackend;
334    use crate::vfs::MemoryFs;
335
336    async fn make_overlay() -> VirtualOverlayBackend {
337        // Create mock inner backend
338        let (mock, _) = MockBackend::new();
339        let inner: Arc<dyn KernelBackend> = Arc::new(mock);
340
341        // Create VFS with /v mounted
342        let mut vfs = VfsRouter::new();
343        let mem = MemoryFs::new();
344        mem.write(Path::new("blobs/test.bin"), b"blob data").await.unwrap();
345        mem.mkdir(Path::new("jobs")).await.unwrap();
346        vfs.mount("/v", mem);
347
348        VirtualOverlayBackend::new(inner, Arc::new(vfs))
349    }
350
351    #[tokio::test]
352    async fn test_virtual_path_detection() {
353        assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v")));
354        assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/")));
355        assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/jobs")));
356        assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/blobs/test.bin")));
357
358        assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/docs")));
359        assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/g/repo")));
360        assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/")));
361        assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/var")));
362    }
363
364    #[tokio::test]
365    async fn test_read_virtual_path() {
366        let overlay = make_overlay().await;
367        let content = overlay.read(Path::new("/v/blobs/test.bin"), None).await.unwrap();
368        assert_eq!(content, b"blob data");
369    }
370
371    #[tokio::test]
372    async fn test_write_virtual_path() {
373        let overlay = make_overlay().await;
374        overlay
375            .write(Path::new("/v/blobs/new.bin"), b"new data", WriteMode::Overwrite)
376            .await
377            .unwrap();
378        let content = overlay.read(Path::new("/v/blobs/new.bin"), None).await.unwrap();
379        assert_eq!(content, b"new data");
380    }
381
382    #[tokio::test]
383    async fn test_list_virtual_path() {
384        let overlay = make_overlay().await;
385        let entries = overlay.list(Path::new("/v")).await.unwrap();
386        let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
387        assert!(names.contains(&"blobs"));
388        assert!(names.contains(&"jobs"));
389    }
390
391    #[tokio::test]
392    async fn test_root_listing_includes_v() {
393        let overlay = make_overlay().await;
394        let entries = overlay.list(Path::new("/")).await.unwrap();
395        let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
396        assert!(names.contains(&"v"), "Root listing should include 'v' directory");
397    }
398
399    #[tokio::test]
400    async fn test_stat_virtual_path() {
401        let overlay = make_overlay().await;
402        let info = overlay.stat(Path::new("/v/blobs/test.bin")).await.unwrap();
403        assert!(info.is_file());
404        assert_eq!(info.size, 9); // "blob data".len()
405    }
406
407    #[tokio::test]
408    async fn test_exists_virtual_path() {
409        let overlay = make_overlay().await;
410        assert!(overlay.exists(Path::new("/v/blobs/test.bin")).await);
411        assert!(!overlay.exists(Path::new("/v/blobs/nonexistent")).await);
412    }
413
414    #[tokio::test]
415    async fn test_mkdir_virtual_path() {
416        let overlay = make_overlay().await;
417        overlay.mkdir(Path::new("/v/newdir")).await.unwrap();
418        assert!(overlay.exists(Path::new("/v/newdir")).await);
419    }
420
421    #[tokio::test]
422    async fn test_remove_virtual_path() {
423        let overlay = make_overlay().await;
424        overlay.remove(Path::new("/v/blobs/test.bin"), false).await.unwrap();
425        assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
426    }
427
428    #[tokio::test]
429    async fn test_rename_within_virtual() {
430        let overlay = make_overlay().await;
431        overlay
432            .rename(Path::new("/v/blobs/test.bin"), Path::new("/v/blobs/renamed.bin"))
433            .await
434            .unwrap();
435        assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
436        assert!(overlay.exists(Path::new("/v/blobs/renamed.bin")).await);
437    }
438
439    #[tokio::test]
440    async fn test_rename_across_boundary_fails() {
441        let overlay = make_overlay().await;
442        let result = overlay
443            .rename(Path::new("/v/blobs/test.bin"), Path::new("/docs/test.bin"))
444            .await;
445        assert!(matches!(result, Err(BackendError::InvalidOperation(_))));
446    }
447
448    #[tokio::test]
449    async fn test_backend_type() {
450        let overlay = make_overlay().await;
451        assert_eq!(overlay.backend_type(), "virtual-overlay");
452    }
453
454    #[tokio::test]
455    async fn test_resolve_real_path_virtual() {
456        let overlay = make_overlay().await;
457        // Virtual paths don't resolve to real paths
458        assert!(overlay.resolve_real_path(Path::new("/v/blobs/test.bin")).is_none());
459    }
460}