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 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 // ═══════════════════════════════════════════════════════════════════════
103 // Tool Dispatch
104 // ═══════════════════════════════════════════════════════════════════════
105
106 /// Call a tool by name with the given arguments and execution context.
107 ///
108 /// For local backends, this executes the tool directly via ToolRegistry.
109 /// For remote backends (e.g. kaijutsu), this may serialize the call and
110 /// forward it to the parent process.
111 async fn call_tool(
112 &self,
113 name: &str,
114 args: ToolArgs,
115 ctx: &mut dyn ToolCtx,
116 ) -> BackendResult<ToolResult>;
117
118 /// List available external tools.
119 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>>;
120
121 /// Get information about a specific tool.
122 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>>;
123
124 // ═══════════════════════════════════════════════════════════════════════
125 // Backend Information
126 // ═══════════════════════════════════════════════════════════════════════
127
128 /// Returns true if this backend is read-only.
129 fn read_only(&self) -> bool;
130
131 /// What the kernel can do with one path: the query behind `test -r`,
132 /// `test -w`, and `test -x`.
133 ///
134 /// Neither [`KernelBackend::read_only`] nor `DirEntry.permissions`
135 /// answers "can this path be written" alone. A read-only wrapper over an
136 /// OS-writable directory reports permissive mode bits and refuses every
137 /// write; a `DevFs` mount reports `read_only() == false` (so `>
138 /// /dev/null` works) while its `/dev` directory accepts nothing.
139 /// [`PathAccess::resolve`] combines the two, and is the only way to build
140 /// a `PathAccess` — a caller cannot consult one fact by accident.
141 ///
142 /// The default answers from `stat` plus this backend's whole-backend
143 /// `read_only()`, which is right for a backend that is uniformly
144 /// read-only or uniformly writable. A backend whose mounts differ —
145 /// `LocalBackend`, which routes through a `VfsRouter` — overrides this to
146 /// ask the mount that owns the path.
147 ///
148 /// Errors exactly as `stat` does: a path that does not exist is an error,
149 /// not a `PathAccess` of all-false.
150 async fn path_access(&self, path: &Path) -> BackendResult<PathAccess> {
151 let entry = self.stat(path).await?;
152 Ok(PathAccess::resolve(entry.permissions, self.read_only()))
153 }
154
155 /// Returns the backend type identifier (e.g. "local", "kaijutsu").
156 fn backend_type(&self) -> &str;
157
158 /// List all mount points.
159 fn mounts(&self) -> Vec<MountInfo>;
160
161 /// Resolve a VFS path to a real filesystem path.
162 ///
163 /// Returns `Some(path)` if the VFS path maps to a real filesystem (like
164 /// LocalFs), or `None` if the path is virtual (like MemoryFs). Tools like
165 /// `git` that hand paths to external C libraries need the real path.
166 fn resolve_real_path(&self, path: &Path) -> Option<PathBuf>;
167}