1use std::path::Path;
13
14use anyhow::{Context, Result, bail};
15
16use super::worker_binary::{bridge_launch, stage_profile};
17use super::{Controller, execute_checked, scp_command_spec, ssh_command_spec};
18use hel::hel_targets::{self, CommandExecutor, CommandSpec, ProcessExecutor};
19use hel::hel_worker_launch::{
20 REVIEWER_DIR, ReviewMcpDelivery, ReviewMcpServer, ReviewerLaunchConfig,
21 reviewer_staging_profile_home,
22};
23
24pub fn reviewer_stager() -> mj_client::session::ReviewerStager {
28 mj_client::session::ReviewerStager::new(ControllerReviewerStager)
29}
30
31struct ControllerReviewerStager;
32
33impl mj_client::session::ReviewerStagerBackend for ControllerReviewerStager {
34 fn stage(
35 &self,
36 config: hel::hel_config::HelConfig,
37 session: hel::hel_state::SessionRecord,
38 profile_id: String,
39 generation: u64,
40 ) -> Result<ReviewerLaunchConfig> {
41 let session_id = session.id.clone();
42 let controller = Controller {
43 config,
44 state: hel::hel_state::HelState {
45 sessions: std::collections::BTreeMap::from([(session_id.clone(), session)]),
46 ..hel::hel_state::HelState::default()
47 },
48 };
49 controller.stage_reviewer_profile(&session_id, &profile_id, generation)
50 }
51}
52
53impl Controller {
54 pub fn stage_reviewer_profile(
60 &self,
61 session_id: &str,
62 profile_id: &str,
63 generation: u64,
64 ) -> Result<ReviewerLaunchConfig> {
65 self.stage_reviewer_profile_controlled(
66 session_id,
67 profile_id,
68 generation,
69 &[],
70 &ProcessExecutor,
71 )
72 }
73
74 pub fn stage_reviewer_profile_with_mcp(
82 &self,
83 session_id: &str,
84 profile_id: &str,
85 generation: u64,
86 mcp_servers: &[ReviewMcpServer],
87 dispatch_tool: bool,
88 ) -> Result<ReviewerLaunchConfig> {
89 let mut servers = mcp_servers.to_vec();
90 if dispatch_tool {
91 let (_, worker_root) = self.worker_placement(session_id)?;
92 servers.push(review_dispatch_server(&worker_root));
93 }
94 self.stage_reviewer_profile_controlled(
95 session_id,
96 profile_id,
97 generation,
98 &servers,
99 &ProcessExecutor,
100 )
101 }
102
103 pub fn stage_reviewer_profile_controlled(
104 &self,
105 session_id: &str,
106 profile_id: &str,
107 generation: u64,
108 mcp_servers: &[ReviewMcpServer],
109 executor: &impl CommandExecutor,
110 ) -> Result<ReviewerLaunchConfig> {
111 let profile = self
112 .config
113 .profiles
114 .get(profile_id)
115 .with_context(|| format!("unknown profile {profile_id:?}"))?;
116 if !profile.kind.supports_injected_mcp() {
117 bail!(
118 "Muse Code cannot be a reviewer: muse-acp does not accept the required MCP tools. Select another reviewer profile."
119 );
120 }
121 let session = self
122 .state
123 .sessions
124 .get(session_id)
125 .with_context(|| format!("unknown session {session_id}"))?;
126 let target = self
127 .config
128 .targets
129 .get(&session.target_template_id)
130 .context("session target template is missing")?;
131 let execution_policy = target.execution_policy();
132 let (backend, worker_root) = self.worker_placement(session_id)?;
133
134 let staging = tempfile::tempdir().context("create reviewer staging directory")?;
135 let local = staging.path().join("profile");
136 stage_profile(profile, &local).with_context(|| format!("stage profile {profile_id:?}"))?;
137 if !mcp_servers.is_empty()
141 && ReviewMcpDelivery::for_harness(profile.kind) == ReviewMcpDelivery::HarnessProfile
142 {
143 configure_staged_review_mcp(profile.kind, &local, mcp_servers)
144 .with_context(|| format!("configure reviewer MCP servers for {profile_id:?}"))?;
145 }
146 upload_reviewer_profile(executor, &backend, &worker_root, generation, &local)?;
147
148 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
149 let mut environment = profile.environment.clone();
150 environment.remove(profile.home_env());
154 Ok(ReviewerLaunchConfig {
155 profile_id: profile_id.to_owned(),
156 harness: profile.kind,
157 bridge_command: bridge_command.into(),
158 bridge_args,
159 environment,
160 execution_policy,
161 model: None,
162 effort: None,
163 generation,
164 mcp_servers: match ReviewMcpDelivery::for_harness(profile.kind) {
168 ReviewMcpDelivery::Acp => mcp_servers.to_vec(),
169 ReviewMcpDelivery::HarnessProfile => Vec::new(),
170 },
171 })
172 }
173}
174
175fn configure_staged_review_mcp(
183 harness: hel::hel_config::HarnessKind,
184 profile_stage: &Path,
185 servers: &[ReviewMcpServer],
186) -> Result<()> {
187 let (file, kimi) = match harness {
188 hel::hel_config::HarnessKind::Claude => (".claude.json", false),
189 hel::hel_config::HarnessKind::Kimi => ("mcp.json", true),
190 other => bail!("{other:?} does not read MCP servers from its profile"),
191 };
192 let path = profile_stage.join(file);
193 let mut document = match std::fs::read(&path) {
194 Ok(body) => serde_json::from_slice::<serde_json::Value>(&body)
195 .with_context(|| format!("parse staged reviewer configuration {}", path.display()))?,
196 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
197 serde_json::Value::Object(serde_json::Map::new())
198 }
199 Err(error) => {
200 return Err(error)
201 .with_context(|| format!("read staged reviewer configuration {}", path.display()));
202 }
203 };
204 let root = document.as_object_mut().with_context(|| {
205 format!(
206 "staged reviewer configuration {} must contain a JSON object",
207 path.display()
208 )
209 })?;
210 let configured = root
211 .entry("mcpServers")
212 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()))
213 .as_object_mut()
214 .with_context(|| {
215 format!(
216 "mcpServers in staged reviewer configuration {} must be a JSON object",
217 path.display()
218 )
219 })?;
220 for server in servers {
221 let mut entry = serde_json::json!({
222 "command": server.command,
223 "args": server.args,
224 });
225 if kimi {
226 let object = entry
227 .as_object_mut()
228 .expect("the server entry is a JSON object");
229 object.insert("transport".into(), "stdio".into());
230 object.insert("runtime_id".into(), "local".into());
231 } else {
232 let object = entry
233 .as_object_mut()
234 .expect("the server entry is a JSON object");
235 object.insert("type".into(), "stdio".into());
236 }
237 configured.insert(server.name.clone(), entry);
238 }
239 let mut body = serde_json::to_vec_pretty(&document)?;
240 body.push(b'\n');
241 hel::hel_config::atomic_write(&path, &body)
242 .with_context(|| format!("write staged reviewer configuration {}", path.display()))
243}
244
245fn review_dispatch_server(worker_root: &str) -> ReviewMcpServer {
249 let socket = format!(
250 "{worker_root}/{}/{}",
251 REVIEWER_DIR,
252 hel::hel_review::mcp::REVIEW_DISPATCH_SOCKET
253 );
254 ReviewMcpServer {
255 name: hel::hel_review::mcp::REVIEW_MCP_SERVER_NAME.to_owned(),
256 command: Path::new(worker_root).join("hel"),
257 args: vec![
258 "worker".to_owned(),
259 "review-mcp".to_owned(),
260 "--socket".to_owned(),
261 socket,
262 ],
263 }
264}
265
266fn reviewer_profile_home(worker_root: &str, generation: u64) -> String {
272 reviewer_staging_profile_home(Path::new(worker_root), generation)
273 .to_string_lossy()
274 .into_owned()
275}
276
277fn upload_reviewer_profile(
283 executor: &impl CommandExecutor,
284 locator: &hel_targets::TargetLocator,
285 worker_root: &str,
286 generation: u64,
287 local: &Path,
288) -> Result<()> {
289 let home = reviewer_profile_home(worker_root, generation);
290 match locator {
291 hel_targets::TargetLocator::LocalBare { .. } => {
292 for command in [
293 CommandSpec::new("rm", ["-rf", "--", &home])
294 .purpose("clear the local reviewer profile"),
295 CommandSpec::new("mkdir", ["-p", &home])
296 .purpose("create the local reviewer profile directory"),
297 CommandSpec::new(
298 "cp",
299 [
300 "-R".to_owned(),
301 format!("{}/.", local.display()),
302 home.clone(),
303 ],
304 )
305 .purpose("install the local reviewer profile"),
306 CommandSpec::new("chmod", ["-R", "go-rwx", &home])
307 .purpose("restrict local reviewer profile permissions"),
308 ] {
309 execute_checked(executor, command)?;
310 }
311 }
312 hel_targets::TargetLocator::LocalPodman { container_id, .. }
313 | hel_targets::TargetLocator::LocalDocker { container_id }
314 | hel_targets::TargetLocator::AppleContainer { container_id } => {
315 let engine = match locator {
316 hel_targets::TargetLocator::LocalPodman { .. } => "podman",
317 hel_targets::TargetLocator::LocalDocker { .. } => "docker",
318 hel_targets::TargetLocator::AppleContainer { .. } => "container",
319 _ => unreachable!("matched local container target"),
320 };
321 for arguments in [
322 vec![
323 "exec".to_owned(),
324 container_id.clone(),
325 "rm".to_owned(),
326 "-rf".to_owned(),
327 "--".to_owned(),
328 home.clone(),
329 ],
330 vec![
331 "exec".to_owned(),
332 container_id.clone(),
333 "mkdir".to_owned(),
334 "-p".to_owned(),
335 home.clone(),
336 ],
337 vec![
338 "cp".to_owned(),
339 format!("{}/.", local.display()),
340 format!("{container_id}:{home}"),
341 ],
342 vec![
343 "exec".to_owned(),
344 container_id.clone(),
345 "chmod".to_owned(),
346 "-R".to_owned(),
347 "go-rwx".to_owned(),
348 home.clone(),
349 ],
350 ] {
351 execute_checked(
352 executor,
353 CommandSpec::new(engine, arguments).purpose("stage the reviewer profile"),
354 )?;
355 }
356 }
357 hel_targets::TargetLocator::AwsEc2 { ssh, .. }
358 | hel_targets::TargetLocator::SshBare { ssh, .. } => {
359 let incoming = format!("{home}.incoming");
360 execute_checked(
361 executor,
362 ssh_command_spec(ssh, ["mkdir", "-p", worker_root])
363 .purpose("create the reviewer directory"),
364 )?;
365 execute_checked(
366 executor,
367 ssh_command_spec(ssh, ["rm", "-rf", "--", &incoming, &home])
368 .purpose("clear the reviewer profile"),
369 )?;
370 execute_checked(
371 executor,
372 scp_command_spec(ssh, local, &incoming, true)
373 .purpose("upload the reviewer profile"),
374 )?;
375 execute_checked(
376 executor,
377 ssh_command_spec(ssh, ["mv", &incoming, &home])
378 .purpose("install the reviewer profile"),
379 )?;
380 execute_checked(
381 executor,
382 ssh_command_spec(ssh, ["chmod", "-R", "go-rwx", &home])
383 .purpose("restrict reviewer profile permissions"),
384 )?;
385 }
386 hel_targets::TargetLocator::SshPodman {
387 ssh, container_id, ..
388 }
389 | hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
390 let engine = match locator {
391 hel_targets::TargetLocator::SshPodman { .. } => "podman",
392 hel_targets::TargetLocator::SshDocker { .. } => "docker",
393 _ => unreachable!("matched remote container target"),
394 };
395 let upload = format!("{worker_root}/.reviewer-upload-{generation}");
396 execute_checked(
397 executor,
398 ssh_command_spec(ssh, ["rm", "-rf", "--", &upload])
399 .purpose("clear remote reviewer staging"),
400 )?;
401 execute_checked(
402 executor,
403 scp_command_spec(ssh, local, &upload, true)
404 .purpose("upload the remote reviewer profile"),
405 )?;
406 for arguments in [
407 vec![
408 engine.to_owned(),
409 "exec".to_owned(),
410 container_id.clone(),
411 "rm".to_owned(),
412 "-rf".to_owned(),
413 "--".to_owned(),
414 home.clone(),
415 ],
416 vec![
417 engine.to_owned(),
418 "exec".to_owned(),
419 container_id.clone(),
420 "mkdir".to_owned(),
421 "-p".to_owned(),
422 home.clone(),
423 ],
424 vec![
425 engine.to_owned(),
426 "cp".to_owned(),
427 format!("{upload}/."),
428 format!("{container_id}:{home}"),
429 ],
430 vec![
431 engine.to_owned(),
432 "exec".to_owned(),
433 container_id.clone(),
434 "chmod".to_owned(),
435 "-R".to_owned(),
436 "go-rwx".to_owned(),
437 home.clone(),
438 ],
439 ] {
440 execute_checked(
441 executor,
442 ssh_command_spec(ssh, arguments).purpose("stage the remote reviewer profile"),
443 )?;
444 }
445 execute_checked(
446 executor,
447 ssh_command_spec(ssh, ["rm", "-rf", "--", &upload])
448 .purpose("remove remote reviewer staging"),
449 )?;
450 }
451 }
452 if home.trim().is_empty() {
453 bail!("the reviewer profile home resolved to an empty path");
454 }
455 Ok(())
456}
457
458#[cfg(test)]
459mod tests {
460 use std::cell::RefCell;
461 use std::collections::BTreeMap;
462
463 use super::*;
464 use crate::hel_controller::test_support::checkpoint_test_session;
465 use hel::hel_config::{HarnessKind, HarnessProfile, HelConfig, TargetTemplate};
466 use hel::hel_state::{HelState, SessionState};
467 use hel::hel_targets::CommandOutput;
468
469 struct RecordingExecutor {
470 commands: RefCell<Vec<CommandSpec>>,
471 }
472
473 impl RecordingExecutor {
474 fn new() -> Self {
475 Self {
476 commands: RefCell::new(Vec::new()),
477 }
478 }
479
480 fn script(&self) -> Vec<String> {
482 self.commands
483 .borrow()
484 .iter()
485 .map(|command| format!("{} {}", command.program, command.args.join(" ")))
486 .collect()
487 }
488 }
489
490 impl CommandExecutor for RecordingExecutor {
491 fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
492 self.commands.borrow_mut().push(command.clone());
493 Ok(CommandOutput {
494 status: 0,
495 stdout: Vec::new(),
496 stderr: Vec::new(),
497 })
498 }
499 }
500
501 const SESSION_ID: &str = "0123456789abcdef0123456789abcdef";
504
505 fn fixture(directory: &Path, locator: hel::hel_state::TargetLocator) -> (Controller, String) {
506 let session_id = SESSION_ID;
507 let mut session = checkpoint_test_session(session_id);
508 session.target_template_id = "local".into();
509 session.state = SessionState::Running;
510 session.target = Some(locator);
511 let mut config = HelConfig::default();
512 config
513 .targets
514 .insert("local".into(), TargetTemplate::LocalBare);
515 for (id, kind) in [
516 ("codex", HarnessKind::Codex),
517 ("claude", HarnessKind::Claude),
518 ] {
519 let home = directory.join(id);
520 std::fs::create_dir_all(&home).unwrap();
521 config.profiles.insert(
522 id.to_owned(),
523 HarnessProfile {
524 kind,
525 home,
526 environment: BTreeMap::from([("EXTRA".into(), "1".into())]),
527 context_window_bytes: None,
528 },
529 );
530 }
531 (
532 Controller {
533 config,
534 state: HelState {
535 sessions: BTreeMap::from([(session_id.into(), session)]),
536 ..HelState::default()
537 },
538 },
539 session_id.to_owned(),
540 )
541 }
542
543 #[test]
544 fn staging_copies_the_chosen_profile_into_the_worker_root() {
545 let directory = tempfile::tempdir().unwrap();
546 let worker_root = directory.path().join(SESSION_ID);
547 std::fs::create_dir_all(directory.path().join("claude")).unwrap();
548 std::fs::write(directory.path().join("claude/CLAUDE.md"), b"reviewer").unwrap();
550 let (controller, session_id) = fixture(
551 directory.path(),
552 hel::hel_state::TargetLocator::LocalBare {
553 worker_root: worker_root.clone(),
554 },
555 );
556 let executor = RecordingExecutor::new();
557
558 let config = controller
559 .stage_reviewer_profile_controlled(&session_id, "claude", 0, &[], &executor)
560 .unwrap();
561
562 assert_eq!(config.profile_id, "claude");
563 assert_eq!(config.harness, HarnessKind::Claude);
564 assert_eq!(config.generation, 0);
565 assert_eq!(config.model, None);
566 assert_eq!(config.effort, None);
567 assert!(
569 !config
570 .environment
571 .contains_key(HarnessKind::Claude.home_env())
572 );
573 assert_eq!(
574 config.environment.get("EXTRA").map(String::as_str),
575 Some("1")
576 );
577
578 let home = format!("{}/reviewer/profile", worker_root.display());
579 let script = executor.script();
580 let cleared = script
581 .iter()
582 .position(|line| line.starts_with("rm ") && line.contains(&home))
583 .expect("the previous reviewer profile is cleared");
584 let copied = script
585 .iter()
586 .position(|line| line.starts_with("cp ") && line.ends_with(&home))
587 .expect("the staged profile is installed");
588 assert!(
589 cleared < copied,
590 "a stale profile must go before the new one lands: {script:?}"
591 );
592 assert!(
593 script
594 .iter()
595 .any(|line| line.contains("go-rwx") && line.contains(&home)),
596 "the reviewer profile must not be world readable: {script:?}"
597 );
598 }
599
600 #[test]
601 fn local_container_targets_stage_the_reviewer_through_their_engine() {
602 let directory = tempfile::tempdir().unwrap();
603 let container_id = hel::hel_targets::resource_name(SESSION_ID).unwrap();
604 for (locator, engine) in [
605 (
606 hel::hel_state::TargetLocator::LocalPodman {
607 container_id: container_id.clone(),
608 workspace_storage: Default::default(),
609 },
610 "podman",
611 ),
612 (
613 hel::hel_state::TargetLocator::LocalDocker {
614 container_id: container_id.clone(),
615 },
616 "docker",
617 ),
618 ] {
619 let (controller, session_id) = fixture(directory.path(), locator);
620 let executor = RecordingExecutor::new();
621
622 controller
623 .stage_reviewer_profile_controlled(&session_id, "codex", 3, &[], &executor)
624 .unwrap();
625
626 let script = executor.script();
627 assert!(
628 script
629 .iter()
630 .all(|line| line.starts_with(&format!("{engine} "))),
631 "a container target is reached only through its engine: {script:?}"
632 );
633 let home = script
634 .iter()
635 .find_map(|line| {
636 line.split(' ')
637 .find(|word| word.contains("/reviewer/profile"))
638 })
639 .expect("the reviewer profile is placed")
640 .to_owned();
641 assert!(
642 home.contains(&format!("/{session_id}")),
643 "the reviewer lives under this session's worker root: {home}"
644 );
645 assert!(
647 !script.iter().any(|line| {
648 line.contains("run") || line.contains("git") || line.contains("create")
649 }),
650 "staging a reviewer provisions nothing: {script:?}"
651 );
652 }
653 }
654
655 #[test]
656 fn an_unknown_profile_is_refused_before_anything_is_copied() {
657 let directory = tempfile::tempdir().unwrap();
658 let (controller, session_id) = fixture(
659 directory.path(),
660 hel::hel_state::TargetLocator::LocalBare {
661 worker_root: directory.path().join(SESSION_ID),
662 },
663 );
664 let executor = RecordingExecutor::new();
665
666 let error = controller
667 .stage_reviewer_profile_controlled(&session_id, "missing", 0, &[], &executor)
668 .unwrap_err();
669
670 assert!(format!("{error:#}").contains("unknown profile"));
671 assert!(executor.commands.borrow().is_empty());
672 }
673
674 #[test]
675 fn a_new_generation_travels_to_the_worker_so_it_starts_a_fresh_reviewer() {
676 let directory = tempfile::tempdir().unwrap();
677 let (controller, session_id) = fixture(
678 directory.path(),
679 hel::hel_state::TargetLocator::LocalBare {
680 worker_root: directory.path().join(SESSION_ID),
681 },
682 );
683 let executor = RecordingExecutor::new();
684
685 let first = controller
686 .stage_reviewer_profile_controlled(&session_id, "codex", 0, &[], &executor)
687 .unwrap();
688 let second = controller
689 .stage_reviewer_profile_controlled(&session_id, "codex", 1, &[], &executor)
690 .unwrap();
691
692 assert!(first.reusable_for(&first));
693 assert!(
694 !first.reusable_for(&second),
695 "a new generation must not reload the old conversation"
696 );
697 }
698}