1use super::*;
2
3impl Controller {
4 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 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 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 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 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 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 binary_refresh: worker_binary_refresh_plan(&backend, session_id)?,
290 launch_refresh: Some(worker_launch_refresh_plan(&backend, session_id, &launch)?),
291 restart: CommandPlan {
292 description: format!("restart Mjolnir worker for session {session_id}"),
293 commands: vec![
294 stop_worker_command(&backend, &worker_root),
295 start_worker_command(&backend, &worker_root),
296 ],
297 },
298 })
299 }
300
301 fn session_launch_config(
306 &self,
307 session_id: &str,
308 backend: &targets::TargetLocator,
309 ) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
310 let session = self
311 .state
312 .sessions
313 .get(session_id)
314 .with_context(|| format!("unknown session {session_id}"))?;
315 session.validate_configuration(&self.config)?;
316 let profile = self
317 .config
318 .profiles
319 .get(&session.last_profile)
320 .context("session profile is missing")?;
321 let bundle = session
322 .project_directory
323 .is_none()
324 .then(|| session.project_bundle(&self.config))
325 .flatten();
326 let target = session.target_runtime_settings(&self.config)?;
327 let subagent = self.state.subagents.get(session_id);
328 let (workspace_session_id, workspace_container) = match subagent.as_ref() {
331 Some(child) => {
332 let parent = self
333 .state
334 .sessions
335 .get(&child.parent_session_id)
336 .context("sub-agent parent session is missing")?;
337 (parent.id.clone(), parent.container_workspace.clone())
338 }
339 None => (session_id.to_owned(), session.container_workspace.clone()),
340 };
341 let (mut launch, project_memory, target_profile_home) = worker_launch_config(
342 session,
343 profile,
344 bundle,
345 backend,
346 LaunchWorkspace {
347 session_id: &workspace_session_id,
348 container: workspace_container.as_deref(),
349 parent_worktree: self.subagent_parent_worktree(session_id),
350 },
351 &target,
352 )?;
353 apply_jev_switch(&mut launch, self.config.jev.enabled);
354 apply_continuation_switch(&mut launch, self.config.automatic_continuation_enabled());
355 launch.subagents = session
356 .subagents
357 .clone()
358 .unwrap_or_default()
359 .for_launch(profile.kind, subagent.is_some());
360 launch.handback_tool = subagent.as_ref().is_some_and(|child| child.handback_tool);
362 launch.review_capture =
368 mj_core::review::settings::can_review(&self.config) && subagent.is_none();
369 launch.bifrost_binary = mj_review::bifrost::configured_bifrost_binary();
370 if let Some(subagent) = &subagent {
371 let parent = self
372 .state
373 .sessions
374 .get(&subagent.parent_session_id)
375 .context("sub-agent parent session is missing")?;
376 let parent_profile = self
377 .config
378 .profiles
379 .get(&parent.last_profile)
380 .context("sub-agent parent profile is missing")?;
381 let parent_target = parent.target_runtime_settings(&self.config)?;
382 let parent_locator = parent
383 .target
384 .as_ref()
385 .context("sub-agent parent has no live target")?;
386 let parent_backend = backend_locator(parent_locator, parent, &self.config)?;
387 let parent_bundle = parent
388 .project_directory
389 .is_none()
390 .then(|| parent.project_bundle(&self.config))
391 .flatten();
392 let (parent_launch, _, _) = worker_launch_config(
393 parent,
394 parent_profile,
395 parent_bundle,
396 &parent_backend,
397 LaunchWorkspace {
398 session_id: &parent.id,
399 container: parent.container_workspace.as_deref(),
400 parent_worktree: None,
401 },
402 &parent_target,
403 )?;
404 launch.cwd = if subagent.working_directory.as_os_str().is_empty() {
405 parent_launch.cwd
406 } else {
407 parent_launch.cwd.join(&subagent.working_directory)
408 };
409 launch.additional_directories = parent_launch.additional_directories;
410 }
411 Ok((launch, project_memory, target_profile_home))
412 }
413
414 pub(in crate::controller) fn current_worker_launch_config(
415 &self,
416 session_id: &str,
417 backend: &targets::TargetLocator,
418 ) -> Result<WorkerLaunchConfig> {
419 let operation = crate::database::load_move_operation(session_id)?;
420 self.worker_launch_config_for_move(session_id, backend, operation.as_ref())
421 }
422
423 fn worker_launch_config_for_move(
424 &self,
425 session_id: &str,
426 backend: &targets::TargetLocator,
427 operation: Option<&mj_core::state::MoveOperation>,
428 ) -> Result<WorkerLaunchConfig> {
429 let session = self
430 .state
431 .sessions
432 .get(session_id)
433 .with_context(|| format!("unknown session {session_id}"))?;
434 let (mut launch, _, _) = self.session_launch_config(session_id, backend)?;
435 if operation.is_some_and(|operation| {
436 operation.source_checkpoint_only
437 && operation.destination_target.is_none()
438 && matches!(
439 operation.phase,
440 mj_core::state::MovePhase::Preparing
441 | mj_core::state::MovePhase::ClosingSource
442 | mj_core::state::MovePhase::Failed
443 | mj_core::state::MovePhase::Cancelled
444 )
445 && session.last_profile == operation.source_profile_id
446 && session.target == operation.source_target
447 && matches!(
448 session.state,
449 mj_core::state::SessionState::Running
450 | mj_core::state::SessionState::Disconnected
451 | mj_core::state::SessionState::Closing
452 )
453 }) {
454 launch.run_mode = mj_core::worker_launch::WorkerRunMode::CheckpointOnly;
455 }
456 Ok(launch)
457 }
458
459 pub fn project_memory_sync_target(&self, session_id: &str) -> Result<ProjectMemorySyncTarget> {
460 let session = self
461 .state
462 .sessions
463 .get(session_id)
464 .with_context(|| format!("unknown session {session_id}"))?;
465 session.validate_configuration(&self.config)?;
466 let locator = session
467 .target
468 .as_ref()
469 .context("session target is missing")?;
470 let backend = backend_locator(locator, session, &self.config)?;
471 let profile = self
472 .config
473 .profiles
474 .get(&session.last_profile)
475 .context("session profile is missing")?;
476 let bundle = session
477 .project_directory
478 .is_none()
479 .then(|| session.project_bundle(&self.config))
480 .flatten();
481 let workspace = if let Some(project_directory) = &session.project_directory {
482 (project_directory.to_string_lossy().into_owned(), Vec::new())
483 } else {
484 workspace_paths(
485 &backend,
486 bundle.context("session bundle is missing")?,
487 session_id,
488 session.container_workspace.as_deref(),
489 )?
490 };
491 let target_home = target_profile_home(&backend, session_id, profile);
492 let launch = project_memory_launch(
493 session,
494 bundle,
495 &workspace,
496 &target_home,
497 self.subagent_parent_worktree(session_id),
498 )?;
499 Ok(ProjectMemorySyncTarget {
500 canonical_root: canonical_memory_root(&launch.project_key),
501 })
502 }
503}
504
505pub(super) fn apply_jev_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
509 if enabled {
510 return;
511 }
512 for environment in [&mut launch.target_environment, &mut launch.environment] {
513 environment.remove("TYPESAFE_API_KEY");
514 environment.insert(
515 mj_core::jev::DISABLED_ENVIRONMENT.to_owned(),
516 "1".to_owned(),
517 );
518 }
519}
520
521pub(super) fn apply_continuation_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
524 if enabled {
525 return;
526 }
527 for environment in [&mut launch.target_environment, &mut launch.environment] {
528 environment.insert(
529 mj_core::jev::CONTINUATION_DISABLED_ENVIRONMENT.to_owned(),
530 "1".to_owned(),
531 );
532 }
533}
534
535#[cfg(test)]
542pub(super) fn subagent_tools_enabled(
543 session: &mj_core::state::SessionRecord,
544 is_child: bool,
545) -> bool {
546 session
547 .subagents
548 .clone()
549 .unwrap_or_default()
550 .for_launch(session.harness_kind, is_child)
551 .uses_mjolnir()
552}
553
554pub(super) fn worker_workspace_for_recovery(
555 backend: &targets::TargetLocator,
556 directory: &Path,
557) -> Option<WorkerWorkspace> {
558 let target = match backend {
559 targets::TargetLocator::LocalBare { .. } => mj_core::state::ManagedWorktreeTarget::Local,
560 targets::TargetLocator::SshBare { ssh, .. } => mj_core::state::ManagedWorktreeTarget::Ssh {
561 destination: ssh.destination.clone(),
562 ssh_args: ssh.ssh_args.clone(),
563 },
564 targets::TargetLocator::LocalPodman { .. }
565 | targets::TargetLocator::LocalDocker { .. }
566 | targets::TargetLocator::AppleContainer { .. }
567 | targets::TargetLocator::AwsEc2 { .. }
568 | targets::TargetLocator::SshPodman { .. }
569 | targets::TargetLocator::SshDocker { .. } => return None,
570 };
571 Some(WorkerWorkspace {
572 target,
573 directory: directory.to_path_buf(),
574 })
575}
576
577pub(super) struct LaunchWorkspace<'a> {
578 pub session_id: &'a str,
579 pub container: Option<&'a Path>,
580 pub parent_worktree: Option<&'a mj_core::state::ManagedWorktree>,
581}
582
583pub(super) fn worker_launch_config(
584 session: &mj_core::state::SessionRecord,
585 profile: &mj_core::config::HarnessProfile,
586 bundle: Option<&ProjectBundle>,
587 backend: &targets::TargetLocator,
588 worker_workspace: LaunchWorkspace<'_>,
589 target: &mj_core::state::TargetRuntimeSettings,
590) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
591 let session_id = session.id.as_str();
592 let execution_policy = profile
593 .kind
594 .effective_execution_policy(target.execution_policy);
595 let target_profile_home = target_profile_home(backend, session_id, profile);
596 let workspace = if let Some(project_directory) = &session.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 worker_workspace.session_id,
603 worker_workspace.container,
604 )?
605 };
606 let mut additional_directories = workspace.1.iter().map(PathBuf::from).collect::<Vec<_>>();
607 additional_directories.extend(
608 session
609 .additional_mounts
610 .iter()
611 .map(|resource| resource.destination.clone()),
612 );
613 if profile.kind == mj_core::config::HarnessKind::Muse && !additional_directories.is_empty() {
614 bail!(
615 "{} ACP does not support multiple workspace roots; use a single-repository bundle",
616 profile.kind.display_name()
617 );
618 }
619 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
620 let mut target_environment = target.environment.clone();
621 for name in [
631 "MJ_TURN_STALL_TIMEOUT_MS",
632 "MJ_TURN_TOOL_STALL_TIMEOUT_MS",
633 "RUST_LOG",
634 ] {
635 if let Ok(value) = std::env::var(name) {
636 target_environment.insert(name.to_owned(), value);
637 }
638 }
639 if let Some(key) = mj_core::activity::verdict::api_key() {
642 target_environment.insert("TYPESAFE_API_KEY".to_owned(), key);
643 }
644 if let Some(build_cache) = &session.build_cache {
647 target_environment.insert(
648 "MBX_CACHE_DIR".into(),
649 build_cache.directory.to_string_lossy().into_owned(),
650 );
651 target_environment.insert(
652 "MJ_MBX_CONFIG_DIR".into(),
653 mj_core::config::build_cache_configuration_directory(&build_cache.directory)
654 .to_string_lossy()
655 .into_owned(),
656 );
657 target_environment.insert(
660 "MBX_SHIMS_DIR".into(),
661 Path::new(&targets::worker_root(backend, session_id)?)
662 .join("mbx-shims")
663 .to_string_lossy()
664 .into_owned(),
665 );
666 target_environment.insert("MBX_SUMMARY".into(), "off".into());
669 target_environment.insert("MBX_SAVINGS".into(), "off".into());
670 }
671 let mut environment = target_environment.clone();
672 environment.extend(profile.environment.resolved().clone());
673 if session
674 .target
675 .as_ref()
676 .is_some_and(|target| target.skills_scope() == mj_core::skills::SkillsScope::Localhost)
677 {
678 environment.insert(
681 "MJ_CONFIG_DIR".into(),
682 std::path::absolute(mj_core::config::config_dir())
683 .context("resolve host Mjolnir configuration directory")?
684 .to_string_lossy()
685 .into_owned(),
686 );
687 environment.insert(
688 "MJ_DATA_DIR".into(),
689 std::path::absolute(data_dir())
690 .context("resolve host Mjolnir data directory")?
691 .to_string_lossy()
692 .into_owned(),
693 );
694 if let Some(instance) = mj_core::config::instance_name() {
695 environment.insert("MJ_INSTANCE".into(), instance);
696 } else {
697 environment.remove("MJ_INSTANCE");
698 }
699 }
700 profile
701 .kind
702 .configure_home_environment(Path::new(&target_profile_home), &mut environment);
703 profile
704 .kind
705 .configure_execution_environment(execution_policy, &mut environment)?;
706 let mut project_memory = project_memory_launch(
707 session,
708 bundle,
709 &workspace,
710 &target_profile_home,
711 worker_workspace.parent_worktree,
712 )?;
713 project_memory.mcp_delivery = project_memory_mcp_delivery(profile.kind, backend);
714 project_memory.history_socket =
715 Some(Path::new(&targets::worker_root(backend, &session.id)?).join("control.sock"));
716 if profile.kind == mj_core::config::HarnessKind::Claude {
717 environment.insert(
718 "CLAUDE_CODE_PROJECT_DIR_NAME".into(),
719 project_memory_replica_slug(&project_memory.project_key, session_id),
720 );
721 }
722 apply_claude_setup_token(
723 &mut environment,
724 profile.kind,
725 &mj_core::credentials::claude_oauth_token_path(&session.last_profile),
726 );
727 let excluded_environment =
728 exclude_harness_environment(&session.last_profile, profile, &mut environment);
729 Ok((
730 WorkerLaunchConfig {
731 goal_resume_request: None,
732 target_environment,
733 seed_image_environment: backend.container_engine().is_some(),
734 run_mode: Default::default(),
735 session_id: session_id.to_string(),
736 subagents: mj_core::subagent::SubagentPolicy::Native,
737 handback_tool: false,
738 review_capture: false,
739 bifrost_binary: None,
740 harness: profile.kind,
741 harness_home: PathBuf::from(&target_profile_home),
742 authentication_marker: profile
748 .authentication_marker()
749 .strip_prefix(&profile.home)
750 .ok()
751 .map(|name| name.to_string_lossy().into_owned()),
752 bridge_command: PathBuf::from(bridge_command),
753 bridge_args,
754 harness_runtime: harness_runtime_policy(backend),
755 environment,
756 excluded_environment,
757 cwd: PathBuf::from(&workspace.0),
758 additional_directories,
759 native_session_id: session.native_session_id.clone(),
760 project_memory: Some(project_memory.clone()),
761 execution_policy,
762 },
763 project_memory,
764 target_profile_home,
765 ))
766}
767
768pub(super) fn exclude_harness_environment(
775 profile_id: &str,
776 profile: &mj_core::config::HarnessProfile,
777 environment: &mut std::collections::BTreeMap<String, String>,
778) -> Vec<String> {
779 static REPORTED: std::sync::Mutex<std::collections::BTreeSet<String>> =
780 std::sync::Mutex::new(std::collections::BTreeSet::new());
781 let before = environment.clone();
782 let excluded = profile.exclude_harness_environment(environment);
783 let removed = excluded
784 .iter()
785 .filter(|name| before.contains_key(*name))
786 .cloned()
787 .collect::<Vec<_>>();
788 if !removed.is_empty()
789 && REPORTED
790 .lock()
791 .map(|mut reported| reported.insert(profile_id.to_owned()))
792 .unwrap_or(true)
793 {
794 tracing::info!(
795 profile_id,
796 removed = removed.join(", "),
797 "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"
798 );
799 }
800 excluded
801}
802
803pub(super) fn harness_runtime_policy(backend: &targets::TargetLocator) -> HarnessRuntimePolicy {
804 match backend {
805 targets::TargetLocator::LocalBare { .. }
806 | targets::TargetLocator::AwsEc2 { .. }
807 | targets::TargetLocator::SshBare { .. } => HarnessRuntimePolicy::Managed,
808 _ => HarnessRuntimePolicy::Ambient,
809 }
810}