systemprompt_loader/bundle/
error.rs1use 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 #[error("the source pins neither a sha256 digest nor an ed25519 key")]
53 NoVerification,
54}
55
56pub type BundleCause = Box<dyn std::error::Error + Send + Sync + 'static>;
57
58#[derive(Debug, Error)]
59pub enum BundleError {
60 #[error("source {source_name}: fetch failed: {}", describe(detail, cause.as_deref()))]
61 Fetch {
62 source_name: String,
63 detail: String,
64 #[source]
65 cause: Option<BundleCause>,
66 },
67
68 #[error("source {source_name}: registry rejected the credentials")]
69 Auth { source_name: String },
70
71 #[error("verification failed: {0}")]
72 Verify(#[from] VerifyFailure),
73
74 #[error("extract failed at {path}: {}", describe(detail, cause.as_deref()))]
75 Extract {
76 path: PathBuf,
77 detail: String,
78 #[source]
79 cause: Option<BundleCause>,
80 },
81
82 #[error("io error: {0}")]
83 Io(#[from] std::io::Error),
84
85 #[error("policy: {}", describe(detail, cause.as_deref()))]
86 Policy {
87 detail: String,
88 #[source]
89 cause: Option<BundleCause>,
90 },
91
92 #[error(
93 "server.role '{role}' reads the services composition a worker writes to {} and never \
94 fetches bundles itself; none is there yet (share services.cache_dir with the worker)",
95 cache.display()
96 )]
97 NoSharedComposition { role: &'static str, cache: PathBuf },
98
99 #[error("{kind} {id} is claimed by both {first} and {second}")]
100 Ownership {
101 id: String,
102 kind: String,
103 first: String,
104 second: String,
105 },
106
107 #[error("no cached bundle for source {name} and no usable fallback")]
108 SourceMissing { name: String },
109
110 #[error("bundle exceeds the {bytes} byte limit")]
111 TooLarge { bytes: u64 },
112}
113
114pub type BundleResult<T> = Result<T, BundleError>;
115
116fn describe(
117 detail: &str,
118 cause: Option<&(dyn std::error::Error + Send + Sync + 'static)>,
119) -> String {
120 match cause {
121 Some(cause) if detail.is_empty() => cause.to_string(),
122 Some(cause) => format!("{detail}: {cause}"),
123 None => detail.to_owned(),
124 }
125}
126
127impl BundleError {
128 #[must_use]
129 pub fn fetch(source_name: &str, detail: impl Into<String>) -> Self {
130 Self::Fetch {
131 source_name: source_name.to_owned(),
132 detail: detail.into(),
133 cause: None,
134 }
135 }
136
137 #[must_use]
138 pub fn fetch_cause(source_name: &str, cause: impl Into<BundleCause>) -> Self {
139 Self::fetch_context(source_name, String::new(), cause)
140 }
141
142 #[must_use]
143 pub fn fetch_context(
144 source_name: &str,
145 context: impl Into<String>,
146 cause: impl Into<BundleCause>,
147 ) -> Self {
148 Self::Fetch {
149 source_name: source_name.to_owned(),
150 detail: context.into(),
151 cause: Some(cause.into()),
152 }
153 }
154
155 #[must_use]
156 pub fn policy(detail: impl Into<String>) -> Self {
157 Self::Policy {
158 detail: detail.into(),
159 cause: None,
160 }
161 }
162
163 #[must_use]
164 pub fn policy_context(context: impl Into<String>, cause: impl Into<BundleCause>) -> Self {
165 Self::Policy {
166 detail: context.into(),
167 cause: Some(cause.into()),
168 }
169 }
170
171 #[must_use]
172 pub fn extract(path: impl Into<PathBuf>, detail: impl Into<String>) -> Self {
173 Self::Extract {
174 path: path.into(),
175 detail: detail.into(),
176 cause: None,
177 }
178 }
179
180 #[must_use]
181 pub fn extract_cause(path: impl Into<PathBuf>, cause: impl Into<BundleCause>) -> Self {
182 Self::Extract {
183 path: path.into(),
184 detail: String::new(),
185 cause: Some(cause.into()),
186 }
187 }
188}