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