Skip to main content

khive_runtime/
error.rs

1//! Runtime error types.
2
3use std::fmt;
4
5use thiserror::Error;
6
7/// Convenience alias for `Result<T, RuntimeError>`.
8pub type RuntimeResult<T> = Result<T, RuntimeError>;
9
10/// A single missing pack dependency.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct MissingPackDependency {
13    pub from: String,
14    pub requires: String,
15}
16
17impl fmt::Display for MissingPackDependency {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(
20            f,
21            "pack '{}' requires '{}', but '{}' is not in the loaded pack set",
22            self.from, self.requires, self.requires
23        )
24    }
25}
26
27impl std::error::Error for MissingPackDependency {}
28
29/// Multiple missing pack dependencies collected into one error.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct MissingPackDependencies {
32    pub missing: Vec<MissingPackDependency>,
33}
34
35impl fmt::Display for MissingPackDependencies {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        let parts: Vec<String> = self.missing.iter().map(ToString::to_string).collect();
38        write!(f, "{}", parts.join("; "))
39    }
40}
41
42impl std::error::Error for MissingPackDependencies {}
43
44/// Circular pack dependency detected during topological sort.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct CircularPackDependency {
47    pub cycle: Vec<String>,
48}
49
50impl fmt::Display for CircularPackDependency {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(
53            f,
54            "circular dependency detected among packs: {}",
55            self.cycle.join(" -> ")
56        )
57    }
58}
59
60impl std::error::Error for CircularPackDependency {}
61
62/// All errors produced by the khive-runtime layer.
63///
64/// Variants cover storage, query, validation, namespace isolation, and permission failures.
65/// Callers should match on `InvalidInput` for bad arguments, `NotFound` for missing records,
66/// and `NamespaceMismatch` (reported as not-found) for cross-namespace access attempts.
67#[derive(Debug, Error)]
68pub enum RuntimeError {
69    #[error("storage: {0}")]
70    Storage(#[from] khive_storage::StorageError),
71
72    #[error("sqlite: {0}")]
73    Sqlite(#[from] khive_db::SqliteError),
74
75    #[error("query: {0}")]
76    Query(#[from] khive_query::QueryError),
77
78    #[error("not found: {0}")]
79    NotFound(String),
80
81    #[error("invalid input: {0}")]
82    InvalidInput(String),
83
84    #[error("unconfigured: {0} is not set")]
85    Unconfigured(String),
86
87    #[error("unknown embedding model: {0}")]
88    UnknownModel(String),
89
90    #[error("embedding: {0}")]
91    Embedding(#[from] lattice_embed::EmbedError),
92
93    #[error("ambiguous: {0}")]
94    Ambiguous(String),
95
96    #[error("fusion: {0}")]
97    Fusion(#[from] khive_fusion::FuseError),
98
99    #[error("internal: {0}")]
100    Internal(String),
101
102    #[error("missing pack dependency: {0}")]
103    MissingPackDependency(MissingPackDependency),
104
105    #[error("missing pack dependencies: {0}")]
106    MissingPackDependencies(MissingPackDependencies),
107
108    #[error("{0}")]
109    CircularPackDependency(CircularPackDependency),
110
111    #[error("pack '{name}' registered twice (indices {first_idx} and {second_idx})")]
112    PackRedeclared {
113        name: String,
114        first_idx: usize,
115        second_idx: usize,
116    },
117
118    /// Two packs declared the same `Visibility::Verb` handler name.
119    /// `Visibility::Subhandler` entries are pack-prefixed and do not
120    /// participate in cross-pack collision checks.
121    #[error(
122        "verb collision: verb {verb:?} declared by both pack {first_pack:?} and pack \
123         {second_pack:?}; rename one handler or use Visibility::Subhandler for internal verbs"
124    )]
125    VerbCollision {
126        verb: String,
127        first_pack: String,
128        second_pack: String,
129    },
130
131    /// Gate denied this verb invocation.
132    ///
133    /// Returned by `VerbRegistry::dispatch` when the configured `Gate` returns
134    /// `GateDecision::Deny`. The pack is never invoked. The `reason` field
135    /// carries the deny message produced by the gate implementation.
136    #[error("permission denied for verb {verb:?}: {reason}")]
137    PermissionDenied { verb: String, reason: String },
138
139    /// A structured [`khive_types::KhiveError`] converted into the runtime
140    /// layer. The full structured error is preserved so callers can inspect
141    /// `kind`, `code`, `details`, and `retry_hint` without information loss.
142    #[error("{0}")]
143    Khive(khive_types::KhiveError),
144
145    /// Record exists but belongs to a different namespace than the provided token.
146    ///
147    /// Externally reported as "not found in this namespace" to avoid leaking
148    /// cross-namespace existence information (timing-oracle mitigation).
149    #[error("not found in this namespace")]
150    NamespaceMismatch { id: uuid::Uuid },
151
152    /// A short-prefix lookup matched more than one record.
153    ///
154    /// `prefix` is the 8+ hex-char prefix supplied by the caller.
155    /// `matches` holds the full UUIDs of all matching records (at most 2 are
156    /// reported to bound the scan — callers must supply the full UUID to disambiguate).
157    #[error("ambiguous prefix {prefix:?}: matches {}", format_uuid_list(matches))]
158    AmbiguousPrefix {
159        prefix: String,
160        matches: Vec<uuid::Uuid>,
161    },
162
163    /// Cross-backend `merge_entity` is unsupported in v1.
164    ///
165    /// Both entities must reside on the same backend. To merge entities on different
166    /// backends, manually export `from_id`, delete it, and re-import on `into_id`'s backend.
167    #[error(
168        "cross-backend merge is not supported: \
169         into_id {into_id} is on backend '{into_backend}', \
170         from_id {from_id} is on backend '{from_backend}'. \
171         Both entities must be on the same backend to merge."
172    )]
173    CrossBackendMergeUnsupported {
174        into_id: uuid::Uuid,
175        from_id: uuid::Uuid,
176        into_backend: String,
177        from_backend: String,
178    },
179
180    // ── Remote Resolution and Content-Hash Verification ──────────────────────
181    /// A `kg://` ref names a remote not declared in `schema.yaml`.
182    #[error("unknown remote: {name:?}")]
183    UnknownRemote { name: String },
184
185    /// A remote cache entry is absent and `--fetch` was not requested.
186    #[error("remote cache missing for remote={remote:?} namespace={namespace:?}")]
187    RemoteCacheMissing { remote: String, namespace: String },
188
189    /// A short ID matches multiple entities in the same namespace or remote cache.
190    #[error("ambiguous id {id:?}: matched {count} records")]
191    AmbiguousId { id: String, count: usize },
192
193    /// A write operation targeted a remote namespace, which is read-only.
194    #[error("cross-namespace write denied: cannot write to remote namespace {namespace:?}")]
195    CrossNamespaceWrite { namespace: String },
196
197    /// A remote fetch failed (network error, authentication failure, etc.).
198    #[error("remote fetch error for remote={remote:?}: {message}")]
199    RemoteFetchError { remote: String, message: String },
200
201    /// A caller-supplied write budget was exceeded during a Compound apply.
202    ///
203    /// `max_new_entries` is the limit passed by the caller. `attempted_new_entries`
204    /// is `consumed + 1`, i.e. the create that would have exceeded the cap.
205    /// `None` budget never produces this error (unlimited path).
206    #[error(
207        "write budget exceeded: max_new_entries={max_new_entries}, \
208         attempted_new_entries={attempted_new_entries}"
209    )]
210    WriteBudgetExceeded {
211        max_new_entries: u64,
212        attempted_new_entries: u64,
213    },
214}
215
216fn format_uuid_list(uuids: &[uuid::Uuid]) -> String {
217    let shorts: Vec<String> = uuids
218        .iter()
219        .map(|u| u.to_string()[..8].to_string())
220        .collect();
221    shorts.join(", ")
222}
223
224impl From<khive_types::KhiveError> for RuntimeError {
225    fn from(e: khive_types::KhiveError) -> Self {
226        Self::Khive(e)
227    }
228}