Skip to main content

mur_common/muragent/
installer.rs

1//! Install a validated `.muragent` archive onto the local host.
2//!
3//! Single source of truth for the `.muragent` install flow, shared by every
4//! surface (CLI, Hub, Commander). The flow is:
5//!
6//! 1. Run the full 11-step validation pipeline (`validator::validate`).
7//! 2. Validate the agent slug shape — prevents `agents/../../etc`.
8//! 3. Check the trust store: a key change without a rotation manifest is a
9//!    hard refuse (§7.1.1).
10//! 4. Detect collision vs update by matching `agent.original_uuid` against
11//!    any existing agent at the same slug. Same UUID → update (preserves
12//!    `data/`); different UUID → error.
13//! 5. Extract the payload to `<mur_home>/agents/<slug>/`.
14//! 6. Upsert the trust store entry, marking surface and timestamps.
15//!
16//! UI/print decisions belong to the caller; this module returns a structured
17//! `InstallOutcome` describing what happened.
18
19use std::fs;
20use std::path::{Path, PathBuf};
21
22use base64::{Engine, engine::general_purpose::STANDARD as B64};
23
24use crate::AgentProfile;
25use crate::muragent::MuragentError;
26use crate::muragent::manifest::MuragentManifest;
27use crate::muragent::reader::MuragentArchive;
28use crate::muragent::validator::{self, ValidationResult};
29use crate::trust::rotation::RotationManifest;
30use crate::trust::{self, TrustEntry, TrustLevel, TrustStore};
31
32/// Files in the .muragent that belong to the package envelope (not payload).
33const ENVELOPE_FILES: &[&str] = &["manifest.yaml", "manifest.signed.json", "signatures.json"];
34
35/// Host-local files a `.muragent` must never carry. `identity.key` is the
36/// agent's *private* signing key, minted locally and stripped by export; a
37/// package that ships one of these is malformed or hostile, since extracting it
38/// would plant or overwrite the agent's identity (impersonation, or breaking
39/// re-export). Matched against the top-level extraction path.
40const RESERVED_LOCAL_FILES: &[&str] = &[
41    "identity.key",
42    "identity.pub",
43    "identity.key.prev",
44    "identity.pub.prev",
45];
46
47/// Result of a successful install or update.
48#[derive(Debug)]
49pub struct InstallOutcome {
50    pub manifest: MuragentManifest,
51    pub trust_level: TrustLevel,
52    pub fingerprint_hex: String,
53    pub fingerprint_words: String,
54    /// `false` when extracting into a freshly-created agent dir; `true` when
55    /// the agent already existed at the slug with matching UUID and the
56    /// payload was replaced in place (preserving `data/`).
57    pub was_update: bool,
58}
59
60/// Install or update a `.muragent` archive. See module docs for the flow.
61///
62/// `mur_home` is the root for agent dirs (`<mur_home>/agents/<slug>/`). The
63/// trust store is read and written via [`TrustStore::load`] / `save`, which
64/// honour `$MUR_HOME` independently — callers should either pass the same
65/// path the trust store would resolve, or set `MUR_HOME` consistently.
66///
67/// `surface` is recorded in the trust entry's `last_seen_surface` field.
68/// Conventional values: `"cli"`, `"hub"`, `"commander"`.
69pub fn install(
70    archive: &MuragentArchive,
71    mur_home: &Path,
72    surface: &str,
73) -> Result<InstallOutcome, MuragentError> {
74    // Step 1: validation pipeline — fatal on any failure per §7.5
75    let result = validator::validate(archive)?;
76
77    // Step 1.5: reject host-local identity material in the payload. Extraction
78    // writes every payload file into the agent dir, so a package shipping
79    // `identity.key` would plant/overwrite the agent's private signing key.
80    reject_reserved_local_files(archive)?;
81
82    // Step 2: slug shape
83    let slug = result.manifest.agent.slug.clone();
84    let display_name = result.manifest.agent.display_name.clone();
85    crate::validate_agent_name(&slug).map_err(|e| {
86        MuragentError::Other(format!("invalid agent slug '{slug}' in manifest: {e}"))
87    })?;
88
89    // Step 3: trust store key-change check
90    let mut trust_store = TrustStore::load()?;
91    let author_pubkey_b64 = B64.encode(result.author_pubkey);
92    let existing_by_pubkey = trust_store.find_by_pubkey(&author_pubkey_b64).cloned();
93
94    if existing_by_pubkey.is_none() {
95        let by_name = trust_store.find_by_display_name(&display_name);
96        if !by_name.is_empty() {
97            // Key change detected — look for a rotation manifest before refusing.
98            let old_entry = by_name
99                .into_iter()
100                .find(|e| e.trust_level != TrustLevel::Superseded)
101                .cloned();
102            match try_apply_rotation(
103                &mut trust_store,
104                old_entry.as_ref(),
105                &author_pubkey_b64,
106                &display_name,
107                mur_home,
108            ) {
109                Ok(()) => {} // rotation accepted; trust store updated in-place
110                Err(reason) => {
111                    return Err(MuragentError::TrustRefused(format!(
112                        "agent '{}' has a new signing key but no valid rotation manifest: {}",
113                        display_name, reason
114                    )));
115                }
116            }
117        }
118    }
119
120    // Step 4-5: detect update vs collision; extract payload
121    let agent_dir = mur_home.join("agents").join(&slug);
122    let was_update = if agent_dir.exists() {
123        let existing_profile = agent_dir.join("profile.yaml");
124        let mut is_same_agent = false;
125        if existing_profile.exists() {
126            let existing_yaml = fs::read_to_string(&existing_profile).map_err(MuragentError::Io)?;
127            if let Ok(existing) = serde_yaml_ng::from_str::<AgentProfile>(&existing_yaml)
128                && existing.id == result.manifest.agent.original_uuid
129            {
130                is_same_agent = true;
131            }
132        }
133        if !is_same_agent {
134            return Err(MuragentError::Other(format!(
135                "agent '{slug}' already exists at {} with a different UUID",
136                agent_dir.display()
137            )));
138        }
139        // Same UUID — clear everything except data/, then extract
140        clear_except_data(&agent_dir)?;
141        true
142    } else {
143        fs::create_dir_all(&agent_dir).map_err(MuragentError::Io)?;
144        false
145    };
146
147    extract_payload(archive, &agent_dir)?;
148
149    // Step 6: trust upsert
150    let fingerprint_hex = trust::short_fingerprint(&result.author_pubkey);
151    let fingerprint_words = trust::word_list_fingerprint(&result.author_pubkey);
152    let (trust_level, _) = upsert_trust(
153        &mut trust_store,
154        &result,
155        &author_pubkey_b64,
156        &existing_by_pubkey,
157        surface,
158    )?;
159    trust_store.save()?;
160
161    Ok(InstallOutcome {
162        manifest: result.manifest,
163        trust_level,
164        fingerprint_hex,
165        fingerprint_words,
166        was_update,
167    })
168}
169
170/// Refuse a package that carries any [`RESERVED_LOCAL_FILES`] entry at its top
171/// level — extracting it would plant/overwrite host-minted identity material.
172fn reject_reserved_local_files(archive: &MuragentArchive) -> Result<(), MuragentError> {
173    for path in archive.files.keys() {
174        if RESERVED_LOCAL_FILES.contains(&path.as_str()) {
175            return Err(MuragentError::Other(format!(
176                "package contains reserved local file '{path}' \
177                 (private identity material is host-minted and must not be shipped)"
178            )));
179        }
180    }
181    Ok(())
182}
183
184/// Convert a display name to a filesystem-safe slug for rotation manifest lookup.
185fn display_name_slug(name: &str) -> String {
186    name.to_lowercase()
187        .chars()
188        .map(|c| if c.is_alphanumeric() { c } else { '-' })
189        .collect::<String>()
190        .split('-')
191        .filter(|s| !s.is_empty())
192        .collect::<Vec<_>>()
193        .join("-")
194}
195
196fn rotation_manifest_path(mur_home: &Path, display_name: &str) -> PathBuf {
197    mur_home
198        .join("trust")
199        .join("rotations")
200        .join(format!("{}.yaml", display_name_slug(display_name)))
201}
202
203/// Try to load and apply a key rotation manifest. Returns Ok(()) if the
204/// rotation is valid and the trust store has been updated in-place. Returns
205/// Err(reason) if the manifest is missing, invalid, or replayed.
206fn try_apply_rotation(
207    trust_store: &mut TrustStore,
208    old_entry: Option<&TrustEntry>,
209    new_pubkey_b64: &str,
210    display_name: &str,
211    mur_home: &Path,
212) -> Result<(), String> {
213    let manifest_path = rotation_manifest_path(mur_home, display_name);
214    if !manifest_path.exists() {
215        return Err(
216            "no rotation manifest is present (possible impersonation; place \
217             <display_name>.yaml in ~/.mur/trust/rotations/ if intentional)"
218                .into(),
219        );
220    }
221
222    let yaml =
223        fs::read_to_string(&manifest_path).map_err(|e| format!("read rotation manifest: {e}"))?;
224    let manifest: RotationManifest =
225        serde_yaml_ng::from_str(&yaml).map_err(|e| format!("parse rotation manifest: {e}"))?;
226
227    // Cross-check: manifest must reference the known old key and the incoming new key.
228    if let Some(entry) = old_entry
229        && manifest.old_pubkey != entry.public_key
230    {
231        return Err("rotation manifest old_pubkey does not match the known trust entry".into());
232    }
233    if manifest.new_pubkey != new_pubkey_b64 {
234        return Err("rotation manifest new_pubkey does not match the package's signing key".into());
235    }
236
237    // Cryptographic verification (old key signs, new key countersigns).
238    manifest.verify()?;
239
240    // Replay prevention: issued_at must be strictly newer than last_rotation_at.
241    // Compare parsed instants, not raw strings — RFC3339 is not lexicographically
242    // ordered across offsets/precision ("Z" vs "+00:00", fractional seconds), so a
243    // string compare could let a replayed manifest slip through.
244    if let Some(entry) = old_entry
245        && let Some(last_at) = &entry.last_rotation_at
246    {
247        let issued = chrono::DateTime::parse_from_rfc3339(&manifest.issued_at)
248            .map_err(|e| format!("rotation manifest issued_at is not valid RFC3339: {e}"))?;
249        let last = chrono::DateTime::parse_from_rfc3339(last_at)
250            .map_err(|e| format!("stored last_rotation_at is not valid RFC3339: {e}"))?;
251        if issued <= last {
252            return Err(format!(
253                "rotation manifest issued_at ({}) is not newer than last_rotation_at ({})",
254                manifest.issued_at, last_at
255            ));
256        }
257    }
258
259    // Apply: mark old entry Superseded, insert new entry.
260    let now = chrono::Utc::now().to_rfc3339();
261    if let Some(entry) = old_entry.cloned() {
262        trust_store.upsert(TrustEntry {
263            trust_level: TrustLevel::Superseded,
264            superseded_at: Some(manifest.issued_at.clone()),
265            last_rotation_at: Some(manifest.issued_at.clone()),
266            ..entry
267        });
268    }
269    trust_store.upsert(TrustEntry {
270        public_key: new_pubkey_b64.to_string(),
271        display_name_seen: display_name.to_string(),
272        first_seen: now.clone(),
273        last_seen: now,
274        last_seen_surface: String::new(), // filled by caller during upsert_trust
275        trust_level: TrustLevel::Pending,
276        fingerprint: String::new(), // filled by caller
277        word_list: String::new(),   // filled by caller
278        rotated_from: old_entry.map(|e| e.public_key.clone()),
279        superseded_at: None,
280        last_rotation_at: Some(manifest.issued_at.clone()),
281    });
282
283    Ok(())
284}
285
286/// Files and directories that must survive an in-place update: the agent's
287/// runtime `data/` and its local identity keypair (private material the
288/// incoming, sanitized package never carries — clobbering it would orphan the
289/// agent's signing key and break re-export).
290const PRESERVE_ON_UPDATE: &[&str] = &[
291    "data",
292    "identity.key",
293    "identity.pub",
294    "identity.key.prev",
295    "identity.pub.prev",
296];
297
298/// Remove every entry in `dir` except the [`PRESERVE_ON_UPDATE`] set. Used by
299/// the update path.
300fn clear_except_data(dir: &Path) -> Result<(), MuragentError> {
301    for entry in fs::read_dir(dir).map_err(MuragentError::Io)? {
302        let entry = entry.map_err(MuragentError::Io)?;
303        if PRESERVE_ON_UPDATE
304            .iter()
305            .any(|keep| entry.file_name() == *keep)
306        {
307            continue;
308        }
309        let path = entry.path();
310        if path.is_dir() {
311            fs::remove_dir_all(&path).map_err(MuragentError::Io)?;
312        } else {
313            fs::remove_file(&path).map_err(MuragentError::Io)?;
314        }
315    }
316    Ok(())
317}
318
319fn extract_payload(archive: &MuragentArchive, agent_dir: &Path) -> Result<(), MuragentError> {
320    for (path, data) in &archive.files {
321        if ENVELOPE_FILES.contains(&path.as_str()) || RESERVED_LOCAL_FILES.contains(&path.as_str())
322        {
323            continue;
324        }
325        let dest = agent_dir.join(path);
326        if let Some(parent) = dest.parent() {
327            fs::create_dir_all(parent).map_err(MuragentError::Io)?;
328        }
329        fs::write(&dest, data).map_err(MuragentError::Io)?;
330    }
331    Ok(())
332}
333
334fn upsert_trust(
335    trust_store: &mut TrustStore,
336    result: &ValidationResult,
337    author_pubkey_b64: &str,
338    existing: &Option<TrustEntry>,
339    surface: &str,
340) -> Result<(TrustLevel, PathBuf), MuragentError> {
341    let now = chrono::Utc::now().to_rfc3339();
342    let first_seen = existing
343        .as_ref()
344        .map(|e| e.first_seen.clone())
345        .unwrap_or_else(|| now.clone());
346    // Promotion to Known is a UI decision, not an install-flow decision.
347    // First-time-seen authors land at Pending and stay there until the
348    // surface explicitly promotes them.
349    let level = existing
350        .as_ref()
351        .map(|e| e.trust_level.clone())
352        .unwrap_or(TrustLevel::Pending);
353
354    trust_store.upsert(TrustEntry {
355        public_key: author_pubkey_b64.to_string(),
356        display_name_seen: result.manifest.agent.display_name.clone(),
357        first_seen,
358        last_seen: now,
359        last_seen_surface: surface.to_string(),
360        trust_level: level.clone(),
361        fingerprint: trust::short_fingerprint(&result.author_pubkey),
362        word_list: trust::word_list_fingerprint(&result.author_pubkey),
363        rotated_from: existing.as_ref().and_then(|e| e.rotated_from.clone()),
364        superseded_at: existing.as_ref().and_then(|e| e.superseded_at.clone()),
365        last_rotation_at: existing.as_ref().and_then(|e| e.last_rotation_at.clone()),
366    });
367
368    Ok((level, PathBuf::new()))
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::identity::AgentIdentity;
375    use crate::muragent::writer::{MuragentWriter, build_manifest_from_profile};
376    use tempfile::TempDir;
377
378    fn make_test_package(tmp: &TempDir) -> std::path::PathBuf {
379        let out = tmp.path().join("test.muragent");
380        let profile = AgentProfile::default_for_tests();
381        let identity = AgentIdentity::generate();
382        let manifest = build_manifest_from_profile(&profile, "2.13.0");
383        let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
384        let mut writer = MuragentWriter::new(manifest, profile_yaml, identity);
385        writer.add_icon("icon-512.png", b"fake-png".to_vec());
386        writer.write(&out).unwrap();
387        out
388    }
389
390    #[test]
391    fn install_extracts_sys_prompt_and_skills() {
392        // Regression: `.muragent` export must bundle the system prompt and
393        // skill files so the loaded agent keeps its persona + non-dangling
394        // skill registrations.
395        let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
396        let tmp = TempDir::new().unwrap();
397        let mur_home = tmp.path().join("mur");
398        let prev = std::env::var_os("MUR_HOME");
399        unsafe { std::env::set_var("MUR_HOME", &mur_home) };
400
401        let profile = AgentProfile::default_for_tests();
402        let manifest = build_manifest_from_profile(&profile, "2.13.0");
403        let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
404        let mut writer = MuragentWriter::new(manifest, profile_yaml, AgentIdentity::generate());
405        writer.set_sys_prompt("You are a helpful test agent.".into());
406        writer.add_skill("demo.md", b"# demo skill\nbody".to_vec());
407        let out = tmp.path().join("withextras.muragent");
408        writer.write(&out).unwrap();
409
410        let archive = MuragentArchive::read(&out).unwrap();
411        let outcome = install(&archive, &mur_home, "test").unwrap();
412        let agent_dir = mur_home.join("agents").join(&outcome.manifest.agent.slug);
413
414        let prompt = fs::read_to_string(agent_dir.join("sys_prompt.md")).unwrap();
415        assert_eq!(prompt, "You are a helpful test agent.");
416        let skill = fs::read_to_string(agent_dir.join("skills").join("demo.md")).unwrap();
417        assert_eq!(skill, "# demo skill\nbody");
418
419        unsafe {
420            if let Some(p) = prev {
421                std::env::set_var("MUR_HOME", p);
422            } else {
423                std::env::remove_var("MUR_HOME");
424            }
425        }
426    }
427
428    fn make_test_package_with_identity(
429        tmp: &TempDir,
430        identity: &AgentIdentity,
431    ) -> std::path::PathBuf {
432        let out = tmp
433            .path()
434            .join(format!("{}.muragent", &identity.pubkey_text()[..8]));
435        let profile = AgentProfile::default_for_tests();
436        let manifest = build_manifest_from_profile(&profile, "2.13.0");
437        let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
438        let mut writer = MuragentWriter::new(manifest, profile_yaml, identity.clone());
439        writer.add_icon("icon-512.png", b"fake-png".to_vec());
440        writer.write(&out).unwrap();
441        out
442    }
443
444    #[test]
445    fn rotation_manifest_missing_still_refuses() {
446        let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
447        let tmp = TempDir::new().unwrap();
448        let mur_home = tmp.path().join("mur");
449        let prev = std::env::var_os("MUR_HOME");
450        unsafe { std::env::set_var("MUR_HOME", &mur_home) };
451
452        let old_identity = AgentIdentity::generate();
453        let pkg_old = make_test_package_with_identity(&tmp, &old_identity);
454        let archive = MuragentArchive::read(&pkg_old).unwrap();
455        let outcome = install(&archive, &mur_home, "test").unwrap();
456        let slug = outcome.manifest.agent.slug.clone();
457
458        let new_identity = AgentIdentity::generate();
459        let profile = AgentProfile::default_for_tests();
460        let out2 = tmp.path().join("new2.muragent");
461        let manifest2 = build_manifest_from_profile(&profile, "2.14.0");
462        let profile_yaml2 = serde_yaml_ng::to_string(&profile).unwrap();
463        let mut writer2 = MuragentWriter::new(manifest2, profile_yaml2, new_identity);
464        writer2.add_icon("icon-512.png", b"fake-png".to_vec());
465        writer2.write(&out2).unwrap();
466        let archive2 = MuragentArchive::read(&out2).unwrap();
467        let agent_dir = mur_home.join("agents").join(&slug);
468        fs::remove_dir_all(&agent_dir).unwrap();
469
470        let err = install(&archive2, &mur_home, "test").unwrap_err();
471        assert!(
472            matches!(err, MuragentError::TrustRefused(_)),
473            "expected TrustRefused, got: {:?}",
474            err
475        );
476
477        unsafe {
478            if let Some(p) = prev {
479                std::env::set_var("MUR_HOME", p);
480            } else {
481                std::env::remove_var("MUR_HOME");
482            }
483        }
484    }
485
486    #[test]
487    fn reserved_local_files_are_rejected() {
488        // A package carrying private identity material must be refused before
489        // extraction (which would plant/overwrite the agent's signing key).
490        use std::collections::BTreeMap;
491        for reserved in RESERVED_LOCAL_FILES {
492            let mut files = BTreeMap::new();
493            files.insert("profile.yaml".to_string(), b"ok".to_vec());
494            files.insert((*reserved).to_string(), b"ATTACKER-KEY".to_vec());
495            let archive = MuragentArchive { files };
496            assert!(
497                reject_reserved_local_files(&archive).is_err(),
498                "must reject package carrying {reserved}"
499            );
500        }
501        // A clean payload passes.
502        let mut files = BTreeMap::new();
503        files.insert("profile.yaml".to_string(), b"ok".to_vec());
504        files.insert("skills/demo.md".to_string(), b"skill".to_vec());
505        let archive = MuragentArchive { files };
506        assert!(reject_reserved_local_files(&archive).is_ok());
507    }
508
509    #[test]
510    fn display_name_slug_roundtrip() {
511        assert_eq!(display_name_slug("My Agent"), "my-agent");
512        assert_eq!(display_name_slug("Coach (Beta)"), "coach-beta");
513        assert_eq!(display_name_slug("test"), "test");
514    }
515
516    #[test]
517    fn install_then_update_preserves_data() {
518        let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
519        let tmp = TempDir::new().unwrap();
520        let mur_home = tmp.path().join("mur");
521        let prev = std::env::var_os("MUR_HOME");
522        unsafe { std::env::set_var("MUR_HOME", &mur_home) };
523
524        let pkg = make_test_package(&tmp);
525        let archive = MuragentArchive::read(&pkg).unwrap();
526        let outcome = install(&archive, &mur_home, "test").unwrap();
527        assert!(!outcome.was_update);
528        let slug = outcome.manifest.agent.slug.clone();
529        let agent_dir = mur_home.join("agents").join(&slug);
530        assert!(agent_dir.join("profile.yaml").exists());
531
532        // Caller writes some data — the update path must preserve it.
533        let data_dir = agent_dir.join("data");
534        fs::create_dir_all(&data_dir).unwrap();
535        fs::write(data_dir.join("history.jsonl"), b"important").unwrap();
536
537        // Re-install (same archive, same UUID) — should preserve data/
538        let outcome2 = install(&archive, &mur_home, "test").unwrap();
539        assert!(outcome2.was_update);
540        let preserved = fs::read(data_dir.join("history.jsonl")).unwrap();
541        assert_eq!(preserved, b"important");
542
543        // Cleanup
544        unsafe {
545            if let Some(p) = prev {
546                std::env::set_var("MUR_HOME", p);
547            } else {
548                std::env::remove_var("MUR_HOME");
549            }
550        }
551    }
552
553    #[test]
554    fn update_preserves_local_identity_keypair() {
555        // Regression: loading a template-mode (.muragent carries no private
556        // key) package over an existing agent must NOT delete the agent's
557        // locally-minted identity keypair, or `mur agent export` afterward
558        // fails with "identity files not found".
559        let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
560        let tmp = TempDir::new().unwrap();
561        let mur_home = tmp.path().join("mur");
562        let prev = std::env::var_os("MUR_HOME");
563        unsafe { std::env::set_var("MUR_HOME", &mur_home) };
564
565        let pkg = make_test_package(&tmp);
566        let archive = MuragentArchive::read(&pkg).unwrap();
567        let outcome = install(&archive, &mur_home, "test").unwrap();
568        let slug = outcome.manifest.agent.slug.clone();
569        let agent_dir = mur_home.join("agents").join(&slug);
570
571        // Simulate a locally-minted keypair (as `mur agent create` writes).
572        fs::write(agent_dir.join("identity.key"), b"PRIVATE-KEY").unwrap();
573        fs::write(agent_dir.join("identity.pub"), b"PUBLIC-KEY").unwrap();
574
575        // Re-install (same archive, same UUID) → update path runs clear_except_data.
576        let outcome2 = install(&archive, &mur_home, "test").unwrap();
577        assert!(outcome2.was_update);
578
579        assert!(
580            agent_dir.join("identity.key").exists(),
581            "identity.key must survive an in-place update"
582        );
583        assert_eq!(
584            fs::read(agent_dir.join("identity.key")).unwrap(),
585            b"PRIVATE-KEY"
586        );
587        assert!(
588            agent_dir.join("identity.pub").exists(),
589            "identity.pub must survive an in-place update"
590        );
591
592        // Cleanup
593        unsafe {
594            if let Some(p) = prev {
595                std::env::set_var("MUR_HOME", p);
596            } else {
597                std::env::remove_var("MUR_HOME");
598            }
599        }
600    }
601}