1use std::fs;
20use std::path::{Path, PathBuf};
21
22use base64::{Engine, engine::general_purpose::STANDARD as B64};
23
24use crate::AgentProfile;
25use crate::agent::McpNetMode;
26use crate::muragent::MuragentError;
27use crate::muragent::manifest::MuragentManifest;
28use crate::muragent::reader::MuragentArchive;
29use crate::muragent::validator::{self, ValidationResult};
30use crate::trust::rotation::RotationManifest;
31use crate::trust::{self, TrustEntry, TrustLevel, TrustStore};
32
33const ENVELOPE_FILES: &[&str] = &["manifest.yaml", "manifest.signed.json", "signatures.json"];
35
36const RESERVED_LOCAL_FILES: &[&str] = &[
42 "identity.key",
43 "identity.pub",
44 "identity.key.prev",
45 "identity.pub.prev",
46];
47
48#[derive(Debug)]
50pub struct InstallOutcome {
51 pub manifest: MuragentManifest,
52 pub trust_level: TrustLevel,
53 pub fingerprint_hex: String,
54 pub fingerprint_words: String,
55 pub was_update: bool,
59 pub downgraded_broad_egress: Vec<String>,
65}
66
67pub fn install(
77 archive: &MuragentArchive,
78 mur_home: &Path,
79 surface: &str,
80) -> Result<InstallOutcome, MuragentError> {
81 install_with_name(archive, mur_home, surface, None)
82}
83
84pub fn install_with_name(
97 archive: &MuragentArchive,
98 mur_home: &Path,
99 surface: &str,
100 install_name: Option<&str>,
101) -> Result<InstallOutcome, MuragentError> {
102 let result = validator::validate(archive)?;
104
105 reject_reserved_local_files(archive)?;
109
110 let manifest_slug = result.manifest.agent.slug.clone();
112 let display_name = result.manifest.agent.display_name.clone();
113 crate::validate_agent_name(&manifest_slug).map_err(|e| {
114 MuragentError::Other(format!(
115 "invalid agent slug '{manifest_slug}' in manifest: {e}"
116 ))
117 })?;
118 let slug = if let Some(n) = install_name {
119 crate::validate_agent_name(n)
120 .map_err(|e| MuragentError::Other(format!("invalid install name '{n}': {e}")))?;
121 n.to_string()
122 } else {
123 manifest_slug
124 };
125
126 let mut trust_store = TrustStore::load()?;
128 let author_pubkey_b64 = B64.encode(result.author_pubkey);
129 let existing_by_pubkey = trust_store.find_by_pubkey(&author_pubkey_b64).cloned();
130
131 if existing_by_pubkey.is_none() {
132 let by_name = trust_store.find_by_display_name(&display_name);
133 if !by_name.is_empty() {
134 let old_entry = by_name
136 .into_iter()
137 .find(|e| e.trust_level != TrustLevel::Superseded)
138 .cloned();
139 match try_apply_rotation(
140 &mut trust_store,
141 old_entry.as_ref(),
142 &author_pubkey_b64,
143 &display_name,
144 mur_home,
145 ) {
146 Ok(()) => {} Err(reason) => {
148 return Err(MuragentError::TrustRefused(format!(
149 "agent '{}' has a new signing key but no valid rotation manifest: {}",
150 display_name, reason
151 )));
152 }
153 }
154 }
155 }
156
157 let agent_dir = mur_home.join("agents").join(&slug);
159 if install_name.is_some() && agent_dir.exists() {
160 return Err(MuragentError::Other(format!(
163 "agent '{slug}' already exists at {} — refusing to overwrite",
164 agent_dir.display()
165 )));
166 }
167 let was_update = if agent_dir.exists() {
168 let existing_profile = agent_dir.join("profile.yaml");
169 let mut is_same_agent = false;
170 if existing_profile.exists() {
171 let existing_yaml = fs::read_to_string(&existing_profile).map_err(MuragentError::Io)?;
172 if let Ok(existing) = serde_yaml_ng::from_str::<AgentProfile>(&existing_yaml)
173 && existing.id == result.manifest.agent.original_uuid
174 {
175 is_same_agent = true;
176 }
177 }
178 if !is_same_agent {
179 return Err(MuragentError::Other(format!(
180 "agent '{slug}' already exists at {} with a different UUID",
181 agent_dir.display()
182 )));
183 }
184 clear_except_data(&agent_dir)?;
186 true
187 } else {
188 fs::create_dir_all(&agent_dir).map_err(MuragentError::Io)?;
189 false
190 };
191
192 extract_payload(archive, &agent_dir)?;
193
194 let downgraded_broad_egress = downgrade_profile_egress(&agent_dir)?;
195
196 let fingerprint_hex = trust::short_fingerprint(&result.author_pubkey);
198 let fingerprint_words = trust::word_list_fingerprint(&result.author_pubkey);
199 let (trust_level, _) = upsert_trust(
200 &mut trust_store,
201 &result,
202 &author_pubkey_b64,
203 &existing_by_pubkey,
204 surface,
205 )?;
206 trust_store.save()?;
207
208 Ok(InstallOutcome {
209 manifest: result.manifest,
210 trust_level,
211 fingerprint_hex,
212 fingerprint_words,
213 was_update,
214 downgraded_broad_egress,
215 })
216}
217
218fn downgrade_broad_egress(profile: &mut AgentProfile) -> Vec<String> {
225 let mut downgraded = Vec::new();
226 for server in &mut profile.mcp_servers {
227 if let Some(network) = server.network.as_mut()
228 && network.mode == McpNetMode::BroadAudited
229 {
230 network.mode = McpNetMode::Inherit;
231 network.authorization = None;
232 downgraded.push(server.name.clone());
233 }
234 }
235 downgraded
236}
237
238fn downgrade_profile_egress(agent_dir: &Path) -> Result<Vec<String>, MuragentError> {
242 let profile_path = agent_dir.join("profile.yaml");
243 let raw = fs::read_to_string(&profile_path).map_err(MuragentError::Io)?;
244 let mut profile: AgentProfile =
245 serde_yaml_ng::from_str(&raw).map_err(|e| MuragentError::Other(e.to_string()))?;
246
247 let downgraded = downgrade_broad_egress(&mut profile);
248 if !downgraded.is_empty() {
249 let out =
250 serde_yaml_ng::to_string(&profile).map_err(|e| MuragentError::Other(e.to_string()))?;
251 fs::write(&profile_path, out).map_err(MuragentError::Io)?;
252 }
253 Ok(downgraded)
254}
255
256fn reject_reserved_local_files(archive: &MuragentArchive) -> Result<(), MuragentError> {
259 for path in archive.files.keys() {
260 if RESERVED_LOCAL_FILES.contains(&path.as_str()) {
261 return Err(MuragentError::Other(format!(
262 "package contains reserved local file '{path}' \
263 (private identity material is host-minted and must not be shipped)"
264 )));
265 }
266 }
267 Ok(())
268}
269
270fn display_name_slug(name: &str) -> String {
272 name.to_lowercase()
273 .chars()
274 .map(|c| if c.is_alphanumeric() { c } else { '-' })
275 .collect::<String>()
276 .split('-')
277 .filter(|s| !s.is_empty())
278 .collect::<Vec<_>>()
279 .join("-")
280}
281
282fn rotation_manifest_path(mur_home: &Path, display_name: &str) -> PathBuf {
283 mur_home
284 .join("trust")
285 .join("rotations")
286 .join(format!("{}.yaml", display_name_slug(display_name)))
287}
288
289fn try_apply_rotation(
293 trust_store: &mut TrustStore,
294 old_entry: Option<&TrustEntry>,
295 new_pubkey_b64: &str,
296 display_name: &str,
297 mur_home: &Path,
298) -> Result<(), String> {
299 let manifest_path = rotation_manifest_path(mur_home, display_name);
300 if !manifest_path.exists() {
301 return Err(
302 "no rotation manifest is present (possible impersonation; place \
303 <display_name>.yaml in ~/.mur/trust/rotations/ if intentional)"
304 .into(),
305 );
306 }
307
308 let yaml =
309 fs::read_to_string(&manifest_path).map_err(|e| format!("read rotation manifest: {e}"))?;
310 let manifest: RotationManifest =
311 serde_yaml_ng::from_str(&yaml).map_err(|e| format!("parse rotation manifest: {e}"))?;
312
313 if let Some(entry) = old_entry
315 && manifest.old_pubkey != entry.public_key
316 {
317 return Err("rotation manifest old_pubkey does not match the known trust entry".into());
318 }
319 if manifest.new_pubkey != new_pubkey_b64 {
320 return Err("rotation manifest new_pubkey does not match the package's signing key".into());
321 }
322
323 manifest.verify()?;
325
326 if let Some(entry) = old_entry
331 && let Some(last_at) = &entry.last_rotation_at
332 {
333 let issued = chrono::DateTime::parse_from_rfc3339(&manifest.issued_at)
334 .map_err(|e| format!("rotation manifest issued_at is not valid RFC3339: {e}"))?;
335 let last = chrono::DateTime::parse_from_rfc3339(last_at)
336 .map_err(|e| format!("stored last_rotation_at is not valid RFC3339: {e}"))?;
337 if issued <= last {
338 return Err(format!(
339 "rotation manifest issued_at ({}) is not newer than last_rotation_at ({})",
340 manifest.issued_at, last_at
341 ));
342 }
343 }
344
345 let now = chrono::Utc::now().to_rfc3339();
347 if let Some(entry) = old_entry.cloned() {
348 trust_store.upsert(TrustEntry {
349 trust_level: TrustLevel::Superseded,
350 superseded_at: Some(manifest.issued_at.clone()),
351 last_rotation_at: Some(manifest.issued_at.clone()),
352 ..entry
353 });
354 }
355 trust_store.upsert(TrustEntry {
356 public_key: new_pubkey_b64.to_string(),
357 display_name_seen: display_name.to_string(),
358 first_seen: now.clone(),
359 last_seen: now,
360 last_seen_surface: String::new(), trust_level: TrustLevel::Pending,
362 fingerprint: String::new(), word_list: String::new(), rotated_from: old_entry.map(|e| e.public_key.clone()),
365 superseded_at: None,
366 last_rotation_at: Some(manifest.issued_at.clone()),
367 });
368
369 Ok(())
370}
371
372const PRESERVE_ON_UPDATE: &[&str] = &[
377 "data",
378 "identity.key",
379 "identity.pub",
380 "identity.key.prev",
381 "identity.pub.prev",
382];
383
384fn clear_except_data(dir: &Path) -> Result<(), MuragentError> {
387 for entry in fs::read_dir(dir).map_err(MuragentError::Io)? {
388 let entry = entry.map_err(MuragentError::Io)?;
389 if PRESERVE_ON_UPDATE
390 .iter()
391 .any(|keep| entry.file_name() == *keep)
392 {
393 continue;
394 }
395 let path = entry.path();
396 if path.is_dir() {
397 fs::remove_dir_all(&path).map_err(MuragentError::Io)?;
398 } else {
399 fs::remove_file(&path).map_err(MuragentError::Io)?;
400 }
401 }
402 Ok(())
403}
404
405fn extract_payload(archive: &MuragentArchive, agent_dir: &Path) -> Result<(), MuragentError> {
406 for (path, data) in &archive.files {
407 if ENVELOPE_FILES.contains(&path.as_str()) || RESERVED_LOCAL_FILES.contains(&path.as_str())
408 {
409 continue;
410 }
411 let dest = agent_dir.join(path);
412 if let Some(parent) = dest.parent() {
413 fs::create_dir_all(parent).map_err(MuragentError::Io)?;
414 }
415 fs::write(&dest, data).map_err(MuragentError::Io)?;
416 }
417 Ok(())
418}
419
420fn upsert_trust(
421 trust_store: &mut TrustStore,
422 result: &ValidationResult,
423 author_pubkey_b64: &str,
424 existing: &Option<TrustEntry>,
425 surface: &str,
426) -> Result<(TrustLevel, PathBuf), MuragentError> {
427 let now = chrono::Utc::now().to_rfc3339();
428 let first_seen = existing
429 .as_ref()
430 .map(|e| e.first_seen.clone())
431 .unwrap_or_else(|| now.clone());
432 let level = existing
436 .as_ref()
437 .map(|e| e.trust_level.clone())
438 .unwrap_or(TrustLevel::Pending);
439
440 trust_store.upsert(TrustEntry {
441 public_key: author_pubkey_b64.to_string(),
442 display_name_seen: result.manifest.agent.display_name.clone(),
443 first_seen,
444 last_seen: now,
445 last_seen_surface: surface.to_string(),
446 trust_level: level.clone(),
447 fingerprint: trust::short_fingerprint(&result.author_pubkey),
448 word_list: trust::word_list_fingerprint(&result.author_pubkey),
449 rotated_from: existing.as_ref().and_then(|e| e.rotated_from.clone()),
450 superseded_at: existing.as_ref().and_then(|e| e.superseded_at.clone()),
451 last_rotation_at: existing.as_ref().and_then(|e| e.last_rotation_at.clone()),
452 });
453
454 Ok((level, PathBuf::new()))
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use crate::identity::AgentIdentity;
461 use crate::muragent::writer::{MuragentWriter, build_manifest_from_profile};
462 use tempfile::TempDir;
463
464 #[test]
465 fn downgrade_broad_egress_resets_mode_and_clears_authorization() {
466 use crate::agent::{EgressAuthorization, McpServerEntry, McpServerNetwork};
467
468 let mut profile = AgentProfile::default_for_tests();
469 profile.mcp_servers = vec![
470 McpServerEntry {
471 name: "broad-server".to_string(),
472 command: "true".to_string(),
473 network: Some(McpServerNetwork {
474 mode: McpNetMode::BroadAudited,
475 authorization: Some(EgressAuthorization {
476 authorized_by: "operator".to_string(),
477 authorized_at_ms: 1,
478 }),
479 ..Default::default()
480 }),
481 ..Default::default()
482 },
483 McpServerEntry {
484 name: "restricted-server".to_string(),
485 command: "true".to_string(),
486 network: Some(McpServerNetwork {
487 mode: McpNetMode::Restricted,
488 allow_hosts: vec!["example.com".to_string()],
489 ..Default::default()
490 }),
491 ..Default::default()
492 },
493 ];
494
495 let downgraded = downgrade_broad_egress(&mut profile);
496
497 assert_eq!(downgraded, vec!["broad-server".to_string()]);
498
499 let broad = &profile.mcp_servers[0].network.as_ref().unwrap();
500 assert_eq!(broad.mode, McpNetMode::Inherit);
501 assert_eq!(broad.authorization, None);
502
503 let restricted = &profile.mcp_servers[1].network.as_ref().unwrap();
504 assert_eq!(restricted.mode, McpNetMode::Restricted);
505 assert_eq!(restricted.allow_hosts, vec!["example.com".to_string()]);
506 }
507
508 fn make_test_package(tmp: &TempDir) -> std::path::PathBuf {
509 let out = tmp.path().join("test.muragent");
510 let profile = AgentProfile::default_for_tests();
511 let identity = AgentIdentity::generate();
512 let manifest = build_manifest_from_profile(&profile, "2.13.0");
513 let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
514 let mut writer = MuragentWriter::new(manifest, profile_yaml, identity);
515 writer.add_icon("icon-512.png", b"fake-png".to_vec());
516 writer.write(&out).unwrap();
517 out
518 }
519
520 #[test]
521 fn install_extracts_sys_prompt_and_skills() {
522 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
526 let tmp = TempDir::new().unwrap();
527 let mur_home = tmp.path().join("mur");
528 let prev = std::env::var_os("MUR_HOME");
529 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
530
531 let profile = AgentProfile::default_for_tests();
532 let manifest = build_manifest_from_profile(&profile, "2.13.0");
533 let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
534 let mut writer = MuragentWriter::new(manifest, profile_yaml, AgentIdentity::generate());
535 writer.set_sys_prompt("You are a helpful test agent.".into());
536 writer.add_skill("demo.md", b"# demo skill\nbody".to_vec());
537 let out = tmp.path().join("withextras.muragent");
538 writer.write(&out).unwrap();
539
540 let archive = MuragentArchive::read(&out).unwrap();
541 let outcome = install(&archive, &mur_home, "test").unwrap();
542 let agent_dir = mur_home.join("agents").join(&outcome.manifest.agent.slug);
543
544 let prompt = fs::read_to_string(agent_dir.join("sys_prompt.md")).unwrap();
545 assert_eq!(prompt, "You are a helpful test agent.");
546 let skill = fs::read_to_string(agent_dir.join("skills").join("demo.md")).unwrap();
547 assert_eq!(skill, "# demo skill\nbody");
548
549 unsafe {
550 if let Some(p) = prev {
551 std::env::set_var("MUR_HOME", p);
552 } else {
553 std::env::remove_var("MUR_HOME");
554 }
555 }
556 }
557
558 fn make_test_package_with_identity(
559 tmp: &TempDir,
560 identity: &AgentIdentity,
561 ) -> std::path::PathBuf {
562 let out = tmp
563 .path()
564 .join(format!("{}.muragent", &identity.pubkey_text()[..8]));
565 let profile = AgentProfile::default_for_tests();
566 let manifest = build_manifest_from_profile(&profile, "2.13.0");
567 let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
568 let mut writer = MuragentWriter::new(manifest, profile_yaml, identity.clone());
569 writer.add_icon("icon-512.png", b"fake-png".to_vec());
570 writer.write(&out).unwrap();
571 out
572 }
573
574 #[test]
575 fn rotation_manifest_missing_still_refuses() {
576 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
577 let tmp = TempDir::new().unwrap();
578 let mur_home = tmp.path().join("mur");
579 let prev = std::env::var_os("MUR_HOME");
580 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
581
582 let old_identity = AgentIdentity::generate();
583 let pkg_old = make_test_package_with_identity(&tmp, &old_identity);
584 let archive = MuragentArchive::read(&pkg_old).unwrap();
585 let outcome = install(&archive, &mur_home, "test").unwrap();
586 let slug = outcome.manifest.agent.slug.clone();
587
588 let new_identity = AgentIdentity::generate();
589 let profile = AgentProfile::default_for_tests();
590 let out2 = tmp.path().join("new2.muragent");
591 let manifest2 = build_manifest_from_profile(&profile, "2.14.0");
592 let profile_yaml2 = serde_yaml_ng::to_string(&profile).unwrap();
593 let mut writer2 = MuragentWriter::new(manifest2, profile_yaml2, new_identity);
594 writer2.add_icon("icon-512.png", b"fake-png".to_vec());
595 writer2.write(&out2).unwrap();
596 let archive2 = MuragentArchive::read(&out2).unwrap();
597 let agent_dir = mur_home.join("agents").join(&slug);
598 fs::remove_dir_all(&agent_dir).unwrap();
599
600 let err = install(&archive2, &mur_home, "test").unwrap_err();
601 assert!(
602 matches!(err, MuragentError::TrustRefused(_)),
603 "expected TrustRefused, got: {:?}",
604 err
605 );
606
607 unsafe {
608 if let Some(p) = prev {
609 std::env::set_var("MUR_HOME", p);
610 } else {
611 std::env::remove_var("MUR_HOME");
612 }
613 }
614 }
615
616 #[test]
617 fn reserved_local_files_are_rejected() {
618 use std::collections::BTreeMap;
621 for reserved in RESERVED_LOCAL_FILES {
622 let mut files = BTreeMap::new();
623 files.insert("profile.yaml".to_string(), b"ok".to_vec());
624 files.insert((*reserved).to_string(), b"ATTACKER-KEY".to_vec());
625 let archive = MuragentArchive { files };
626 assert!(
627 reject_reserved_local_files(&archive).is_err(),
628 "must reject package carrying {reserved}"
629 );
630 }
631 let mut files = BTreeMap::new();
633 files.insert("profile.yaml".to_string(), b"ok".to_vec());
634 files.insert("skills/demo.md".to_string(), b"skill".to_vec());
635 let archive = MuragentArchive { files };
636 assert!(reject_reserved_local_files(&archive).is_ok());
637 }
638
639 #[test]
640 fn display_name_slug_roundtrip() {
641 assert_eq!(display_name_slug("My Agent"), "my-agent");
642 assert_eq!(display_name_slug("Coach (Beta)"), "coach-beta");
643 assert_eq!(display_name_slug("test"), "test");
644 }
645
646 #[test]
647 fn install_then_update_preserves_data() {
648 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
649 let tmp = TempDir::new().unwrap();
650 let mur_home = tmp.path().join("mur");
651 let prev = std::env::var_os("MUR_HOME");
652 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
653
654 let pkg = make_test_package(&tmp);
655 let archive = MuragentArchive::read(&pkg).unwrap();
656 let outcome = install(&archive, &mur_home, "test").unwrap();
657 assert!(!outcome.was_update);
658 let slug = outcome.manifest.agent.slug.clone();
659 let agent_dir = mur_home.join("agents").join(&slug);
660 assert!(agent_dir.join("profile.yaml").exists());
661
662 let data_dir = agent_dir.join("data");
664 fs::create_dir_all(&data_dir).unwrap();
665 fs::write(data_dir.join("history.jsonl"), b"important").unwrap();
666
667 let outcome2 = install(&archive, &mur_home, "test").unwrap();
669 assert!(outcome2.was_update);
670 let preserved = fs::read(data_dir.join("history.jsonl")).unwrap();
671 assert_eq!(preserved, b"important");
672
673 unsafe {
675 if let Some(p) = prev {
676 std::env::set_var("MUR_HOME", p);
677 } else {
678 std::env::remove_var("MUR_HOME");
679 }
680 }
681 }
682
683 #[test]
684 fn update_preserves_local_identity_keypair() {
685 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
690 let tmp = TempDir::new().unwrap();
691 let mur_home = tmp.path().join("mur");
692 let prev = std::env::var_os("MUR_HOME");
693 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
694
695 let pkg = make_test_package(&tmp);
696 let archive = MuragentArchive::read(&pkg).unwrap();
697 let outcome = install(&archive, &mur_home, "test").unwrap();
698 let slug = outcome.manifest.agent.slug.clone();
699 let agent_dir = mur_home.join("agents").join(&slug);
700
701 fs::write(agent_dir.join("identity.key"), b"PRIVATE-KEY").unwrap();
703 fs::write(agent_dir.join("identity.pub"), b"PUBLIC-KEY").unwrap();
704
705 let outcome2 = install(&archive, &mur_home, "test").unwrap();
707 assert!(outcome2.was_update);
708
709 assert!(
710 agent_dir.join("identity.key").exists(),
711 "identity.key must survive an in-place update"
712 );
713 assert_eq!(
714 fs::read(agent_dir.join("identity.key")).unwrap(),
715 b"PRIVATE-KEY"
716 );
717 assert!(
718 agent_dir.join("identity.pub").exists(),
719 "identity.pub must survive an in-place update"
720 );
721
722 unsafe {
724 if let Some(p) = prev {
725 std::env::set_var("MUR_HOME", p);
726 } else {
727 std::env::remove_var("MUR_HOME");
728 }
729 }
730 }
731
732 #[test]
733 fn install_with_name_installs_under_override_name_and_refuses_collision() {
734 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
735 let tmp = TempDir::new().unwrap();
736 let mur_home = tmp.path().join("mur");
737 let prev = std::env::var_os("MUR_HOME");
738 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
739
740 let pkg = make_test_package(&tmp);
741 let archive = MuragentArchive::read(&pkg).unwrap();
742
743 let outcome = install_with_name(&archive, &mur_home, "test", Some("clone-x")).unwrap();
744 assert!(!outcome.was_update);
745 let clone_dir = mur_home.join("agents").join("clone-x");
746 assert!(clone_dir.join("profile.yaml").exists());
747
748 let archive2 = MuragentArchive::read(&pkg).unwrap();
751 let err = install_with_name(&archive2, &mur_home, "test", Some("clone-x")).unwrap_err();
752 assert!(
753 matches!(err, MuragentError::Other(_)),
754 "expected refuse-on-collision error, got: {:?}",
755 err
756 );
757
758 unsafe {
759 if let Some(p) = prev {
760 std::env::set_var("MUR_HOME", p);
761 } else {
762 std::env::remove_var("MUR_HOME");
763 }
764 }
765 }
766}