Skip to main content

kaish_tool_api/
backend.rs

1//! The `KernelBackend` trait — kaish's abstract I/O and tool-dispatch layer.
2//!
3//! The trait lives here (not in `kaish-types`) because it is async and refers
4//! to [`ToolCtx`](crate::ToolCtx). Its data types (errors, results, ops) and
5//! the concrete implementations (`LocalBackend`, overlay, …) live elsewhere —
6//! the data in `kaish-types::backend`, the impls in `kaish-kernel`.
7
8use std::path::{Path, PathBuf};
9
10use async_trait::async_trait;
11
12use kaish_types::backend::{
13    BackendError, BackendResult, MountInfo, PatchOp, ReadRange, ToolInfo, ToolResult, WriteMode,
14};
15use kaish_types::{DirEntry, PathAccess, ToolArgs};
16
17use crate::ctx::ToolCtx;
18
19/// Abstract backend interface for file operations and tool dispatch.
20///
21/// Implementations select where a path resolves and how tools are dispatched:
22/// - `LocalBackend` — VfsRouter-backed local filesystem (the default).
23/// - `KaijutsuBackend` — CRDT-backed blocks when embedded in kaijutsu.
24#[async_trait]
25pub trait KernelBackend: Send + Sync {
26    // ═══════════════════════════════════════════════════════════════════════
27    // File Operations
28    // ═══════════════════════════════════════════════════════════════════════
29
30    /// Read file contents, optionally with a range specification.
31    async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>>;
32
33    /// Write content to a file with the specified mode.
34    async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()>;
35
36    /// Append content to a file.
37    async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()>;
38
39    /// Apply a sequence of patch operations to a file.
40    ///
41    /// Operations apply in order to one snapshot of the file, and the result
42    /// is written once. If an operation fails — a CAS `expected` mismatch, an
43    /// offset or line number past the end — the batch stops before the write
44    /// and the file keeps every byte it had. A caller holding an error never
45    /// has to work out how much of the batch survived.
46    ///
47    /// The snapshot accumulates, so each operation sees the edits before it:
48    /// offsets and line numbers are relative to the content as patched so far,
49    /// not to the file as it was on entry. An insert at line 1 shifts the line
50    /// numbers every later operation in the same batch uses.
51    ///
52    /// The promise covers the operations, not the write that follows them.
53    /// Persisting the result is `write`'s business, with whatever crash,
54    /// concurrency, and I/O-error behavior the implementation gives it — a
55    /// write that fails partway through can still leave a partial file.
56    async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()>;
57
58    /// List a directory's entries.
59    async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>>;
60
61    /// Stat a path (following symlinks).
62    async fn stat(&self, path: &Path) -> BackendResult<DirEntry>;
63
64    /// Create a directory.
65    async fn mkdir(&self, path: &Path) -> BackendResult<()>;
66
67    /// Set the modification time of an existing path.
68    ///
69    /// Read-only or purely-virtual mounts reject rather than silently
70    /// succeeding — `touch` on an existing file must route through here, never
71    /// escape to the host via `resolve_real_path`.
72    async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> BackendResult<()>;
73
74    /// Remove a file, directory, or symlink; `recursive` descends into a
75    /// directory. The final component is never followed: a symlink is
76    /// unlinked and its target kept, and a link to a directory is not
77    /// descended into.
78    async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()>;
79
80    /// Rename/move a path. Neither side follows a final symlink: a symlink
81    /// source moves as a link, and a symlink at the destination is replaced,
82    /// never written through.
83    async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()>;
84
85    /// Whether a path exists, following symlinks: a dangling link does not
86    /// exist, and an error reads as `false`. Use `lstat` to ask whether a
87    /// link is present.
88    async fn exists(&self, path: &Path) -> bool;
89
90    /// Stat a path without following symlinks.
91    async fn lstat(&self, path: &Path) -> BackendResult<DirEntry>;
92
93    /// Read a symlink's target as stored, without resolving it.
94    async fn read_link(&self, path: &Path) -> BackendResult<PathBuf>;
95
96    /// Create a symlink at `link` pointing to `target`. The target is stored
97    /// verbatim; a relative target resolves from the link's directory. An
98    /// absolute target is rewritten relative to the link when both are on one
99    /// mount, and refused when they are not.
100    async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()>;
101
102    /// Resolve `path` to its canonical form: follow every symlink hop, fold
103    /// `.` and `..` lexically. The final component may be missing when
104    /// `allow_missing_final` is true (GNU `readlink -f` semantics); a
105    /// missing INTERMEDIATE component is always an error. Symlink hops are
106    /// capped at 40, matching Linux `MAXSYMLINKS`; exceeding the cap is an
107    /// error, never a silent stop.
108    ///
109    /// The default walks component by component through
110    /// [`KernelBackend::lstat`] and [`KernelBackend::read_link`], so it
111    /// inherits whatever containment those already give. `LocalBackend`
112    /// overrides this to delegate straight to the VFS layer's single-shot
113    /// resolver instead of one round trip per hop.
114    async fn canonicalize(&self, path: &Path, allow_missing_final: bool) -> BackendResult<PathBuf> {
115        let components: Vec<_> = path.components().collect();
116        let total = components.len();
117        let mut current = PathBuf::new();
118
119        for (idx, component) in components.iter().enumerate() {
120            let is_last = idx + 1 == total;
121            match component {
122                std::path::Component::RootDir => {}
123                std::path::Component::CurDir => {}
124                std::path::Component::ParentDir => {
125                    current.pop();
126                }
127                std::path::Component::Normal(_) => {
128                    current.push(component);
129                    current =
130                        resolve_symlink_hop(self, current, is_last && allow_missing_final).await?;
131                }
132                std::path::Component::Prefix(_) => {
133                    current.push(component);
134                }
135            }
136        }
137        Ok(current)
138    }
139
140    // ═══════════════════════════════════════════════════════════════════════
141    // Tool Dispatch
142    // ═══════════════════════════════════════════════════════════════════════
143
144    /// Call a tool by name with the given arguments and execution context.
145    ///
146    /// For local backends, this executes the tool directly via ToolRegistry.
147    /// For remote backends (e.g. kaijutsu), this may serialize the call and
148    /// forward it to the parent process.
149    async fn call_tool(
150        &self,
151        name: &str,
152        args: ToolArgs,
153        ctx: &mut dyn ToolCtx,
154    ) -> BackendResult<ToolResult>;
155
156    /// List available external tools.
157    async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>>;
158
159    /// Get information about a specific tool.
160    async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>>;
161
162    // ═══════════════════════════════════════════════════════════════════════
163    // Backend Information
164    // ═══════════════════════════════════════════════════════════════════════
165
166    /// Returns true if this backend is read-only.
167    fn read_only(&self) -> bool;
168
169    /// What the kernel can do with one path: the query behind `test -r`,
170    /// `test -w`, and `test -x`.
171    ///
172    /// Neither [`KernelBackend::read_only`] nor `DirEntry.permissions`
173    /// answers "can this path be written" alone. A read-only wrapper over an
174    /// OS-writable directory reports permissive mode bits and refuses every
175    /// write; a `DevFs` mount reports `read_only() == false` (so `>
176    /// /dev/null` works) while its `/dev` directory accepts nothing.
177    /// [`PathAccess::resolve`] combines the two, and is the only way to build
178    /// a `PathAccess` — a caller cannot consult one fact by accident.
179    ///
180    /// The default answers from `stat` plus this backend's whole-backend
181    /// `read_only()`, which is right for a backend that is uniformly
182    /// read-only or uniformly writable. A backend whose mounts differ —
183    /// `LocalBackend`, which routes through a `VfsRouter` — overrides this to
184    /// ask the mount that owns the path.
185    ///
186    /// Errors exactly as `stat` does: a path that does not exist is an error,
187    /// not a `PathAccess` of all-false.
188    async fn path_access(&self, path: &Path) -> BackendResult<PathAccess> {
189        let entry = self.stat(path).await?;
190        Ok(PathAccess::resolve(entry.permissions, self.read_only()))
191    }
192
193    /// Returns the backend type identifier (e.g. "local", "kaijutsu").
194    fn backend_type(&self) -> &str;
195
196    /// List all mount points.
197    fn mounts(&self) -> Vec<MountInfo>;
198
199    /// Resolve a VFS path to a real filesystem path.
200    ///
201    /// Returns `Some(path)` if the VFS path maps to a real filesystem (like
202    /// LocalFs), or `None` if the path is virtual (like MemoryFs). Tools like
203    /// `git` that hand paths to external C libraries need the real path.
204    fn resolve_real_path(&self, path: &Path) -> Option<PathBuf>;
205}
206
207/// Symlink hops [`KernelBackend::canonicalize`]'s default walk follows
208/// before refusing, matching Linux's `MAXSYMLINKS`.
209const MAX_SYMLINK_HOPS: usize = 40;
210
211/// Follow the symlink chain starting at `path`, if any, to the entry it
212/// names. `allow_missing` permits `path` itself to be absent; every hop
213/// short of it must exist.
214async fn resolve_symlink_hop<B: KernelBackend + ?Sized>(
215    backend: &B,
216    path: PathBuf,
217    allow_missing: bool,
218) -> BackendResult<PathBuf> {
219    let mut current = path;
220    for _ in 0..MAX_SYMLINK_HOPS {
221        match backend.lstat(&current).await {
222            Ok(entry) if entry.is_symlink() => {
223                let target = backend.read_link(&current).await?;
224                current = if target.is_absolute() {
225                    target
226                } else {
227                    let parent = current.parent().unwrap_or(Path::new(""));
228                    parent.join(target)
229                };
230                current = fold_dots(current);
231            }
232            Ok(_) => return Ok(current),
233            Err(BackendError::NotFound(_)) if allow_missing => return Ok(current),
234            Err(e) => return Err(e),
235        }
236    }
237    Err(BackendError::InvalidOperation(format!(
238        "too many levels of symbolic links: {}",
239        current.display()
240    )))
241}
242
243/// Collapse `.` and `..` lexically in a path: `..` past the start is
244/// dropped, not accumulated, matching the VFS layer's own clamp-at-root
245/// rule for a root-relative path.
246fn fold_dots(path: PathBuf) -> PathBuf {
247    let mut out = PathBuf::new();
248    for component in path.components() {
249        match component {
250            std::path::Component::ParentDir => {
251                out.pop();
252            }
253            std::path::Component::CurDir => {}
254            other => out.push(other),
255        }
256    }
257    out
258}