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 let (mut launch, project_memory, target_profile_home) =
50 self.session_launch_config(session_id, backend)?;
51
52 if session.native_session_id.is_some()
53 && profile.kind == mj_core::config::HarnessKind::Codex
54 {
55 launch.goal_resume_request = Some(mj_core::state::new_session_id()?);
56 }
57 let staging = tempfile::tempdir().context("create worker staging directory")?;
58 let launch_path = staging.path().join("launch.json");
59 launch.write(&launch_path)?;
60 let ownership_path = staging.path().join("ownership.json");
61 WorkerOwnership {
62 version: WorkerOwnership::VERSION,
63 workspace_id: session.workspace_id.clone(),
64 session_id: session_id.to_string(),
65 profile_id: session.last_profile.clone(),
66 bundle_id: session.bundle_id.clone(),
67 target_template_id: session.target_template_id.clone(),
68 instance_id: Some(mj_core::config::instance_identity()),
69 }
70 .write(&ownership_path)?;
71 let profile_stage = staging.path().join("profile");
75 let started = Instant::now();
76 let result = stage_profile(profile, &profile_stage);
77 tracing::debug!(
78 session_id,
79 elapsed_ms = started.elapsed().as_millis(),
80 "profile staging completed"
81 );
82 result?;
83 stage_managed_skills(
84 profile.kind,
85 &profile_stage,
86 session
87 .target
88 .as_ref()
89 .context("session target is missing")?
90 .skills_scope(),
91 )?;
92 stage_codex_catalog(
93 &session.last_profile,
94 profile,
95 &profile_stage,
96 &fetch_catalog_over_https,
97 &SharedCatalogCache,
98 )?;
99 append_hel_target_environment(profile.kind, &profile_stage, backend)?;
100 append_container_storage_guidance(
101 profile.kind,
102 &profile_stage,
103 backend,
104 session.container_workspace.as_deref(),
105 targets::has_managed_temporary_volume(backend, executor)?,
106 )?;
107 append_subagent_policy(
108 profile.kind,
109 &profile_stage,
110 &launch.subagents,
111 self.config.subagents.max_concurrent,
112 )?;
113 apply_staged_execution_setting(profile.kind, launch.execution_policy, &profile_stage)?;
114 if profile.kind == mj_core::config::HarnessKind::Claude {
115 if let Some(role) = launch.subagents.parent_role() {
116 configure_claude_subagent_mcp(&profile_stage, worker_root, role)?;
117 } else if launch.handback_tool {
118 configure_claude_subagent_mcp(
119 &profile_stage,
120 worker_root,
121 mj_core::subagent::SubagentMcpRole::Child,
122 )?;
123 }
124 }
125 stage_memory_replica(
126 &project_memory,
127 Path::new(&target_profile_home),
128 &profile_stage,
129 )?;
130 if project_memory.mcp_delivery == ProjectMemoryMcpDelivery::HarnessProfile {
131 configure_kimi_project_memory_mcp(&profile_stage, worker_root, &project_memory)?;
132 }
133 let worker_binary = worker_binary_for(backend, executor)?;
134
135 install_worker_files(
136 executor,
137 backend,
138 session_id,
139 worker_root,
140 &target_profile_home,
141 &worker_binary,
142 &launch_path,
143 &ownership_path,
144 &profile_stage,
145 )?;
146 if session.build_cache.is_some() {
147 self.install_build_cache_shim(session, backend, &launch, executor)
148 .context("install shared machine build cache configuration")?;
149 }
150 prepare_installed_managed_harness(executor, backend, worker_root, &launch)
151 }
152
153 pub(in crate::controller) fn install_build_cache_shim(
158 &self,
159 session: &mj_core::state::SessionRecord,
160 backend: &targets::TargetLocator,
161 launch: &WorkerLaunchConfig,
162 executor: &impl CommandExecutor,
163 ) -> Result<()> {
164 let worker_root = targets::worker_root(backend, &session.id)?;
165 let binary =
169 crate::controller::mbx::binary_for(backend, executor).inspect_err(|error| {
170 executor.notify_notice(&format!(
171 "The Rust build cache could not be prepared: {error:#}."
172 ));
173 })?;
174 let (configuration, config_roots) =
175 self.build_cache_configuration(session, backend, launch, executor)?;
176 install_mbx_files(
177 executor,
178 backend,
179 &session.id,
180 &worker_root,
181 &binary,
182 &configuration,
183 &config_roots,
184 )
185 }
186
187 pub(in crate::controller) fn prepare_build_cache_links(
188 &self,
189 session: &mj_core::state::SessionRecord,
190 backend: &targets::TargetLocator,
191 launch: &WorkerLaunchConfig,
192 executor: &impl CommandExecutor,
193 ) -> Result<()> {
194 let (configuration, config_roots) =
195 self.build_cache_configuration(session, backend, launch, executor)?;
196 link_mbx_configuration(executor, backend, &configuration, &config_roots)
197 }
198
199 fn build_cache_configuration(
200 &self,
201 session: &mj_core::state::SessionRecord,
202 backend: &targets::TargetLocator,
203 launch: &WorkerLaunchConfig,
204 executor: &impl CommandExecutor,
205 ) -> Result<(PathBuf, Vec<PathBuf>)> {
206 let configuration = crate::controller::mbx::prepare_session_configuration(
207 &self.config,
208 backend,
209 session
210 .build_cache
211 .as_ref()
212 .context("session has no build cache")?,
213 executor,
214 )?;
215 let config_roots = [&launch.target_environment, &launch.environment]
216 .into_iter()
217 .filter_map(|environment| environment.get("XDG_CONFIG_HOME").map(PathBuf::from))
218 .collect::<std::collections::BTreeSet<_>>();
219 Ok((configuration, config_roots.into_iter().collect()))
220 }
221
222 pub fn diagnose_worker(&self, session_id: &str) -> Option<String> {
226 self.diagnose_worker_controlled(session_id, &crate::targets::ProcessExecutor)
227 }
228
229 pub fn diagnose_worker_controlled(
230 &self,
231 session_id: &str,
232 executor: &impl CommandExecutor,
233 ) -> Option<String> {
234 let session = self.state.sessions.get(session_id)?;
235 let locator = session.target.as_ref()?;
236 let backend = match backend_locator(locator, session, &self.config) {
237 Ok(backend) => backend,
238 Err(error) => {
239 tracing::debug!(
240 session_id,
241 error = format!("{error:#}"),
242 "could not construct a worker diagnostic probe"
243 );
244 return None;
245 }
246 };
247 let worker_root = match targets::worker_root(&backend, session_id) {
248 Ok(root) => root,
249 Err(error) => {
250 tracing::debug!(
251 session_id,
252 error = format!("{error:#}"),
253 "could not derive the worker diagnostic root"
254 );
255 return None;
256 }
257 };
258 let binary_failure = worker_binary_probe_failure(executor, &backend, &worker_root);
259 let probe = match probe_worker(executor, &backend, &worker_root) {
260 Ok(probe) => probe.to_string(),
261 Err(error) => format!("the worker could not be probed: {error:#}"),
262 };
263 Some(match binary_failure {
264 Some(binary_failure) => format!("{binary_failure}; {probe}"),
265 None => probe,
266 })
267 }
268
269 pub fn worker_recovery_plan(
273 &self,
274 session_id: &str,
275 operation: Option<&mj_core::state::MoveOperation>,
276 ) -> Result<WorkerRecoveryPlan> {
277 let (backend, worker_root) = self.worker_placement(session_id)?;
278 let launch = self.worker_launch_config_for_move(session_id, &backend, operation)?;
279 let workspace = worker_workspace_for_recovery(&backend, &launch.cwd);
280 Ok(WorkerRecoveryPlan {
281 source_target: self.state.sessions[session_id]
282 .target
283 .clone()
284 .context("session target is missing")?,
285 target: targets::target_recovery_plan(&backend, session_id)?,
286 workspace,
287 liveness_probe: worker_liveness_command(&backend, &worker_root),
288 binary_refresh: worker_binary_refresh_plan(&backend, session_id)?,
289 launch_refresh: Some(worker_launch_refresh_plan(&backend, session_id, &launch)?),
290 restart: CommandPlan {
291 description: format!("restart Mjolnir worker for session {session_id}"),
292 commands: vec![
293 stop_worker_command(&backend, &worker_root),
294 start_worker_command(&backend, &worker_root),
295 ],
296 },
297 })
298 }
299
300 fn session_launch_config(
305 &self,
306 session_id: &str,
307 backend: &targets::TargetLocator,
308 ) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
309 let session = self
310 .state
311 .sessions
312 .get(session_id)
313 .with_context(|| format!("unknown session {session_id}"))?;
314 session.validate_configuration(&self.config)?;
315 let profile = self
316 .config
317 .profiles
318 .get(&session.last_profile)
319 .context("session profile is missing")?;
320 let bundle = session
321 .project_directory
322 .is_none()
323 .then(|| session.project_bundle(&self.config))
324 .flatten();
325 let target = session.target_runtime_settings(&self.config)?;
326 let subagent = self.state.subagents.get(session_id);
327 let (workspace_session_id, workspace_container) = match subagent.as_ref() {
330 Some(child) => {
331 let parent = self
332 .state
333 .sessions
334 .get(&child.parent_session_id)
335 .context("sub-agent parent session is missing")?;
336 (parent.id.clone(), parent.container_workspace.clone())
337 }
338 None => (session_id.to_owned(), session.container_workspace.clone()),
339 };
340 let (mut launch, project_memory, target_profile_home) = worker_launch_config(
341 session,
342 profile,
343 bundle,
344 backend,
345 LaunchWorkspace {
346 session_id: &workspace_session_id,
347 container: workspace_container.as_deref(),
348 parent_worktree: self.subagent_parent_worktree(session_id),
349 },
350 &target,
351 )?;
352 apply_jev_switch(&mut launch, self.config.jev.enabled);
353 apply_continuation_switch(&mut launch, self.config.automatic_continuation_enabled());
354 launch.subagents = session
355 .subagents
356 .clone()
357 .unwrap_or_default()
358 .for_launch(profile.kind, subagent.is_some());
359 launch.handback_tool = subagent.as_ref().is_some_and(|child| child.handback_tool);
361 launch.review_capture =
367 mj_core::review::settings::can_review(&self.config) && subagent.is_none();
368 launch.bifrost_binary = mj_review::bifrost::configured_bifrost_binary();
369 if let Some(subagent) = &subagent {
370 let parent = self
371 .state
372 .sessions
373 .get(&subagent.parent_session_id)
374 .context("sub-agent parent session is missing")?;
375 let parent_profile = self
376 .config
377 .profiles
378 .get(&parent.last_profile)
379 .context("sub-agent parent profile is missing")?;
380 let parent_target = parent.target_runtime_settings(&self.config)?;
381 let parent_locator = parent
382 .target
383 .as_ref()
384 .context("sub-agent parent has no live target")?;
385 let parent_backend = backend_locator(parent_locator, parent, &self.config)?;
386 let parent_bundle = parent
387 .project_directory
388 .is_none()
389 .then(|| parent.project_bundle(&self.config))
390 .flatten();
391 let (parent_launch, _, _) = worker_launch_config(
392 parent,
393 parent_profile,
394 parent_bundle,
395 &parent_backend,
396 LaunchWorkspace {
397 session_id: &parent.id,
398 container: parent.container_workspace.as_deref(),
399 parent_worktree: None,
400 },
401 &parent_target,
402 )?;
403 launch.cwd = if subagent.working_directory.as_os_str().is_empty() {
404 parent_launch.cwd
405 } else {
406 parent_launch.cwd.join(&subagent.working_directory)
407 };
408 launch.additional_directories = parent_launch.additional_directories;
409 }
410 Ok((launch, project_memory, target_profile_home))
411 }
412
413 pub(in crate::controller) fn current_worker_launch_config(
414 &self,
415 session_id: &str,
416 backend: &targets::TargetLocator,
417 ) -> Result<WorkerLaunchConfig> {
418 let operation = crate::database::load_move_operation(session_id)?;
419 self.worker_launch_config_for_move(session_id, backend, operation.as_ref())
420 }
421
422 fn worker_launch_config_for_move(
423 &self,
424 session_id: &str,
425 backend: &targets::TargetLocator,
426 operation: Option<&mj_core::state::MoveOperation>,
427 ) -> Result<WorkerLaunchConfig> {
428 let session = self
429 .state
430 .sessions
431 .get(session_id)
432 .with_context(|| format!("unknown session {session_id}"))?;
433 let (mut launch, _, _) = self.session_launch_config(session_id, backend)?;
434 if operation.is_some_and(|operation| {
435 operation.source_checkpoint_only
436 && operation.destination_target.is_none()
437 && matches!(
438 operation.phase,
439 mj_core::state::MovePhase::Preparing
440 | mj_core::state::MovePhase::ClosingSource
441 | mj_core::state::MovePhase::Failed
442 | mj_core::state::MovePhase::Cancelled
443 )
444 && session.last_profile == operation.source_profile_id
445 && session.target == operation.source_target
446 && matches!(
447 session.state,
448 mj_core::state::SessionState::Running
449 | mj_core::state::SessionState::Disconnected
450 | mj_core::state::SessionState::Closing
451 )
452 }) {
453 launch.run_mode = mj_core::worker_launch::WorkerRunMode::CheckpointOnly;
454 }
455 Ok(launch)
456 }
457
458 pub fn project_memory_sync_target(&self, session_id: &str) -> Result<ProjectMemorySyncTarget> {
459 let session = self
460 .state
461 .sessions
462 .get(session_id)
463 .with_context(|| format!("unknown session {session_id}"))?;
464 session.validate_configuration(&self.config)?;
465 let locator = session
466 .target
467 .as_ref()
468 .context("session target is missing")?;
469 let backend = backend_locator(locator, session, &self.config)?;
470 let profile = self
471 .config
472 .profiles
473 .get(&session.last_profile)
474 .context("session profile is missing")?;
475 let bundle = session
476 .project_directory
477 .is_none()
478 .then(|| session.project_bundle(&self.config))
479 .flatten();
480 let workspace = if let Some(project_directory) = &session.project_directory {
481 (project_directory.to_string_lossy().into_owned(), Vec::new())
482 } else {
483 workspace_paths(
484 &backend,
485 bundle.context("session bundle is missing")?,
486 session_id,
487 session.container_workspace.as_deref(),
488 )?
489 };
490 let target_home = target_profile_home(&backend, session_id, profile);
491 let launch = project_memory_launch(
492 session,
493 bundle,
494 &workspace,
495 &target_home,
496 self.subagent_parent_worktree(session_id),
497 )?;
498 Ok(ProjectMemorySyncTarget {
499 canonical_root: canonical_memory_root(&launch.project_key),
500 })
501 }
502}
503
504pub(super) fn apply_jev_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
508 if enabled {
509 return;
510 }
511 for environment in [&mut launch.target_environment, &mut launch.environment] {
512 environment.remove("TYPESAFE_API_KEY");
513 environment.insert(
514 mj_core::jev::DISABLED_ENVIRONMENT.to_owned(),
515 "1".to_owned(),
516 );
517 }
518}
519
520pub(super) fn apply_continuation_switch(launch: &mut WorkerLaunchConfig, enabled: bool) {
523 if enabled {
524 return;
525 }
526 for environment in [&mut launch.target_environment, &mut launch.environment] {
527 environment.insert(
528 mj_core::jev::CONTINUATION_DISABLED_ENVIRONMENT.to_owned(),
529 "1".to_owned(),
530 );
531 }
532}
533
534#[cfg(test)]
541pub(super) fn subagent_tools_enabled(
542 session: &mj_core::state::SessionRecord,
543 is_child: bool,
544) -> bool {
545 session
546 .subagents
547 .clone()
548 .unwrap_or_default()
549 .for_launch(session.harness_kind, is_child)
550 .uses_mjolnir()
551}
552
553pub(super) fn worker_workspace_for_recovery(
554 backend: &targets::TargetLocator,
555 directory: &Path,
556) -> Option<WorkerWorkspace> {
557 let target = match backend {
558 targets::TargetLocator::LocalBare { .. } => mj_core::state::ManagedWorktreeTarget::Local,
559 targets::TargetLocator::SshBare { ssh, .. } => mj_core::state::ManagedWorktreeTarget::Ssh {
560 destination: ssh.destination.clone(),
561 ssh_args: ssh.ssh_args.clone(),
562 },
563 targets::TargetLocator::LocalPodman { .. }
564 | targets::TargetLocator::LocalDocker { .. }
565 | targets::TargetLocator::AppleContainer { .. }
566 | targets::TargetLocator::AwsEc2 { .. }
567 | targets::TargetLocator::SshPodman { .. }
568 | targets::TargetLocator::SshDocker { .. } => return None,
569 };
570 Some(WorkerWorkspace {
571 target,
572 directory: directory.to_path_buf(),
573 })
574}
575
576pub(super) struct LaunchWorkspace<'a> {
577 pub session_id: &'a str,
578 pub container: Option<&'a Path>,
579 pub parent_worktree: Option<&'a mj_core::state::ManagedWorktree>,
580}
581
582pub(super) fn worker_launch_config(
583 session: &mj_core::state::SessionRecord,
584 profile: &mj_core::config::HarnessProfile,
585 bundle: Option<&ProjectBundle>,
586 backend: &targets::TargetLocator,
587 worker_workspace: LaunchWorkspace<'_>,
588 target: &mj_core::state::TargetRuntimeSettings,
589) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
590 let session_id = session.id.as_str();
591 let execution_policy = profile
592 .kind
593 .effective_execution_policy(target.execution_policy);
594 let target_profile_home = target_profile_home(backend, session_id, profile);
595 let workspace = if let Some(project_directory) = &session.project_directory {
596 (project_directory.to_string_lossy().into_owned(), Vec::new())
597 } else {
598 workspace_paths(
599 backend,
600 bundle.context("session bundle is missing")?,
601 worker_workspace.session_id,
602 worker_workspace.container,
603 )?
604 };
605 let mut additional_directories = workspace.1.iter().map(PathBuf::from).collect::<Vec<_>>();
606 additional_directories.extend(
607 session
608 .additional_mounts
609 .iter()
610 .map(|resource| resource.destination.clone()),
611 );
612 if profile.kind == mj_core::config::HarnessKind::Muse && !additional_directories.is_empty() {
613 bail!(
614 "{} ACP does not support multiple workspace roots; use a single-repository bundle",
615 profile.kind.display_name()
616 );
617 }
618 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
619 let mut target_environment = target.environment.clone();
620 for name in [
630 "MJ_TURN_STALL_TIMEOUT_MS",
631 "MJ_TURN_TOOL_STALL_TIMEOUT_MS",
632 "RUST_LOG",
633 ] {
634 if let Ok(value) = std::env::var(name) {
635 target_environment.insert(name.to_owned(), value);
636 }
637 }
638 if let Some(key) = mj_core::activity::verdict::api_key() {
641 target_environment.insert("TYPESAFE_API_KEY".to_owned(), key);
642 }
643 if let Some(build_cache) = &session.build_cache {
646 target_environment.insert(
647 "MBX_CACHE_DIR".into(),
648 build_cache.directory.to_string_lossy().into_owned(),
649 );
650 target_environment.insert(
651 "MJ_MBX_CONFIG_DIR".into(),
652 mj_core::config::build_cache_configuration_directory(&build_cache.directory)
653 .to_string_lossy()
654 .into_owned(),
655 );
656 target_environment.insert(
659 "MBX_SHIMS_DIR".into(),
660 Path::new(&targets::worker_root(backend, session_id)?)
661 .join("mbx-shims")
662 .to_string_lossy()
663 .into_owned(),
664 );
665 target_environment.insert("MBX_SUMMARY".into(), "off".into());
668 target_environment.insert("MBX_SAVINGS".into(), "off".into());
669 }
670 let mut environment = target_environment.clone();
671 environment.extend(profile.environment.resolved().clone());
672 if session
673 .target
674 .as_ref()
675 .is_some_and(|target| target.skills_scope() == mj_core::skills::SkillsScope::Localhost)
676 {
677 environment.insert(
680 "MJ_CONFIG_DIR".into(),
681 std::path::absolute(mj_core::config::config_dir())
682 .context("resolve host Mjolnir configuration directory")?
683 .to_string_lossy()
684 .into_owned(),
685 );
686 environment.insert(
687 "MJ_DATA_DIR".into(),
688 std::path::absolute(data_dir())
689 .context("resolve host Mjolnir data directory")?
690 .to_string_lossy()
691 .into_owned(),
692 );
693 if let Some(instance) = mj_core::config::instance_name() {
694 environment.insert("MJ_INSTANCE".into(), instance);
695 } else {
696 environment.remove("MJ_INSTANCE");
697 }
698 }
699 profile
700 .kind
701 .configure_home_environment(Path::new(&target_profile_home), &mut environment);
702 profile
703 .kind
704 .configure_execution_environment(execution_policy, &mut environment)?;
705 let mut project_memory = project_memory_launch(
706 session,
707 bundle,
708 &workspace,
709 &target_profile_home,
710 worker_workspace.parent_worktree,
711 )?;
712 project_memory.mcp_delivery = project_memory_mcp_delivery(profile.kind, backend);
713 project_memory.history_socket =
714 Some(Path::new(&targets::worker_root(backend, &session.id)?).join("control.sock"));
715 if profile.kind == mj_core::config::HarnessKind::Claude {
716 environment.insert(
717 "CLAUDE_CODE_PROJECT_DIR_NAME".into(),
718 project_memory_replica_slug(&project_memory.project_key, session_id),
719 );
720 }
721 apply_claude_setup_token(
722 &mut environment,
723 profile.kind,
724 &mj_core::credentials::claude_oauth_token_path(&session.last_profile),
725 );
726 let excluded_environment =
727 exclude_harness_environment(&session.last_profile, profile, &mut environment);
728 Ok((
729 WorkerLaunchConfig {
730 goal_resume_request: None,
731 target_environment,
732 seed_image_environment: backend.container_engine().is_some(),
733 run_mode: Default::default(),
734 session_id: session_id.to_string(),
735 subagents: mj_core::subagent::SubagentPolicy::Native,
736 handback_tool: false,
737 review_capture: false,
738 bifrost_binary: None,
739 harness: profile.kind,
740 harness_home: PathBuf::from(&target_profile_home),
741 authentication_marker: profile
747 .authentication_marker()
748 .strip_prefix(&profile.home)
749 .ok()
750 .map(|name| name.to_string_lossy().into_owned()),
751 bridge_command: PathBuf::from(bridge_command),
752 bridge_args,
753 harness_runtime: harness_runtime_policy(backend),
754 environment,
755 excluded_environment,
756 cwd: PathBuf::from(&workspace.0),
757 additional_directories,
758 native_session_id: session.native_session_id.clone(),
759 project_memory: Some(project_memory.clone()),
760 execution_policy,
761 },
762 project_memory,
763 target_profile_home,
764 ))
765}
766
767pub(super) fn exclude_harness_environment(
774 profile_id: &str,
775 profile: &mj_core::config::HarnessProfile,
776 environment: &mut std::collections::BTreeMap<String, String>,
777) -> Vec<String> {
778 static REPORTED: std::sync::Mutex<std::collections::BTreeSet<String>> =
779 std::sync::Mutex::new(std::collections::BTreeSet::new());
780 let before = environment.clone();
781 let excluded = profile.exclude_harness_environment(environment);
782 let removed = excluded
783 .iter()
784 .filter(|name| before.contains_key(*name))
785 .cloned()
786 .collect::<Vec<_>>();
787 if !removed.is_empty()
788 && REPORTED
789 .lock()
790 .map(|mut reported| reported.insert(profile_id.to_owned()))
791 .unwrap_or(true)
792 {
793 tracing::info!(
794 profile_id,
795 removed = removed.join(", "),
796 "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"
797 );
798 }
799 excluded
800}
801
802pub(super) fn harness_runtime_policy(backend: &targets::TargetLocator) -> HarnessRuntimePolicy {
803 match backend {
804 targets::TargetLocator::LocalBare { .. }
805 | targets::TargetLocator::AwsEc2 { .. }
806 | targets::TargetLocator::SshBare { .. } => HarnessRuntimePolicy::Managed,
807 _ => HarnessRuntimePolicy::Ambient,
808 }
809}