Skip to main content

capsule_runtime/
errors.rs

1use std::path::PathBuf;
2
3use thiserror::Error;
4
5/// One `capabilities.shell.allow` entry that resolved to a script whose interpreter's package
6/// tree nothing declared could reach inside a `sealed` composed root.
7///
8/// Defined here rather than in [`crate::reachability`] — which is a `pub(crate)` module — because
9/// it is a field of a public [`RuntimeError`] variant and so has to be nameable by anything that
10/// matches on one (`murmur-cli`'s `CliError` conversion, above all).
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct UnreachableEntrypoint {
13    /// The `capabilities.shell.allow` entry, verbatim as the manifest wrote it.
14    pub binary: String,
15    /// Where that entry resolves on this host — the first `PATH` match, i.e. the one `execvp`
16    /// would run.
17    pub resolved_path: PathBuf,
18    /// The bare interpreter name read out of the file's `#!` line, with one level of
19    /// `env NAME` indirection resolved. This is the name a covering
20    /// `interpreter_runtime`/`staged_runtime` grant would have to declare.
21    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    /// A `capabilities.filesystem.read_only` entry is not a usable workdir subpath: it is
131    /// absolute, or it escapes the workdir via `..`. The exact parallel of
132    /// [`Self::InvalidFilesystemScope`], raised from
133    /// [`crate::protected_paths::ProtectedPaths::from_declared`] at staging — before any registry
134    /// pull, workdir creation or component instantiation — so no call is ever checked against a
135    /// rule the runtime could not build.
136    #[error("invalid read-only path '{path}': {message}")]
137    InvalidReadOnlyPath { path: String, message: String },
138
139    /// An artifact entry's `config:` block cannot be delivered as `MURMUR_ARTIFACT_CONFIG`: it is
140    /// not a string-keyed mapping, does not serialize to JSON, or exceeds
141    /// [`crate::artifact_config::MAX_ARTIFACT_CONFIG_BYTES`]. Raised at staging (and from `mur run
142    /// --explain-scope`), before any registry pull, workdir creation or component instantiation,
143    /// so a malformed channel is refused once at launch rather than one tool call at a time.
144    #[error("invalid config for artifact '{artifact}': {message}")]
145    InvalidArtifactConfig { artifact: String, message: String },
146
147    /// A `capabilities.state.store` name is not a single usable directory segment under
148    /// `~/.murmur/state/`. Raised at staging (and from `mur run --explain-scope`), before any
149    /// registry pull, workdir creation or component instantiation, so a bad name never gets as far
150    /// as creating something on disk.
151    #[error("invalid state store name '{store}': {message}")]
152    InvalidStateStore { store: String, message: String },
153
154    /// A `context.record_store` value, or a `mur run --context` value, is not a single usable
155    /// directory segment of a conversation record path. Raised at staging, before any registry
156    /// pull, workdir creation or component instantiation, so a bad name never gets as far as
157    /// creating something on disk.
158    ///
159    /// Distinct from [`Self::InvalidStateStore`] because the remedies point at different keys, and
160    /// because a record is on by default while a state store is declared: an operator seeing this
161    /// may not have written a `context:` block at all, only a `--context` flag.
162    #[error("invalid {field} '{value}': {message}")]
163    InvalidConversationRecord {
164        field: String,
165        value: String,
166        message: String,
167    },
168
169    /// `mur run --resume` named a session whose context kept no conversation record on disk, so
170    /// there is nothing to continue. Raised at staging, before this launch's workdir exists: a
171    /// resume that silently started fresh would be indistinguishable from one that worked.
172    #[error(
173        "cannot resume session {session}: context '{context_id}' has no conversation record ({reason})"
174    )]
175    ResumeRecordMissing {
176        session: String,
177        context_id: String,
178        reason: String,
179    },
180
181    /// `mur run --resume-mode compact` on a capsule with no hook bound to `on-compaction`. There
182    /// is nothing to produce the summary, and falling back to `full` would silently give the
183    /// operator the mode they did not ask for.
184    #[error(
185        "--resume-mode compact needs a hook bound to on-compaction; this capsule declares none"
186    )]
187    ResumeCompactionHookMissing,
188
189    /// The state store directory a validated name resolves to could not be resolved or created.
190    /// Distinct from [`Self::InvalidStateStore`]: the declaration is well-formed and the failure is
191    /// the host's — an unset `HOME`, or a path that cannot be made a `0700` directory.
192    #[error("state store '{store}' is unavailable at {path}: {message}")]
193    StateStoreUnavailable {
194        store: String,
195        path: String,
196        message: String,
197    },
198
199    /// The declared containment floor is stronger than what this host's kernel can back.
200    /// Raised in the stage/launch seam, before any registry pull, component compile or workdir
201    /// creation — `achieved` always comes from a live host probe, never from the manifest.
202    #[error(
203        "declared containment class '{declared}' is not achievable on this host (achieved: '{achieved}'): {reason}"
204    )]
205    ContainmentFloorUnmet {
206        declared: murmur_artifact::ContainmentClass,
207        achieved: murmur_artifact::ContainmentClass,
208        reason: String,
209    },
210    /// A capsule declares an `exports.files.root` that, once resolved against the session's
211    /// accessible workdir, lands somewhere else — the root already exists as a symlink pointing
212    /// out of it.
213    ///
214    /// Refused at launch rather than per request: an export root outside the workdir is a
215    /// misconfiguration the operator can fix, and discovering it one served file at a time would
216    /// mean the first file had already left.
217    #[error(
218        "{field} '{declared}' resolves to '{resolved}', which is outside the capsule \
219         workdir '{workdir}'"
220    )]
221    ExportRootOutsideWorkdir {
222        /// The manifest key that declared it — `exports.files.root` or
223        /// `exports.peer_files.root`. Carried rather than hard-coded because the two roots are
224        /// separate authorisers and an operator fixing one must be told which.
225        field: String,
226        declared: String,
227        resolved: String,
228        workdir: String,
229    },
230
231    /// A capsule declares an artifact whose name collides with a tool the runtime provides
232    /// itself.
233    ///
234    /// The four names in [`crate::runtime::RESERVED_TOOL_NAMES`] are answered inside
235    /// `dispatch_agent_tool_unfenced` before any allowlist check is reached, and their synthetic
236    /// manifests are written after the staging loop. An artifact installed under one of them would
237    /// have its manifest overwritten and every call to it answered by the runtime, so the collision
238    /// is refused rather than resolved into a tool the capsule can never reach.
239    #[error(
240        "artifact '{name}' collides with a tool the runtime provides itself; the reserved names \
241         are {}",
242        crate::runtime::RESERVED_TOOL_NAMES.join(", ")
243    )]
244    ReservedToolName {
245        /// The declared artifact name that collides.
246        name: String,
247    },
248
249    /// A capsule's `capabilities.env.allow` names variables the credential backstop strips from
250    /// every guest environment.
251    ///
252    /// The strip in `build_wasi_env_allowlist` is not negotiable from a manifest, so the entries
253    /// can only ever deliver nothing; the declaration is refused rather than left silently inert.
254    #[error(
255        "capabilities.env.allow names {}; these entries are removed from every guest environment \
256         before any guest is built, so the grant would deliver nothing",
257        crate::runtime::describe_stripped_env_allow_entries(entries)
258    )]
259    EnvAllowStrippedByBackstop {
260        /// Every distinct stripped entry, in first-declaration order.
261        entries: Vec<crate::runtime::StrippedEnvAllowEntry>,
262    },
263
264    /// A synthetic tool manifest was written under a name absent from
265    /// [`crate::runtime::RESERVED_TOOL_NAMES`].
266    ///
267    /// The inverse of [`Self::ReservedToolName`], and the reason a further runtime-provided tool
268    /// cannot be added without reserving its name: an unreserved name is claimable by an artifact,
269    /// which would then be installed and silently shadowed at dispatch.
270    #[error(
271        "'{name}' is written as a runtime-provided tool but is not one of {}; add it to \
272         RESERVED_TOOL_NAMES so no artifact can claim the name",
273        crate::runtime::RESERVED_TOOL_NAMES.join(", ")
274    )]
275    RuntimeProvidedToolNotReserved {
276        /// The name the synthetic write was attempted under.
277        name: String,
278    },
279
280    /// A capsule declares `exports.peer_files` together with `lifecycle.after_task: sleep`, and
281    /// either declared no `max_ttl` or declared one above the persistent ceiling.
282    ///
283    /// An ephemeral capsule needs no ceiling: teardown destroys the minting key, so every
284    /// outstanding handle stops verifying at once and the declared lifetime can never outlive the
285    /// process. `sleep` withdraws that bound deliberately, which makes the declared lifetime the
286    /// only one there is — so it has to be declared, and it has to be short.
287    #[error(
288        "exports.peer_files with lifecycle.after_task: sleep requires exports.peer_files.max_ttl \
289         to be declared and at most {ceiling_secs}s{}; a handle's lifetime is not a durability \
290         mechanism",
291        match declared_secs {
292            Some(secs) => format!(" (declared {secs}s)"),
293            None => String::new(),
294        }
295    )]
296    PersistentCapsuleNeedsHandleTtl {
297        /// The declared `max_ttl` in seconds, or `None` when the manifest declared none.
298        declared_secs: Option<u64>,
299        ceiling_secs: u64,
300    },
301
302    /// A capsule declares `capabilities.shell.staged_runtime` without an effective `sealed`
303    /// containment floor.
304    ///
305    /// Deliberately distinct from [`Self::ContainmentFloorUnmet`]: that one compares the declared
306    /// floor against what the *host* can back, and its remedy is to lower the floor or move hosts.
307    /// This one is decided against the declared floor alone and never looks at the host at all, so
308    /// it fires identically on a machine that could deliver `sealed` — a staged runtime has no
309    /// composed root to be staged into unless the capsule asked for one, and quietly launching it
310    /// without the mount would leave the interpreter simply absent. Its remedy is to raise the
311    /// declared floor (or drop the grant), which is the opposite advice.
312    ///
313    /// `binaries` names every offending `staged_runtime` binary so the operator does not have to
314    /// re-run to find the second one.
315    #[error(
316        "capabilities.shell.staged_runtime is declared for {} but the effective containment floor \
317         is '{declared}' — staging a runtime tree requires the 'sealed' floor, because there is no \
318         composed root to bind-mount it into below that",
319        .binaries.join(", ")
320    )]
321    StagedRuntimeRequiresSealed {
322        binaries: Vec<String>,
323        declared: murmur_artifact::ContainmentClass,
324    },
325
326    /// A `sealed` capsule allowlists a script whose interpreter's package tree nothing declared
327    /// could make reachable inside the composed root.
328    ///
329    /// Deliberately distinct from [`Self::StagedRuntimeRequiresSealed`], which it sits next to in
330    /// `stage_session`: that one fires when a grant *was* declared at too low a floor, this one
331    /// fires when no grant was declared at all and the floor is already `sealed`. Their remedies
332    /// point in opposite directions — raise the floor there, add a grant here.
333    ///
334    /// `entries` names every offending allowlist entry so an operator fixing this does not have to
335    /// re-run to discover the second script, following the same precedent.
336    #[error(
337        "capabilities.shell.allow grants {} under the 'sealed' containment floor, but nothing \
338         declared makes the interpreted entrypoint's own package tree reachable inside the \
339         composed root — the script's ELF/DT_NEEDED closure is empty, so staging it stages \
340         nothing its interpreter imports, and the capsule would fail with a module-not-found \
341         error partway into a run rather than here. Declare \
342         capabilities.shell.interpreter_runtime or capabilities.shell.staged_runtime naming the \
343         interpreter (measure the real directories with \
344         `strace -f -e trace=openat,getdents64 <the command>`), or use a copy of the interpreter \
345         and its packages that already lives under a fixed sealed runtime path",
346        .entries
347            .iter()
348            .map(UnreachableEntrypoint::to_string)
349            .collect::<Vec<_>>()
350            .join("; ")
351    )]
352    ShellBinaryPackageUnreachable { entries: Vec<UnreachableEntrypoint> },
353
354    /// A `sealed` session's composed root could not be built in the forked child, *after* the
355    /// pre-launch probe reported the mechanism available.
356    ///
357    /// Deliberately distinct from [`Self::ContainmentFloorUnmet`]: that one is a refusal decided
358    /// before anything is staged, on a host that never claimed to offer `sealed`. This one is a
359    /// host that claimed it and then failed to deliver — a race, a mount table that changed under
360    /// the runtime, or an edge case the cheap probe cannot catch. Conflating the two would tell an
361    /// operator to lower their declared floor when the real answer is that something moved.
362    ///
363    /// `detail` is the message the child wrote to the `pre_exec` diagnostic pipe, which names the
364    /// exact step (`unshare`, a specific bind mount, `pivot_root`) and its errno.
365    #[error(
366        "the sealed containment class was achievable at launch but its composed root could not be \
367         built for this subprocess: {detail}"
368    )]
369    SealedRootConstructionFailed { detail: String },
370
371    /// This host cannot give a capsule's native subprocess tree its own network namespace, so the
372    /// egress boundary that mediates `capabilities.network.allow` cannot be built.
373    ///
374    /// Raised before any registry pull, component compile or workdir creation, and raised for
375    /// **every** Linux capsule that can spawn a subprocess — including one whose
376    /// `capabilities.network.allow` is empty. That breadth is the point: with the namespace
377    /// missing, an empty allowlist would mean *unrestricted* egress rather than none, because the
378    /// seccomp `connect`/`sendto` interception that used to back it was deleted as unsound rather
379    /// than kept as a fallback. There is deliberately no path that continues at reduced
380    /// enforcement.
381    ///
382    /// Deliberately distinct from [`Self::ContainmentFloorUnmet`], which this would otherwise
383    /// look like: that one compares a *declared* containment class against what the host can
384    /// back, and its remedy is to lower the declared floor. A network namespace is not part of
385    /// the containment ladder — every class needs it equally — so lowering the floor fixes
386    /// nothing here, and offering that advice would be actively misleading.
387    #[error(
388        "this host cannot give the capsule's subprocess tree its own network namespace, so \
389         capabilities.network.allow cannot be enforced for it: {reason}"
390    )]
391    EgressNamespaceUnavailable {
392        blocker: crate::network_namespace::EgressNamespaceBlocker,
393        reason: String,
394    },
395
396    #[error(
397        "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)"
398    )]
399    CapsuleExportMissing,
400
401    #[error(
402        "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)"
403    )]
404    ToolExportMissing { name: String },
405
406    #[error("capsule execution trapped: {0}")]
407    CapsuleTrap(String),
408
409    #[error(
410        "capsule execution exceeded its {seconds}s deadline and was interrupted; \
411         raise capabilities.limits.deadline_seconds in murmur.yaml if the capsule \
412         legitimately needs longer"
413    )]
414    CapsuleDeadlineExceeded { seconds: u64 },
415
416    #[error("capsule execution exceeded its configured resource limits: {message}")]
417    CapsuleResourceLimit { message: String },
418
419    #[error(
420        "shell subprocess '{binary}' was killed for exceeding \
421         capabilities.resources.{limit}: {detail}"
422    )]
423    ShellResourceLimitExceeded {
424        binary: String,
425        limit: String,
426        detail: String,
427    },
428
429    #[error(
430        "tool '{tool}' returned data_path '{path}', which does not resolve to a readable file \
431         beneath the session workdir ({code}): {detail}"
432    )]
433    ToolDataPathRefused {
434        tool: String,
435        path: String,
436        code: String,
437        detail: String,
438    },
439
440    #[error(
441        "this capsule can spawn native subprocesses but no cgroup v2 scope could be created to \
442         bound them ({reason}); on Linux the runtime refuses to launch rather than run a \
443         subprocess tree with no aggregate memory/pids/cpu ceiling — see the systemd user \
444         delegation requirement in docs/content/reference/resource-limits-manual-verification.md"
445    )]
446    CgroupDelegationUnavailable { reason: String },
447
448    #[error(
449        "session workdir grew to {observed_bytes} bytes, past the {max_bytes} byte ceiling \
450         (capabilities.resources.workdir_max_bytes)"
451    )]
452    WorkdirSizeExceeded { max_bytes: u64, observed_bytes: u64 },
453
454    #[error("failed to initialize WASI for {path}: {message}")]
455    WasiInit { path: String, message: String },
456
457    #[error("inference driver is not configured; add inference.driver.artifact to murmur.yaml")]
458    DriverNotConfigured,
459
460    #[error("inference driver '{0}' is not installed in the local tool registry")]
461    DriverNotInstalled(String),
462
463    /// The configured `transport: http` driver's bundled manifest has no usable `inference_auth:`
464    /// block, so the runtime cannot tell how its provider expects the key. Refused at staging,
465    /// before any component runs, whether or not `inference.api_key` is set. `reason` is `None`
466    /// when the block is absent and says what is wrong when it is malformed.
467    #[error(
468        "inference driver '{name}@{version}' declares no usable inference_auth: block{}",
469        .reason.as_ref().map(|reason| format!(" ({reason})")).unwrap_or_default()
470    )]
471    DriverDeclaresNoInferenceAuth {
472        name: String,
473        version: String,
474        reason: Option<String>,
475    },
476
477    /// `inference.api_key: ${variable}` names a credential that neither the global config's
478    /// `credentials:` map nor the launching environment holds. Refused at staging, before the
479    /// session directory exists.
480    #[error(
481        "murmur.yaml: inference.api_key references ${{{variable}}}, but neither \
482         credentials.{variable} in {} nor the environment variable {variable} is set",
483        .credentials_file
484            .as_ref()
485            .map(|path| path.display().to_string())
486            .unwrap_or_else(|| "the global config".to_string())
487    )]
488    InferenceCredentialNotFound {
489        variable: String,
490        credentials_file: Option<std::path::PathBuf>,
491    },
492
493    /// `spend.machine_tokens_per_day` is in effect and the shared ledger it is kept in cannot be
494    /// used — an unset `HOME`, a `~/.murmur/spend` that cannot be made a `0700` directory, or a
495    /// daily file that cannot be opened. Raised at staging, before the session directory exists
496    /// and before any provider request.
497    #[error("spend ledger at {path} is unavailable: {message}")]
498    SpendLedgerUnavailable { path: String, message: String },
499
500    #[error("agent loop failed: {0}")]
501    AgentLoopFailed(String),
502
503    #[error("internal_port {port} is already bound; choose another port or omit internal_port to use an OS-assigned port")]
504    PortInUse { port: u16 },
505
506    /// A session whose manifest declares `capabilities.spawn.allow` could not register with
507    /// `mur-roost`, so the daemon does not hold the grants it would referee that session's
508    /// delegations against.
509    ///
510    /// Fatal rather than a warning: a capsule that can delegate but is unknown to the daemon is a
511    /// capsule running outside the only thing that bounds what it may spawn. A capsule that
512    /// declares no spawn capability never registers and never reaches this.
513    ///
514    /// `reason` is the transport or daemon-side failure verbatim. Neither this nor anything
515    /// derived from it carries a credential or an approval: the registration request's tokens
516    /// travel in headers, and the HTTP client never puts a request header into an error.
517    #[error(
518        "failed to register this session with mur-roost at {roost_url}: {reason}; a capsule \
519         declaring capabilities.spawn.allow must be known to the daemon that referees its \
520         spawns, so the launch is refused rather than run unrefereed"
521    )]
522    SpawnRegistrationFailed { roost_url: String, reason: String },
523
524    /// `MURMUR_SPAWNER` was set to something that is not a spawner handle.
525    ///
526    /// `reason` names what about the value could not be read, and nothing derived from it carries
527    /// the value itself: a handle names a parent's address and session, which is not this
528    /// process's to print back out.
529    #[error(
530        "MURMUR_SPAWNER does not carry a readable spawner handle: {reason}; a delegated child \
531         must be able to tell its spawner that it finished, so the launch is refused rather than \
532         run unreportable"
533    )]
534    SpawnerHandleUnreadable { reason: String },
535
536    /// A native tool's `bin/<name>` payload was built for another operating system or CPU
537    /// architecture than this host.
538    ///
539    /// Raised before any byte of any native binary in the same staging batch reaches the session
540    /// workdir: an image this host cannot exec would otherwise be written, made executable, and
541    /// then die on a raw `Exec format error` mid-run, at a point where nothing left says which
542    /// artifact was wrong.
543    ///
544    /// Both platforms are `current_platform()`-shaped strings, or the bare `darwin` a fat Mach-O
545    /// identifies as.
546    #[error(
547        "native tool '{name}' cannot run on this host: its binary is built for \
548         {binary_platform}, this host is {host_platform}"
549    )]
550    NativeBinaryPlatformMismatch {
551        name: String,
552        binary_platform: String,
553        host_platform: String,
554    },
555
556    #[error("runtime failure: {0}")]
557    Runtime(String),
558}
559
560impl RuntimeError {
561    #[must_use]
562    pub fn artifact_not_found(name: impl Into<String>, version: impl Into<String>) -> Self {
563        Self::ArtifactNotFound {
564            name: name.into(),
565            version: version.into(),
566        }
567    }
568
569    #[must_use]
570    pub fn artifact_integrity_failed(name: impl Into<String>, version: impl Into<String>) -> Self {
571        Self::ArtifactIntegrityFailed {
572            name: name.into(),
573            version: version.into(),
574        }
575    }
576
577    #[must_use]
578    pub fn wasi(path: PathBuf, message: impl Into<String>) -> Self {
579        Self::WasiInit {
580            path: path.display().to_string(),
581            message: message.into(),
582        }
583    }
584}