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