1use std::path::PathBuf;
2
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct UnreachableEntrypoint {
13 pub binary: String,
15 pub resolved_path: PathBuf,
18 pub interpreter: String,
22}
23
24impl std::fmt::Display for UnreachableEntrypoint {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(
27 f,
28 "'{}' ({}, a script run by '{}')",
29 self.binary,
30 self.resolved_path.display(),
31 self.interpreter
32 )
33 }
34}
35
36#[derive(Debug, Error)]
37pub enum RuntimeError {
38 #[error("artifact {name}@{version} not found in registry")]
39 ArtifactNotFound { name: String, version: String },
40
41 #[error("artifact integrity check failed for {name}@{version}")]
42 ArtifactIntegrityFailed { name: String, version: String },
43
44 #[error("lockfile missing artifact entry for '{name}'")]
45 LockMissingEntry { name: String },
46
47 #[error(
48 "lockfile version mismatch for '{name}': manifest requested {requested}, lock pinned {pinned}"
49 )]
50 LockVersionMismatch {
51 name: String,
52 requested: String,
53 pinned: String,
54 },
55
56 #[error("failed to read artifact archive for {name}@{version}: {message}")]
57 ArtifactArchive {
58 name: String,
59 version: String,
60 message: String,
61 },
62
63 #[error("failed to compile component for {name}@{version}: {message}")]
64 ToolComponentCompile {
65 name: String,
66 version: String,
67 message: String,
68 },
69
70 #[error("failed to compile capsule component: {0}")]
71 CapsuleCompile(String),
72
73 #[error("failed to create workdir at {path}: {source}")]
74 CreateWorkdir {
75 path: String,
76 #[source]
77 source: std::io::Error,
78 },
79
80 #[error("failed to write tool manifest at {path}: {source}")]
81 WriteToolManifest {
82 path: String,
83 #[source]
84 source: std::io::Error,
85 },
86
87 #[error("failed to read inference.system_prompt_file at {path}: {source}")]
88 SystemPromptFileRead {
89 path: String,
90 #[source]
91 source: std::io::Error,
92 },
93
94 #[error("failed to read inference.compaction.system_prompt_file at {path}: {source}")]
95 CompactionSystemPromptFileRead {
96 path: String,
97 #[source]
98 source: std::io::Error,
99 },
100
101 #[error(
102 "inference.system_prompt_artifact '{name}': skill.md not found or unreadable; \
103 ensure the skill is declared in artifacts: and the capsule was staged"
104 )]
105 SystemPromptArtifactRead {
106 name: String,
107 #[source]
108 source: std::io::Error,
109 },
110
111 #[error("skill source path not found: {path}")]
112 SkillSourceNotFound { path: String },
113
114 #[error("skill source directory '{path}' contains no skill.md")]
115 SkillSourceMissingSkillMd { path: String },
116
117 #[error("failed to read skill source at {path}: {source}")]
118 SkillSourceRead {
119 path: String,
120 #[source]
121 source: std::io::Error,
122 },
123
124 #[error("invalid network capability allow entry '{entry}': {message}")]
125 InvalidNetworkAllowEntry { entry: String, message: String },
126
127 #[error("invalid filesystem capability scope '{scope}': {message}")]
128 InvalidFilesystemScope { scope: String, message: String },
129
130 #[error(
134 "declared containment class '{declared}' is not achievable on this host (achieved: '{achieved}'): {reason}"
135 )]
136 ContainmentFloorUnmet {
137 declared: murmur_artifact::ContainmentClass,
138 achieved: murmur_artifact::ContainmentClass,
139 reason: String,
140 },
141
142 #[error(
156 "capabilities.shell.staged_runtime is declared for {} but the effective containment floor \
157 is '{declared}' — staging a runtime tree requires the 'sealed' floor, because there is no \
158 composed root to bind-mount it into below that",
159 .binaries.join(", ")
160 )]
161 StagedRuntimeRequiresSealed {
162 binaries: Vec<String>,
163 declared: murmur_artifact::ContainmentClass,
164 },
165
166 #[error(
177 "capabilities.shell.allow grants {} under the 'sealed' containment floor, but nothing \
178 declared makes the interpreted entrypoint's own package tree reachable inside the \
179 composed root — the script's ELF/DT_NEEDED closure is empty, so staging it stages \
180 nothing its interpreter imports, and the capsule would fail with a module-not-found \
181 error partway into a run rather than here. Declare \
182 capabilities.shell.interpreter_runtime or capabilities.shell.staged_runtime naming the \
183 interpreter (measure the real directories with \
184 `strace -f -e trace=openat,getdents64 <the command>`), or use a copy of the interpreter \
185 and its packages that already lives under a fixed sealed runtime path",
186 .entries
187 .iter()
188 .map(UnreachableEntrypoint::to_string)
189 .collect::<Vec<_>>()
190 .join("; ")
191 )]
192 ShellBinaryPackageUnreachable { entries: Vec<UnreachableEntrypoint> },
193
194 #[error(
206 "the sealed containment class was achievable at launch but its composed root could not be \
207 built for this subprocess: {detail}"
208 )]
209 SealedRootConstructionFailed { detail: String },
210
211 #[error(
228 "this host cannot give the capsule's subprocess tree its own network namespace, so \
229 capabilities.network.allow cannot be enforced for it: {reason}"
230 )]
231 EgressNamespaceUnavailable {
232 blocker: crate::network_namespace::EgressNamespaceBlocker,
233 reason: String,
234 },
235
236 #[error(
237 "capsule component missing export murmur:capsule/run@0.1.0#run; rebuild the artifact against the versioned WIT (run `mur install` for a default artifact, or rebuild from source otherwise)"
238 )]
239 CapsuleExportMissing,
240
241 #[error(
242 "tool component '{name}' missing export murmur:tool/run@0.1.0#run; rebuild the artifact against the versioned WIT (run `mur install` for a default artifact, or rebuild from source otherwise)"
243 )]
244 ToolExportMissing { name: String },
245
246 #[error("capsule execution trapped: {0}")]
247 CapsuleTrap(String),
248
249 #[error(
250 "capsule execution exceeded its {seconds}s deadline and was interrupted; \
251 raise capabilities.limits.deadline_seconds in murmur.yaml if the capsule \
252 legitimately needs longer"
253 )]
254 CapsuleDeadlineExceeded { seconds: u64 },
255
256 #[error("capsule execution exceeded its configured resource limits: {message}")]
257 CapsuleResourceLimit { message: String },
258
259 #[error(
260 "shell subprocess '{binary}' was killed for exceeding \
261 capabilities.resources.{limit}: {detail}"
262 )]
263 ShellResourceLimitExceeded {
264 binary: String,
265 limit: String,
266 detail: String,
267 },
268
269 #[error(
270 "this capsule can spawn native subprocesses but no cgroup v2 scope could be created to \
271 bound them ({reason}); on Linux the runtime refuses to launch rather than run a \
272 subprocess tree with no aggregate memory/pids/cpu ceiling — see the systemd user \
273 delegation requirement in docs/content/reference/resource-limits-manual-verification.md"
274 )]
275 CgroupDelegationUnavailable { reason: String },
276
277 #[error(
278 "session workdir grew to {observed_bytes} bytes, past the {max_bytes} byte ceiling \
279 (capabilities.resources.workdir_max_bytes)"
280 )]
281 WorkdirSizeExceeded { max_bytes: u64, observed_bytes: u64 },
282
283 #[error("failed to initialize WASI for {path}: {message}")]
284 WasiInit { path: String, message: String },
285
286 #[error("inference driver is not configured; add inference.driver.artifact to murmur.yaml")]
287 DriverNotConfigured,
288
289 #[error("inference driver '{0}' is not installed in the local tool registry")]
290 DriverNotInstalled(String),
291
292 #[error("agent loop failed: {0}")]
293 AgentLoopFailed(String),
294
295 #[error("internal_port {port} is already bound; choose another port or omit internal_port to use an OS-assigned port")]
296 PortInUse { port: u16 },
297
298 #[error("runtime failure: {0}")]
299 Runtime(String),
300}
301
302impl RuntimeError {
303 #[must_use]
304 pub fn artifact_not_found(name: impl Into<String>, version: impl Into<String>) -> Self {
305 Self::ArtifactNotFound {
306 name: name.into(),
307 version: version.into(),
308 }
309 }
310
311 #[must_use]
312 pub fn artifact_integrity_failed(name: impl Into<String>, version: impl Into<String>) -> Self {
313 Self::ArtifactIntegrityFailed {
314 name: name.into(),
315 version: version.into(),
316 }
317 }
318
319 #[must_use]
320 pub fn wasi(path: PathBuf, message: impl Into<String>) -> Self {
321 Self::WasiInit {
322 path: path.display().to_string(),
323 message: message.into(),
324 }
325 }
326}