Skip to main content

mj_controller/controller/worker_binary/
launch.rs

1use super::*;
2
3impl Controller {
4    /// Where this session's worker lives. This is decided from the session
5    /// record and configuration alone, so a caller can name the worker root
6    /// before anything is installed into it.
7    pub(in crate::controller) fn worker_placement(
8        &self,
9        session_id: &str,
10    ) -> Result<(targets::TargetLocator, String)> {
11        let session = self
12            .state
13            .sessions
14            .get(session_id)
15            .with_context(|| format!("unknown session {session_id}"))?;
16        let locator = session
17            .target
18            .as_ref()
19            .context("session target is missing")?;
20        if let Some(owner) = crate::worker_lifecycle::current(session_id) {
21            owner.verify_target(locator)?;
22        }
23        let backend = backend_locator(locator, session, &self.config)?;
24        let worker_root = targets::worker_root(&backend, session_id)?;
25        Ok((backend, worker_root))
26    }
27
28    pub(in crate::controller) fn prepare_worker_files(
29        &self,
30        session_id: &str,
31        backend: &targets::TargetLocator,
32        worker_root: &str,
33        executor: &impl CommandExecutor,
34    ) -> Result<()> {
35        let owner = crate::worker_lifecycle::require(session_id)?;
36        let session = self
37            .state
38            .sessions
39            .get(session_id)
40            .with_context(|| format!("unknown session {session_id}"))?;
41        if let Some(target) = session.target.as_ref() {
42            owner.verify_target(target)?;
43        }
44        let profile = self
45            .config
46            .profiles
47            .get(&session.last_profile)
48            .context("session profile is missing")?;
49        profile.ensure_ready(&session.last_profile)?;
50        let (mut launch, project_memory, target_profile_home) =
51            self.session_launch_config(session_id, backend)?;
52
53        if session.native_session_id.is_some()
54            && profile.kind == mj_core::config::HarnessKind::Codex
55        {
56            launch.goal_resume_request = Some(mj_core::state::new_session_id()?);
57        }
58        let staging = tempfile::tempdir().context("create worker staging directory")?;
59        let launch_path = staging.path().join("launch.json");
60        launch.write(&launch_path)?;
61        let ownership_path = staging.path().join("ownership.json");
62        WorkerOwnership {
63            version: WorkerOwnership::VERSION,
64            workspace_id: session.workspace_id.clone(),
65            session_id: session_id.to_string(),
66            profile_id: session.last_profile.clone(),
67            bundle_id: session.bundle_id.clone(),
68            target_template_id: session.target_template_id.clone(),
69            instance_id: Some(mj_core::config::instance_identity()),
70        }
71        .write(&ownership_path)?;
72        // Every session runs from a staged copy of its profile, on every target,
73        // so `target_profile_home` is always a home the session owns and the
74        // stage is always installed there.
75        let profile_stage = staging.path().join("profile");
76        let started = Instant::now();
77        let result = stage_profile(profile, &profile_stage);
78        tracing::debug!(
79            session_id,
80            elapsed_ms = started.elapsed().as_millis(),
81            "profile staging completed"
82        );
83        result?;
84        stage_managed_skills(
85            profile.kind,
86            &profile_stage,
87            session
88                .target
89                .as_ref()
90                .context("session target is missing")?
91                .skills_scope(),
92        )?;
93        stage_codex_catalog(
94            &session.last_profile,
95            profile,
96            &profile_stage,
97            &fetch_catalog_over_https,
98            &SharedCatalogCache,
99        )?;
100        append_hel_target_environment(profile.kind, &profile_stage, backend)?;
101        append_container_storage_guidance(
102            profile.kind,
103            &profile_stage,
104            backend,
105            session.container_workspace.as_deref(),
106            targets::has_managed_temporary_volume(backend, executor)?,
107        )?;
108        append_subagent_policy(
109            profile.kind,
110            &profile_stage,
111            &launch.subagents,
112            self.config.subagents.max_concurrent,
113        )?;
114        apply_staged_execution_setting(profile.kind, launch.execution_policy, &profile_stage)?;
115        if profile.kind == mj_core::config::HarnessKind::Claude {
116            if let Some(role) = launch.subagents.parent_role() {
117                configure_claude_subagent_mcp(&profile_stage, worker_root, role)?;
118            } else if launch.handback_tool {
119                configure_claude_subagent_mcp(
120                    &profile_stage,
121                    worker_root,
122                    mj_core::subagent::SubagentMcpRole::Child,
123                )?;
124            }
125        }
126        stage_memory_replica(
127            &project_memory,
128            Path::new(&target_profile_home),
129            &profile_stage,
130        )?;
131        if project_memory.mcp_delivery == ProjectMemoryMcpDelivery::HarnessProfile {
132            configure_kimi_project_memory_mcp(&profile_stage, worker_root, &project_memory)?;
133        }
134        let worker_binary = worker_binary_for(backend, executor)?;
135
136        install_worker_files(
137            executor,
138            backend,
139            session_id,
140            worker_root,
141            &target_profile_home,
142            &worker_binary,
143            &launch_path,
144            &ownership_path,
145            &profile_stage,
146        )?;
147        if session.build_cache.is_some() {
148            self.install_build_cache_shim(session, backend, &launch, executor)
149                .context("install shared machine build cache configuration")?;
150        }
151        prepare_installed_managed_harness(executor, backend, worker_root, &launch)
152    }
153
154    /// Put the pinned mbx binary and its `cargo` shim in the session's `bin`
155    /// directory, which the worker prepends to `PATH` for the harness, its
156    /// terminals, and `bash -lc` shells. mbx invoked as `cargo` removes that
157    /// directory from `PATH` and runs the image's real Cargo underneath.
158    pub(in crate::controller) fn install_build_cache_shim(
159        &self,
160        session: &mj_core::state::SessionRecord,
161        backend: &targets::TargetLocator,
162        launch: &WorkerLaunchConfig,
163        executor: &impl CommandExecutor,
164    ) -> Result<()> {
165        let worker_root = targets::worker_root(backend, &session.id)?;
166        // The download is the one build-cache failure worth telling the user
167        // about: it is fixable, and it is the only step that reaches the
168        // network.
169        let binary =
170            crate::controller::mbx::binary_for(backend, executor).inspect_err(|error| {
171                executor.notify_notice(&format!(
172                    "The Rust build cache could not be prepared: {error:#}."
173                ));
174            })?;
175        let (configuration, config_roots) =
176            self.build_cache_configuration(session, backend, launch, executor)?;
177        install_mbx_files(
178            executor,
179            backend,
180            &session.id,
181            &worker_root,
182            &binary,
183            &configuration,
184            &config_roots,
185        )
186    }
187
188    pub(in crate::controller) fn prepare_build_cache_links(
189        &self,
190        session: &mj_core::state::SessionRecord,
191        backend: &targets::TargetLocator,
192        launch: &WorkerLaunchConfig,
193        executor: &impl CommandExecutor,
194    ) -> Result<()> {
195        let (configuration, config_roots) =
196            self.build_cache_configuration(session, backend, launch, executor)?;
197        link_mbx_configuration(executor, backend, &configuration, &config_roots)
198    }
199
200    fn build_cache_configuration(
201        &self,
202        session: &mj_core::state::SessionRecord,
203        backend: &targets::TargetLocator,
204        launch: &WorkerLaunchConfig,
205        executor: &impl CommandExecutor,
206    ) -> Result<(PathBuf, Vec<PathBuf>)> {
207        let configuration = crate::controller::mbx::prepare_session_configuration(
208            &self.config,
209            backend,
210            session
211                .build_cache
212                .as_ref()
213                .context("session has no build cache")?,
214            executor,
215        )?;
216        let config_roots = [&launch.target_environment, &launch.environment]
217            .into_iter()
218            .filter_map(|environment| environment.get("XDG_CONFIG_HOME").map(PathBuf::from))
219            .collect::<std::collections::BTreeSet<_>>();
220        Ok((configuration, config_roots.into_iter().collect()))
221    }
222
223    /// Probe the installed binary and the worker's recorded state after a
224    /// session becomes unreachable. Returns `None` only when the session has
225    /// no target to probe; a probe that fails says why.
226    pub fn diagnose_worker(&self, session_id: &str) -> Option<String> {
227        self.diagnose_worker_controlled(session_id, &crate::targets::ProcessExecutor)
228    }
229
230    pub fn diagnose_worker_controlled(
231        &self,
232        session_id: &str,
233        executor: &impl CommandExecutor,
234    ) -> Option<String> {
235        let session = self.state.sessions.get(session_id)?;
236        let locator = session.target.as_ref()?;
237        let backend = match backend_locator(locator, session, &self.config) {
238            Ok(backend) => backend,
239            Err(error) => {
240                tracing::debug!(
241                    session_id,
242                    error = format!("{error:#}"),
243                    "could not construct a worker diagnostic probe"
244                );
245                return None;
246            }
247        };
248        let worker_root = match targets::worker_root(&backend, session_id) {
249            Ok(root) => root,
250            Err(error) => {
251                tracing::debug!(
252                    session_id,
253                    error = format!("{error:#}"),
254                    "could not derive the worker diagnostic root"
255                );
256                return None;
257            }
258        };
259        let binary_failure = worker_binary_probe_failure(executor, &backend, &worker_root);
260        let probe = match probe_worker(executor, &backend, &worker_root) {
261            Ok(probe) => probe.to_string(),
262            Err(error) => format!("the worker could not be probed: {error:#}"),
263        };
264        Some(match binary_failure {
265            Some(binary_failure) => format!("{binary_failure}; {probe}"),
266            None => probe,
267        })
268    }
269
270    /// A non-destructive liveness probe plus commands that replace a confirmed
271    /// dead session worker without touching its durable relay files. The
272    /// session manager runs both off its async actor.
273    pub fn worker_recovery_plan(
274        &self,
275        session_id: &str,
276        operation: Option<&mj_core::state::MoveOperation>,
277    ) -> Result<WorkerRecoveryPlan> {
278        let (backend, worker_root) = self.worker_placement(session_id)?;
279        let launch = self.worker_launch_config_for_move(session_id, &backend, operation)?;
280        let workspace = worker_workspace_for_recovery(&backend, &launch.cwd);
281        Ok(WorkerRecoveryPlan {
282            source_target: self.state.sessions[session_id]
283                .target
284                .clone()
285                .context("session target is missing")?,
286            target: targets::target_recovery_plan(&backend, session_id)?,
287            workspace,
288            liveness_probe: worker_liveness_command(&backend, &worker_root),
289            exit_record: Some(worker_exit_record_command(&backend, &worker_root)),
290            binary_refresh: worker_binary_refresh_plan(&backend, session_id)?,
291            launch_refresh: Some(worker_launch_refresh_plan(&backend, session_id, &launch)?),
292            restart: CommandPlan {
293                description: format!("restart Mjolnir worker for session {session_id}"),
294                commands: vec![
295                    stop_worker_command(&backend, &worker_root),
296                    start_worker_command(&backend, &worker_root),
297                ],
298            },
299        })
300    }
301
302    /// The launch config for a session, including what depends on its
303    /// sub-agent role. The first launch and every relaunch use this, so a
304    /// relaunched worker keeps its delegation tools and a relaunched child
305    /// keeps its parent's workspace.
306    fn session_launch_config(
307        &self,
308        session_id: &str,
309        backend: &targets::TargetLocator,
310    ) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
311        let session = self
312            .state
313            .sessions
314            .get(session_id)
315            .with_context(|| format!("unknown session {session_id}"))?;
316        session.validate_configuration(&self.config)?;
317        let profile = self
318            .config
319            .profiles
320            .get(&session.last_profile)
321            .context("session profile is missing")?;
322        let bundle = session
323            .project_directory
324            .is_none()
325            .then(|| session.project_bundle(&self.config))
326            .flatten();
327        let target = session.target_runtime_settings(&self.config)?;
328        let subagent = self.state.subagents.get(session_id);
329        // A sub-agent child shares its parent's container, so it works in the
330        // parent's workspace. The parent record is authoritative for that path.
331        let (workspace_session_id, workspace_container) = match subagent.as_ref() {
332            Some(child) => {
333                let parent = self
334                    .state
335                    .sessions
336                    .get(&child.parent_session_id)
337                    .context("sub-agent parent session is missing")?;
338                (parent.id.clone(), parent.container_workspace.clone())
339            }
340            None => (session_id.to_owned(), session.container_workspace.clone()),
341        };
342        let (mut launch, project_memory, target_profile_home) = worker_launch_config(
343            session,
344            profile,
345            bundle,
346            backend,
347            LaunchWorkspace {
348                session_id: &workspace_session_id,
349                container: workspace_container.as_deref(),
350                parent_worktree: self.subagent_parent_worktree(session_id),
351            },
352            &target,
353        )?;
354        apply_jev_switch(&mut launch, self.config.jev.enabled);
355        apply_continuation_switch(&mut launch, self.config.automatic_continuation_enabled());
356        launch.subagents = session
357            .subagents
358            .clone()
359            .unwrap_or_default()
360            .for_launch(profile.kind, subagent.is_some());
361        // Registration decided whether this child can be given the tool.
362        launch.handback_tool = subagent.as_ref().is_some_and(|child| child.handback_tool);
363        // The child's harness opens on its spawn model instead of switching
364        // to it from the profile default before the first turn (issue 1217).
365        launch.initial_model = subagent.as_ref().and_then(|child| child.model.clone());
366        // Capturing the working tree is only ever useful to a turn review, so
367        // it is spent only on a session a review can run for: one whose
368        // configuration has an eligible reviewer, and that is not a child. A child
369        // works in its parent's tree, and reviewing it would report the
370        // parent's work as the child's.
371        launch.review_capture =
372            mj_core::review::settings::can_review(&self.config) && subagent.is_none();
373        launch.bifrost_binary = mj_review::bifrost::configured_bifrost_binary();
374        if let Some(subagent) = &subagent {
375            let parent = self
376                .state
377                .sessions
378                .get(&subagent.parent_session_id)
379                .context("sub-agent parent session is missing")?;
380            let parent_profile = self
381                .config
382                .profiles
383                .get(&parent.last_profile)
384                .context("sub-agent parent profile is missing")?;
385            let parent_target = parent.target_runtime_settings(&self.config)?;
386            let parent_locator = parent
387                .target
388                .as_ref()
389                .context("sub-agent parent has no live target")?;
390            let parent_backend = backend_locator(parent_locator, parent, &self.config)?;
391            let parent_bundle = parent
392                .project_directory
393                .is_none()
394                .then(|| parent.project_bundle(&self.config))
395                .flatten();
396            let (parent_launch, _, _) = worker_launch_config(
397                parent,
398                parent_profile,
399                parent_bundle,
400                &parent_backend,
401                LaunchWorkspace {
402                    session_id: &parent.id,
403                    container: parent.container_workspace.as_deref(),
404                    parent_worktree: None,
405                },
406                &parent_target,
407            )?;
408            launch.cwd = if subagent.working_directory.as_os_str().is_empty() {
409                parent_launch.cwd
410            } else {
411                parent_launch.cwd.join(&subagent.working_directory)
412            };
413            launch.additional_directories = parent_launch.additional_directories;
414        }
415        Ok((launch, project_memory, target_profile_home))
416    }
417
418    pub(in crate::controller) fn current_worker_launch_config(
419        &self,
420        session_id: &str,
421        backend: &targets::TargetLocator,
422    ) -> Result<WorkerLaunchConfig> {
423        let operation = crate::database::load_move_operation(session_id)?;
424        self.worker_launch_config_for_move(session_id, backend, operation.as_ref())
425    }
426
427    fn worker_launch_config_for_move(
428        &self,
429        session_id: &str,
430        backend: &targets::TargetLocator,
431        operation: Option<&mj_core::state::MoveOperation>,
432    ) -> Result<WorkerLaunchConfig> {
433        let session = self
434            .state
435            .sessions
436            .get(session_id)
437            .with_context(|| format!("unknown session {session_id}"))?;
438        let (mut launch, _, _) = self.session_launch_config(session_id, backend)?;
439        if operation.is_some_and(|operation| {
440            operation.source_checkpoint_only
441                && operation.destination_target.is_none()
442                && matches!(
443                    operation.phase,
444                    mj_core::state::MovePhase::Preparing
445                        | mj_core::state::MovePhase::ClosingSource
446                        | mj_core::state::MovePhase::Failed
447                        | mj_core::state::MovePhase::Cancelled
448                )
449                && session.last_profile == operation.source_profile_id
450                && session.target == operation.source_target
451                && matches!(
452                    session.state,
453                    mj_core::state::SessionState::Running
454                        | mj_core::state::SessionState::Disconnected
455                        | mj_core::state::SessionState::Closing
456                )
457        }) {
458            launch.run_mode = mj_core::worker_launch::WorkerRunMode::CheckpointOnly;
459        }
460        Ok(launch)
461    }
462
463    pub fn project_memory_sync_target(&self, session_id: &str) -> Result<ProjectMemorySyncTarget> {
464        let session = self
465            .state
466            .sessions
467            .get(session_id)
468            .with_context(|| format!("unknown session {session_id}"))?;
469        session.validate_configuration(&self.config)?;
470        let locator = session
471            .target
472            .as_ref()
473            .context("session target is missing")?;
474        let backend = backend_locator(locator, session, &self.config)?;
475        let profile = self
476            .config
477            .profiles
478            .get(&session.last_profile)
479            .context("session profile is missing")?;
480        let bundle = session
481            .project_directory
482            .is_none()
483            .then(|| session.project_bundle(&self.config))
484            .flatten();
485        let workspace = if let Some(project_directory) = &session.project_directory {
486            (project_directory.to_string_lossy().into_owned(), Vec::new())
487        } else {
488            workspace_paths(
489                &backend,
490                bundle.context("session bundle is missing")?,
491                session_id,
492                session.container_workspace.as_deref(),
493            )?
494        };
495        let target_home = target_profile_home(&backend, session_id, profile);
496        let launch = project_memory_launch(
497            session,
498            bundle,
499            &workspace,
500            &target_home,
501            self.subagent_parent_worktree(session_id),
502        )?;
503        Ok(ProjectMemorySyncTarget {
504            canonical_root: canonical_memory_root(&launch.project_key),
505        })
506    }
507}
508
509/// Carries `[jev] enabled = false` to the worker, which reads it from its
510/// launch environment, and keeps the Jev key out of the worker and the
511/// harness so nothing can reach the service.
512pub(super) fn apply_jev_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
513    if enabled {
514        return;
515    }
516    for environment in [&mut launch.target_environment, &mut launch.environment] {
517        environment.remove("TYPESAFE_API_KEY");
518        environment.insert(
519            mj_core::jev::DISABLED_ENVIRONMENT.to_owned(),
520            "1".to_owned(),
521        );
522    }
523}
524
525/// Tell the worker when nobody will act on a `Continue` verdict, so it does
526/// not park the assessment as deferred. Travels like the Jev switch.
527pub(super) fn apply_continuation_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
528    if enabled {
529        return;
530    }
531    for environment in [&mut launch.target_environment, &mut launch.environment] {
532        environment.insert(
533            mj_core::jev::CONTINUATION_DISABLED_ENVIRONMENT.to_owned(),
534            "1".to_owned(),
535        );
536    }
537}
538
539/// Whether this session gets Mjolnir's delegation tools in place of its
540/// harness's own. The session's stored choice governs; `None` means native
541/// sub-agents, so a session created before the per-session choice existed (or
542/// through `mj new` with neither flag given) gets its harness's own
543/// sub-agents. A child never gets them, and only Claude and Codex can receive
544/// them at all.
545#[cfg(test)]
546pub(super) fn subagent_tools_enabled(
547    session: &mj_core::state::SessionRecord,
548    is_child: bool,
549) -> bool {
550    session
551        .subagents
552        .clone()
553        .unwrap_or_default()
554        .for_launch(session.harness_kind, is_child)
555        .uses_mjolnir()
556}
557
558pub(super) fn worker_workspace_for_recovery(
559    backend: &targets::TargetLocator,
560    directory: &Path,
561) -> Option<WorkerWorkspace> {
562    let target = match backend {
563        targets::TargetLocator::LocalBare { .. } => mj_core::state::ManagedWorktreeTarget::Local,
564        targets::TargetLocator::SshBare { ssh, .. } => mj_core::state::ManagedWorktreeTarget::Ssh {
565            destination: ssh.destination.clone(),
566            ssh_args: ssh.ssh_args.clone(),
567        },
568        targets::TargetLocator::LocalPodman { .. }
569        | targets::TargetLocator::LocalDocker { .. }
570        | targets::TargetLocator::AppleContainer { .. }
571        | targets::TargetLocator::AwsEc2 { .. }
572        | targets::TargetLocator::SshPodman { .. }
573        | targets::TargetLocator::SshDocker { .. } => return None,
574    };
575    Some(WorkerWorkspace {
576        target,
577        directory: directory.to_path_buf(),
578    })
579}
580
581pub(super) struct LaunchWorkspace<'a> {
582    pub session_id: &'a str,
583    pub container: Option<&'a Path>,
584    pub parent_worktree: Option<&'a mj_core::state::ManagedWorktree>,
585}
586
587pub(super) fn worker_launch_config(
588    session: &mj_core::state::SessionRecord,
589    profile: &mj_core::config::HarnessProfile,
590    bundle: Option<&ProjectBundle>,
591    backend: &targets::TargetLocator,
592    worker_workspace: LaunchWorkspace<'_>,
593    target: &mj_core::state::TargetRuntimeSettings,
594) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
595    let session_id = session.id.as_str();
596    let execution_policy = profile
597        .kind
598        .effective_execution_policy(target.execution_policy);
599    let target_profile_home = target_profile_home(backend, session_id, profile);
600    let workspace = if let Some(project_directory) = &session.project_directory {
601        (project_directory.to_string_lossy().into_owned(), Vec::new())
602    } else {
603        workspace_paths(
604            backend,
605            bundle.context("session bundle is missing")?,
606            worker_workspace.session_id,
607            worker_workspace.container,
608        )?
609    };
610    let mut additional_directories = workspace.1.iter().map(PathBuf::from).collect::<Vec<_>>();
611    additional_directories.extend(
612        session
613            .additional_mounts
614            .iter()
615            .map(|resource| resource.destination.clone()),
616    );
617    if profile.kind == mj_core::config::HarnessKind::Muse && !additional_directories.is_empty() {
618        bail!(
619            "{} ACP does not support multiple workspace roots; use a single-repository bundle",
620            profile.kind.display_name()
621        );
622    }
623    let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
624    let mut target_environment = target.environment.clone();
625    // The turn bounds are read by the worker process, which re-execs with a
626    // cleared environment, so a value set for the daemon cannot reach it by
627    // inheritance. Carry the two knobs explicitly when the daemon was started
628    // with them, so shortening a timeout for a test works on every target and
629    // not only on the container targets that can set it in configuration.
630    // `RUST_LOG` travels the same way and for the same reason: a worker that
631    // has gone quiet is diagnosed from its own log, and the log level cannot
632    // be raised after the fact on a worker that re-execs with a cleared
633    // environment.
634    for name in [
635        "MJ_TURN_STALL_TIMEOUT_MS",
636        "MJ_TURN_TOOL_STALL_TIMEOUT_MS",
637        "RUST_LOG",
638    ] {
639        if let Ok(value) = std::env::var(name) {
640            target_environment.insert(name.to_owned(), value);
641        }
642    }
643    // Resolve the daemon's local key before launching remote or container
644    // workers, whose home directories do not contain its secrets file.
645    if let Some(key) = mj_core::activity::verdict::api_key() {
646        target_environment.insert("TYPESAFE_API_KEY".to_owned(), key);
647    }
648    // The build cache reaches the harness, its terminals, and the reviewer
649    // sidecar, all of which run Cargo through the mbx shim.
650    if let Some(build_cache) = &session.build_cache {
651        target_environment.insert(
652            "MBX_CACHE_DIR".into(),
653            build_cache.directory.to_string_lossy().into_owned(),
654        );
655        target_environment.insert(
656            "MJ_MBX_CONFIG_DIR".into(),
657            mj_core::config::build_cache_configuration_directory(&build_cache.directory)
658                .to_string_lossy()
659                .into_owned(),
660        );
661        // Compiler symlinks name this worker's private executable. Sharing
662        // them lets another container replace them with an unreachable path.
663        target_environment.insert(
664            "MBX_SHIMS_DIR".into(),
665            Path::new(&targets::worker_root(backend, session_id)?)
666                .join("mbx-shims")
667                .to_string_lossy()
668                .into_owned(),
669        );
670        // The per-build summary and savings lines are for a human at a
671        // terminal; in a harness session they only add noise to Cargo output.
672        target_environment.insert("MBX_SUMMARY".into(), "off".into());
673        target_environment.insert("MBX_SAVINGS".into(), "off".into());
674    }
675    let mut environment = target_environment.clone();
676    environment.extend(profile.environment.resolved().clone());
677    if session
678        .target
679        .as_ref()
680        .is_some_and(|target| target.skills_scope() == mj_core::skills::SkillsScope::Localhost)
681    {
682        // Harnesses clear their environment. Host CLI commands must still
683        // address the daemon that created this session, including named instances.
684        environment.insert(
685            "MJ_CONFIG_DIR".into(),
686            std::path::absolute(mj_core::config::config_dir())
687                .context("resolve host Mjolnir configuration directory")?
688                .to_string_lossy()
689                .into_owned(),
690        );
691        environment.insert(
692            "MJ_DATA_DIR".into(),
693            std::path::absolute(data_dir())
694                .context("resolve host Mjolnir data directory")?
695                .to_string_lossy()
696                .into_owned(),
697        );
698        if let Some(instance) = mj_core::config::instance_name() {
699            environment.insert("MJ_INSTANCE".into(), instance);
700        } else {
701            environment.remove("MJ_INSTANCE");
702        }
703    }
704    profile
705        .kind
706        .configure_home_environment(Path::new(&target_profile_home), &mut environment);
707    profile
708        .kind
709        .configure_execution_environment(execution_policy, &mut environment)?;
710    let mut project_memory = project_memory_launch(
711        session,
712        bundle,
713        &workspace,
714        &target_profile_home,
715        worker_workspace.parent_worktree,
716    )?;
717    project_memory.mcp_delivery = project_memory_mcp_delivery(profile.kind, backend);
718    project_memory.history_socket =
719        Some(Path::new(&targets::worker_root(backend, &session.id)?).join("control.sock"));
720    if profile.kind == mj_core::config::HarnessKind::Claude {
721        environment.insert(
722            "CLAUDE_CODE_PROJECT_DIR_NAME".into(),
723            project_memory_replica_slug(&project_memory.project_key, session_id),
724        );
725    }
726    apply_claude_setup_token(
727        &mut environment,
728        profile.kind,
729        &mj_core::credentials::claude_oauth_token_path(&session.last_profile),
730    );
731    let excluded_environment =
732        exclude_harness_environment(&session.last_profile, profile, &mut environment);
733    Ok((
734        WorkerLaunchConfig {
735            goal_resume_request: None,
736            target_environment,
737            seed_image_environment: backend.container_engine().is_some(),
738            run_mode: Default::default(),
739            session_id: session_id.to_string(),
740            subagents: mj_core::subagent::SubagentPolicy::Native,
741            handback_tool: false,
742            initial_model: None,
743            review_capture: false,
744            bifrost_binary: None,
745            harness: profile.kind,
746            harness_home: PathBuf::from(&target_profile_home),
747            // The staged home mirrors the profile home, so the marker's path
748            // within the profile home is the one the worker must check and
749            // the credential sync must write. Kimi's is nested
750            // (`credentials/kimi-code.json`); its file name alone named a file
751            // at the top of the staged home that Kimi never reads.
752            authentication_marker: profile
753                .authentication_marker()
754                .strip_prefix(&profile.home)
755                .ok()
756                .map(|name| name.to_string_lossy().into_owned()),
757            bridge_command: PathBuf::from(bridge_command),
758            bridge_args,
759            harness_runtime: harness_runtime_policy(backend),
760            environment,
761            excluded_environment,
762            cwd: PathBuf::from(&workspace.0),
763            additional_directories,
764            native_session_id: session.native_session_id.clone(),
765            project_memory: Some(project_memory.clone()),
766            execution_policy,
767        },
768        project_memory,
769        target_profile_home,
770    ))
771}
772
773/// Leave out of a launch every variable the profile's harness must never see,
774/// and name them for the worker, which removes them again once it has added
775/// the target's own login environment.
776///
777/// Saying so once per profile is enough: the launch config is rebuilt for
778/// every recovery check, and the variables do not change between them.
779pub(super) fn exclude_harness_environment(
780    profile_id: &str,
781    profile: &mj_core::config::HarnessProfile,
782    environment: &mut std::collections::BTreeMap<String, String>,
783) -> Vec<String> {
784    static REPORTED: std::sync::Mutex<std::collections::BTreeSet<String>> =
785        std::sync::Mutex::new(std::collections::BTreeSet::new());
786    let before = environment.clone();
787    let excluded = profile.exclude_harness_environment(environment);
788    let removed = excluded
789        .iter()
790        .filter(|name| before.contains_key(*name))
791        .cloned()
792        .collect::<Vec<_>>();
793    if !removed.is_empty()
794        && REPORTED
795            .lock()
796            .map(|mut reported| reported.insert(profile_id.to_owned()))
797            .unwrap_or(true)
798    {
799        tracing::info!(
800            profile_id,
801            removed = removed.join(", "),
802            "left API key settings out of the harness environment: this Codex profile signs in with ChatGPT and must not fall back to an API key"
803        );
804    }
805    excluded
806}
807
808pub(super) fn harness_runtime_policy(backend: &targets::TargetLocator) -> HarnessRuntimePolicy {
809    match backend {
810        targets::TargetLocator::LocalBare { .. }
811        | targets::TargetLocator::AwsEc2 { .. }
812        | targets::TargetLocator::SshBare { .. } => HarnessRuntimePolicy::Managed,
813        _ => HarnessRuntimePolicy::Ambient,
814    }
815}