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 let tmp = profile_path.with_extension("yaml.tmp");
254 fs::write(&tmp, out).map_err(MuragentError::Io)?;
255 fs::rename(&tmp, &profile_path).map_err(MuragentError::Io)?;
256 }
257 Ok(downgraded)
258}
259
260fn reject_reserved_local_files(archive: &MuragentArchive) -> Result<(), MuragentError> {
263 for path in archive.files.keys() {
264 if RESERVED_LOCAL_FILES.contains(&path.as_str()) {
265 return Err(MuragentError::Other(format!(
266 "package contains reserved local file '{path}' \
267 (private identity material is host-minted and must not be shipped)"
268 )));
269 }
270 }
271 Ok(())
272}
273
274fn display_name_slug(name: &str) -> String {
276 name.to_lowercase()
277 .chars()
278 .map(|c| if c.is_alphanumeric() { c } else { '-' })
279 .collect::<String>()
280 .split('-')
281 .filter(|s| !s.is_empty())
282 .collect::<Vec<_>>()
283 .join("-")
284}
285
286fn rotation_manifest_path(mur_home: &Path, display_name: &str) -> PathBuf {
287 mur_home
288 .join("trust")
289 .join("rotations")
290 .join(format!("{}.yaml", display_name_slug(display_name)))
291}
292
293fn try_apply_rotation(
297 trust_store: &mut TrustStore,
298 old_entry: Option<&TrustEntry>,
299 new_pubkey_b64: &str,
300 display_name: &str,
301 mur_home: &Path,
302) -> Result<(), String> {
303 let manifest_path = rotation_manifest_path(mur_home, display_name);
304 if !manifest_path.exists() {
305 return Err(
306 "no rotation manifest is present (possible impersonation; place \
307 <display_name>.yaml in ~/.mur/trust/rotations/ if intentional)"
308 .into(),
309 );
310 }
311
312 let yaml =
313 fs::read_to_string(&manifest_path).map_err(|e| format!("read rotation manifest: {e}"))?;
314 let manifest: RotationManifest =
315 serde_yaml_ng::from_str(&yaml).map_err(|e| format!("parse rotation manifest: {e}"))?;
316
317 if let Some(entry) = old_entry
319 && manifest.old_pubkey != entry.public_key
320 {
321 return Err("rotation manifest old_pubkey does not match the known trust entry".into());
322 }
323 if manifest.new_pubkey != new_pubkey_b64 {
324 return Err("rotation manifest new_pubkey does not match the package's signing key".into());
325 }
326
327 manifest.verify()?;
329
330 if let Some(entry) = old_entry
335 && let Some(last_at) = &entry.last_rotation_at
336 {
337 let issued = chrono::DateTime::parse_from_rfc3339(&manifest.issued_at)
338 .map_err(|e| format!("rotation manifest issued_at is not valid RFC3339: {e}"))?;
339 let last = chrono::DateTime::parse_from_rfc3339(last_at)
340 .map_err(|e| format!("stored last_rotation_at is not valid RFC3339: {e}"))?;
341 if issued <= last {
342 return Err(format!(
343 "rotation manifest issued_at ({}) is not newer than last_rotation_at ({})",
344 manifest.issued_at, last_at
345 ));
346 }
347 }
348
349 let now = chrono::Utc::now().to_rfc3339();
351 if let Some(entry) = old_entry.cloned() {
352 trust_store.upsert(TrustEntry {
353 trust_level: TrustLevel::Superseded,
354 superseded_at: Some(manifest.issued_at.clone()),
355 last_rotation_at: Some(manifest.issued_at.clone()),
356 ..entry
357 });
358 }
359 trust_store.upsert(TrustEntry {
360 public_key: new_pubkey_b64.to_string(),
361 display_name_seen: display_name.to_string(),
362 first_seen: now.clone(),
363 last_seen: now,
364 last_seen_surface: String::new(), trust_level: TrustLevel::Pending,
366 fingerprint: String::new(), word_list: String::new(), rotated_from: old_entry.map(|e| e.public_key.clone()),
369 superseded_at: None,
370 last_rotation_at: Some(manifest.issued_at.clone()),
371 });
372
373 Ok(())
374}
375
376const PRESERVE_ON_UPDATE: &[&str] = &[
381 "data",
382 "identity.key",
383 "identity.pub",
384 "identity.key.prev",
385 "identity.pub.prev",
386];
387
388fn clear_except_data(dir: &Path) -> Result<(), MuragentError> {
391 for entry in fs::read_dir(dir).map_err(MuragentError::Io)? {
392 let entry = entry.map_err(MuragentError::Io)?;
393 if PRESERVE_ON_UPDATE
394 .iter()
395 .any(|keep| entry.file_name() == *keep)
396 {
397 continue;
398 }
399 let path = entry.path();
400 if path.is_dir() {
401 fs::remove_dir_all(&path).map_err(MuragentError::Io)?;
402 } else {
403 fs::remove_file(&path).map_err(MuragentError::Io)?;
404 }
405 }
406 Ok(())
407}
408
409fn extract_payload(archive: &MuragentArchive, agent_dir: &Path) -> Result<(), MuragentError> {
410 for (path, data) in &archive.files {
411 if ENVELOPE_FILES.contains(&path.as_str()) || RESERVED_LOCAL_FILES.contains(&path.as_str())
412 {
413 continue;
414 }
415 let dest = agent_dir.join(path);
416 if let Some(parent) = dest.parent() {
417 fs::create_dir_all(parent).map_err(MuragentError::Io)?;
418 }
419 fs::write(&dest, data).map_err(MuragentError::Io)?;
420 }
421 Ok(())
422}
423
424fn upsert_trust(
425 trust_store: &mut TrustStore,
426 result: &ValidationResult,
427 author_pubkey_b64: &str,
428 existing: &Option<TrustEntry>,
429 surface: &str,
430) -> Result<(TrustLevel, PathBuf), MuragentError> {
431 let now = chrono::Utc::now().to_rfc3339();
432 let first_seen = existing
433 .as_ref()
434 .map(|e| e.first_seen.clone())
435 .unwrap_or_else(|| now.clone());
436 let level = existing
440 .as_ref()
441 .map(|e| e.trust_level.clone())
442 .unwrap_or(TrustLevel::Pending);
443
444 trust_store.upsert(TrustEntry {
445 public_key: author_pubkey_b64.to_string(),
446 display_name_seen: result.manifest.agent.display_name.clone(),
447 first_seen,
448 last_seen: now,
449 last_seen_surface: surface.to_string(),
450 trust_level: level.clone(),
451 fingerprint: trust::short_fingerprint(&result.author_pubkey),
452 word_list: trust::word_list_fingerprint(&result.author_pubkey),
453 rotated_from: existing.as_ref().and_then(|e| e.rotated_from.clone()),
454 superseded_at: existing.as_ref().and_then(|e| e.superseded_at.clone()),
455 last_rotation_at: existing.as_ref().and_then(|e| e.last_rotation_at.clone()),
456 });
457
458 Ok((level, PathBuf::new()))
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::identity::AgentIdentity;
465 use crate::muragent::writer::{MuragentWriter, build_manifest_from_profile};
466 use tempfile::TempDir;
467
468 #[test]
469 fn downgrade_broad_egress_resets_mode_and_clears_authorization() {
470 use crate::agent::{EgressAuthorization, McpServerEntry, McpServerNetwork};
471
472 let mut profile = AgentProfile::default_for_tests();
473 profile.mcp_servers = vec![
474 McpServerEntry {
475 name: "broad-server".to_string(),
476 command: "true".to_string(),
477 network: Some(McpServerNetwork {
478 mode: McpNetMode::BroadAudited,
479 authorization: Some(EgressAuthorization {
480 authorized_by: "operator".to_string(),
481 authorized_at_ms: 1,
482 }),
483 ..Default::default()
484 }),
485 ..Default::default()
486 },
487 McpServerEntry {
488 name: "restricted-server".to_string(),
489 command: "true".to_string(),
490 network: Some(McpServerNetwork {
491 mode: McpNetMode::Restricted,
492 allow_hosts: vec!["example.com".to_string()],
493 ..Default::default()
494 }),
495 ..Default::default()
496 },
497 ];
498
499 let downgraded = downgrade_broad_egress(&mut profile);
500
501 assert_eq!(downgraded, vec!["broad-server".to_string()]);
502
503 let broad = &profile.mcp_servers[0].network.as_ref().unwrap();
504 assert_eq!(broad.mode, McpNetMode::Inherit);
505 assert_eq!(broad.authorization, None);
506
507 let restricted = &profile.mcp_servers[1].network.as_ref().unwrap();
508 assert_eq!(restricted.mode, McpNetMode::Restricted);
509 assert_eq!(restricted.allow_hosts, vec!["example.com".to_string()]);
510 }
511
512 fn make_test_package(tmp: &TempDir) -> std::path::PathBuf {
513 let out = tmp.path().join("test.muragent");
514 let profile = AgentProfile::default_for_tests();
515 let identity = AgentIdentity::generate();
516 let manifest = build_manifest_from_profile(&profile, "2.13.0");
517 let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
518 let mut writer = MuragentWriter::new(manifest, profile_yaml, identity);
519 writer.add_icon("icon-512.png", b"fake-png".to_vec());
520 writer.write(&out).unwrap();
521 out
522 }
523
524 #[test]
525 fn install_extracts_sys_prompt_and_skills() {
526 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
530 let tmp = TempDir::new().unwrap();
531 let mur_home = tmp.path().join("mur");
532 let prev = std::env::var_os("MUR_HOME");
533 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
534
535 let profile = AgentProfile::default_for_tests();
536 let manifest = build_manifest_from_profile(&profile, "2.13.0");
537 let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
538 let mut writer = MuragentWriter::new(manifest, profile_yaml, AgentIdentity::generate());
539 writer.set_sys_prompt("You are a helpful test agent.".into());
540 writer.add_skill("demo.md", b"# demo skill\nbody".to_vec());
541 let out = tmp.path().join("withextras.muragent");
542 writer.write(&out).unwrap();
543
544 let archive = MuragentArchive::read(&out).unwrap();
545 let outcome = install(&archive, &mur_home, "test").unwrap();
546 let agent_dir = mur_home.join("agents").join(&outcome.manifest.agent.slug);
547
548 let prompt = fs::read_to_string(agent_dir.join("sys_prompt.md")).unwrap();
549 assert_eq!(prompt, "You are a helpful test agent.");
550 let skill = fs::read_to_string(agent_dir.join("skills").join("demo.md")).unwrap();
551 assert_eq!(skill, "# demo skill\nbody");
552
553 unsafe {
554 if let Some(p) = prev {
555 std::env::set_var("MUR_HOME", p);
556 } else {
557 std::env::remove_var("MUR_HOME");
558 }
559 }
560 }
561
562 fn make_test_package_with_identity(
563 tmp: &TempDir,
564 identity: &AgentIdentity,
565 ) -> std::path::PathBuf {
566 let out = tmp
567 .path()
568 .join(format!("{}.muragent", &identity.pubkey_text()[..8]));
569 let profile = AgentProfile::default_for_tests();
570 let manifest = build_manifest_from_profile(&profile, "2.13.0");
571 let profile_yaml = serde_yaml_ng::to_string(&profile).unwrap();
572 let mut writer = MuragentWriter::new(manifest, profile_yaml, identity.clone());
573 writer.add_icon("icon-512.png", b"fake-png".to_vec());
574 writer.write(&out).unwrap();
575 out
576 }
577
578 #[test]
579 fn rotation_manifest_missing_still_refuses() {
580 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
581 let tmp = TempDir::new().unwrap();
582 let mur_home = tmp.path().join("mur");
583 let prev = std::env::var_os("MUR_HOME");
584 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
585
586 let old_identity = AgentIdentity::generate();
587 let pkg_old = make_test_package_with_identity(&tmp, &old_identity);
588 let archive = MuragentArchive::read(&pkg_old).unwrap();
589 let outcome = install(&archive, &mur_home, "test").unwrap();
590 let slug = outcome.manifest.agent.slug.clone();
591
592 let new_identity = AgentIdentity::generate();
593 let profile = AgentProfile::default_for_tests();
594 let out2 = tmp.path().join("new2.muragent");
595 let manifest2 = build_manifest_from_profile(&profile, "2.14.0");
596 let profile_yaml2 = serde_yaml_ng::to_string(&profile).unwrap();
597 let mut writer2 = MuragentWriter::new(manifest2, profile_yaml2, new_identity);
598 writer2.add_icon("icon-512.png", b"fake-png".to_vec());
599 writer2.write(&out2).unwrap();
600 let archive2 = MuragentArchive::read(&out2).unwrap();
601 let agent_dir = mur_home.join("agents").join(&slug);
602 fs::remove_dir_all(&agent_dir).unwrap();
603
604 let err = install(&archive2, &mur_home, "test").unwrap_err();
605 assert!(
606 matches!(err, MuragentError::TrustRefused(_)),
607 "expected TrustRefused, got: {:?}",
608 err
609 );
610
611 unsafe {
612 if let Some(p) = prev {
613 std::env::set_var("MUR_HOME", p);
614 } else {
615 std::env::remove_var("MUR_HOME");
616 }
617 }
618 }
619
620 #[test]
621 fn reserved_local_files_are_rejected() {
622 use std::collections::BTreeMap;
625 for reserved in RESERVED_LOCAL_FILES {
626 let mut files = BTreeMap::new();
627 files.insert("profile.yaml".to_string(), b"ok".to_vec());
628 files.insert((*reserved).to_string(), b"ATTACKER-KEY".to_vec());
629 let archive = MuragentArchive { files };
630 assert!(
631 reject_reserved_local_files(&archive).is_err(),
632 "must reject package carrying {reserved}"
633 );
634 }
635 let mut files = BTreeMap::new();
637 files.insert("profile.yaml".to_string(), b"ok".to_vec());
638 files.insert("skills/demo.md".to_string(), b"skill".to_vec());
639 let archive = MuragentArchive { files };
640 assert!(reject_reserved_local_files(&archive).is_ok());
641 }
642
643 #[test]
644 fn display_name_slug_roundtrip() {
645 assert_eq!(display_name_slug("My Agent"), "my-agent");
646 assert_eq!(display_name_slug("Coach (Beta)"), "coach-beta");
647 assert_eq!(display_name_slug("test"), "test");
648 }
649
650 #[test]
651 fn install_then_update_preserves_data() {
652 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
653 let tmp = TempDir::new().unwrap();
654 let mur_home = tmp.path().join("mur");
655 let prev = std::env::var_os("MUR_HOME");
656 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
657
658 let pkg = make_test_package(&tmp);
659 let archive = MuragentArchive::read(&pkg).unwrap();
660 let outcome = install(&archive, &mur_home, "test").unwrap();
661 assert!(!outcome.was_update);
662 let slug = outcome.manifest.agent.slug.clone();
663 let agent_dir = mur_home.join("agents").join(&slug);
664 assert!(agent_dir.join("profile.yaml").exists());
665
666 let data_dir = agent_dir.join("data");
668 fs::create_dir_all(&data_dir).unwrap();
669 fs::write(data_dir.join("history.jsonl"), b"important").unwrap();
670
671 let outcome2 = install(&archive, &mur_home, "test").unwrap();
673 assert!(outcome2.was_update);
674 let preserved = fs::read(data_dir.join("history.jsonl")).unwrap();
675 assert_eq!(preserved, b"important");
676
677 unsafe {
679 if let Some(p) = prev {
680 std::env::set_var("MUR_HOME", p);
681 } else {
682 std::env::remove_var("MUR_HOME");
683 }
684 }
685 }
686
687 #[test]
688 fn update_preserves_local_identity_keypair() {
689 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
694 let tmp = TempDir::new().unwrap();
695 let mur_home = tmp.path().join("mur");
696 let prev = std::env::var_os("MUR_HOME");
697 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
698
699 let pkg = make_test_package(&tmp);
700 let archive = MuragentArchive::read(&pkg).unwrap();
701 let outcome = install(&archive, &mur_home, "test").unwrap();
702 let slug = outcome.manifest.agent.slug.clone();
703 let agent_dir = mur_home.join("agents").join(&slug);
704
705 fs::write(agent_dir.join("identity.key"), b"PRIVATE-KEY").unwrap();
707 fs::write(agent_dir.join("identity.pub"), b"PUBLIC-KEY").unwrap();
708
709 let outcome2 = install(&archive, &mur_home, "test").unwrap();
711 assert!(outcome2.was_update);
712
713 assert!(
714 agent_dir.join("identity.key").exists(),
715 "identity.key must survive an in-place update"
716 );
717 assert_eq!(
718 fs::read(agent_dir.join("identity.key")).unwrap(),
719 b"PRIVATE-KEY"
720 );
721 assert!(
722 agent_dir.join("identity.pub").exists(),
723 "identity.pub must survive an in-place update"
724 );
725
726 unsafe {
728 if let Some(p) = prev {
729 std::env::set_var("MUR_HOME", p);
730 } else {
731 std::env::remove_var("MUR_HOME");
732 }
733 }
734 }
735
736 #[test]
737 fn install_with_name_installs_under_override_name_and_refuses_collision() {
738 let _guard = crate::trust::test_env_lock::MUR_HOME_LOCK.lock().unwrap();
739 let tmp = TempDir::new().unwrap();
740 let mur_home = tmp.path().join("mur");
741 let prev = std::env::var_os("MUR_HOME");
742 unsafe { std::env::set_var("MUR_HOME", &mur_home) };
743
744 let pkg = make_test_package(&tmp);
745 let archive = MuragentArchive::read(&pkg).unwrap();
746
747 let outcome = install_with_name(&archive, &mur_home, "test", Some("clone-x")).unwrap();
748 assert!(!outcome.was_update);
749 let clone_dir = mur_home.join("agents").join("clone-x");
750 assert!(clone_dir.join("profile.yaml").exists());
751
752 let archive2 = MuragentArchive::read(&pkg).unwrap();
755 let err = install_with_name(&archive2, &mur_home, "test", Some("clone-x")).unwrap_err();
756 assert!(
757 matches!(err, MuragentError::Other(_)),
758 "expected refuse-on-collision error, got: {:?}",
759 err
760 );
761
762 unsafe {
763 if let Some(p) = prev {
764 std::env::set_var("MUR_HOME", p);
765 } else {
766 std::env::remove_var("MUR_HOME");
767 }
768 }
769 }
770}