1use super::*;
2
3impl Controller {
4 pub(in crate::controller) fn worker_placement(
8 &self,
9 session_id: &str,
10 ) -> Result<(targets::TargetLocator, String)> {
11 let session = self
12 .state
13 .sessions
14 .get(session_id)
15 .with_context(|| format!("unknown session {session_id}"))?;
16 let locator = session
17 .target
18 .as_ref()
19 .context("session target is missing")?;
20 let backend = backend_locator(locator, session, &self.config)?;
21 let worker_root = targets::worker_root(&backend, session_id)?;
22 Ok((backend, worker_root))
23 }
24
25 pub(in crate::controller) fn prepare_worker_files(
26 &self,
27 session_id: &str,
28 backend: &targets::TargetLocator,
29 worker_root: &str,
30 executor: &impl CommandExecutor,
31 ) -> Result<()> {
32 let session = self
33 .state
34 .sessions
35 .get(session_id)
36 .with_context(|| format!("unknown session {session_id}"))?;
37 session.validate_configuration(&self.config)?;
38 let profile = self
39 .config
40 .profiles
41 .get(&session.last_profile)
42 .context("session profile is missing")?;
43 let bundle = session
44 .project_directory
45 .is_none()
46 .then(|| self.config.bundles.get(&session.bundle_id))
47 .flatten();
48 let target = self
49 .config
50 .targets
51 .get(&session.target_template_id)
52 .context("session target template is missing")?;
53 let subagent = crate::database::load_subagent(session_id)?;
54 let (workspace_session_id, workspace_container) = match subagent.as_ref() {
57 Some(child) => {
58 let parent = self
59 .state
60 .sessions
61 .get(&child.parent_session_id)
62 .context("sub-agent parent session is missing")?;
63 (parent.id.clone(), parent.container_workspace.clone())
64 }
65 None => (session_id.to_owned(), session.container_workspace.clone()),
66 };
67 let (mut launch, project_memory, target_profile_home) = worker_launch_config(
68 session,
69 profile,
70 bundle,
71 backend,
72 &workspace_session_id,
73 workspace_container.as_deref(),
74 target,
75 )?;
76 launch.subagent_tools =
77 subagent_tools_enabled(session, self.config.subagents.enabled, subagent.is_some());
78 if let Some(subagent) = &subagent {
79 let parent = self
80 .state
81 .sessions
82 .get(&subagent.parent_session_id)
83 .context("sub-agent parent session is missing")?;
84 let parent_profile = self
85 .config
86 .profiles
87 .get(&parent.last_profile)
88 .context("sub-agent parent profile is missing")?;
89 let parent_target = self
90 .config
91 .targets
92 .get(&parent.target_template_id)
93 .context("sub-agent parent target template is missing")?;
94 let parent_locator = parent
95 .target
96 .as_ref()
97 .context("sub-agent parent has no live target")?;
98 let parent_backend = backend_locator(parent_locator, parent, &self.config)?;
99 let parent_bundle = parent
100 .project_directory
101 .is_none()
102 .then(|| self.config.bundles.get(&parent.bundle_id))
103 .flatten();
104 let (parent_launch, _, _) = worker_launch_config(
105 parent,
106 parent_profile,
107 parent_bundle,
108 &parent_backend,
109 &parent.id,
110 parent.container_workspace.as_deref(),
111 parent_target,
112 )?;
113 launch.cwd = if subagent.working_directory.as_os_str().is_empty() {
114 parent_launch.cwd
115 } else {
116 parent_launch.cwd.join(&subagent.working_directory)
117 };
118 launch.additional_directories = parent_launch.additional_directories;
119 }
120
121 if session.native_session_id.is_some()
122 && profile.kind == mj_core::config::HarnessKind::Codex
123 {
124 launch.goal_resume_request = Some(mj_core::state::new_session_id()?);
125 }
126 let staging = tempfile::tempdir().context("create worker staging directory")?;
127 let launch_path = staging.path().join("launch.json");
128 launch.write(&launch_path)?;
129 let ownership_path = staging.path().join("ownership.json");
130 WorkerOwnership {
131 version: WorkerOwnership::VERSION,
132 workspace_id: session.workspace_id.clone(),
133 session_id: session_id.to_string(),
134 profile_id: session.last_profile.clone(),
135 bundle_id: session.bundle_id.clone(),
136 target_template_id: session.target_template_id.clone(),
137 instance_id: Some(mj_core::config::instance_identity()),
138 }
139 .write(&ownership_path)?;
140 let profile_stage = staging.path().join("profile");
141 if !matches!(backend, targets::TargetLocator::LocalBare { .. })
142 || matches!(
143 profile.kind,
144 mj_core::config::HarnessKind::Claude | mj_core::config::HarnessKind::Muse
145 )
146 || crate::controller::requires_private_profile_home(profile)
147 {
148 let started = Instant::now();
149 let result = stage_profile(profile, &profile_stage);
150 tracing::debug!(
151 session_id,
152 elapsed_ms = started.elapsed().as_millis(),
153 "profile staging completed"
154 );
155 result?;
156 stage_codex_catalog(
157 &session.last_profile,
158 profile,
159 &profile_stage,
160 &fetch_catalog_over_https,
161 &SharedCatalogCache,
162 )?;
163 append_hel_target_environment(profile.kind, &profile_stage, backend)?;
164 apply_staged_execution_setting(profile.kind, launch.execution_policy, &profile_stage)?;
165 if launch.subagent_tools && profile.kind == mj_core::config::HarnessKind::Claude {
166 configure_claude_subagent_mcp(&profile_stage, worker_root)?;
167 }
168 stage_memory_replica(
169 &project_memory,
170 Path::new(&target_profile_home),
171 &profile_stage,
172 )?;
173 if project_memory.mcp_delivery == ProjectMemoryMcpDelivery::HarnessProfile {
174 configure_kimi_project_memory_mcp(&profile_stage, worker_root, &project_memory)?;
175 }
176 } else {
177 seed_local_memory_replica(&project_memory)?;
178 }
179 let worker_binary = worker_binary_for(backend, executor)?;
180
181 install_worker_files(
182 executor,
183 backend,
184 session_id,
185 worker_root,
186 &target_profile_home,
187 &worker_binary,
188 &launch_path,
189 &ownership_path,
190 &profile_stage,
191 )?;
192 if session.build_cache.is_some()
195 && let Err(error) = self.install_build_cache_shim(session, backend, executor)
196 {
197 tracing::warn!(
198 session_id,
199 "installing the mbx build cache failed: {error:#}"
200 );
201 }
202 prepare_installed_managed_harness(executor, backend, worker_root, &launch)
203 }
204
205 fn install_build_cache_shim(
210 &self,
211 session: &mj_core::state::SessionRecord,
212 backend: &targets::TargetLocator,
213 executor: &impl CommandExecutor,
214 ) -> Result<()> {
215 let worker_root = targets::worker_root(backend, &session.id)?;
216 let binary =
220 crate::controller::mbx::binary_for(backend, executor).inspect_err(|error| {
221 executor.notify_notice(&format!(
222 "The Rust build cache is unavailable: {error:#}; this session builds without it."
223 ));
224 })?;
225 let configuration = self
226 .config
227 .targets
228 .get(&session.target_template_id)
229 .map(|template| {
230 crate::controller::backend::backend_target(
231 template,
232 session.resource_allocation.as_ref(),
233 crate::controller::backend::ContainerOverrides::for_session(session),
234 )
235 })
236 .transpose()?
237 .and_then(|target| {
238 crate::controller::mbx::host_configuration(
239 &target,
240 &self.config.build_cache,
241 executor,
242 )
243 });
244 install_mbx_files(
245 executor,
246 backend,
247 &session.id,
248 &worker_root,
249 &binary,
250 configuration.as_deref(),
251 )
252 }
253
254 pub fn diagnose_worker(&self, session_id: &str) -> Option<String> {
258 self.diagnose_worker_controlled(session_id, &crate::targets::ProcessExecutor)
259 }
260
261 pub fn diagnose_worker_controlled(
262 &self,
263 session_id: &str,
264 executor: &impl CommandExecutor,
265 ) -> Option<String> {
266 let session = self.state.sessions.get(session_id)?;
267 let locator = session.target.as_ref()?;
268 let backend = match backend_locator(locator, session, &self.config) {
269 Ok(backend) => backend,
270 Err(error) => {
271 tracing::debug!(
272 session_id,
273 error = format!("{error:#}"),
274 "could not construct a worker diagnostic probe"
275 );
276 return None;
277 }
278 };
279 let worker_root = match targets::worker_root(&backend, session_id) {
280 Ok(root) => root,
281 Err(error) => {
282 tracing::debug!(
283 session_id,
284 error = format!("{error:#}"),
285 "could not derive the worker diagnostic root"
286 );
287 return None;
288 }
289 };
290 let binary_failure = worker_binary_probe_failure(executor, &backend, &worker_root);
291 let last_words = worker_last_words(executor, &backend, &worker_root);
292 match (binary_failure, last_words) {
293 (Some(binary_failure), Some(last_words)) => {
294 Some(format!("{binary_failure}; {last_words}"))
295 }
296 (Some(binary_failure), None) => Some(binary_failure),
297 (None, last_words) => last_words,
298 }
299 }
300
301 pub fn worker_recovery_plan(&self, session_id: &str) -> Result<WorkerRecoveryPlan> {
305 let (backend, worker_root) = self.worker_placement(session_id)?;
306 let launch = self.current_worker_launch_config(session_id, &backend)?;
307 let workspace = worker_workspace_for_recovery(&backend, &launch.cwd);
308 Ok(WorkerRecoveryPlan {
309 source_target: self.state.sessions[session_id]
310 .target
311 .clone()
312 .context("session target is missing")?,
313 target: targets::target_recovery_plan(&backend, session_id)?,
314 workspace,
315 liveness_probe: worker_liveness_command(&backend, &worker_root),
316 binary_refresh: worker_binary_refresh_plan(&backend, session_id)?,
317 launch_refresh: Some(worker_launch_refresh_plan(&backend, session_id, &launch)?),
318 restart: CommandPlan {
319 description: format!("restart Mjolnir worker for session {session_id}"),
320 commands: vec![
321 stop_worker_command(&backend, &worker_root),
322 start_worker_command(&backend, &worker_root),
323 ],
324 },
325 })
326 }
327
328 pub(in crate::controller) fn current_worker_launch_config(
329 &self,
330 session_id: &str,
331 backend: &targets::TargetLocator,
332 ) -> Result<WorkerLaunchConfig> {
333 let session = self
334 .state
335 .sessions
336 .get(session_id)
337 .with_context(|| format!("unknown session {session_id}"))?;
338 session.validate_configuration(&self.config)?;
339 let profile = self
340 .config
341 .profiles
342 .get(&session.last_profile)
343 .context("session profile is missing")?;
344 let bundle = session
345 .project_directory
346 .is_none()
347 .then(|| self.config.bundles.get(&session.bundle_id))
348 .flatten();
349 let target = self
350 .config
351 .targets
352 .get(&session.target_template_id)
353 .context("session target template is missing")?;
354 let (mut launch, _, _) = worker_launch_config(
355 session,
356 profile,
357 bundle,
358 backend,
359 session_id,
360 session.container_workspace.as_deref(),
361 target,
362 )?;
363 if crate::database::load_move_operation(session_id)?.is_some_and(|operation| {
364 operation.source_checkpoint_only
365 && operation.destination_target.is_none()
366 && matches!(
367 operation.phase,
368 mj_core::state::MovePhase::Preparing
369 | mj_core::state::MovePhase::ClosingSource
370 | mj_core::state::MovePhase::Failed
371 | mj_core::state::MovePhase::Cancelled
372 )
373 && session.last_profile == operation.source_profile_id
374 && session.target == operation.source_target
375 && matches!(
376 session.state,
377 mj_core::state::SessionState::Running
378 | mj_core::state::SessionState::Disconnected
379 | mj_core::state::SessionState::Closing
380 )
381 }) {
382 launch.run_mode = mj_core::worker_launch::WorkerRunMode::CheckpointOnly;
383 }
384 Ok(launch)
385 }
386
387 pub fn project_memory_sync_target(&self, session_id: &str) -> Result<ProjectMemorySyncTarget> {
388 let session = self
389 .state
390 .sessions
391 .get(session_id)
392 .with_context(|| format!("unknown session {session_id}"))?;
393 session.validate_configuration(&self.config)?;
394 let locator = session
395 .target
396 .as_ref()
397 .context("session target is missing")?;
398 let backend = backend_locator(locator, session, &self.config)?;
399 let profile = self
400 .config
401 .profiles
402 .get(&session.last_profile)
403 .context("session profile is missing")?;
404 let bundle = session
405 .project_directory
406 .is_none()
407 .then(|| self.config.bundles.get(&session.bundle_id))
408 .flatten();
409 let workspace = if let Some(project_directory) = &session.project_directory {
410 (project_directory.to_string_lossy().into_owned(), Vec::new())
411 } else {
412 workspace_paths(
413 &backend,
414 bundle.context("session bundle is missing")?,
415 session_id,
416 session.container_workspace.as_deref(),
417 )?
418 };
419 let target_home = target_profile_home(&backend, session_id, profile);
420 let launch = project_memory_launch(session, bundle, &workspace, &target_home)?;
421 Ok(ProjectMemorySyncTarget {
422 canonical_root: canonical_memory_root(&launch.project_key),
423 })
424 }
425}
426
427pub(super) fn subagent_tools_enabled(
433 session: &mj_core::state::SessionRecord,
434 global_enabled: bool,
435 is_child: bool,
436) -> bool {
437 session.mjolnir_subagents.unwrap_or(global_enabled)
438 && !is_child
439 && matches!(
440 session.harness_kind,
441 mj_core::config::HarnessKind::Claude | mj_core::config::HarnessKind::Codex
442 )
443}
444
445pub(super) fn worker_workspace_for_recovery(
446 backend: &targets::TargetLocator,
447 directory: &Path,
448) -> Option<WorkerWorkspace> {
449 let target = match backend {
450 targets::TargetLocator::LocalBare { .. } => mj_core::state::ManagedWorktreeTarget::Local,
451 targets::TargetLocator::SshBare { ssh, .. } => mj_core::state::ManagedWorktreeTarget::Ssh {
452 destination: ssh.destination.clone(),
453 ssh_args: ssh.ssh_args.clone(),
454 },
455 targets::TargetLocator::LocalPodman { .. }
456 | targets::TargetLocator::LocalDocker { .. }
457 | targets::TargetLocator::AppleContainer { .. }
458 | targets::TargetLocator::AwsEc2 { .. }
459 | targets::TargetLocator::SshPodman { .. }
460 | targets::TargetLocator::SshDocker { .. } => return None,
461 };
462 Some(WorkerWorkspace {
463 target,
464 directory: directory.to_path_buf(),
465 })
466}
467
468pub(super) fn worker_launch_config(
469 session: &mj_core::state::SessionRecord,
470 profile: &mj_core::config::HarnessProfile,
471 bundle: Option<&ProjectBundle>,
472 backend: &targets::TargetLocator,
473 workspace_session_id: &str,
474 workspace_container: Option<&Path>,
475 target: &mj_core::config::TargetTemplate,
476) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
477 let session_id = session.id.as_str();
478 let execution_policy = profile
479 .kind
480 .effective_execution_policy(target.execution_policy());
481 let target_profile_home = target_profile_home(backend, session_id, profile);
482 let workspace = if let Some(project_directory) = &session.project_directory {
483 (project_directory.to_string_lossy().into_owned(), Vec::new())
484 } else {
485 workspace_paths(
486 backend,
487 bundle.context("session bundle is missing")?,
488 workspace_session_id,
489 workspace_container,
490 )?
491 };
492 let mut additional_directories = workspace.1.iter().map(PathBuf::from).collect::<Vec<_>>();
493 additional_directories.extend(
494 session
495 .additional_mounts
496 .iter()
497 .map(|resource| resource.destination.clone()),
498 );
499 if profile.kind == mj_core::config::HarnessKind::Muse && !additional_directories.is_empty() {
500 bail!(
501 "{} ACP does not support multiple workspace roots; use a single-repository bundle",
502 profile.kind.display_name()
503 );
504 }
505 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
506 use mj_core::config::TargetTemplate;
507 let target_environment = match target {
508 TargetTemplate::LocalPodman { container }
509 | TargetTemplate::LocalDocker { container }
510 | TargetTemplate::AppleContainer { container }
511 | TargetTemplate::SshPodman { container, .. }
512 | TargetTemplate::SshDocker { container, .. } => container.environment.clone(),
513 _ => Default::default(),
514 };
515 let mut target_environment = target_environment;
516 if let Some(build_cache) = &session.build_cache {
519 target_environment.insert(
520 "MBX_CACHE_DIR".into(),
521 build_cache.directory.to_string_lossy().into_owned(),
522 );
523 if let Some(max_size) = &build_cache.max_size {
524 target_environment.insert("MBX_GC_MAX_SIZE".into(), max_size.clone());
525 }
526 }
527 let mut environment = target_environment.clone();
528 environment.extend(profile.environment.clone());
529 profile
530 .kind
531 .configure_home_environment(Path::new(&target_profile_home), &mut environment);
532 profile
533 .kind
534 .configure_execution_environment(execution_policy, &mut environment)?;
535 let mut project_memory =
536 project_memory_launch(session, bundle, &workspace, &target_profile_home)?;
537 project_memory.mcp_delivery = project_memory_mcp_delivery(profile.kind, backend);
538 if profile.kind == mj_core::config::HarnessKind::Claude {
539 environment.insert(
540 "CLAUDE_CODE_PROJECT_DIR_NAME".into(),
541 project_memory_replica_slug(&project_memory.project_key, session_id),
542 );
543 }
544 apply_claude_setup_token(
545 &mut environment,
546 profile.kind,
547 &mj_core::credentials::claude_oauth_token_path(&session.last_profile),
548 );
549 Ok((
550 WorkerLaunchConfig {
551 goal_resume_request: None,
552 target_environment,
553 seed_image_environment: backend.container_engine().is_some(),
554 run_mode: Default::default(),
555 session_id: session_id.to_string(),
556 subagent_tools: false,
557 harness: profile.kind,
558 authentication_marker: profile
561 .authentication_marker()
562 .file_name()
563 .map(|name| name.to_string_lossy().into_owned()),
564 bridge_command: PathBuf::from(bridge_command),
565 bridge_args,
566 harness_runtime: harness_runtime_policy(backend),
567 environment,
568 cwd: PathBuf::from(&workspace.0),
569 additional_directories,
570 native_session_id: session.native_session_id.clone(),
571 project_memory: profile
572 .kind
573 .supports_injected_mcp()
574 .then(|| project_memory.clone()),
575 execution_policy,
576 },
577 project_memory,
578 target_profile_home,
579 ))
580}
581
582pub(super) fn harness_runtime_policy(backend: &targets::TargetLocator) -> HarnessRuntimePolicy {
583 match backend {
584 targets::TargetLocator::LocalBare { .. }
585 | targets::TargetLocator::AwsEc2 { .. }
586 | targets::TargetLocator::SshBare { .. } => HarnessRuntimePolicy::Managed,
587 _ => HarnessRuntimePolicy::Ambient,
588 }
589}