Skip to main content

mj_controller/controller/worker_binary/
launch.rs

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    /// Where this session's worker lives. This is decided from the session
41    /// record and configuration alone, so a caller can name the worker root
42    /// before anything is installed into it.
43    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        // Every session runs from a staged copy of its profile, on every target,
109        // so `target_profile_home` is always a home the session owns and the
110        // stage is always installed there.
111        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        // A session without project memory gets no replica, no baseline and
165        // no history server: nothing of the workspace's memory reaches it.
166        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    /// Refresh and verify the shared-cache copy, then install marked launchers
202    /// in the session's `bin` directory on the worker PATH.
203    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    /// Probe the installed binary and the worker's recorded state after a
299    /// session becomes unreachable. Returns `None` only when the session has
300    /// no target to probe; a probe that fails says why.
301    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    /// A non-destructive liveness probe plus commands that replace a confirmed
346    /// dead session worker without touching its durable relay files. The
347    /// session manager runs both off its async actor.
348    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    /// The launch config for a session, including what depends on its
378    /// sub-agent role. The first launch and every relaunch use this, so a
379    /// relaunched worker keeps its delegation tools and a relaunched child
380    /// keeps its parent's workspace.
381    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        // A sub-agent child shares its parent's container, so it works in the
410        // parent's workspace. The parent record is authoritative for that path.
411        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            // This controller stages the message-only registration for every
448            // top-level session; the worker trusts only this marker.
449            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        // Registration decided whether this child can be given the tool.
463        launch.handback_tool = subagent.as_ref().is_some_and(|child| child.handback_tool);
464        // The child's harness opens on its spawn model instead of switching
465        // to it from the profile default before the first turn (issue 1217).
466        launch.initial_model = subagent.as_ref().and_then(|child| child.model.clone());
467        // Capturing the working tree is only ever useful to a turn review, so
468        // it is spent only on a session a review can run for: one whose
469        // configuration has an eligible reviewer, and that is not a child. A child
470        // works in its parent's tree, and reviewing it would report the
471        // parent's work as the child's.
472        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    /// Where the session's project memory is synchronized, or `None` for a
566    /// session that has none.
567    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
620/// Carries `[jev] enabled = false` to the worker, which reads it from its
621/// launch environment, and keeps the Jev key out of the worker and the
622/// harness so nothing can reach the service.
623pub(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
636/// Tell the worker when nobody will act on a `Continue` verdict, so it does
637/// not park the assessment as deferred. Travels like the Jev switch.
638pub(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/// Whether this session gets Mjolnir's delegation tools in place of its
651/// harness's own. The session's stored choice governs; `None` means native
652/// sub-agents, so a session created before the per-session choice existed (or
653/// through `mj new` with neither flag given) gets its harness's own
654/// sub-agents. A child never gets them, and only Claude and Codex can receive
655/// them at all.
656#[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        // Provisioning writes inherited settings to this file under the
766        // container's configured home. A root Podman worker can have
767        // HOME=/root, so the session Git config includes the file by path.
768        target_environment.insert(
769            mj_core::worker_launch::SESSION_GIT_CONFIG_INCLUDE_PATH.into(),
770            "/home/hel/.gitconfig".into(),
771        );
772    }
773    // The turn bounds are read by the worker process, which re-execs with a
774    // cleared environment, so a value set for the daemon cannot reach it by
775    // inheritance. Carry the two knobs explicitly when the daemon was started
776    // with them, so shortening a timeout for a test works on every target and
777    // not only on the container targets that can set it in configuration.
778    // `RUST_LOG` travels the same way and for the same reason: a worker that
779    // has gone quiet is diagnosed from its own log, and the log level cannot
780    // be raised after the fact on a worker that re-execs with a cleared
781    // environment.
782    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    // Resolve the daemon's local key before launching remote or container
792    // workers, whose home directories do not contain its secrets file.
793    if let Some(key) = mj_core::activity::verdict::api_key() {
794        target_environment.insert("TYPESAFE_API_KEY".to_owned(), key);
795    }
796    // The build cache reaches the harness, its terminals, and the reviewer
797    // sidecar, all of which run Cargo through the mbx shim.
798    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        // Compiler symlinks name this worker's private executable. Sharing
810        // them lets another container replace them with an unreachable path.
811        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        // The per-build summary and savings lines are for a human at a
819        // terminal; in a harness session they only add noise to Cargo output.
820        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    // This ownership fact is generated by the controller, so a profile cannot
826    // impersonate another session or turn a child into a top-level sender.
827    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        // Harnesses clear their environment. Host CLI commands must still
834        // address the daemon that created this session, including named instances.
835        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    // The project's identity also keys the shared tool cache and names the
862    // Claude project directory, so it is worked out for every session. Only
863    // the memory launch config below depends on the session's choice.
864    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            // Claude Code reads and offers its auto-memory directory unless
899            // told not to. Set after the profile's own environment so the
900            // session's choice wins.
901            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        // Claude Code rejects bypassPermissions under uid 0 unless it is
914        // told the process is already running in an isolated sandbox.
915        // Podman container exec inherits HOME and the configured user from
916        // the container, preserving both for stopped legacy sessions.
917        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            // The staged home mirrors the profile home, so the marker's path
934            // within the profile home is the one the worker must check and
935            // the credential sync must write. Kimi's is nested
936            // (`credentials/kimi-code.json`); its file name alone named a file
937            // at the top of the staged home that Kimi never reads.
938            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
959/// Leave out of a launch every variable the profile's harness must never see,
960/// and name them for the worker, which removes them again once it has added
961/// the target's own login environment.
962///
963/// Saying so once per profile is enough: the launch config is rebuilt for
964/// every recovery check, and the variables do not change between them.
965pub(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}