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