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 if let Some(project_memory) = &project_memory {
167 stage_memory_replica(
168 project_memory,
169 Path::new(&target_profile_home),
170 &profile_stage,
171 )?;
172 if project_memory.mcp_delivery == ProjectMemoryMcpDelivery::HarnessProfile {
173 configure_kimi_history_mcp(
174 &profile_stage,
175 worker_root,
176 project_memory.history_socket.as_deref(),
177 )?;
178 }
179 }
180 let worker_binary = worker_binary_for(backend, executor)?;
181
182 install_worker_files(
183 executor,
184 backend,
185 session_id,
186 worker_root,
187 &target_profile_home,
188 &worker_binary,
189 &launch_path,
190 &ownership_path,
191 &profile_stage,
192 )?;
193 if session.build_cache.is_some() {
194 self.install_build_cache_shim(session, backend, &launch, executor)
195 .context("install shared machine build cache configuration")?;
196 }
197 prepare_installed_managed_harness(executor, backend, worker_root, &launch)
198 }
199
200 pub(in crate::controller) fn install_build_cache_shim(
203 &self,
204 session: &mj_core::state::SessionRecord,
205 backend: &targets::TargetLocator,
206 launch: &WorkerLaunchConfig,
207 executor: &impl CommandExecutor,
208 ) -> Result<()> {
209 let Some(cache) = session.build_cache.as_ref() else {
210 return Ok(());
211 };
212 let worker_root = targets::worker_root(backend, &session.id)?;
213 let config_roots = build_cache_config_roots(launch);
214 let configuration = mj_core::config::build_cache_configuration_directory(&cache.directory);
215 if link_legacy_mbx_configuration(
216 executor,
217 backend,
218 &worker_root,
219 &configuration,
220 &config_roots,
221 )? {
222 return Ok(());
223 }
224 let binary = match crate::controller::mbx::sync_mbx_binary_for_container(
225 backend,
226 &cache.directory,
227 executor,
228 ) {
229 Ok(crate::controller::mbx::CachedMbxSync::Ready(binary)) => binary,
230 Ok(crate::controller::mbx::CachedMbxSync::Unavailable(reason)) => {
231 disable_mbx_cache_shims(&session.id, backend, &worker_root, executor, &reason);
232 return Ok(());
233 }
234 Err(error) => {
235 disable_mbx_cache_shims(
236 &session.id,
237 backend,
238 &worker_root,
239 executor,
240 &format!("{error:#}"),
241 );
242 return Ok(());
243 }
244 };
245 if let Err(error) = verify_mbx_binary(executor, backend, &binary.path, &binary.version) {
246 disable_mbx_cache_shims(
247 &session.id,
248 backend,
249 &worker_root,
250 executor,
251 &format!("{error:#}"),
252 );
253 return Ok(());
254 }
255 let (configuration, config_roots) =
256 self.build_cache_configuration(session, backend, launch, executor)?;
257 install_mbx_shims(
258 executor,
259 backend,
260 &worker_root,
261 &binary.path,
262 &configuration,
263 &config_roots,
264 )
265 }
266
267 pub(in crate::controller) fn prepare_build_cache_links(
268 &self,
269 session: &mj_core::state::SessionRecord,
270 backend: &targets::TargetLocator,
271 launch: &WorkerLaunchConfig,
272 executor: &impl CommandExecutor,
273 ) -> Result<()> {
274 self.install_build_cache_shim(session, backend, launch, executor)
275 }
276
277 fn build_cache_configuration(
278 &self,
279 session: &mj_core::state::SessionRecord,
280 backend: &targets::TargetLocator,
281 launch: &WorkerLaunchConfig,
282 executor: &impl CommandExecutor,
283 ) -> Result<(PathBuf, Vec<PathBuf>)> {
284 let configuration = crate::controller::mbx::prepare_session_configuration(
285 &self.config,
286 backend,
287 session
288 .build_cache
289 .as_ref()
290 .context("session has no build cache")?,
291 executor,
292 )?;
293 let config_roots = build_cache_config_roots(launch);
294 Ok((configuration, config_roots))
295 }
296
297 pub fn diagnose_worker(&self, session_id: &str) -> Option<String> {
301 self.diagnose_worker_controlled(session_id, &crate::targets::ProcessExecutor)
302 }
303
304 pub fn diagnose_worker_controlled(
305 &self,
306 session_id: &str,
307 executor: &impl CommandExecutor,
308 ) -> Option<String> {
309 let session = self.state.sessions.get(session_id)?;
310 let locator = session.target.as_ref()?;
311 let backend = match backend_locator(locator, session, &self.config) {
312 Ok(backend) => backend,
313 Err(error) => {
314 tracing::debug!(
315 session_id,
316 error = format!("{error:#}"),
317 "could not construct a worker diagnostic probe"
318 );
319 return None;
320 }
321 };
322 let worker_root = match targets::worker_root(&backend, session_id) {
323 Ok(root) => root,
324 Err(error) => {
325 tracing::debug!(
326 session_id,
327 error = format!("{error:#}"),
328 "could not derive the worker diagnostic root"
329 );
330 return None;
331 }
332 };
333 let binary_failure = worker_binary_probe_failure(executor, &backend, &worker_root);
334 let probe = match probe_worker(executor, &backend, &worker_root) {
335 Ok(probe) => probe.to_string(),
336 Err(error) => format!("the worker could not be probed: {error:#}"),
337 };
338 Some(match binary_failure {
339 Some(binary_failure) => format!("{binary_failure}; {probe}"),
340 None => probe,
341 })
342 }
343
344 pub fn worker_recovery_plan(
348 &self,
349 session_id: &str,
350 operation: Option<&mj_core::state::MoveOperation>,
351 ) -> Result<WorkerRecoveryPlan> {
352 let (backend, worker_root) = self.worker_placement(session_id)?;
353 let launch = self.worker_launch_config_for_move(session_id, &backend, operation)?;
354 let workspace = worker_workspace_for_recovery(&backend, &launch.cwd);
355 Ok(WorkerRecoveryPlan {
356 source_target: self.state.sessions[session_id]
357 .target
358 .clone()
359 .context("session target is missing")?,
360 target: targets::target_recovery_plan(&backend, session_id)?,
361 workspace,
362 liveness_probe: worker_liveness_command(&backend, &worker_root),
363 exit_record: Some(worker_exit_record_command(&backend, &worker_root)),
364 binary_refresh: worker_binary_refresh_plan(&backend, session_id)?,
365 launch_refresh: Some(worker_launch_refresh_plan(&backend, session_id, &launch)?),
366 restart: CommandPlan {
367 description: format!("restart Mjolnir worker for session {session_id}"),
368 commands: vec![
369 stop_worker_command(&backend, &worker_root),
370 start_worker_command(&backend, &worker_root),
371 ],
372 },
373 })
374 }
375
376 fn session_launch_config(
381 &self,
382 session_id: &str,
383 backend: &targets::TargetLocator,
384 ) -> Result<(
385 WorkerLaunchConfig,
386 Option<ProjectMemoryLaunchConfig>,
387 String,
388 )> {
389 let session = self
390 .state
391 .sessions
392 .get(session_id)
393 .with_context(|| format!("unknown session {session_id}"))?;
394 session.validate_configuration(&self.config)?;
395 let profile = self
396 .config
397 .profiles
398 .get(&session.last_profile)
399 .context("session profile is missing")?;
400 let checkout = self.state.checkout(session_id)?;
401 let bundle = checkout
402 .project_directory()
403 .is_none()
404 .then(|| session.project_bundle(&self.config))
405 .flatten();
406 let target = session.target_runtime_settings(&self.config)?;
407 let subagent = self.state.subagents.get(session_id);
408 let (workspace_session_id, workspace_container) = match subagent.as_ref() {
411 Some(child) => {
412 let parent = self
413 .state
414 .sessions
415 .get(&child.parent_session_id)
416 .context("sub-agent parent session is missing")?;
417 (parent.id.clone(), parent.container_workspace.clone())
418 }
419 None => (session_id.to_owned(), session.container_workspace.clone()),
420 };
421 let (mut launch, project_memory, target_profile_home) = worker_launch_config_with_checkout(
422 session,
423 &checkout,
424 profile,
425 bundle,
426 backend,
427 LaunchWorkspace {
428 session_id: &workspace_session_id,
429 container: workspace_container.as_deref(),
430 parent_worktree: self.subagent_parent_worktree(session_id),
431 },
432 &target,
433 )?;
434 if subagent.is_some() {
435 launch.environment.insert(
436 mj_core::worker_launch::SESSION_MANAGED_SUBAGENT_ENV.into(),
437 "1".into(),
438 );
439 launch
440 .environment
441 .remove(mj_core::worker_launch::SESSION_MESSAGE_MCP_ENV);
442 } else {
443 launch
444 .environment
445 .remove(mj_core::worker_launch::SESSION_MANAGED_SUBAGENT_ENV);
446 launch.environment.insert(
449 mj_core::worker_launch::SESSION_MESSAGE_MCP_ENV.into(),
450 "1".into(),
451 );
452 }
453 apply_jev_switch(&mut launch, self.config.jev.enabled);
454 apply_continuation_switch(&mut launch, self.config.automatic_continuation_enabled());
455 launch.agent_mailboxes_enabled = self.config.agent_mailboxes_enabled_for(session);
456 launch.subagents = session
457 .subagents
458 .clone()
459 .unwrap_or_default()
460 .for_launch(profile.kind, subagent.is_some());
461 launch.handback_tool = subagent.as_ref().is_some_and(|child| child.handback_tool);
463 launch.initial_model = subagent.as_ref().and_then(|child| child.model.clone());
466 launch.review_capture =
472 mj_core::review::settings::can_review(&self.config) && subagent.is_none();
473 if let Some(subagent) = &subagent {
474 let parent = self
475 .state
476 .sessions
477 .get(&subagent.parent_session_id)
478 .context("sub-agent parent session is missing")?;
479 let parent_profile = self
480 .config
481 .profiles
482 .get(&parent.last_profile)
483 .context("sub-agent parent profile is missing")?;
484 let parent_target = parent.target_runtime_settings(&self.config)?;
485 let parent_locator = parent
486 .target
487 .as_ref()
488 .context("sub-agent parent has no live target")?;
489 let parent_backend = backend_locator(parent_locator, parent, &self.config)?;
490 let parent_checkout = self.state.checkout(&parent.id)?;
491 let parent_bundle = parent_checkout
492 .project_directory()
493 .is_none()
494 .then(|| parent.project_bundle(&self.config))
495 .flatten();
496 let (parent_launch, _, _) = worker_launch_config_with_checkout(
497 parent,
498 &parent_checkout,
499 parent_profile,
500 parent_bundle,
501 &parent_backend,
502 LaunchWorkspace {
503 session_id: &parent.id,
504 container: parent.container_workspace.as_deref(),
505 parent_worktree: None,
506 },
507 &parent_target,
508 )?;
509 launch.cwd = if subagent.working_directory.as_os_str().is_empty() {
510 parent_launch.cwd
511 } else {
512 parent_launch.cwd.join(&subagent.working_directory)
513 };
514 launch.additional_directories = parent_launch.additional_directories;
515 }
516 Ok((launch, project_memory, target_profile_home))
517 }
518
519 pub(in crate::controller) fn current_worker_launch_config(
520 &self,
521 session_id: &str,
522 backend: &targets::TargetLocator,
523 ) -> Result<WorkerLaunchConfig> {
524 let operation = crate::database::load_move_operation(session_id)?;
525 self.worker_launch_config_for_move(session_id, backend, operation.as_ref())
526 }
527
528 fn worker_launch_config_for_move(
529 &self,
530 session_id: &str,
531 backend: &targets::TargetLocator,
532 operation: Option<&mj_core::state::MoveOperation>,
533 ) -> Result<WorkerLaunchConfig> {
534 let session = self
535 .state
536 .sessions
537 .get(session_id)
538 .with_context(|| format!("unknown session {session_id}"))?;
539 let (mut launch, _, _) = self.session_launch_config(session_id, backend)?;
540 if operation.is_some_and(|operation| {
541 operation.source_checkpoint_only
542 && operation.destination_target.is_none()
543 && matches!(
544 operation.phase,
545 mj_core::state::MovePhase::Preparing
546 | mj_core::state::MovePhase::ClosingSource
547 | mj_core::state::MovePhase::Failed
548 | mj_core::state::MovePhase::Cancelled
549 )
550 && session.last_profile == operation.source_profile_id
551 && session.target == operation.source_target
552 && matches!(
553 session.state,
554 mj_core::state::SessionState::Running
555 | mj_core::state::SessionState::Disconnected
556 | mj_core::state::SessionState::Closing
557 )
558 }) {
559 launch.run_mode = mj_core::worker_launch::WorkerRunMode::CheckpointOnly;
560 }
561 Ok(launch)
562 }
563
564 pub fn project_memory_sync_target(
567 &self,
568 session_id: &str,
569 ) -> Result<Option<ProjectMemorySyncTarget>> {
570 let session = self
571 .state
572 .sessions
573 .get(session_id)
574 .with_context(|| format!("unknown session {session_id}"))?;
575 if session.no_project_memory {
576 return Ok(None);
577 }
578 session.validate_configuration(&self.config)?;
579 let locator = session
580 .target
581 .as_ref()
582 .context("session target is missing")?;
583 let backend = backend_locator(locator, session, &self.config)?;
584 let profile = self
585 .config
586 .profiles
587 .get(&session.last_profile)
588 .context("session profile is missing")?;
589 let checkout = self.state.checkout(session_id)?;
590 let bundle = checkout
591 .project_directory()
592 .is_none()
593 .then(|| session.project_bundle(&self.config))
594 .flatten();
595 let workspace = if let Some(project_directory) = checkout.project_directory() {
596 (project_directory.to_string_lossy().into_owned(), Vec::new())
597 } else {
598 workspace_paths(
599 &backend,
600 bundle.context("session bundle is missing")?,
601 session_id,
602 session.container_workspace.as_deref(),
603 )?
604 };
605 let target_home = target_profile_home(&backend, session_id, profile);
606 let launch = project_memory_launch(
607 session,
608 bundle,
609 &workspace,
610 &target_home,
611 self.subagent_parent_worktree(session_id),
612 )?;
613 Ok(Some(ProjectMemorySyncTarget {
614 canonical_root: canonical_memory_root(&launch.project_key),
615 }))
616 }
617}
618
619pub(super) fn apply_jev_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
623 if enabled {
624 return;
625 }
626 for environment in [&mut launch.target_environment, &mut launch.environment] {
627 environment.remove("TYPESAFE_API_KEY");
628 environment.insert(
629 mj_core::jev::DISABLED_ENVIRONMENT.to_owned(),
630 "1".to_owned(),
631 );
632 }
633}
634
635pub(super) fn apply_continuation_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
638 if enabled {
639 return;
640 }
641 for environment in [&mut launch.target_environment, &mut launch.environment] {
642 environment.insert(
643 mj_core::jev::CONTINUATION_DISABLED_ENVIRONMENT.to_owned(),
644 "1".to_owned(),
645 );
646 }
647}
648
649#[cfg(test)]
656pub(super) fn subagent_tools_enabled(
657 session: &mj_core::state::SessionRecord,
658 is_child: bool,
659) -> bool {
660 session
661 .subagents
662 .clone()
663 .unwrap_or_default()
664 .for_launch(session.harness_kind, is_child)
665 .uses_mjolnir()
666}
667
668pub(super) fn worker_workspace_for_recovery(
669 backend: &targets::TargetLocator,
670 directory: &Path,
671) -> Option<WorkerWorkspace> {
672 let target = match backend {
673 targets::TargetLocator::LocalBare { .. } => mj_core::state::ManagedWorktreeTarget::Local,
674 targets::TargetLocator::SshBare { ssh, .. } => mj_core::state::ManagedWorktreeTarget::Ssh {
675 destination: ssh.destination.clone(),
676 ssh_args: ssh.ssh_args.clone(),
677 },
678 targets::TargetLocator::LocalPodman { .. }
679 | targets::TargetLocator::LocalDocker { .. }
680 | targets::TargetLocator::AppleContainer { .. }
681 | targets::TargetLocator::AwsEc2 { .. }
682 | targets::TargetLocator::SshPodman { .. }
683 | targets::TargetLocator::SshDocker { .. } => return None,
684 };
685 Some(WorkerWorkspace {
686 target,
687 directory: directory.to_path_buf(),
688 })
689}
690
691pub(super) struct LaunchWorkspace<'a> {
692 pub session_id: &'a str,
693 pub container: Option<&'a Path>,
694 pub parent_worktree: Option<&'a mj_core::state::ManagedWorktree>,
695}
696
697#[cfg(test)]
698pub(super) fn worker_launch_config(
699 session: &mj_core::state::SessionRecord,
700 profile: &mj_core::config::HarnessProfile,
701 bundle: Option<&ProjectBundle>,
702 backend: &targets::TargetLocator,
703 worker_workspace: LaunchWorkspace<'_>,
704 target: &mj_core::state::TargetRuntimeSettings,
705) -> Result<(
706 WorkerLaunchConfig,
707 Option<ProjectMemoryLaunchConfig>,
708 String,
709)> {
710 worker_launch_config_with_checkout(
711 session,
712 &session.checkout(),
713 profile,
714 bundle,
715 backend,
716 worker_workspace,
717 target,
718 )
719}
720
721pub(super) fn worker_launch_config_with_checkout(
722 session: &mj_core::state::SessionRecord,
723 checkout: &mj_core::state::Checkout<'_>,
724 profile: &mj_core::config::HarnessProfile,
725 bundle: Option<&ProjectBundle>,
726 backend: &targets::TargetLocator,
727 worker_workspace: LaunchWorkspace<'_>,
728 target: &mj_core::state::TargetRuntimeSettings,
729) -> Result<(
730 WorkerLaunchConfig,
731 Option<ProjectMemoryLaunchConfig>,
732 String,
733)> {
734 let session_id = session.id.as_str();
735 let execution_policy = target.execution_policy;
736 let target_profile_home = target_profile_home(backend, session_id, profile);
737 let workspace = if let Some(project_directory) = checkout.project_directory() {
738 (project_directory.to_string_lossy().into_owned(), Vec::new())
739 } else {
740 workspace_paths(
741 backend,
742 bundle.context("session bundle is missing")?,
743 worker_workspace.session_id,
744 worker_workspace.container,
745 )?
746 };
747 let mut additional_directories = workspace.1.iter().map(PathBuf::from).collect::<Vec<_>>();
748 additional_directories.extend(
749 session
750 .additional_mounts
751 .iter()
752 .map(|resource| resource.destination.clone()),
753 );
754 if profile.kind == mj_core::config::HarnessKind::Muse && !additional_directories.is_empty() {
755 bail!(
756 "{} ACP does not support multiple workspace roots; use a single-repository bundle",
757 profile.kind.display_name()
758 );
759 }
760 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
761 let mut target_environment = target.environment.clone();
762 let podman_container = backend.container_engine() == Some("podman");
763 if podman_container {
764 target_environment.insert(
768 mj_core::worker_launch::SESSION_GIT_CONFIG_INCLUDE_PATH.into(),
769 "/home/hel/.gitconfig".into(),
770 );
771 }
772 for name in [
782 "MJ_TURN_STALL_TIMEOUT_MS",
783 "MJ_TURN_TOOL_STALL_TIMEOUT_MS",
784 "RUST_LOG",
785 ] {
786 if let Ok(value) = std::env::var(name) {
787 target_environment.insert(name.to_owned(), value);
788 }
789 }
790 if let Some(key) = mj_core::activity::verdict::api_key() {
793 target_environment.insert("TYPESAFE_API_KEY".to_owned(), key);
794 }
795 if let Some(build_cache) = &session.build_cache {
798 target_environment.insert(
799 "MBX_CACHE_DIR".into(),
800 build_cache.directory.to_string_lossy().into_owned(),
801 );
802 target_environment.insert(
803 "MJ_MBX_CONFIG_DIR".into(),
804 mj_core::config::build_cache_configuration_directory(&build_cache.directory)
805 .to_string_lossy()
806 .into_owned(),
807 );
808 target_environment.insert(
811 "MBX_SHIMS_DIR".into(),
812 Path::new(&targets::worker_root(backend, session_id)?)
813 .join("mbx-shims")
814 .to_string_lossy()
815 .into_owned(),
816 );
817 target_environment.insert("MBX_SUMMARY".into(), "off".into());
820 target_environment.insert("MBX_SAVINGS".into(), "off".into());
821 }
822 let mut environment = target_environment.clone();
823 environment.extend(profile.environment.resolved().clone());
824 environment.insert("MJ_SESSION_ID".into(), session_id.to_owned());
827 if session
828 .target
829 .as_ref()
830 .is_some_and(|target| target.skills_scope() == mj_core::skills::SkillsScope::Localhost)
831 {
832 environment.insert(
835 "MJ_CONFIG_DIR".into(),
836 std::path::absolute(mj_core::config::config_dir())
837 .context("resolve host Mjolnir configuration directory")?
838 .to_string_lossy()
839 .into_owned(),
840 );
841 environment.insert(
842 "MJ_DATA_DIR".into(),
843 std::path::absolute(data_dir())
844 .context("resolve host Mjolnir data directory")?
845 .to_string_lossy()
846 .into_owned(),
847 );
848 if let Some(instance) = mj_core::config::instance_name() {
849 environment.insert("MJ_INSTANCE".into(), instance);
850 } else {
851 environment.remove("MJ_INSTANCE");
852 }
853 }
854 profile
855 .kind
856 .configure_home_environment(Path::new(&target_profile_home), &mut environment);
857 profile
858 .kind
859 .configure_execution_environment(execution_policy, &mut environment)?;
860 let mut project_memory = project_memory_launch(
864 session,
865 bundle,
866 &workspace,
867 &target_profile_home,
868 worker_workspace.parent_worktree,
869 )?;
870 project_memory.mcp_delivery = project_memory_mcp_delivery(profile.kind, backend);
871 project_memory.history_socket =
872 Some(Path::new(&targets::worker_root(backend, &session.id)?).join("control.sock"));
873 if let Some(cache) = &target.tool_cache {
874 let private = Path::new(&targets::worker_root(backend, session_id)?).join("tool-cache");
875 for (name, value) in [
876 (
877 "MJ_TOOL_CACHE_DIR",
878 cache.directory.to_string_lossy().into_owned(),
879 ),
880 ("MJ_TOOL_CACHE_PROJECT", project_memory.project_key.clone()),
881 ("MJ_TOOL_CACHE_HOME", private.to_string_lossy().into_owned()),
882 (
883 "MJ_NX_HOME_DIR",
884 cache.nx_home.to_string_lossy().into_owned(),
885 ),
886 ] {
887 target_environment.insert(name.into(), value.clone());
888 environment.insert(name.into(), value);
889 }
890 }
891 if profile.kind == mj_core::config::HarnessKind::Claude {
892 environment.insert(
893 "CLAUDE_CODE_PROJECT_DIR_NAME".into(),
894 project_memory_replica_slug(&project_memory.project_key, session_id),
895 );
896 if session.no_project_memory {
897 environment.insert(CLAUDE_DISABLE_AUTO_MEMORY_ENV.into(), "1".into());
901 }
902 }
903 let project_memory = (!session.no_project_memory).then_some(project_memory);
904 apply_claude_setup_token(
905 &mut environment,
906 profile.kind,
907 &mj_core::credentials::claude_oauth_token_path(&session.last_profile),
908 );
909 let excluded_environment =
910 exclude_harness_environment(&session.last_profile, profile, &mut environment);
911 if podman_container && profile.kind == mj_core::config::HarnessKind::Claude {
912 environment.insert("IS_SANDBOX".into(), "1".into());
917 }
918 Ok((
919 WorkerLaunchConfig {
920 goal_resume_request: None,
921 target_environment,
922 seed_image_environment: backend.container_engine().is_some(),
923 run_mode: Default::default(),
924 session_id: session_id.to_string(),
925 subagents: mj_core::subagent::SubagentPolicy::Native,
926 handback_tool: false,
927 agent_mailboxes_enabled: true,
928 initial_model: None,
929 review_capture: false,
930 harness: profile.kind,
931 harness_home: PathBuf::from(&target_profile_home),
932 authentication_marker: profile
938 .authentication_marker()
939 .strip_prefix(&profile.home)
940 .ok()
941 .map(|name| name.to_string_lossy().into_owned()),
942 bridge_command: PathBuf::from(bridge_command),
943 bridge_args,
944 harness_runtime: harness_runtime_policy(backend),
945 environment,
946 excluded_environment,
947 cwd: PathBuf::from(&workspace.0),
948 additional_directories,
949 native_session_id: session.native_session_id.clone(),
950 project_memory: project_memory.clone(),
951 execution_policy,
952 },
953 project_memory,
954 target_profile_home,
955 ))
956}
957
958pub(super) fn exclude_harness_environment(
965 profile_id: &str,
966 profile: &mj_core::config::HarnessProfile,
967 environment: &mut std::collections::BTreeMap<String, String>,
968) -> Vec<String> {
969 static REPORTED: std::sync::Mutex<std::collections::BTreeSet<String>> =
970 std::sync::Mutex::new(std::collections::BTreeSet::new());
971 let before = environment.clone();
972 let excluded = profile.exclude_harness_environment(environment);
973 let removed = excluded
974 .iter()
975 .filter(|name| before.contains_key(*name))
976 .cloned()
977 .collect::<Vec<_>>();
978 if !removed.is_empty()
979 && REPORTED
980 .lock()
981 .map(|mut reported| reported.insert(profile_id.to_owned()))
982 .unwrap_or(true)
983 {
984 tracing::info!(
985 profile_id,
986 removed = removed.join(", "),
987 "left API key settings out of the harness environment: this Codex profile must not use an OpenAI API key"
988 );
989 }
990 excluded
991}
992
993pub(super) fn harness_runtime_policy(backend: &targets::TargetLocator) -> HarnessRuntimePolicy {
994 match backend {
995 targets::TargetLocator::LocalBare { .. }
996 | targets::TargetLocator::AwsEc2 { .. }
997 | targets::TargetLocator::SshBare { .. } => HarnessRuntimePolicy::Managed,
998 _ => HarnessRuntimePolicy::Ambient,
999 }
1000}