Skip to main content

systemprompt_loader/bundle/
error.rs

1//! Error surface for fetching, verifying, extracting and composing bundles.
2//!
3//! Every variant is a hard failure: a bundle that cannot be proven is never
4//! installed. `Display` renders source names, paths and digests only — an
5//! auth token or registry credential never reaches a log line through here.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::path::PathBuf;
11
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15pub enum VerifyFailure {
16    #[error("archive digest is {actual}, profile pins {expected}")]
17    DigestMismatch { expected: String, actual: String },
18
19    #[error("manifest signature does not verify against any pinned key")]
20    BadSignature,
21
22    #[error("manifest is signed by unpinned key {key_id}")]
23    UnknownKey { key_id: String },
24
25    #[error("checksum mismatch for {path}")]
26    FileChecksum { path: String },
27
28    #[error("recomputed content hash does not match the manifest")]
29    ContentHash,
30
31    #[error("bundle requires core {required}, this build is {actual}")]
32    RequiresCore { required: String, actual: String },
33
34    #[error("bundle claims non-marketplace directory {dir}")]
35    NotMarketplaceOnly { dir: String },
36
37    #[error("bundle is unsigned but the profile pins ed25519 keys")]
38    MissingSignature,
39
40    #[error("unsupported signature algorithm {alg}")]
41    UnsupportedAlgorithm { alg: String },
42
43    #[error("bundle declares format {format}, this build reads {supported}")]
44    UnsupportedFormat { format: u32, supported: u32 },
45
46    #[error("extracted tree has {count} file(s) not listed in the manifest")]
47    UnexpectedFiles { count: usize },
48
49    #[error("bundle is missing bundle.json")]
50    MissingManifest,
51}
52
53#[derive(Debug, Error)]
54pub enum BundleError {
55    #[error("source {source_name}: fetch failed: {detail}")]
56    Fetch { source_name: String, detail: String },
57
58    #[error("source {source_name}: registry rejected the credentials")]
59    Auth { source_name: String },
60
61    #[error("verification failed: {0}")]
62    Verify(#[from] VerifyFailure),
63
64    #[error("extract failed at {path}: {detail}")]
65    Extract { path: PathBuf, detail: String },
66
67    #[error("io error: {0}")]
68    Io(#[from] std::io::Error),
69
70    #[error("policy: {detail}")]
71    Policy { detail: String },
72
73    #[error("{kind} {id} is claimed by both {first} and {second}")]
74    Ownership {
75        id: String,
76        kind: String,
77        first: String,
78        second: String,
79    },
80
81    #[error("no cached bundle for source {name} and no usable fallback")]
82    SourceMissing { name: String },
83
84    #[error("bundle exceeds the {bytes} byte limit")]
85    TooLarge { bytes: u64 },
86}
87
88pub type BundleResult<T> = Result<T, BundleError>;
89
90impl BundleError {
91    #[must_use]
92    pub fn fetch(source_name: &str, detail: impl std::fmt::Display) -> Self {
93        Self::Fetch {
94            source_name: source_name.to_owned(),
95            detail: detail.to_string(),
96        }
97    }
98
99    #[must_use]
100    pub fn policy(detail: impl std::fmt::Display) -> Self {
101        Self::Policy {
102            detail: detail.to_string(),
103        }
104    }
105
106    #[must_use]
107    pub fn extract(path: impl Into<PathBuf>, detail: impl std::fmt::Display) -> Self {
108        Self::Extract {
109            path: path.into(),
110            detail: detail.to_string(),
111        }
112    }
113}