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