1use super::*;
2
3fn disable_mbx_cache_shims(
4 session_id: &str,
5 backend: &targets::TargetLocator,
6 worker_root: &str,
7 executor: &impl CommandExecutor,
8 reason: &str,
9) {
10 let cleanup = remove_generated_mbx_shims(executor, backend, worker_root);
11 if let Err(error) = &cleanup {
12 tracing::warn!(
13 session_id,
14 "could not remove marked shared-mbx launchers: {error:#}"
15 );
16 }
17 let cleanup_note = cleanup
18 .err()
19 .map(|error| format!(" Mjolnir could not remove its marked launchers: {error:#}."))
20 .unwrap_or_default();
21 tracing::warn!(
22 session_id,
23 "shared mbx is unavailable in this container: {reason}"
24 );
25 executor.notify_notice(&format!(
26 "The Rust build cache is unavailable in this container: {reason}.{cleanup_note} The session will start without it."
27 ));
28}
29
30fn build_cache_config_roots(launch: &WorkerLaunchConfig) -> Vec<PathBuf> {
31 [&launch.target_environment, &launch.environment]
32 .into_iter()
33 .filter_map(|environment| environment.get("XDG_CONFIG_HOME").map(PathBuf::from))
34 .collect::<std::collections::BTreeSet<_>>()
35 .into_iter()
36 .collect()
37}
38
39impl Controller {
40 pub(in crate::controller) fn worker_placement(
44 &self,
45 session_id: &str,
46 ) -> Result<(targets::TargetLocator, String)> {
47 let session = self
48 .state
49 .sessions
50 .get(session_id)
51 .with_context(|| format!("unknown session {session_id}"))?;
52 let locator = session
53 .target
54 .as_ref()
55 .context("session target is missing")?;
56 if let Some(owner) = crate::worker_lifecycle::current(session_id) {
57 owner.verify_target(locator)?;
58 }
59 let backend = backend_locator(locator, session, &self.config)?;
60 let worker_root = targets::worker_root(&backend, session_id)?;
61 Ok((backend, worker_root))
62 }
63
64 pub(in crate::controller) fn prepare_worker_files(
65 &self,
66 session_id: &str,
67 backend: &targets::TargetLocator,
68 worker_root: &str,
69 executor: &impl CommandExecutor,
70 ) -> Result<()> {
71 let owner = crate::worker_lifecycle::require(session_id)?;
72 let session = self
73 .state
74 .sessions
75 .get(session_id)
76 .with_context(|| format!("unknown session {session_id}"))?;
77 if let Some(target) = session.target.as_ref() {
78 owner.verify_target(target)?;
79 }
80 let profile = self
81 .config
82 .profiles
83 .get(&session.last_profile)
84 .context("session profile is missing")?;
85 profile.ensure_ready(&session.last_profile)?;
86 let (mut launch, project_memory, target_profile_home) =
87 self.session_launch_config(session_id, backend)?;
88
89 if session.native_session_id.is_some()
90 && profile.kind == mj_core::config::HarnessKind::Codex
91 {
92 launch.goal_resume_request = Some(mj_core::state::new_session_id()?);
93 }
94 let staging = tempfile::tempdir().context("create worker staging directory")?;
95 let launch_path = staging.path().join("launch.json");
96 launch.write(&launch_path)?;
97 let ownership_path = staging.path().join("ownership.json");
98 WorkerOwnership {
99 version: WorkerOwnership::VERSION,
100 workspace_id: session.workspace_id.clone(),
101 session_id: session_id.to_string(),
102 profile_id: session.last_profile.clone(),
103 bundle_id: session.bundle_id.clone(),
104 target_template_id: session.target_template_id.clone(),
105 instance_id: Some(mj_core::config::instance_identity()),
106 }
107 .write(&ownership_path)?;
108 let profile_stage = staging.path().join("profile");
112 let started = Instant::now();
113 let result = stage_profile(profile, &profile_stage);
114 tracing::debug!(
115 session_id,
116 elapsed_ms = started.elapsed().as_millis(),
117 "profile staging completed"
118 );
119 result?;
120 stage_managed_skills(
121 profile.kind,
122 &profile_stage,
123 session
124 .target
125 .as_ref()
126 .context("session target is missing")?
127 .skills_scope(),
128 )?;
129 stage_codex_catalog(
130 &session.last_profile,
131 profile,
132 &profile_stage,
133 &fetch_catalog_over_https,
134 &SharedCatalogCache,
135 )?;
136 append_hel_target_environment(profile.kind, &profile_stage, backend)?;
137 append_container_storage_guidance(
138 profile.kind,
139 &profile_stage,
140 backend,
141 session.container_workspace.as_deref(),
142 targets::has_managed_temporary_volume(backend, executor)?,
143 )?;
144 append_subagent_policy(
145 profile.kind,
146 &profile_stage,
147 &launch.subagents,
148 self.config.subagents.max_concurrent,
149 )?;
150 apply_staged_execution_setting(profile.kind, launch.execution_policy, &profile_stage)?;
151 if profile.kind == mj_core::config::HarnessKind::Claude {
152 configure_claude_mailbox_hook(
153 &profile_stage,
154 worker_root,
155 launch.agent_mailboxes_enabled,
156 )?;
157 configure_claude_subagent_mcp(
158 &profile_stage,
159 worker_root,
160 launch.subagent_mcp_role(),
161 launch.agent_mailboxes_enabled,
162 )?;
163 }
164 stage_memory_replica(
165 &project_memory,
166 Path::new(&target_profile_home),
167 &profile_stage,
168 )?;
169 if project_memory.mcp_delivery == ProjectMemoryMcpDelivery::HarnessProfile {
170 configure_kimi_history_mcp(
171 &profile_stage,
172 worker_root,
173 project_memory.history_socket.as_deref(),
174 )?;
175 }
176 let worker_binary = worker_binary_for(backend, executor)?;
177
178 install_worker_files(
179 executor,
180 backend,
181 session_id,
182 worker_root,
183 &target_profile_home,
184 &worker_binary,
185 &launch_path,
186 &ownership_path,
187 &profile_stage,
188 )?;
189 if session.build_cache.is_some() {
190 self.install_build_cache_shim(session, backend, &launch, executor)
191 .context("install shared machine build cache configuration")?;
192 }
193 prepare_installed_managed_harness(executor, backend, worker_root, &launch)
194 }
195
196 pub(in crate::controller) fn install_build_cache_shim(
199 &self,
200 session: &mj_core::state::SessionRecord,
201 backend: &targets::TargetLocator,
202 launch: &WorkerLaunchConfig,
203 executor: &impl CommandExecutor,
204 ) -> Result<()> {
205 let Some(cache) = session.build_cache.as_ref() else {
206 return Ok(());
207 };
208 let worker_root = targets::worker_root(backend, &session.id)?;
209 let config_roots = build_cache_config_roots(launch);
210 let configuration = crate::controller::mbx::shared_configuration_file(&cache.directory);
211 if link_legacy_mbx_configuration(
212 executor,
213 backend,
214 &worker_root,
215 &configuration,
216 &config_roots,
217 )? {
218 return Ok(());
219 }
220 let binary = match crate::controller::mbx::sync_mbx_binary_for_container(
221 backend,
222 &cache.directory,
223 executor,
224 ) {
225 Ok(crate::controller::mbx::CachedMbxSync::Ready(binary)) => binary,
226 Ok(crate::controller::mbx::CachedMbxSync::Unavailable(reason)) => {
227 disable_mbx_cache_shims(&session.id, backend, &worker_root, executor, &reason);
228 return Ok(());
229 }
230 Err(error) => {
231 disable_mbx_cache_shims(
232 &session.id,
233 backend,
234 &worker_root,
235 executor,
236 &format!("{error:#}"),
237 );
238 return Ok(());
239 }
240 };
241 if let Err(error) = verify_mbx_binary(executor, backend, &binary.path, &binary.version) {
242 disable_mbx_cache_shims(
243 &session.id,
244 backend,
245 &worker_root,
246 executor,
247 &format!("{error:#}"),
248 );
249 return Ok(());
250 }
251 let (configuration, config_roots) =
252 self.build_cache_configuration(session, backend, launch, executor)?;
253 install_mbx_shims(
254 executor,
255 backend,
256 &worker_root,
257 &binary.path,
258 &configuration,
259 &config_roots,
260 )
261 }
262
263 pub(in crate::controller) fn prepare_build_cache_links(
264 &self,
265 session: &mj_core::state::SessionRecord,
266 backend: &targets::TargetLocator,
267 launch: &WorkerLaunchConfig,
268 executor: &impl CommandExecutor,
269 ) -> Result<()> {
270 self.install_build_cache_shim(session, backend, launch, executor)
271 }
272
273 fn build_cache_configuration(
274 &self,
275 session: &mj_core::state::SessionRecord,
276 backend: &targets::TargetLocator,
277 launch: &WorkerLaunchConfig,
278 executor: &impl CommandExecutor,
279 ) -> Result<(PathBuf, Vec<PathBuf>)> {
280 let configuration = crate::controller::mbx::prepare_session_configuration(
281 &self.config,
282 backend,
283 session
284 .build_cache
285 .as_ref()
286 .context("session has no build cache")?,
287 executor,
288 )?;
289 let config_roots = build_cache_config_roots(launch);
290 Ok((configuration, config_roots))
291 }
292
293 pub fn diagnose_worker(&self, session_id: &str) -> Option<String> {
297 self.diagnose_worker_controlled(session_id, &crate::targets::ProcessExecutor)
298 }
299
300 pub fn diagnose_worker_controlled(
301 &self,
302 session_id: &str,
303 executor: &impl CommandExecutor,
304 ) -> Option<String> {
305 let session = self.state.sessions.get(session_id)?;
306 let locator = session.target.as_ref()?;
307 let backend = match backend_locator(locator, session, &self.config) {
308 Ok(backend) => backend,
309 Err(error) => {
310 tracing::debug!(
311 session_id,
312 error = format!("{error:#}"),
313 "could not construct a worker diagnostic probe"
314 );
315 return None;
316 }
317 };
318 let worker_root = match targets::worker_root(&backend, session_id) {
319 Ok(root) => root,
320 Err(error) => {
321 tracing::debug!(
322 session_id,
323 error = format!("{error:#}"),
324 "could not derive the worker diagnostic root"
325 );
326 return None;
327 }
328 };
329 let binary_failure = worker_binary_probe_failure(executor, &backend, &worker_root);
330 let probe = match probe_worker(executor, &backend, &worker_root) {
331 Ok(probe) => probe.to_string(),
332 Err(error) => format!("the worker could not be probed: {error:#}"),
333 };
334 Some(match binary_failure {
335 Some(binary_failure) => format!("{binary_failure}; {probe}"),
336 None => probe,
337 })
338 }
339
340 pub fn worker_recovery_plan(
344 &self,
345 session_id: &str,
346 operation: Option<&mj_core::state::MoveOperation>,
347 ) -> Result<WorkerRecoveryPlan> {
348 let (backend, worker_root) = self.worker_placement(session_id)?;
349 let launch = self.worker_launch_config_for_move(session_id, &backend, operation)?;
350 let workspace = worker_workspace_for_recovery(&backend, &launch.cwd);
351 Ok(WorkerRecoveryPlan {
352 source_target: self.state.sessions[session_id]
353 .target
354 .clone()
355 .context("session target is missing")?,
356 target: targets::target_recovery_plan(&backend, session_id)?,
357 workspace,
358 liveness_probe: worker_liveness_command(&backend, &worker_root),
359 exit_record: Some(worker_exit_record_command(&backend, &worker_root)),
360 binary_refresh: worker_binary_refresh_plan(&backend, session_id)?,
361 launch_refresh: Some(worker_launch_refresh_plan(&backend, session_id, &launch)?),
362 restart: CommandPlan {
363 description: format!("restart Mjolnir worker for session {session_id}"),
364 commands: vec![
365 stop_worker_command(&backend, &worker_root),
366 start_worker_command(&backend, &worker_root),
367 ],
368 },
369 })
370 }
371
372 fn session_launch_config(
377 &self,
378 session_id: &str,
379 backend: &targets::TargetLocator,
380 ) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
381 let session = self
382 .state
383 .sessions
384 .get(session_id)
385 .with_context(|| format!("unknown session {session_id}"))?;
386 session.validate_configuration(&self.config)?;
387 let profile = self
388 .config
389 .profiles
390 .get(&session.last_profile)
391 .context("session profile is missing")?;
392 let checkout = self.state.checkout(session_id)?;
393 let bundle = checkout
394 .project_directory()
395 .is_none()
396 .then(|| session.project_bundle(&self.config))
397 .flatten();
398 let target = session.target_runtime_settings(&self.config)?;
399 let subagent = self.state.subagents.get(session_id);
400 let (workspace_session_id, workspace_container) = match subagent.as_ref() {
403 Some(child) => {
404 let parent = self
405 .state
406 .sessions
407 .get(&child.parent_session_id)
408 .context("sub-agent parent session is missing")?;
409 (parent.id.clone(), parent.container_workspace.clone())
410 }
411 None => (session_id.to_owned(), session.container_workspace.clone()),
412 };
413 let (mut launch, project_memory, target_profile_home) = worker_launch_config_with_checkout(
414 session,
415 &checkout,
416 profile,
417 bundle,
418 backend,
419 LaunchWorkspace {
420 session_id: &workspace_session_id,
421 container: workspace_container.as_deref(),
422 parent_worktree: self.subagent_parent_worktree(session_id),
423 },
424 &target,
425 )?;
426 if subagent.is_some() {
427 launch.environment.insert(
428 mj_core::worker_launch::SESSION_MANAGED_SUBAGENT_ENV.into(),
429 "1".into(),
430 );
431 launch
432 .environment
433 .remove(mj_core::worker_launch::SESSION_MESSAGE_MCP_ENV);
434 } else {
435 launch
436 .environment
437 .remove(mj_core::worker_launch::SESSION_MANAGED_SUBAGENT_ENV);
438 launch.environment.insert(
441 mj_core::worker_launch::SESSION_MESSAGE_MCP_ENV.into(),
442 "1".into(),
443 );
444 }
445 apply_jev_switch(&mut launch, self.config.jev.enabled);
446 apply_continuation_switch(&mut launch, self.config.automatic_continuation_enabled());
447 launch.agent_mailboxes_enabled = self.config.agent_mailboxes_enabled();
448 launch.subagents = session
449 .subagents
450 .clone()
451 .unwrap_or_default()
452 .for_launch(profile.kind, subagent.is_some());
453 launch.handback_tool = subagent.as_ref().is_some_and(|child| child.handback_tool);
455 launch.initial_model = subagent.as_ref().and_then(|child| child.model.clone());
458 launch.review_capture =
464 mj_core::review::settings::can_review(&self.config) && subagent.is_none();
465 if let Some(subagent) = &subagent {
466 let parent = self
467 .state
468 .sessions
469 .get(&subagent.parent_session_id)
470 .context("sub-agent parent session is missing")?;
471 let parent_profile = self
472 .config
473 .profiles
474 .get(&parent.last_profile)
475 .context("sub-agent parent profile is missing")?;
476 let parent_target = parent.target_runtime_settings(&self.config)?;
477 let parent_locator = parent
478 .target
479 .as_ref()
480 .context("sub-agent parent has no live target")?;
481 let parent_backend = backend_locator(parent_locator, parent, &self.config)?;
482 let parent_checkout = self.state.checkout(&parent.id)?;
483 let parent_bundle = parent_checkout
484 .project_directory()
485 .is_none()
486 .then(|| parent.project_bundle(&self.config))
487 .flatten();
488 let (parent_launch, _, _) = worker_launch_config_with_checkout(
489 parent,
490 &parent_checkout,
491 parent_profile,
492 parent_bundle,
493 &parent_backend,
494 LaunchWorkspace {
495 session_id: &parent.id,
496 container: parent.container_workspace.as_deref(),
497 parent_worktree: None,
498 },
499 &parent_target,
500 )?;
501 launch.cwd = if subagent.working_directory.as_os_str().is_empty() {
502 parent_launch.cwd
503 } else {
504 parent_launch.cwd.join(&subagent.working_directory)
505 };
506 launch.additional_directories = parent_launch.additional_directories;
507 }
508 Ok((launch, project_memory, target_profile_home))
509 }
510
511 pub(in crate::controller) fn current_worker_launch_config(
512 &self,
513 session_id: &str,
514 backend: &targets::TargetLocator,
515 ) -> Result<WorkerLaunchConfig> {
516 let operation = crate::database::load_move_operation(session_id)?;
517 self.worker_launch_config_for_move(session_id, backend, operation.as_ref())
518 }
519
520 fn worker_launch_config_for_move(
521 &self,
522 session_id: &str,
523 backend: &targets::TargetLocator,
524 operation: Option<&mj_core::state::MoveOperation>,
525 ) -> Result<WorkerLaunchConfig> {
526 let session = self
527 .state
528 .sessions
529 .get(session_id)
530 .with_context(|| format!("unknown session {session_id}"))?;
531 let (mut launch, _, _) = self.session_launch_config(session_id, backend)?;
532 if operation.is_some_and(|operation| {
533 operation.source_checkpoint_only
534 && operation.destination_target.is_none()
535 && matches!(
536 operation.phase,
537 mj_core::state::MovePhase::Preparing
538 | mj_core::state::MovePhase::ClosingSource
539 | mj_core::state::MovePhase::Failed
540 | mj_core::state::MovePhase::Cancelled
541 )
542 && session.last_profile == operation.source_profile_id
543 && session.target == operation.source_target
544 && matches!(
545 session.state,
546 mj_core::state::SessionState::Running
547 | mj_core::state::SessionState::Disconnected
548 | mj_core::state::SessionState::Closing
549 )
550 }) {
551 launch.run_mode = mj_core::worker_launch::WorkerRunMode::CheckpointOnly;
552 }
553 Ok(launch)
554 }
555
556 pub fn project_memory_sync_target(&self, session_id: &str) -> Result<ProjectMemorySyncTarget> {
557 let session = self
558 .state
559 .sessions
560 .get(session_id)
561 .with_context(|| format!("unknown session {session_id}"))?;
562 session.validate_configuration(&self.config)?;
563 let locator = session
564 .target
565 .as_ref()
566 .context("session target is missing")?;
567 let backend = backend_locator(locator, session, &self.config)?;
568 let profile = self
569 .config
570 .profiles
571 .get(&session.last_profile)
572 .context("session profile is missing")?;
573 let checkout = self.state.checkout(session_id)?;
574 let bundle = checkout
575 .project_directory()
576 .is_none()
577 .then(|| session.project_bundle(&self.config))
578 .flatten();
579 let workspace = if let Some(project_directory) = checkout.project_directory() {
580 (project_directory.to_string_lossy().into_owned(), Vec::new())
581 } else {
582 workspace_paths(
583 &backend,
584 bundle.context("session bundle is missing")?,
585 session_id,
586 session.container_workspace.as_deref(),
587 )?
588 };
589 let target_home = target_profile_home(&backend, session_id, profile);
590 let launch = project_memory_launch(
591 session,
592 bundle,
593 &workspace,
594 &target_home,
595 self.subagent_parent_worktree(session_id),
596 )?;
597 Ok(ProjectMemorySyncTarget {
598 canonical_root: canonical_memory_root(&launch.project_key),
599 })
600 }
601}
602
603pub(super) fn apply_jev_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
607 if enabled {
608 return;
609 }
610 for environment in [&mut launch.target_environment, &mut launch.environment] {
611 environment.remove("TYPESAFE_API_KEY");
612 environment.insert(
613 mj_core::jev::DISABLED_ENVIRONMENT.to_owned(),
614 "1".to_owned(),
615 );
616 }
617}
618
619pub(super) fn apply_continuation_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
622 if enabled {
623 return;
624 }
625 for environment in [&mut launch.target_environment, &mut launch.environment] {
626 environment.insert(
627 mj_core::jev::CONTINUATION_DISABLED_ENVIRONMENT.to_owned(),
628 "1".to_owned(),
629 );
630 }
631}
632
633#[cfg(test)]
640pub(super) fn subagent_tools_enabled(
641 session: &mj_core::state::SessionRecord,
642 is_child: bool,
643) -> bool {
644 session
645 .subagents
646 .clone()
647 .unwrap_or_default()
648 .for_launch(session.harness_kind, is_child)
649 .uses_mjolnir()
650}
651
652pub(super) fn worker_workspace_for_recovery(
653 backend: &targets::TargetLocator,
654 directory: &Path,
655) -> Option<WorkerWorkspace> {
656 let target = match backend {
657 targets::TargetLocator::LocalBare { .. } => mj_core::state::ManagedWorktreeTarget::Local,
658 targets::TargetLocator::SshBare { ssh, .. } => mj_core::state::ManagedWorktreeTarget::Ssh {
659 destination: ssh.destination.clone(),
660 ssh_args: ssh.ssh_args.clone(),
661 },
662 targets::TargetLocator::LocalPodman { .. }
663 | targets::TargetLocator::LocalDocker { .. }
664 | targets::TargetLocator::AppleContainer { .. }
665 | targets::TargetLocator::AwsEc2 { .. }
666 | targets::TargetLocator::SshPodman { .. }
667 | targets::TargetLocator::SshDocker { .. } => return None,
668 };
669 Some(WorkerWorkspace {
670 target,
671 directory: directory.to_path_buf(),
672 })
673}
674
675pub(super) struct LaunchWorkspace<'a> {
676 pub session_id: &'a str,
677 pub container: Option<&'a Path>,
678 pub parent_worktree: Option<&'a mj_core::state::ManagedWorktree>,
679}
680
681#[cfg(test)]
682pub(super) fn worker_launch_config(
683 session: &mj_core::state::SessionRecord,
684 profile: &mj_core::config::HarnessProfile,
685 bundle: Option<&ProjectBundle>,
686 backend: &targets::TargetLocator,
687 worker_workspace: LaunchWorkspace<'_>,
688 target: &mj_core::state::TargetRuntimeSettings,
689) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
690 worker_launch_config_with_checkout(
691 session,
692 &session.checkout(),
693 profile,
694 bundle,
695 backend,
696 worker_workspace,
697 target,
698 )
699}
700
701pub(super) fn worker_launch_config_with_checkout(
702 session: &mj_core::state::SessionRecord,
703 checkout: &mj_core::state::Checkout<'_>,
704 profile: &mj_core::config::HarnessProfile,
705 bundle: Option<&ProjectBundle>,
706 backend: &targets::TargetLocator,
707 worker_workspace: LaunchWorkspace<'_>,
708 target: &mj_core::state::TargetRuntimeSettings,
709) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
710 let session_id = session.id.as_str();
711 let execution_policy = target.execution_policy;
712 let target_profile_home = target_profile_home(backend, session_id, profile);
713 let workspace = if let Some(project_directory) = checkout.project_directory() {
714 (project_directory.to_string_lossy().into_owned(), Vec::new())
715 } else {
716 workspace_paths(
717 backend,
718 bundle.context("session bundle is missing")?,
719 worker_workspace.session_id,
720 worker_workspace.container,
721 )?
722 };
723 let mut additional_directories = workspace.1.iter().map(PathBuf::from).collect::<Vec<_>>();
724 additional_directories.extend(
725 session
726 .additional_mounts
727 .iter()
728 .map(|resource| resource.destination.clone()),
729 );
730 if profile.kind == mj_core::config::HarnessKind::Muse && !additional_directories.is_empty() {
731 bail!(
732 "{} ACP does not support multiple workspace roots; use a single-repository bundle",
733 profile.kind.display_name()
734 );
735 }
736 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
737 let mut target_environment = target.environment.clone();
738 let podman_container = backend.container_engine() == Some("podman");
739 if podman_container {
740 target_environment.insert(
744 mj_core::worker_launch::SESSION_GIT_CONFIG_INCLUDE_PATH.into(),
745 "/home/hel/.gitconfig".into(),
746 );
747 }
748 for name in [
758 "MJ_TURN_STALL_TIMEOUT_MS",
759 "MJ_TURN_TOOL_STALL_TIMEOUT_MS",
760 "RUST_LOG",
761 ] {
762 if let Ok(value) = std::env::var(name) {
763 target_environment.insert(name.to_owned(), value);
764 }
765 }
766 if let Some(key) = mj_core::activity::verdict::api_key() {
769 target_environment.insert("TYPESAFE_API_KEY".to_owned(), key);
770 }
771 if let Some(build_cache) = &session.build_cache {
774 target_environment.insert(
775 "MBX_CACHE_DIR".into(),
776 build_cache.directory.to_string_lossy().into_owned(),
777 );
778 target_environment.insert(
779 "MJ_MBX_CONFIG_DIR".into(),
780 mj_core::config::build_cache_configuration_directory(&build_cache.directory)
781 .to_string_lossy()
782 .into_owned(),
783 );
784 target_environment.insert(
787 "MBX_SHIMS_DIR".into(),
788 Path::new(&targets::worker_root(backend, session_id)?)
789 .join("mbx-shims")
790 .to_string_lossy()
791 .into_owned(),
792 );
793 target_environment.insert("MBX_SUMMARY".into(), "off".into());
796 target_environment.insert("MBX_SAVINGS".into(), "off".into());
797 }
798 let mut environment = target_environment.clone();
799 environment.extend(profile.environment.resolved().clone());
800 environment.insert("MJ_SESSION_ID".into(), session_id.to_owned());
803 if session
804 .target
805 .as_ref()
806 .is_some_and(|target| target.skills_scope() == mj_core::skills::SkillsScope::Localhost)
807 {
808 environment.insert(
811 "MJ_CONFIG_DIR".into(),
812 std::path::absolute(mj_core::config::config_dir())
813 .context("resolve host Mjolnir configuration directory")?
814 .to_string_lossy()
815 .into_owned(),
816 );
817 environment.insert(
818 "MJ_DATA_DIR".into(),
819 std::path::absolute(data_dir())
820 .context("resolve host Mjolnir data directory")?
821 .to_string_lossy()
822 .into_owned(),
823 );
824 if let Some(instance) = mj_core::config::instance_name() {
825 environment.insert("MJ_INSTANCE".into(), instance);
826 } else {
827 environment.remove("MJ_INSTANCE");
828 }
829 }
830 profile
831 .kind
832 .configure_home_environment(Path::new(&target_profile_home), &mut environment);
833 profile
834 .kind
835 .configure_execution_environment(execution_policy, &mut environment)?;
836 let mut project_memory = project_memory_launch(
837 session,
838 bundle,
839 &workspace,
840 &target_profile_home,
841 worker_workspace.parent_worktree,
842 )?;
843 project_memory.mcp_delivery = project_memory_mcp_delivery(profile.kind, backend);
844 project_memory.history_socket =
845 Some(Path::new(&targets::worker_root(backend, &session.id)?).join("control.sock"));
846 if profile.kind == mj_core::config::HarnessKind::Claude {
847 environment.insert(
848 "CLAUDE_CODE_PROJECT_DIR_NAME".into(),
849 project_memory_replica_slug(&project_memory.project_key, session_id),
850 );
851 }
852 apply_claude_setup_token(
853 &mut environment,
854 profile.kind,
855 &mj_core::credentials::claude_oauth_token_path(&session.last_profile),
856 );
857 let excluded_environment =
858 exclude_harness_environment(&session.last_profile, profile, &mut environment);
859 if podman_container && profile.kind == mj_core::config::HarnessKind::Claude {
860 environment.insert("IS_SANDBOX".into(), "1".into());
865 }
866 Ok((
867 WorkerLaunchConfig {
868 goal_resume_request: None,
869 target_environment,
870 seed_image_environment: backend.container_engine().is_some(),
871 run_mode: Default::default(),
872 session_id: session_id.to_string(),
873 subagents: mj_core::subagent::SubagentPolicy::Native,
874 handback_tool: false,
875 agent_mailboxes_enabled: true,
876 initial_model: None,
877 review_capture: false,
878 harness: profile.kind,
879 harness_home: PathBuf::from(&target_profile_home),
880 authentication_marker: profile
886 .authentication_marker()
887 .strip_prefix(&profile.home)
888 .ok()
889 .map(|name| name.to_string_lossy().into_owned()),
890 bridge_command: PathBuf::from(bridge_command),
891 bridge_args,
892 harness_runtime: harness_runtime_policy(backend),
893 environment,
894 excluded_environment,
895 cwd: PathBuf::from(&workspace.0),
896 additional_directories,
897 native_session_id: session.native_session_id.clone(),
898 project_memory: Some(project_memory.clone()),
899 execution_policy,
900 },
901 project_memory,
902 target_profile_home,
903 ))
904}
905
906pub(super) fn exclude_harness_environment(
913 profile_id: &str,
914 profile: &mj_core::config::HarnessProfile,
915 environment: &mut std::collections::BTreeMap<String, String>,
916) -> Vec<String> {
917 static REPORTED: std::sync::Mutex<std::collections::BTreeSet<String>> =
918 std::sync::Mutex::new(std::collections::BTreeSet::new());
919 let before = environment.clone();
920 let excluded = profile.exclude_harness_environment(environment);
921 let removed = excluded
922 .iter()
923 .filter(|name| before.contains_key(*name))
924 .cloned()
925 .collect::<Vec<_>>();
926 if !removed.is_empty()
927 && REPORTED
928 .lock()
929 .map(|mut reported| reported.insert(profile_id.to_owned()))
930 .unwrap_or(true)
931 {
932 tracing::info!(
933 profile_id,
934 removed = removed.join(", "),
935 "left API key settings out of the harness environment: this Codex profile must not use an OpenAI API key"
936 );
937 }
938 excluded
939}
940
941pub(super) fn harness_runtime_policy(backend: &targets::TargetLocator) -> HarnessRuntimePolicy {
942 match backend {
943 targets::TargetLocator::LocalBare { .. }
944 | targets::TargetLocator::AwsEc2 { .. }
945 | targets::TargetLocator::SshBare { .. } => HarnessRuntimePolicy::Managed,
946 _ => HarnessRuntimePolicy::Ambient,
947 }
948}