1use std::path::Path;
13
14use anyhow::{Context, Result, bail, ensure};
15
16use super::worker_binary::{bridge_launch, container_upload_ownership_args, 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 ensure!(
117 profile.enabled,
118 "reviewer profile {profile_id:?} is disabled"
119 );
120 if !profile.kind.supports_injected_mcp() {
121 bail!(
122 "Muse Code cannot be a reviewer: muse-acp does not accept the required MCP tools. Select another reviewer profile."
123 );
124 }
125 let session = self
126 .state
127 .sessions
128 .get(session_id)
129 .with_context(|| format!("unknown session {session_id}"))?;
130 let target = self
131 .config
132 .targets
133 .get(&session.target_template_id)
134 .context("session target template is missing")?;
135 let execution_policy = target.execution_policy();
136 let (backend, worker_root) = self.worker_placement(session_id)?;
137
138 let staging = tempfile::tempdir().context("create reviewer staging directory")?;
139 let local = staging.path().join("profile");
140 stage_profile(profile, &local).with_context(|| format!("stage profile {profile_id:?}"))?;
141 if !mcp_servers.is_empty()
145 && ReviewMcpDelivery::for_harness(profile.kind) == ReviewMcpDelivery::HarnessProfile
146 {
147 configure_staged_review_mcp(profile.kind, &local, mcp_servers)
148 .with_context(|| format!("configure reviewer MCP servers for {profile_id:?}"))?;
149 }
150 upload_reviewer_profile(executor, &backend, &worker_root, generation, &local)?;
151
152 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
153 let mut environment = profile.environment.clone();
154 environment.remove(profile.home_env());
158 Ok(ReviewerLaunchConfig {
159 profile_id: profile_id.to_owned(),
160 harness: profile.kind,
161 bridge_command: bridge_command.into(),
162 bridge_args,
163 environment,
164 execution_policy,
165 model: None,
166 effort: None,
167 generation,
168 mcp_servers: match ReviewMcpDelivery::for_harness(profile.kind) {
172 ReviewMcpDelivery::Acp => mcp_servers.to_vec(),
173 ReviewMcpDelivery::HarnessProfile => Vec::new(),
174 },
175 })
176 }
177}
178
179fn configure_staged_review_mcp(
187 harness: hel::hel_config::HarnessKind,
188 profile_stage: &Path,
189 servers: &[ReviewMcpServer],
190) -> Result<()> {
191 let (file, kimi) = match harness {
192 hel::hel_config::HarnessKind::Claude => (".claude.json", false),
193 hel::hel_config::HarnessKind::Kimi => ("mcp.json", true),
194 other => bail!("{other:?} does not read MCP servers from its profile"),
195 };
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 hel::hel_config::atomic_write(&path, &body)
246 .with_context(|| format!("write staged reviewer configuration {}", path.display()))
247}
248
249fn review_dispatch_server(worker_root: &str) -> ReviewMcpServer {
253 let socket = format!(
254 "{worker_root}/{}/{}",
255 REVIEWER_DIR,
256 hel::hel_review::mcp::REVIEW_DISPATCH_SOCKET
257 );
258 ReviewMcpServer {
259 name: hel::hel_review::mcp::REVIEW_MCP_SERVER_NAME.to_owned(),
260 command: Path::new(worker_root).join("hel"),
261 args: vec![
262 "worker".to_owned(),
263 "review-mcp".to_owned(),
264 "--socket".to_owned(),
265 socket,
266 ],
267 }
268}
269
270fn reviewer_profile_home(worker_root: &str, generation: u64) -> String {
276 reviewer_staging_profile_home(Path::new(worker_root), generation)
277 .to_string_lossy()
278 .into_owned()
279}
280
281fn upload_reviewer_profile(
287 executor: &impl CommandExecutor,
288 locator: &hel_targets::TargetLocator,
289 worker_root: &str,
290 generation: u64,
291 local: &Path,
292) -> Result<()> {
293 let home = reviewer_profile_home(worker_root, generation);
294 match locator {
295 hel_targets::TargetLocator::LocalBare { .. } => {
296 for command in [
297 CommandSpec::new("rm", ["-rf", "--", &home])
298 .purpose("clear the local reviewer profile"),
299 CommandSpec::new("mkdir", ["-p", &home])
300 .purpose("create the local reviewer profile directory"),
301 CommandSpec::new(
302 "cp",
303 [
304 "-R".to_owned(),
305 format!("{}/.", local.display()),
306 home.clone(),
307 ],
308 )
309 .purpose("install the local reviewer profile"),
310 CommandSpec::new("chmod", ["-R", "go-rwx", &home])
311 .purpose("restrict local reviewer profile permissions"),
312 ] {
313 execute_checked(executor, command)?;
314 }
315 }
316 hel_targets::TargetLocator::LocalPodman { container_id, .. }
317 | hel_targets::TargetLocator::LocalDocker { container_id }
318 | hel_targets::TargetLocator::AppleContainer { container_id } => {
319 let engine = match locator {
320 hel_targets::TargetLocator::LocalPodman { .. } => "podman",
321 hel_targets::TargetLocator::LocalDocker { .. } => "docker",
322 hel_targets::TargetLocator::AppleContainer { .. } => "container",
323 _ => unreachable!("matched local container target"),
324 };
325 for arguments in [
326 vec![
327 "exec".to_owned(),
328 container_id.clone(),
329 "rm".to_owned(),
330 "-rf".to_owned(),
331 "--".to_owned(),
332 home.clone(),
333 ],
334 vec![
335 "exec".to_owned(),
336 container_id.clone(),
337 "mkdir".to_owned(),
338 "-p".to_owned(),
339 home.clone(),
340 ],
341 vec![
342 "cp".to_owned(),
343 format!("{}/.", local.display()),
344 format!("{container_id}:{home}"),
345 ],
346 container_upload_ownership_args(container_id, worker_root, &[&home]),
347 vec![
348 "exec".to_owned(),
349 container_id.clone(),
350 "chmod".to_owned(),
351 "-R".to_owned(),
352 "go-rwx".to_owned(),
353 home.clone(),
354 ],
355 ] {
356 execute_checked(
357 executor,
358 CommandSpec::new(engine, arguments).purpose("stage the reviewer profile"),
359 )?;
360 }
361 }
362 hel_targets::TargetLocator::AwsEc2 { ssh, .. }
363 | hel_targets::TargetLocator::SshBare { ssh, .. } => {
364 let incoming = format!("{home}.incoming");
365 execute_checked(
366 executor,
367 ssh_command_spec(ssh, ["mkdir", "-p", worker_root])
368 .purpose("create the reviewer directory"),
369 )?;
370 execute_checked(
371 executor,
372 ssh_command_spec(ssh, ["rm", "-rf", "--", &incoming, &home])
373 .purpose("clear the reviewer profile"),
374 )?;
375 execute_checked(
376 executor,
377 scp_command_spec(ssh, local, &incoming, true)
378 .purpose("upload the reviewer profile"),
379 )?;
380 execute_checked(
381 executor,
382 ssh_command_spec(ssh, ["mv", &incoming, &home])
383 .purpose("install the reviewer profile"),
384 )?;
385 execute_checked(
386 executor,
387 ssh_command_spec(ssh, ["chmod", "-R", "go-rwx", &home])
388 .purpose("restrict reviewer profile permissions"),
389 )?;
390 }
391 hel_targets::TargetLocator::SshPodman {
392 ssh, container_id, ..
393 }
394 | hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
395 let engine = match locator {
396 hel_targets::TargetLocator::SshPodman { .. } => "podman",
397 hel_targets::TargetLocator::SshDocker { .. } => "docker",
398 _ => unreachable!("matched remote container target"),
399 };
400 let upload = format!("{worker_root}/.reviewer-upload-{generation}");
401 execute_checked(
402 executor,
403 ssh_command_spec(ssh, ["rm", "-rf", "--", &upload])
404 .purpose("clear remote reviewer staging"),
405 )?;
406 execute_checked(
407 executor,
408 scp_command_spec(ssh, local, &upload, true)
409 .purpose("upload the remote reviewer profile"),
410 )?;
411 for arguments in [
412 vec![
413 engine.to_owned(),
414 "exec".to_owned(),
415 container_id.clone(),
416 "rm".to_owned(),
417 "-rf".to_owned(),
418 "--".to_owned(),
419 home.clone(),
420 ],
421 vec![
422 engine.to_owned(),
423 "exec".to_owned(),
424 container_id.clone(),
425 "mkdir".to_owned(),
426 "-p".to_owned(),
427 home.clone(),
428 ],
429 vec![
430 engine.to_owned(),
431 "cp".to_owned(),
432 format!("{upload}/."),
433 format!("{container_id}:{home}"),
434 ],
435 std::iter::once(engine.to_owned())
436 .chain(container_upload_ownership_args(
437 container_id,
438 worker_root,
439 &[&home],
440 ))
441 .collect(),
442 vec![
443 engine.to_owned(),
444 "exec".to_owned(),
445 container_id.clone(),
446 "chmod".to_owned(),
447 "-R".to_owned(),
448 "go-rwx".to_owned(),
449 home.clone(),
450 ],
451 ] {
452 execute_checked(
453 executor,
454 ssh_command_spec(ssh, arguments).purpose("stage the remote reviewer profile"),
455 )?;
456 }
457 execute_checked(
458 executor,
459 ssh_command_spec(ssh, ["rm", "-rf", "--", &upload])
460 .purpose("remove remote reviewer staging"),
461 )?;
462 }
463 }
464 if home.trim().is_empty() {
465 bail!("the reviewer profile home resolved to an empty path");
466 }
467 Ok(())
468}
469
470#[cfg(test)]
471mod tests {
472 use std::cell::RefCell;
473 use std::collections::BTreeMap;
474
475 use super::*;
476 use crate::hel_controller::test_support::checkpoint_test_session;
477 use hel::hel_config::{HarnessKind, HarnessProfile, HelConfig, TargetTemplate};
478 use hel::hel_state::{HelState, SessionState};
479 use hel::hel_targets::CommandOutput;
480
481 struct RecordingExecutor {
482 commands: RefCell<Vec<CommandSpec>>,
483 }
484
485 impl RecordingExecutor {
486 fn new() -> Self {
487 Self {
488 commands: RefCell::new(Vec::new()),
489 }
490 }
491
492 fn script(&self) -> Vec<String> {
494 self.commands
495 .borrow()
496 .iter()
497 .map(|command| format!("{} {}", command.program, command.args.join(" ")))
498 .collect()
499 }
500 }
501
502 impl CommandExecutor for RecordingExecutor {
503 fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
504 self.commands.borrow_mut().push(command.clone());
505 Ok(CommandOutput {
506 status: 0,
507 stdout: Vec::new(),
508 stderr: Vec::new(),
509 })
510 }
511 }
512
513 const SESSION_ID: &str = "0123456789abcdef0123456789abcdef";
516
517 fn fixture(directory: &Path, locator: hel::hel_state::TargetLocator) -> (Controller, String) {
518 let session_id = SESSION_ID;
519 let mut session = checkpoint_test_session(session_id);
520 session.target_template_id = "local".into();
521 session.state = SessionState::Running;
522 session.target = Some(locator);
523 let mut config = HelConfig::default();
524 config
525 .targets
526 .insert("local".into(), TargetTemplate::LocalBare);
527 for (id, kind) in [
528 ("codex", HarnessKind::Codex),
529 ("claude", HarnessKind::Claude),
530 ] {
531 let home = directory.join(id);
532 std::fs::create_dir_all(&home).unwrap();
533 config.profiles.insert(
534 id.to_owned(),
535 HarnessProfile {
536 enabled: true,
537 kind,
538 home,
539 environment: BTreeMap::from([("EXTRA".into(), "1".into())]),
540 context_window_bytes: None,
541 },
542 );
543 }
544 (
545 Controller {
546 config,
547 state: HelState {
548 sessions: BTreeMap::from([(session_id.into(), session)]),
549 ..HelState::default()
550 },
551 },
552 session_id.to_owned(),
553 )
554 }
555
556 #[test]
557 fn staging_copies_the_chosen_profile_into_the_worker_root() {
558 let directory = tempfile::tempdir().unwrap();
559 let worker_root = directory.path().join(SESSION_ID);
560 std::fs::create_dir_all(directory.path().join("claude")).unwrap();
561 std::fs::write(directory.path().join("claude/CLAUDE.md"), b"reviewer").unwrap();
563 let (controller, session_id) = fixture(
564 directory.path(),
565 hel::hel_state::TargetLocator::LocalBare {
566 worker_root: worker_root.clone(),
567 },
568 );
569 let executor = RecordingExecutor::new();
570
571 let config = controller
572 .stage_reviewer_profile_controlled(&session_id, "claude", 0, &[], &executor)
573 .unwrap();
574
575 assert_eq!(config.profile_id, "claude");
576 assert_eq!(config.harness, HarnessKind::Claude);
577 assert_eq!(config.generation, 0);
578 assert_eq!(config.model, None);
579 assert_eq!(config.effort, None);
580 assert!(
582 !config
583 .environment
584 .contains_key(HarnessKind::Claude.home_env())
585 );
586 assert_eq!(
587 config.environment.get("EXTRA").map(String::as_str),
588 Some("1")
589 );
590
591 let home = format!("{}/reviewer/profile", worker_root.display());
592 let script = executor.script();
593 let cleared = script
594 .iter()
595 .position(|line| line.starts_with("rm ") && line.contains(&home))
596 .expect("the previous reviewer profile is cleared");
597 let copied = script
598 .iter()
599 .position(|line| line.starts_with("cp ") && line.ends_with(&home))
600 .expect("the staged profile is installed");
601 assert!(
602 cleared < copied,
603 "a stale profile must go before the new one lands: {script:?}"
604 );
605 assert!(
606 script
607 .iter()
608 .any(|line| line.contains("go-rwx") && line.contains(&home)),
609 "the reviewer profile must not be world readable: {script:?}"
610 );
611 }
612
613 #[test]
614 fn local_container_targets_stage_the_reviewer_through_their_engine() {
615 let directory = tempfile::tempdir().unwrap();
616 let container_id = hel::hel_targets::resource_name(SESSION_ID).unwrap();
617 for (locator, engine) in [
618 (
619 hel::hel_state::TargetLocator::LocalPodman {
620 container_id: container_id.clone(),
621 workspace_storage: Default::default(),
622 },
623 "podman",
624 ),
625 (
626 hel::hel_state::TargetLocator::LocalDocker {
627 container_id: container_id.clone(),
628 },
629 "docker",
630 ),
631 ] {
632 let (controller, session_id) = fixture(directory.path(), locator);
633 let executor = RecordingExecutor::new();
634
635 controller
636 .stage_reviewer_profile_controlled(&session_id, "codex", 3, &[], &executor)
637 .unwrap();
638
639 let script = executor.script();
640 assert!(
641 script
642 .iter()
643 .all(|line| line.starts_with(&format!("{engine} "))),
644 "a container target is reached only through its engine: {script:?}"
645 );
646 let home = script
647 .iter()
648 .find_map(|line| {
649 line.split(' ')
650 .find(|word| word.contains("/reviewer/profile"))
651 })
652 .expect("the reviewer profile is placed")
653 .to_owned();
654 assert!(
655 home.contains(&format!("/{session_id}")),
656 "the reviewer lives under this session's worker root: {home}"
657 );
658 assert!(
660 !script.iter().any(|line| {
661 line.contains("run") || line.contains("git") || line.contains("create")
662 }),
663 "staging a reviewer provisions nothing: {script:?}"
664 );
665 }
666 }
667
668 #[test]
669 fn an_unknown_profile_is_refused_before_anything_is_copied() {
670 let directory = tempfile::tempdir().unwrap();
671 let (controller, session_id) = fixture(
672 directory.path(),
673 hel::hel_state::TargetLocator::LocalBare {
674 worker_root: directory.path().join(SESSION_ID),
675 },
676 );
677 let executor = RecordingExecutor::new();
678
679 let error = controller
680 .stage_reviewer_profile_controlled(&session_id, "missing", 0, &[], &executor)
681 .unwrap_err();
682
683 assert!(format!("{error:#}").contains("unknown profile"));
684 assert!(executor.commands.borrow().is_empty());
685 }
686
687 #[test]
688 fn a_new_generation_travels_to_the_worker_so_it_starts_a_fresh_reviewer() {
689 let directory = tempfile::tempdir().unwrap();
690 let (controller, session_id) = fixture(
691 directory.path(),
692 hel::hel_state::TargetLocator::LocalBare {
693 worker_root: directory.path().join(SESSION_ID),
694 },
695 );
696 let executor = RecordingExecutor::new();
697
698 let first = controller
699 .stage_reviewer_profile_controlled(&session_id, "codex", 0, &[], &executor)
700 .unwrap();
701 let second = controller
702 .stage_reviewer_profile_controlled(&session_id, "codex", 1, &[], &executor)
703 .unwrap();
704
705 assert!(first.reusable_for(&first));
706 assert!(
707 !first.reusable_for(&second),
708 "a new generation must not reload the old conversation"
709 );
710 }
711}