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}