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