1use std::path::Path;
13
14use anyhow::{Context, Result, bail, ensure};
15
16use super::worker_binary::{
17 apply_staged_execution_setting, bridge_launch, container_upload_ownership_args, stage_profile,
18};
19use super::{Controller, execute_checked};
20use crate::targets::{self, CommandExecutor, CommandSpec, ProcessExecutor};
21use mj_core::worker_launch::{
22 ReviewMcpDelivery, ReviewMcpServer, ReviewerLaunchConfig, reviewer_staging_profile_home,
23};
24
25pub fn reviewer_stager() -> mj_client::session::ReviewerStager {
29 mj_client::session::ReviewerStager::new(ControllerReviewerStager)
30}
31
32struct ControllerReviewerStager;
33
34impl mj_client::session::ReviewerStagerBackend for ControllerReviewerStager {
35 fn stage(
36 &self,
37 _config: mj_core::config::Config,
38 session: mj_core::state::SessionRecord,
39 profile_id: String,
40 generation: u64,
41 cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
42 ) -> Result<ReviewerLaunchConfig> {
43 let session_id = session.id.clone();
44 let controller = Controller::load()?;
45 let executor = crate::targets::CancellableProcessExecutor::new(cancelled)
46 .with_deadline(std::time::Duration::from_secs(90));
47 controller.stage_reviewer_profile_controlled(
48 &session_id,
49 &profile_id,
50 generation,
51 &[],
52 &executor,
53 )
54 }
55}
56
57impl Controller {
58 pub fn stage_reviewer_profile(
64 &self,
65 session_id: &str,
66 profile_id: &str,
67 generation: u64,
68 ) -> Result<ReviewerLaunchConfig> {
69 self.stage_reviewer_profile_controlled(
70 session_id,
71 profile_id,
72 generation,
73 &[],
74 &ProcessExecutor,
75 )
76 }
77
78 pub fn stage_reviewer_profile_controlled(
79 &self,
80 session_id: &str,
81 profile_id: &str,
82 generation: u64,
83 mcp_servers: &[ReviewMcpServer],
84 executor: &impl CommandExecutor,
85 ) -> Result<ReviewerLaunchConfig> {
86 let profile = self
87 .config
88 .profiles
89 .get(profile_id)
90 .with_context(|| format!("unknown profile {profile_id:?}"))?;
91 ensure!(
92 profile.enabled,
93 "reviewer profile {profile_id:?} is disabled"
94 );
95 let session = self
96 .state
97 .sessions
98 .get(session_id)
99 .with_context(|| format!("unknown session {session_id}"))?;
100 let target = session.target_runtime_settings(&self.config)?;
101 let execution_policy = profile
102 .kind
103 .effective_execution_policy(target.execution_policy);
104 if execution_policy.is_unconstrained() && !target.execution_policy.is_unconstrained() {
108 let session_unconstrained = self
109 .config
110 .profiles
111 .get(&session.last_profile)
112 .is_some_and(|session_profile| {
113 session_profile
114 .kind
115 .effective_execution_policy(target.execution_policy)
116 .is_unconstrained()
117 });
118 ensure!(
119 session_unconstrained,
120 "{} cannot review this session: it has no guardian approval mode, so it would run unconstrained while the session runs with approvals on target {:?}. Choose another reviewer, or run the session on a container target",
121 profile.kind.display_name(),
122 session.target_template_id
123 );
124 }
125 let (backend, worker_root) = self.worker_placement(session_id)?;
126
127 let staging = tempfile::tempdir().context("create reviewer staging directory")?;
128 let local = staging.path().join("profile");
129 profile.ensure_ready(profile_id)?;
130 stage_profile(profile, &local).with_context(|| format!("stage profile {profile_id:?}"))?;
131 apply_staged_execution_setting(profile.kind, execution_policy, &local)
132 .with_context(|| format!("stage profile {profile_id:?}"))?;
133 if !mcp_servers.is_empty()
137 && ReviewMcpDelivery::for_harness(profile.kind) == ReviewMcpDelivery::HarnessProfile
138 {
139 configure_staged_review_mcp(profile.kind, &local, mcp_servers)
140 .with_context(|| format!("configure reviewer MCP servers for {profile_id:?}"))?;
141 }
142 upload_reviewer_profile(executor, &backend, &worker_root, generation, &local)?;
143
144 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
145 let mut environment = profile.environment.resolved().clone();
146 environment.remove(profile.home_env());
150 let excluded_environment = profile.exclude_harness_environment(&mut environment);
153 if backend.container_engine() == Some("podman")
157 && profile.kind == mj_core::config::HarnessKind::Claude
158 {
159 environment.insert("IS_SANDBOX".into(), "1".into());
160 }
161 Ok(ReviewerLaunchConfig {
162 profile_id: profile_id.to_owned(),
163 harness: profile.kind,
164 bridge_command: bridge_command.into(),
165 bridge_args,
166 environment,
167 excluded_environment,
168 execution_policy,
169 model: None,
170 effort: None,
171 fast_mode: None,
172 generation,
173 mcp_servers: mcp_servers.to_vec(),
176 })
177 }
178}
179
180fn configure_staged_review_mcp(
188 harness: mj_core::config::HarnessKind,
189 profile_stage: &Path,
190 servers: &[ReviewMcpServer],
191) -> Result<()> {
192 let Some(file) = harness.mcp_config_file() else {
193 bail!("{harness:?} does not read MCP servers from its profile");
194 };
195 let kimi = harness == mj_core::config::HarnessKind::Kimi;
196 let path = profile_stage.join(file);
197 let mut document = match std::fs::read(&path) {
198 Ok(body) => serde_json::from_slice::<serde_json::Value>(&body)
199 .with_context(|| format!("parse staged reviewer configuration {}", path.display()))?,
200 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
201 serde_json::Value::Object(serde_json::Map::new())
202 }
203 Err(error) => {
204 return Err(error)
205 .with_context(|| format!("read staged reviewer configuration {}", path.display()));
206 }
207 };
208 let root = document.as_object_mut().with_context(|| {
209 format!(
210 "staged reviewer configuration {} must contain a JSON object",
211 path.display()
212 )
213 })?;
214 let configured = root
215 .entry("mcpServers")
216 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()))
217 .as_object_mut()
218 .with_context(|| {
219 format!(
220 "mcpServers in staged reviewer configuration {} must be a JSON object",
221 path.display()
222 )
223 })?;
224 for server in servers {
225 let mut entry = serde_json::json!({
226 "command": server.command,
227 "args": server.args,
228 });
229 if kimi {
230 let object = entry
231 .as_object_mut()
232 .expect("the server entry is a JSON object");
233 object.insert("transport".into(), "stdio".into());
234 object.insert("runtime_id".into(), "local".into());
235 } else {
236 let object = entry
237 .as_object_mut()
238 .expect("the server entry is a JSON object");
239 object.insert("type".into(), "stdio".into());
240 }
241 configured.insert(server.name.clone(), entry);
242 }
243 let mut body = serde_json::to_vec_pretty(&document)?;
244 body.push(b'\n');
245 mj_core::config::atomic_write(&path, &body)
246 .with_context(|| format!("write staged reviewer configuration {}", path.display()))
247}
248
249fn reviewer_profile_home(worker_root: &str, generation: u64) -> String {
255 reviewer_staging_profile_home(Path::new(worker_root), generation)
256 .to_string_lossy()
257 .into_owned()
258}
259
260fn upload_reviewer_profile(
266 executor: &impl CommandExecutor,
267 locator: &targets::TargetLocator,
268 worker_root: &str,
269 generation: u64,
270 local: &Path,
271) -> Result<()> {
272 let home = reviewer_profile_home(worker_root, generation);
273 match locator {
274 targets::TargetLocator::LocalBare { .. } => {
275 for command in [
276 CommandSpec::new("rm", ["-rf", "--", &home])
277 .purpose("clear the local reviewer profile"),
278 CommandSpec::new("mkdir", ["-p", &home])
279 .purpose("create the local reviewer profile directory"),
280 CommandSpec::new(
281 "cp",
282 [
283 "-R".to_owned(),
284 format!("{}/.", local.display()),
285 home.clone(),
286 ],
287 )
288 .purpose("install the local reviewer profile"),
289 CommandSpec::new("chmod", ["-R", "go-rwx", &home])
290 .purpose("restrict local reviewer profile permissions"),
291 ] {
292 execute_checked(executor, command)?;
293 }
294 }
295 targets::TargetLocator::LocalPodman { container_id, .. }
296 | targets::TargetLocator::LocalDocker { container_id, .. }
297 | targets::TargetLocator::AppleContainer { container_id, .. } => {
298 let engine = match locator {
299 targets::TargetLocator::LocalPodman { .. } => "podman",
300 targets::TargetLocator::LocalDocker { .. } => "docker",
301 targets::TargetLocator::AppleContainer { .. } => "container",
302 _ => unreachable!("matched local container target"),
303 };
304 for arguments in [
305 vec![
306 "exec".to_owned(),
307 container_id.clone(),
308 "rm".to_owned(),
309 "-rf".to_owned(),
310 "--".to_owned(),
311 home.clone(),
312 ],
313 vec![
314 "exec".to_owned(),
315 container_id.clone(),
316 "mkdir".to_owned(),
317 "-p".to_owned(),
318 home.clone(),
319 ],
320 vec![
321 "cp".to_owned(),
322 format!("{}/.", local.display()),
323 format!("{container_id}:{home}"),
324 ],
325 container_upload_ownership_args(engine, container_id, worker_root, &[&home]),
326 vec![
327 "exec".to_owned(),
328 container_id.clone(),
329 "chmod".to_owned(),
330 "-R".to_owned(),
331 "go-rwx".to_owned(),
332 home.clone(),
333 ],
334 ] {
335 execute_checked(
336 executor,
337 CommandSpec::new(engine, arguments).purpose("stage the reviewer profile"),
338 )?;
339 }
340 }
341 targets::TargetLocator::AwsEc2 { ssh, .. }
342 | targets::TargetLocator::SshBare { ssh, .. } => {
343 let incoming = format!("{home}.incoming");
344 execute_checked(
345 executor,
346 crate::targets::ssh_command(ssh, ["mkdir", "-p", worker_root])
347 .purpose("create the reviewer directory"),
348 )?;
349 execute_checked(
350 executor,
351 crate::targets::ssh_command(ssh, ["rm", "-rf", "--", &incoming, &home])
352 .purpose("clear the reviewer profile"),
353 )?;
354 execute_checked(
355 executor,
356 crate::targets::scp_upload(ssh, local, &incoming, true)
357 .purpose("upload the reviewer profile"),
358 )?;
359 execute_checked(
360 executor,
361 crate::targets::ssh_command(ssh, ["mv", &incoming, &home])
362 .purpose("install the reviewer profile"),
363 )?;
364 execute_checked(
365 executor,
366 crate::targets::ssh_command(ssh, ["chmod", "-R", "go-rwx", &home])
367 .purpose("restrict reviewer profile permissions"),
368 )?;
369 }
370 targets::TargetLocator::SshPodman {
371 ssh, container_id, ..
372 }
373 | targets::TargetLocator::SshDocker {
374 ssh, container_id, ..
375 } => {
376 let engine = match locator {
377 targets::TargetLocator::SshPodman { .. } => "podman",
378 targets::TargetLocator::SshDocker { .. } => "docker",
379 _ => unreachable!("matched remote container target"),
380 };
381 let worker = Path::new(worker_root)
384 .file_name()
385 .map(|name| name.to_string_lossy().into_owned())
386 .filter(|name| !name.is_empty())
387 .ok_or_else(|| anyhow::anyhow!("worker root {worker_root:?} names no worker"))?;
388 let upload = format!(
389 "{}/{worker}-reviewer-{generation}",
390 targets::REMOTE_UPLOAD_STAGING
391 );
392 execute_checked(
393 executor,
394 crate::targets::ssh_command(ssh, ["mkdir", "-p", targets::REMOTE_UPLOAD_STAGING])
395 .purpose("create remote reviewer staging"),
396 )?;
397 execute_checked(
398 executor,
399 crate::targets::ssh_command(ssh, ["rm", "-rf", "--", &upload])
400 .purpose("clear remote reviewer staging"),
401 )?;
402 execute_checked(
403 executor,
404 crate::targets::scp_upload(ssh, local, &upload, true)
405 .purpose("upload the remote reviewer profile"),
406 )?;
407 for arguments in [
408 vec![
409 engine.to_owned(),
410 "exec".to_owned(),
411 container_id.clone(),
412 "rm".to_owned(),
413 "-rf".to_owned(),
414 "--".to_owned(),
415 home.clone(),
416 ],
417 vec![
418 engine.to_owned(),
419 "exec".to_owned(),
420 container_id.clone(),
421 "mkdir".to_owned(),
422 "-p".to_owned(),
423 home.clone(),
424 ],
425 vec![
426 engine.to_owned(),
427 "cp".to_owned(),
428 format!("{upload}/."),
429 format!("{container_id}:{home}"),
430 ],
431 std::iter::once(engine.to_owned())
432 .chain(container_upload_ownership_args(
433 engine,
434 container_id,
435 worker_root,
436 &[&home],
437 ))
438 .collect(),
439 vec![
440 engine.to_owned(),
441 "exec".to_owned(),
442 container_id.clone(),
443 "chmod".to_owned(),
444 "-R".to_owned(),
445 "go-rwx".to_owned(),
446 home.clone(),
447 ],
448 ] {
449 execute_checked(
450 executor,
451 crate::targets::ssh_command(ssh, arguments)
452 .purpose("stage the remote reviewer profile"),
453 )?;
454 }
455 execute_checked(
456 executor,
457 crate::targets::ssh_command(ssh, ["rm", "-rf", "--", &upload])
458 .purpose("remove remote reviewer staging"),
459 )?;
460 }
461 }
462 if home.trim().is_empty() {
463 bail!("the reviewer profile home resolved to an empty path");
464 }
465 Ok(())
466}
467
468#[cfg(test)]
469mod tests {
470 use std::cell::RefCell;
471 use std::collections::BTreeMap;
472
473 use super::*;
474 use crate::controller::test_support::checkpoint_test_session;
475 use mj_core::config::{Config, HarnessKind, HarnessProfile, TargetTemplate};
476 use mj_core::state::{SessionState, State};
477
478 use crate::targets::CommandOutput;
479
480 struct RecordingExecutor {
481 commands: RefCell<Vec<CommandSpec>>,
482 }
483
484 impl RecordingExecutor {
485 fn new() -> Self {
486 Self {
487 commands: RefCell::new(Vec::new()),
488 }
489 }
490
491 fn script(&self) -> Vec<String> {
493 self.commands
494 .borrow()
495 .iter()
496 .map(|command| format!("{} {}", command.program, command.args.join(" ")))
497 .collect()
498 }
499 }
500
501 impl CommandExecutor for RecordingExecutor {
502 fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
503 self.commands.borrow_mut().push(command.clone());
504 Ok(CommandOutput {
505 status: 0,
506 stdout: Vec::new(),
507 stderr: Vec::new(),
508 })
509 }
510 }
511
512 const SESSION_ID: &str = "0123456789abcdef0123456789abcdef";
515
516 fn fixture(directory: &Path, locator: mj_core::state::TargetLocator) -> (Controller, String) {
517 let session_id = SESSION_ID;
518 let mut session = checkpoint_test_session(session_id);
519 session.target_template_id = "local".into();
520 session.state = SessionState::Running;
521 let template = match &locator {
522 mj_core::state::TargetLocator::LocalPodman { .. } => {
523 serde_json::from_str(r#"{"kind":"local-podman","image":"test"}"#).unwrap()
524 }
525 mj_core::state::TargetLocator::LocalDocker { .. } => {
526 serde_json::from_str(r#"{"kind":"local-docker","image":"test"}"#).unwrap()
527 }
528 mj_core::state::TargetLocator::SshPodman { .. } => serde_json::from_str(
529 r#"{"kind":"ssh-podman","host":"builder.test","image":"test"}"#,
530 )
531 .unwrap(),
532 _ => TargetTemplate::LocalBare,
533 };
534 session.target = Some(locator);
535 let mut config = Config::default();
536 config.targets.insert("local".into(), template);
537 for (id, kind) in [
538 ("codex", HarnessKind::Codex),
539 ("claude", HarnessKind::Claude),
540 ] {
541 let home = directory.join(id);
542 std::fs::create_dir_all(&home).unwrap();
543 config.profiles.insert(
544 id.to_owned(),
545 HarnessProfile {
546 enabled: true,
547 kind,
548 home,
549 environment: BTreeMap::from([("EXTRA".into(), "1".into())]).into(),
550 context_window_bytes: None,
551 subagents: Default::default(),
552 guardian_review_model: None,
553 },
554 );
555 }
556 (
557 Controller {
558 config,
559 state: State {
560 sessions: [(session_id.into(), session)].into_iter().collect(),
561 ..State::default()
562 },
563 },
564 session_id.to_owned(),
565 )
566 }
567
568 #[test]
569 fn staging_copies_the_chosen_profile_into_the_worker_root() {
570 let directory = tempfile::tempdir().unwrap();
571 let worker_root = directory.path().join(SESSION_ID);
572 std::fs::create_dir_all(directory.path().join("claude")).unwrap();
573 std::fs::write(directory.path().join("claude/CLAUDE.md"), b"reviewer").unwrap();
575 let (controller, session_id) = fixture(
576 directory.path(),
577 mj_core::state::TargetLocator::LocalBare {
578 worker_root: worker_root.clone(),
579 },
580 );
581 let executor = RecordingExecutor::new();
582
583 let config = controller
584 .stage_reviewer_profile_controlled(&session_id, "claude", 0, &[], &executor)
585 .unwrap();
586
587 assert_eq!(config.profile_id, "claude");
588 assert_eq!(config.harness, HarnessKind::Claude);
589 assert_eq!(config.generation, 0);
590 assert_eq!(config.model, None);
591 assert_eq!(config.effort, None);
592 assert!(
594 !config
595 .environment
596 .contains_key(HarnessKind::Claude.home_env())
597 );
598 assert_eq!(
599 config.environment.get("EXTRA").map(String::as_str),
600 Some("1")
601 );
602
603 let home = format!("{}/reviewer/profile", worker_root.display());
604 let script = executor.script();
605 let cleared = script
606 .iter()
607 .position(|line| line.starts_with("rm ") && line.contains(&home))
608 .expect("the previous reviewer profile is cleared");
609 let copied = script
610 .iter()
611 .position(|line| line.starts_with("cp ") && line.ends_with(&home))
612 .expect("the staged profile is installed");
613 assert!(
614 cleared < copied,
615 "a stale profile must go before the new one lands: {script:?}"
616 );
617 assert!(
618 script
619 .iter()
620 .any(|line| line.contains("go-rwx") && line.contains(&home)),
621 "the reviewer profile must not be world readable: {script:?}"
622 );
623 }
624
625 #[test]
627 fn remote_container_targets_stage_the_reviewer_on_the_host_not_in_the_worker_root() {
628 let directory = tempfile::tempdir().unwrap();
632 let container_id = crate::targets::resource_name(SESSION_ID).unwrap();
633 let (controller, session_id) = fixture(
634 directory.path(),
635 mj_core::state::TargetLocator::SshPodman {
636 host: "builder.test".into(),
637 container_id: container_id.clone(),
638 workspace_storage: Default::default(),
639 borrowed_from: None,
640 },
641 );
642 let executor = RecordingExecutor::new();
643
644 controller
645 .stage_reviewer_profile_controlled(&session_id, "codex", 3, &[], &executor)
646 .unwrap();
647
648 let script = executor.script();
649 let upload = script
650 .iter()
651 .find(|line| line.starts_with("scp "))
652 .expect("the profile is uploaded with scp")
653 .clone();
654 let staging = format!(
655 "{}/{session_id}-reviewer-3",
656 crate::targets::REMOTE_UPLOAD_STAGING
657 );
658 assert!(
659 upload.contains(&staging),
660 "the upload lands in host staging: {upload}"
661 );
662 assert!(
663 !upload.contains("/var/lib/hel/workers/"),
664 "the upload does not target the container's worker root: {upload}"
665 );
666 let copy = script
667 .iter()
668 .find(|line| line.contains("cp") && line.contains(&format!("{container_id}:")))
669 .unwrap_or_else(|| {
670 panic!("the engine copies the staged profile into the container: {script:?}")
671 });
672 assert!(
673 copy.contains(&format!("{staging}/.")) && copy.contains(&format!("{container_id}:")),
674 "the engine copies from host staging into the container: {copy}"
675 );
676 let last = script.last().unwrap();
677 assert!(
678 last.contains("'rm'")
679 && last.contains(&format!("'{staging}'"))
680 && !last.contains("podman"),
681 "host staging is removed afterwards: {script:?}"
682 );
683 }
684
685 #[test]
686
687 fn local_container_targets_stage_the_reviewer_through_their_engine() {
688 let directory = tempfile::tempdir().unwrap();
689 let container_id = crate::targets::resource_name(SESSION_ID).unwrap();
690 for (locator, engine) in [
691 (
692 mj_core::state::TargetLocator::LocalPodman {
693 borrowed_from: None,
694 container_id: container_id.clone(),
695 workspace_storage: Default::default(),
696 },
697 "podman",
698 ),
699 (
700 mj_core::state::TargetLocator::LocalDocker {
701 borrowed_from: None,
702 container_id: container_id.clone(),
703 },
704 "docker",
705 ),
706 ] {
707 let (controller, session_id) = fixture(directory.path(), locator);
708 let executor = RecordingExecutor::new();
709
710 controller
711 .stage_reviewer_profile_controlled(&session_id, "codex", 3, &[], &executor)
712 .unwrap();
713
714 let script = executor.script();
715 assert!(
716 script
717 .iter()
718 .all(|line| line.starts_with(&format!("{engine} "))),
719 "a container target is reached only through its engine: {script:?}"
720 );
721 let home = script
722 .iter()
723 .find_map(|line| {
724 line.split(' ')
725 .find(|word| word.contains("/reviewer/profile"))
726 })
727 .expect("the reviewer profile is placed")
728 .to_owned();
729 assert!(
730 home.contains(&format!("/{session_id}")),
731 "the reviewer lives under this session's worker root: {home}"
732 );
733 assert!(
735 !script.iter().any(|line| {
736 line.contains("run") || line.contains("git") || line.contains("create")
737 }),
738 "staging a reviewer provisions nothing: {script:?}"
739 );
740 }
741 }
742
743 #[test]
744 fn a_claude_reviewer_in_a_podman_session_is_told_it_is_sandboxed() {
745 let directory = tempfile::tempdir().unwrap();
746 let container_id = crate::targets::resource_name(SESSION_ID).unwrap();
747 let podman = mj_core::state::TargetLocator::LocalPodman {
748 borrowed_from: None,
749 container_id: container_id.clone(),
750 workspace_storage: Default::default(),
751 };
752 let docker = mj_core::state::TargetLocator::LocalDocker {
753 borrowed_from: None,
754 container_id,
755 };
756 for (locator, profile_id, sandboxed) in [
757 (podman.clone(), "claude", true),
758 (podman, "codex", false),
759 (docker, "claude", false),
760 ] {
761 let (controller, session_id) = fixture(directory.path(), locator);
762 let config = controller
763 .stage_reviewer_profile_controlled(
764 &session_id,
765 profile_id,
766 0,
767 &[],
768 &RecordingExecutor::new(),
769 )
770 .unwrap();
771 assert_eq!(
772 config.environment.get("IS_SANDBOX").map(String::as_str),
773 sandboxed.then_some("1"),
774 "{profile_id}"
775 );
776 }
777 }
778
779 #[test]
780 fn an_unconstrained_reviewer_is_refused_for_a_session_that_runs_with_approvals() {
781 let directory = tempfile::tempdir().unwrap();
782 let (mut controller, session_id) = fixture(
783 directory.path(),
784 mj_core::state::TargetLocator::LocalBare {
785 worker_root: directory.path().join(SESSION_ID),
786 },
787 );
788 controller.config.profiles.insert(
789 "muse".into(),
790 HarnessProfile {
791 enabled: true,
792 kind: HarnessKind::Muse,
793 home: directory.path().join("muse"),
794 environment: BTreeMap::new().into(),
795 context_window_bytes: None,
796 subagents: Default::default(),
797 guardian_review_model: None,
798 },
799 );
800 let executor = RecordingExecutor::new();
801
802 let error = controller
805 .stage_reviewer_profile_controlled(&session_id, "muse", 0, &[], &executor)
806 .unwrap_err();
807
808 assert!(
809 format!("{error:#}").contains("cannot review this session"),
810 "{error:#}"
811 );
812 assert!(executor.commands.borrow().is_empty());
813 }
814
815 #[test]
816 fn a_muse_reviewer_is_staged_with_the_permission_profile_its_policy_enforces() {
817 let directory = tempfile::tempdir().unwrap();
818 let worker_root = directory.path().join(SESSION_ID);
819 let (mut controller, session_id) = fixture(
820 directory.path(),
821 mj_core::state::TargetLocator::LocalBare {
822 worker_root: worker_root.clone(),
823 },
824 );
825 let home = directory.path().join("muse");
828 std::fs::create_dir_all(&home).unwrap();
829 std::fs::write(
830 home.join("settings.json"),
831 br#"{"schema_version":1,"permissions":{"schema_version":1,"default_profile":":auto-review"}}"#,
832 )
833 .unwrap();
834 controller.config.profiles.insert(
835 "muse".into(),
836 HarnessProfile {
837 enabled: true,
838 kind: HarnessKind::Muse,
839 home,
840 environment: BTreeMap::new().into(),
841 context_window_bytes: None,
842 subagents: Default::default(),
843 guardian_review_model: None,
844 },
845 );
846 controller
849 .state
850 .sessions
851 .get_mut(&session_id)
852 .unwrap()
853 .last_profile = "muse".into();
854
855 controller
856 .stage_reviewer_profile_controlled(&session_id, "muse", 0, &[], &ProcessExecutor)
857 .unwrap();
858
859 let staged: serde_json::Value = serde_json::from_slice(
860 &std::fs::read(worker_root.join("reviewer/profile/settings.json")).unwrap(),
861 )
862 .unwrap();
863 assert_eq!(staged["permissions"]["default_profile"], ":unrestricted");
864 }
865
866 #[test]
867 fn a_new_generation_travels_to_the_worker_so_it_starts_a_fresh_reviewer() {
868 let directory = tempfile::tempdir().unwrap();
869 let (controller, session_id) = fixture(
870 directory.path(),
871 mj_core::state::TargetLocator::LocalBare {
872 worker_root: directory.path().join(SESSION_ID),
873 },
874 );
875 let executor = RecordingExecutor::new();
876
877 let first = controller
878 .stage_reviewer_profile_controlled(&session_id, "codex", 0, &[], &executor)
879 .unwrap();
880 let second = controller
881 .stage_reviewer_profile_controlled(&session_id, "codex", 1, &[], &executor)
882 .unwrap();
883
884 assert!(first.reusable_for(&first));
885 assert!(
886 !first.reusable_for(&second),
887 "a new generation must not reload the old conversation"
888 );
889 }
890}