Skip to main content

mj_controller/hel_controller/
worker_binary.rs

1//! Worker binary acquisition, profile staging, and worker installation.
2
3use std::collections::HashMap;
4use std::fs::File;
5use std::io::{ErrorKind, Read, Write};
6use std::path::{Path, PathBuf};
7use std::sync::OnceLock;
8use std::time::Instant;
9
10use anyhow::{Context, Result, bail, ensure};
11use rayon::prelude::*;
12use sha2::{Digest, Sha256};
13
14use crate::hel_session_manager::{
15    ProjectMemorySyncTarget, RemoteWorkerBinaryRefresh, WorkerBinaryRefresh,
16    WorkerBinaryRefreshPlan, WorkerLaunchRefreshPlan, WorkerRecoveryPlan, WorkerWorkspace,
17};
18use hel::hel_config::{
19    ExecutionPolicy, HarnessKind, HarnessProfile, ProjectBundle, ProjectRepository, atomic_write,
20    data_dir,
21};
22use hel::hel_harness_runtime::{
23    CLAUDE_ACP_VERSION, CODEX_ACP_PACKAGE, CODEX_ACP_VERSION, DEEPSEEK_DSH_VERSION,
24};
25use hel::hel_project_memory::{ProjectMemoryIdentity, RepositoryMemoryIdentity};
26use hel::hel_targets::{
27    self, CommandExecutor, CommandPlan, CommandSpec, ProcessExecutor, ProvisionStage, SshTarget,
28};
29use hel::hel_worker_launch::{
30    DISCOVER_LOGIN_PATH_ENV, HarnessRuntimePolicy, ProjectMemoryLaunchConfig,
31    ProjectMemoryMcpDelivery, WorkerLaunchConfig, WorkerOwnership,
32};
33
34use super::backend::backend_locator;
35use super::readiness::WORKER_EXIT_RECORD_MARKER;
36use super::{Controller, execute_checked, scp_command_spec, ssh_command_spec, target_profile_home};
37
38impl Controller {
39    /// Where this session's worker lives. This is decided from the session
40    /// record and configuration alone, so a caller can name the worker root
41    /// before anything is installed into it.
42    pub(super) fn worker_placement(
43        &self,
44        session_id: &str,
45    ) -> Result<(hel_targets::TargetLocator, String)> {
46        let session = self
47            .state
48            .sessions
49            .get(session_id)
50            .with_context(|| format!("unknown session {session_id}"))?;
51        let locator = session
52            .target
53            .as_ref()
54            .context("session target is missing")?;
55        let backend = backend_locator(locator, session, &self.config)?;
56        let worker_root = hel_targets::worker_root(&backend, session_id)?;
57        Ok((backend, worker_root))
58    }
59
60    pub(super) fn prepare_worker_files(
61        &self,
62        session_id: &str,
63        backend: &hel_targets::TargetLocator,
64        worker_root: &str,
65        executor: &impl CommandExecutor,
66    ) -> Result<()> {
67        let session = self
68            .state
69            .sessions
70            .get(session_id)
71            .with_context(|| format!("unknown session {session_id}"))?;
72        let profile = self
73            .config
74            .profiles
75            .get(&session.last_profile)
76            .context("session profile is missing")?;
77        let bundle = session
78            .project_directory
79            .is_none()
80            .then(|| self.config.bundles.get(&session.bundle_id))
81            .flatten();
82        let target = self
83            .config
84            .targets
85            .get(&session.target_template_id)
86            .context("session target template is missing")?;
87        let (launch, project_memory, target_profile_home) = worker_launch_config(
88            session,
89            profile,
90            bundle,
91            backend,
92            session_id,
93            target.execution_policy(),
94        )?;
95
96        let staging = tempfile::tempdir().context("create worker staging directory")?;
97        let launch_path = staging.path().join("launch.json");
98        launch.write(&launch_path)?;
99        let ownership_path = staging.path().join("ownership.json");
100        WorkerOwnership {
101            version: WorkerOwnership::VERSION,
102            workspace_id: session.workspace_id.clone(),
103            session_id: session_id.to_string(),
104            profile_id: session.last_profile.clone(),
105            bundle_id: session.bundle_id.clone(),
106            target_template_id: session.target_template_id.clone(),
107        }
108        .write(&ownership_path)?;
109        let profile_stage = staging.path().join("profile");
110        if !matches!(backend, hel_targets::TargetLocator::LocalBare { .. })
111            || profile.kind == hel::hel_config::HarnessKind::Muse
112        {
113            let started = Instant::now();
114            let result = stage_profile(profile, &profile_stage);
115            tracing::debug!(
116                session_id,
117                elapsed_ms = started.elapsed().as_millis(),
118                "profile staging completed"
119            );
120            result?;
121            append_hel_target_environment(profile.kind, &profile_stage, backend)?;
122            stage_memory_replica(
123                &project_memory,
124                Path::new(&target_profile_home),
125                &profile_stage,
126            )?;
127            if project_memory.mcp_delivery == ProjectMemoryMcpDelivery::HarnessProfile {
128                configure_kimi_project_memory_mcp(&profile_stage, worker_root, &project_memory)?;
129            }
130        } else {
131            seed_local_memory_replica(&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        prepare_installed_managed_harness(executor, backend, worker_root, &launch)
147    }
148
149    /// Probe the installed binary and collect the dead worker's exit record
150    /// and log tail after a session becomes unreachable. Best-effort; returns
151    /// `None` when the target no longer exists or has no diagnostics.
152    pub fn diagnose_worker(&self, session_id: &str) -> Option<String> {
153        self.diagnose_worker_controlled(session_id, &ProcessExecutor)
154    }
155
156    pub fn diagnose_worker_controlled(
157        &self,
158        session_id: &str,
159        executor: &impl CommandExecutor,
160    ) -> Option<String> {
161        let session = self.state.sessions.get(session_id)?;
162        let locator = session.target.as_ref()?;
163        let backend = match backend_locator(locator, session, &self.config) {
164            Ok(backend) => backend,
165            Err(error) => {
166                tracing::debug!(
167                    session_id,
168                    error = format!("{error:#}"),
169                    "could not construct a worker diagnostic probe"
170                );
171                return None;
172            }
173        };
174        let worker_root = match hel_targets::worker_root(&backend, session_id) {
175            Ok(root) => root,
176            Err(error) => {
177                tracing::debug!(
178                    session_id,
179                    error = format!("{error:#}"),
180                    "could not derive the worker diagnostic root"
181                );
182                return None;
183            }
184        };
185        let binary_failure = worker_binary_probe_failure(executor, &backend, &worker_root);
186        let last_words = worker_last_words(executor, &backend, &worker_root);
187        match (binary_failure, last_words) {
188            (Some(binary_failure), Some(last_words)) => {
189                Some(format!("{binary_failure}; {last_words}"))
190            }
191            (Some(binary_failure), None) => Some(binary_failure),
192            (None, last_words) => last_words,
193        }
194    }
195
196    /// A non-destructive liveness probe plus commands that replace a confirmed
197    /// dead session worker without touching its durable relay files. The
198    /// session manager runs both off its async actor.
199    pub fn worker_recovery_plan(&self, session_id: &str) -> Result<WorkerRecoveryPlan> {
200        let (backend, worker_root) = self.worker_placement(session_id)?;
201        let launch = self.current_worker_launch_config(session_id, &backend)?;
202        let workspace = worker_workspace_for_recovery(&backend, &launch.cwd);
203        Ok(WorkerRecoveryPlan {
204            target: hel_targets::target_recovery_plan(&backend, session_id)?,
205            workspace,
206            liveness_probe: worker_liveness_command(&backend, &worker_root),
207            binary_refresh: worker_binary_refresh_plan(&backend, session_id)?,
208            launch_refresh: Some(worker_launch_refresh_plan(&backend, session_id, &launch)?),
209            restart: CommandPlan {
210                description: format!("restart Mjolnir worker for session {session_id}"),
211                commands: vec![
212                    stop_worker_command(&backend, &worker_root),
213                    start_worker_command(&backend, &worker_root),
214                ],
215            },
216        })
217    }
218
219    pub(super) fn current_worker_launch_config(
220        &self,
221        session_id: &str,
222        backend: &hel_targets::TargetLocator,
223    ) -> Result<WorkerLaunchConfig> {
224        let session = self
225            .state
226            .sessions
227            .get(session_id)
228            .with_context(|| format!("unknown session {session_id}"))?;
229        let profile = self
230            .config
231            .profiles
232            .get(&session.last_profile)
233            .context("session profile is missing")?;
234        let bundle = session
235            .project_directory
236            .is_none()
237            .then(|| self.config.bundles.get(&session.bundle_id))
238            .flatten();
239        let target = self
240            .config
241            .targets
242            .get(&session.target_template_id)
243            .context("session target template is missing")?;
244        let (launch, _, _) = worker_launch_config(
245            session,
246            profile,
247            bundle,
248            backend,
249            session_id,
250            target.execution_policy(),
251        )?;
252        Ok(launch)
253    }
254
255    pub fn project_memory_sync_target(&self, session_id: &str) -> Result<ProjectMemorySyncTarget> {
256        let session = self
257            .state
258            .sessions
259            .get(session_id)
260            .with_context(|| format!("unknown session {session_id}"))?;
261        let locator = session
262            .target
263            .as_ref()
264            .context("session target is missing")?;
265        let backend = backend_locator(locator, session, &self.config)?;
266        let profile = self
267            .config
268            .profiles
269            .get(&session.last_profile)
270            .context("session profile is missing")?;
271        let bundle = session
272            .project_directory
273            .is_none()
274            .then(|| self.config.bundles.get(&session.bundle_id))
275            .flatten();
276        let workspace = if let Some(project_directory) = &session.project_directory {
277            (project_directory.to_string_lossy().into_owned(), Vec::new())
278        } else {
279            workspace_paths(
280                &backend,
281                bundle.context("session bundle is missing")?,
282                session_id,
283            )?
284        };
285        let target_home = target_profile_home(&backend, session_id, profile);
286        let launch = project_memory_launch(session, bundle, &workspace, &target_home)?;
287        Ok(ProjectMemorySyncTarget {
288            canonical_root: canonical_memory_root(&launch.project_key),
289        })
290    }
291}
292
293fn worker_workspace_for_recovery(
294    backend: &hel_targets::TargetLocator,
295    directory: &Path,
296) -> Option<WorkerWorkspace> {
297    let target = match backend {
298        hel_targets::TargetLocator::LocalBare { .. } => {
299            hel::hel_state::ManagedWorktreeTarget::Local
300        }
301        hel_targets::TargetLocator::SshBare { ssh, .. } => {
302            hel::hel_state::ManagedWorktreeTarget::Ssh {
303                destination: ssh.destination.clone(),
304                ssh_args: ssh.ssh_args.clone(),
305            }
306        }
307        hel_targets::TargetLocator::LocalPodman { .. }
308        | hel_targets::TargetLocator::LocalDocker { .. }
309        | hel_targets::TargetLocator::AppleContainer { .. }
310        | hel_targets::TargetLocator::AwsEc2 { .. }
311        | hel_targets::TargetLocator::SshPodman { .. }
312        | hel_targets::TargetLocator::SshDocker { .. } => return None,
313    };
314    Some(WorkerWorkspace {
315        target,
316        directory: directory.to_path_buf(),
317    })
318}
319
320fn worker_launch_config(
321    session: &hel::hel_state::SessionRecord,
322    profile: &hel::hel_config::HarnessProfile,
323    bundle: Option<&ProjectBundle>,
324    backend: &hel_targets::TargetLocator,
325    session_id: &str,
326    execution_policy: ExecutionPolicy,
327) -> Result<(WorkerLaunchConfig, ProjectMemoryLaunchConfig, String)> {
328    let target_profile_home = target_profile_home(backend, session_id, profile);
329    let workspace = if let Some(project_directory) = &session.project_directory {
330        (project_directory.to_string_lossy().into_owned(), Vec::new())
331    } else {
332        workspace_paths(
333            backend,
334            bundle.context("session bundle is missing")?,
335            session_id,
336        )?
337    };
338    let mut additional_directories = workspace.1.iter().map(PathBuf::from).collect::<Vec<_>>();
339    additional_directories.extend(
340        session
341            .additional_mounts
342            .iter()
343            .map(|resource| resource.destination.clone()),
344    );
345    if matches!(
346        profile.kind,
347        hel::hel_config::HarnessKind::Deepseek | hel::hel_config::HarnessKind::Muse
348    ) && !additional_directories.is_empty()
349    {
350        bail!(
351            "{} ACP does not support multiple workspace roots; use a single-repository bundle",
352            profile.kind.display_name()
353        );
354    }
355    let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
356    let mut environment = profile.environment.clone();
357    profile
358        .kind
359        .configure_home_environment(Path::new(&target_profile_home), &mut environment);
360    profile
361        .kind
362        .configure_execution_environment(execution_policy, &mut environment)?;
363    configure_login_path_discovery(&mut environment, backend);
364    let mut project_memory =
365        project_memory_launch(session, bundle, &workspace, &target_profile_home)?;
366    project_memory.mcp_delivery = project_memory_mcp_delivery(profile.kind, backend);
367    if profile.kind == hel::hel_config::HarnessKind::Claude {
368        environment.insert(
369            "CLAUDE_CODE_PROJECT_DIR_NAME".into(),
370            project_memory_replica_slug(&project_memory.project_key, session_id),
371        );
372    }
373    apply_claude_setup_token(
374        &mut environment,
375        profile.kind,
376        &hel::hel_credentials::claude_oauth_token_path(&session.last_profile),
377    );
378    Ok((
379        WorkerLaunchConfig {
380            session_id: session_id.to_string(),
381            harness: profile.kind,
382            bridge_command: PathBuf::from(bridge_command),
383            bridge_args,
384            harness_runtime: harness_runtime_policy(backend),
385            environment,
386            cwd: PathBuf::from(&workspace.0),
387            additional_directories,
388            native_session_id: session.native_session_id.clone(),
389            project_memory: profile
390                .kind
391                .supports_injected_mcp()
392                .then(|| project_memory.clone()),
393            execution_policy,
394        },
395        project_memory,
396        target_profile_home,
397    ))
398}
399
400fn harness_runtime_policy(backend: &hel_targets::TargetLocator) -> HarnessRuntimePolicy {
401    match backend {
402        hel_targets::TargetLocator::LocalBare { .. }
403        | hel_targets::TargetLocator::AwsEc2 { .. }
404        | hel_targets::TargetLocator::SshBare { .. } => HarnessRuntimePolicy::Managed,
405        _ => HarnessRuntimePolicy::Ambient,
406    }
407}
408
409/// Hand a Claude worker the profile's long-lived setup token, when it has one.
410///
411/// Claude Code reads `CLAUDE_CODE_OAUTH_TOKEN` ahead of the `/login`
412/// credentials file, and a setup token does not rotate, so a container copy
413/// cannot lose the single-use refresh race with the host. A profile that sets
414/// the variable itself stays authoritative.
415fn apply_claude_setup_token(
416    environment: &mut std::collections::BTreeMap<String, String>,
417    kind: hel::hel_config::HarnessKind,
418    token_path: &Path,
419) {
420    use hel::hel_credentials::CLAUDE_OAUTH_TOKEN_ENV;
421
422    if kind != hel::hel_config::HarnessKind::Claude
423        || environment.contains_key(CLAUDE_OAUTH_TOKEN_ENV)
424    {
425        return;
426    }
427    match hel::hel_credentials::read_claude_oauth_token(token_path) {
428        Ok(Some(token)) => {
429            environment.insert(CLAUDE_OAUTH_TOKEN_ENV.to_owned(), token);
430        }
431        Ok(None) => {}
432        // A stored token Hel cannot read is worth reporting, but the session
433        // still starts on the synced credentials file.
434        Err(error) => tracing::warn!(
435            path = %token_path.display(),
436            %error,
437            "ignoring an unreadable Claude setup token"
438        ),
439    }
440}
441
442fn configure_login_path_discovery(
443    environment: &mut std::collections::BTreeMap<String, String>,
444    backend: &hel_targets::TargetLocator,
445) {
446    environment.remove(DISCOVER_LOGIN_PATH_ENV);
447    if !environment.contains_key("PATH")
448        && matches!(
449            backend,
450            hel_targets::TargetLocator::LocalBare { .. }
451                | hel_targets::TargetLocator::AwsEc2 { .. }
452                | hel_targets::TargetLocator::SshBare { .. }
453        )
454    {
455        environment.insert(DISCOVER_LOGIN_PATH_ENV.into(), "1".into());
456    }
457}
458
459fn project_memory_launch(
460    session: &hel::hel_state::SessionRecord,
461    bundle: Option<&ProjectBundle>,
462    workspace: &(String, Vec<String>),
463    target_profile_home: &str,
464) -> Result<ProjectMemoryLaunchConfig> {
465    let identity = if let Some(worktree) = &session.managed_worktree {
466        ProjectMemoryIdentity::Repository {
467            repository: RepositoryMemoryIdentity::Local {
468                canonical_root: std::fs::canonicalize(&worktree.source_repository)
469                    .unwrap_or_else(|_| worktree.source_repository.clone()),
470            },
471        }
472    } else if let Some(bundle) = bundle {
473        let primary =
474            configured_memory_identity(bundle.primary().context("bundle primary is missing")?)?;
475        let members = bundle
476            .repositories
477            .iter()
478            .map(configured_memory_identity)
479            .collect::<Result<Vec<_>>>()?;
480        ProjectMemoryIdentity::bundle(primary, members)
481    } else {
482        let project = session
483            .project_directory
484            .as_ref()
485            .context("raw session project directory is missing")?;
486        let repository = match session.target.as_ref() {
487            Some(hel::hel_state::TargetLocator::LocalBare { .. }) => {
488                RepositoryMemoryIdentity::Local {
489                    canonical_root: std::fs::canonicalize(project)
490                        .unwrap_or_else(|_| project.clone()),
491                }
492            }
493            _ => RepositoryMemoryIdentity::Remote {
494                target: session.target_template_id.clone(),
495                canonical_root: project.clone(),
496            },
497        };
498        ProjectMemoryIdentity::Repository { repository }
499    };
500    let project_key = identity.key()?;
501    let replica_slug = project_memory_replica_slug(&project_key, &session.id);
502    let project_root = PathBuf::from(target_profile_home)
503        .join("projects")
504        .join(replica_slug);
505    let root = project_root.join("memory");
506    let baseline_root = project_root.join(".hel-memory-baseline");
507    let mut repository_roots = std::collections::BTreeMap::new();
508    if let Some(bundle) = bundle {
509        let target_roots =
510            std::iter::once(workspace.0.as_str()).chain(workspace.1.iter().map(String::as_str));
511        let repositories = std::iter::once(bundle.primary().context("bundle primary is missing")?)
512            .chain(
513                bundle
514                    .repositories
515                    .iter()
516                    .filter(|repository| repository.id != bundle.primary_repo),
517            );
518        repository_roots.extend(
519            repositories
520                .zip(target_roots)
521                .map(|(repository, root)| (repository.id.clone(), PathBuf::from(root))),
522        );
523    }
524    Ok(ProjectMemoryLaunchConfig {
525        project_key,
526        root,
527        baseline_root,
528        repository_roots,
529        mcp_delivery: ProjectMemoryMcpDelivery::Acp,
530    })
531}
532
533fn project_memory_replica_slug(project_key: &str, session_id: &str) -> String {
534    format!("hel-{}-{session_id}", &project_key[..16])
535}
536
537fn project_memory_mcp_delivery(
538    harness: hel::hel_config::HarnessKind,
539    target: &hel_targets::TargetLocator,
540) -> ProjectMemoryMcpDelivery {
541    if harness == hel::hel_config::HarnessKind::Kimi
542        && !matches!(target, hel_targets::TargetLocator::LocalBare { .. })
543    {
544        ProjectMemoryMcpDelivery::HarnessProfile
545    } else {
546        ProjectMemoryMcpDelivery::Acp
547    }
548}
549
550fn configured_memory_identity(repository: &ProjectRepository) -> Result<RepositoryMemoryIdentity> {
551    if let Some(source) = repository.github.as_deref() {
552        let github = crate::hel_setup::github_repository_from_origin(source)
553            .with_context(|| format!("parse repository source {source:?} for project memory"))?;
554        return Ok(RepositoryMemoryIdentity::Github {
555            owner: github.owner.to_ascii_lowercase(),
556            repository: github.repository.to_ascii_lowercase(),
557        });
558    }
559    let root = repository
560        .local
561        .as_ref()
562        .context("project repository has no source for memory identity")?;
563    Ok(RepositoryMemoryIdentity::Local {
564        canonical_root: hel::hel_local_git::main_worktree_root(root)
565            .or_else(|_| std::fs::canonicalize(root).map_err(anyhow::Error::from))
566            .unwrap_or_else(|_| root.clone()),
567    })
568}
569
570fn canonical_memory_root(project_key: &str) -> PathBuf {
571    data_dir().join("projects").join(project_key).join("memory")
572}
573
574fn stage_memory_replica(
575    memory: &ProjectMemoryLaunchConfig,
576    target_profile_home: &Path,
577    profile_stage: &Path,
578) -> Result<()> {
579    let canonical = canonical_memory_root(&memory.project_key);
580    std::fs::create_dir_all(&canonical)?;
581    let replica = memory.root.strip_prefix(target_profile_home)?;
582    let baseline = memory.baseline_root.strip_prefix(target_profile_home)?;
583    copy_profile_entry(&canonical, &profile_stage.join(replica))?;
584    copy_profile_entry(&canonical, &profile_stage.join(baseline))
585}
586
587fn seed_local_memory_replica(memory: &ProjectMemoryLaunchConfig) -> Result<()> {
588    let canonical = canonical_memory_root(&memory.project_key);
589    std::fs::create_dir_all(&canonical)?;
590    let canonical_has_files = directory_has_files(&canonical)?;
591    let replica_has_files = directory_has_files(&memory.root)?;
592    match (canonical_has_files, replica_has_files) {
593        (false, true) => copy_profile_entry(&memory.root, &canonical),
594        (true, false) => copy_profile_entry(&canonical, &memory.root),
595        _ => Ok(()),
596    }?;
597    copy_profile_entry(&canonical, &memory.baseline_root)
598}
599
600/// Kimi's runtime-aware engine cannot infer a runtime identity from an ACP
601/// stdio server. Add Hel's server to the session-private profile instead,
602/// where Kimi's native schema can bind it to the target's local runtime.
603fn configure_kimi_project_memory_mcp(
604    profile_stage: &Path,
605    worker_root: &str,
606    memory: &ProjectMemoryLaunchConfig,
607) -> Result<()> {
608    let path = profile_stage.join("mcp.json");
609    let mut document = match std::fs::read(&path) {
610        Ok(body) => serde_json::from_slice::<serde_json::Value>(&body)
611            .with_context(|| format!("parse staged Kimi MCP configuration {}", path.display()))?,
612        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
613            serde_json::Value::Object(serde_json::Map::new())
614        }
615        Err(error) => {
616            return Err(error)
617                .with_context(|| format!("read staged Kimi MCP configuration {}", path.display()));
618        }
619    };
620    let root = document.as_object_mut().with_context(|| {
621        format!(
622            "staged Kimi MCP configuration {} must contain a JSON object",
623            path.display()
624        )
625    })?;
626    let servers = root
627        .entry("mcpServers")
628        .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()))
629        .as_object_mut()
630        .with_context(|| {
631            format!(
632                "mcpServers in staged Kimi MCP configuration {} must be a JSON object",
633                path.display()
634            )
635        })?;
636
637    let worker = Path::new(worker_root).join("hel");
638    let server = if worker.is_absolute() && memory.root.is_absolute() {
639        serde_json::json!({
640            "transport": "stdio",
641            "command": worker,
642            "args": ["worker", "memory-mcp", "--root", memory.root],
643            "runtime_id": "local"
644        })
645    } else {
646        let worker = worker.to_string_lossy();
647        let memory_root = memory.root.to_string_lossy();
648        serde_json::json!({
649            "transport": "stdio",
650            "command": "sh",
651            "args": [
652                "-c",
653                "exec \"$HOME/$1\" worker memory-mcp --root \"$HOME/$2\"",
654                "mj-project-memory",
655                worker,
656                memory_root
657            ],
658            "runtime_id": "local"
659        })
660    };
661    servers.insert("mj-project-memory".into(), server);
662    let mut body = serde_json::to_vec_pretty(&document)?;
663    body.push(b'\n');
664    atomic_write(&path, &body)
665        .with_context(|| format!("write staged Kimi MCP configuration {}", path.display()))
666}
667
668fn directory_has_files(path: &Path) -> Result<bool> {
669    let entries = match std::fs::read_dir(path) {
670        Ok(entries) => entries,
671        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
672        Err(error) => return Err(error.into()),
673    };
674    for entry in entries {
675        let entry = entry?;
676        let metadata = entry.metadata()?;
677        if metadata.is_file() || (metadata.is_dir() && directory_has_files(&entry.path())?) {
678            return Ok(true);
679        }
680    }
681    Ok(false)
682}
683
684#[derive(Debug, Clone, PartialEq, Eq)]
685pub enum WorkerBinaryAvailability {
686    Local {
687        path: PathBuf,
688        source: String,
689    },
690    Remote {
691        url: String,
692        sha256: String,
693        triple: String,
694    },
695}
696
697/// Sources captured before the daemon starts its managers and coordinators.
698///
699/// Local sources are copied into an immutable, content-addressed cache during
700/// capture. Remote sources retain only their URL, digest, and target triple;
701/// the network fetch still happens when a target is provisioned.
702#[derive(Debug)]
703struct WorkerBinarySourceSnapshot {
704    entries: HashMap<
705        (String, WorkerBinaryRequirement),
706        std::result::Result<WorkerBinaryAvailability, String>,
707    >,
708}
709
710static PINNED_WORKER_BINARY_SOURCES: OnceLock<WorkerBinarySourceSnapshot> = OnceLock::new();
711
712fn packaged_worker_binary_path(directory: &Path, triple: &str) -> PathBuf {
713    directory.join(format!("mj-worker-{triple}"))
714}
715
716/// Linux exposes an unlinked running executable through `/proc` with a
717/// ` (deleted)` suffix. `current_exe` preserves that suffix, but it is not
718/// part of the executable's real file name and must not leak into sibling
719/// lookup after `cargo` or a package upgrade replaces the controller.
720fn running_executable_file_name(controller: &Path) -> Option<std::ffi::OsString> {
721    let name = controller.file_name()?;
722    #[cfg(target_os = "linux")]
723    {
724        use std::os::unix::ffi::{OsStrExt, OsStringExt};
725
726        if let Some(name) = name.as_bytes().strip_suffix(b" (deleted)") {
727            return Some(std::ffi::OsString::from_vec(name.to_vec()));
728        }
729    }
730    Some(name.to_os_string())
731}
732
733/// File names a worker binary may carry when it sits beside the controller or
734/// in a development sibling directory. The controller's own file name comes
735/// first (after the 2.0 rename that is `mj`), then the legacy `hel` name that
736/// older packages shipped, so both resolve without hardcoding one.
737fn worker_sibling_names(controller: &Path) -> Vec<std::ffi::OsString> {
738    use std::ffi::OsString;
739    let mut names = Vec::new();
740    if let Some(own) = running_executable_file_name(controller) {
741        names.push(own);
742    }
743    let legacy = OsString::from("hel");
744    if !names.contains(&legacy) {
745        names.push(legacy);
746    }
747    names
748}
749
750/// A local-bare session runs on the controller host, so it may use the native
751/// worker built or packaged beside `mj`. Managed targets never consider this
752/// name because a macOS or glibc binary is not portable into Linux targets.
753fn select_native_worker(
754    controller: &Path,
755    is_file: impl Fn(&Path) -> bool,
756) -> Option<(PathBuf, &'static str)> {
757    let directory = controller.parent()?;
758    if let (Some(profile), Some(target_dir)) = (directory.file_name(), directory.parent()) {
759        let development_worker = target_dir.join("worker").join(profile).join("mj-worker");
760        if is_file(&development_worker) {
761            return Some((development_worker, "isolated native development worker"));
762        }
763    }
764    let packaged_worker = directory.join("mj-worker");
765    is_file(&packaged_worker).then_some((packaged_worker, "native worker beside mj"))
766}
767
768/// Choose a worker binary that ships beside the controller or in a development
769/// musl sibling directory. `is_file` probes the filesystem; tests pass a
770/// hand-written probe. The static musl sibling is probed before the worker in
771/// the controller's own directory, because in a development checkout that
772/// same-directory candidate resolves to the controller itself, whose glibc may
773/// be newer than the target's.
774fn select_sibling_worker(
775    controller: &Path,
776    triple: &str,
777    is_file: impl Fn(&Path) -> bool,
778) -> Option<(PathBuf, &'static str)> {
779    let directory = controller.parent()?;
780    let names = worker_sibling_names(controller);
781    let mut candidates: Vec<(PathBuf, &'static str)> = Vec::new();
782    // Packaged worker beside the controller, named for the target triple.
783    candidates.push((
784        packaged_worker_binary_path(directory, triple),
785        "beside the mj binary",
786    ));
787    // Development checkout: a controller at target/<profile>/<name> finds its
788    // musl sibling at target/<triple>/<profile>/<name>. The static build is
789    // preferred because the target's glibc may be older than the host's, so it
790    // is probed before the same-directory worker (which is the controller
791    // itself in a development checkout).
792    if let (Some(profile), Some(target_dir)) = (directory.file_name(), directory.parent()) {
793        candidates.push((
794            target_dir
795                .join("worker")
796                .join(triple)
797                .join(profile)
798                .join("mj-worker"),
799            "isolated development musl worker",
800        ));
801        candidates.push((
802            target_dir.join(triple).join(profile).join("mj-worker"),
803            "development musl worker",
804        ));
805        for name in &names {
806            candidates.push((
807                target_dir.join(triple).join(profile).join(name),
808                "development musl sibling",
809            ));
810        }
811    }
812    // A legacy package may put an `hel`-named worker beside an `mj`
813    // controller. Never select the controller's own same-directory path: on
814    // glibc Linux that is not a portable worker, and after an upgrade it is
815    // the replacement controller rather than the still-running executable.
816    let controller_name = running_executable_file_name(controller);
817    for name in names
818        .iter()
819        .filter(|name| Some(name.as_os_str()) != controller_name.as_deref())
820    {
821        candidates.push((directory.join(name), "beside the running executable"));
822    }
823    candidates.into_iter().find(|(path, _)| is_file(path))
824}
825
826#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
827enum WorkerBinaryRequirement {
828    PortableLinux,
829    LocalHost,
830}
831
832impl WorkerBinarySourceSnapshot {
833    fn capture<F>(cache_root: &Path, resolve: F) -> Self
834    where
835        F: Fn(&str, WorkerBinaryRequirement) -> Result<WorkerBinaryAvailability>,
836    {
837        let mut entries = HashMap::new();
838        let mut local_cache = HashMap::<PathBuf, PathBuf>::new();
839        let architectures = [
840            (std::env::consts::ARCH, WorkerBinaryRequirement::LocalHost),
841            ("x86_64", WorkerBinaryRequirement::PortableLinux),
842            ("aarch64", WorkerBinaryRequirement::PortableLinux),
843        ];
844
845        for (arch, requirement) in architectures {
846            let pinned = match resolve(arch, requirement) {
847                Ok(WorkerBinaryAvailability::Local { path, source }) => {
848                    match local_cache.get(&path).cloned().map(Ok).unwrap_or_else(|| {
849                        copy_worker_source_to_cache(&path, cache_root).inspect(|cached| {
850                            local_cache.insert(path.clone(), cached.clone());
851                        })
852                    }) {
853                        Ok(cached) => Ok(WorkerBinaryAvailability::Local {
854                            path: cached,
855                            source,
856                        }),
857                        Err(error) => {
858                            let error = format!(
859                                "pin worker source {} for {arch} ({requirement:?}): {error:#}",
860                                path.display()
861                            );
862                            tracing::warn!(arch, requirement = ?requirement, error = %error);
863                            Err(error)
864                        }
865                    }
866                }
867                Ok(WorkerBinaryAvailability::Remote {
868                    url,
869                    sha256,
870                    triple,
871                }) => Ok(WorkerBinaryAvailability::Remote {
872                    url,
873                    sha256,
874                    triple,
875                }),
876                Err(error) => {
877                    let error = format!("{error:#}");
878                    tracing::debug!(
879                        arch,
880                        requirement = ?requirement,
881                        error = %error,
882                        "worker source was unavailable when the daemon started"
883                    );
884                    Err(error)
885                }
886            };
887            entries.insert((arch.to_owned(), requirement), pinned);
888        }
889
890        Self { entries }
891    }
892
893    fn resolve(
894        &self,
895        arch: &str,
896        requirement: WorkerBinaryRequirement,
897    ) -> Result<WorkerBinaryAvailability> {
898        let Some(source) = self.entries.get(&(arch.to_owned(), requirement)) else {
899            bail!(
900                "worker source for {arch} ({requirement:?}) was not captured when the daemon started"
901            );
902        };
903        match source {
904            Ok(availability) => Ok(availability.clone()),
905            Err(error) => bail!(
906                "worker source for {arch} ({requirement:?}) was unavailable when the daemon started; install it and restart the daemon to retry: {error}"
907            ),
908        }
909    }
910}
911
912/// Capture the worker sources used by this daemon before its asynchronous
913/// managers start. Missing sources are retained as per-architecture errors so
914/// an unused architecture does not prevent daemon startup.
915pub fn pin_worker_binary_sources() -> Result<()> {
916    if PINNED_WORKER_BINARY_SOURCES.get().is_some() {
917        return Ok(());
918    }
919    let current = std::env::current_exe().context("resolve Mjolnir controller binary")?;
920    let cache_root = data_dir().join("workers").join("pinned");
921    let snapshot = WorkerBinarySourceSnapshot::capture(&cache_root, |arch, requirement| {
922        worker_binary_prerequisite_for_current(arch, requirement, &current, &|path| path.is_file())
923    });
924    // The daemon boot path calls this once. If a second caller races it, keep
925    // the first complete snapshot and never replace paths it may already use.
926    let _ = PINNED_WORKER_BINARY_SOURCES.set(snapshot);
927    Ok(())
928}
929
930fn copy_worker_source_to_cache(source: &Path, cache_root: &Path) -> Result<PathBuf> {
931    std::fs::create_dir_all(cache_root)
932        .with_context(|| format!("create pinned worker cache {}", cache_root.display()))?;
933    let mut input =
934        File::open(source).with_context(|| format!("open worker source {}", source.display()))?;
935    let metadata = input
936        .metadata()
937        .with_context(|| format!("stat worker source {}", source.display()))?;
938    let mut temporary = tempfile::NamedTempFile::new_in(cache_root)
939        .with_context(|| format!("create pinned worker staging file {}", cache_root.display()))?;
940    let mut digest = Sha256::new();
941    let mut buffer = [0_u8; 128 * 1024];
942    loop {
943        let count = input
944            .read(&mut buffer)
945            .with_context(|| format!("read worker source {}", source.display()))?;
946        if count == 0 {
947            break;
948        }
949        temporary
950            .write_all(&buffer[..count])
951            .with_context(|| format!("copy worker source {}", source.display()))?;
952        digest.update(&buffer[..count]);
953    }
954    temporary
955        .as_file_mut()
956        .sync_all()
957        .with_context(|| format!("flush pinned worker source {}", source.display()))?;
958    std::fs::set_permissions(temporary.path(), metadata.permissions())
959        .with_context(|| format!("preserve permissions for {}", source.display()))?;
960    let digest = format!("{:x}", digest.finalize());
961    publish_cached_worker(temporary, cache_root, &digest)
962}
963
964/// Publish one immutable cache artifact. persist_noclobber makes the final
965/// publication atomic and never replaces an artifact another daemon may have
966/// already captured.
967fn publish_cached_worker(
968    temporary: tempfile::NamedTempFile,
969    cache_root: &Path,
970    digest: &str,
971) -> Result<PathBuf> {
972    let directory = cache_root.join(digest);
973    std::fs::create_dir_all(&directory)
974        .with_context(|| format!("create pinned worker cache {}", directory.display()))?;
975    let destination = directory.join("hel");
976    if destination.is_file() {
977        return Ok(destination);
978    }
979    match temporary.persist_noclobber(&destination) {
980        Ok(_) => {
981            #[cfg(unix)]
982            File::open(&directory)
983                .and_then(|directory| directory.sync_all())
984                .with_context(|| format!("flush pinned worker cache {}", directory.display()))?;
985            Ok(destination)
986        }
987        Err(error) if error.error.kind() == ErrorKind::AlreadyExists => {
988            if destination.is_file() {
989                Ok(destination)
990            } else {
991                Err(error.error).with_context(|| {
992                    format!("publish pinned worker artifact {}", destination.display())
993                })
994            }
995        }
996        Err(error) => Err(error.error)
997            .with_context(|| format!("publish pinned worker artifact {}", destination.display())),
998    }
999}
1000
1001/// Find a worker source without downloading it.
1002///
1003/// Container provisioning resolves this after discovering the target
1004/// architecture. Doctor uses the same lookup with the selected container's
1005/// expected architecture, so it can recommend a fix without creating a
1006/// container or making a network request.
1007pub fn worker_binary_prerequisite_for_arch(arch: &str) -> Result<WorkerBinaryAvailability> {
1008    worker_binary_for_arch(arch, WorkerBinaryRequirement::PortableLinux)
1009}
1010
1011fn worker_binary_for_arch(
1012    arch: &str,
1013    requirement: WorkerBinaryRequirement,
1014) -> Result<WorkerBinaryAvailability> {
1015    if let Some(snapshot) = PINNED_WORKER_BINARY_SOURCES.get() {
1016        return snapshot.resolve(arch, requirement);
1017    }
1018    let current = std::env::current_exe().context("resolve Mjolnir controller binary")?;
1019    worker_binary_prerequisite_for_current(arch, requirement, &current, &|path| path.is_file())
1020}
1021
1022/// The lookup itself, with the controller's own path and the file probe passed
1023/// in so both can be exercised without the machine they describe.
1024fn worker_binary_prerequisite_for_current(
1025    arch: &str,
1026    requirement: WorkerBinaryRequirement,
1027    current: &Path,
1028    is_file: &dyn Fn(&Path) -> bool,
1029) -> Result<WorkerBinaryAvailability> {
1030    let triple = format!("{arch}-unknown-linux-musl");
1031    if let Some(path) = hel::hel_config::env_override_os("WORKER_BINARY").map(PathBuf::from) {
1032        if !is_file(&path) {
1033            bail!("MJ_WORKER_BINARY is not a file: {}", path.display());
1034        }
1035        return Ok(WorkerBinaryAvailability::Local {
1036            path,
1037            source: "MJ_WORKER_BINARY".into(),
1038        });
1039    }
1040    // A rebuilt or renamed checkout leaves a running controller pointing at a
1041    // path that no longer holds a binary. Every lookup derived from that path
1042    // is meaningless, so remember the fact and skip those lookups.
1043    let controller_replaced = !is_file(current);
1044    let mut candidates = Vec::new();
1045    if let Some(directory) = hel::hel_config::env_override_os("WORKER_DIR").map(PathBuf::from) {
1046        candidates.push((
1047            packaged_worker_binary_path(&directory, &triple),
1048            "MJ_WORKER_DIR",
1049        ));
1050        candidates.push((directory.join(&triple).join("hel"), "MJ_WORKER_DIR"));
1051    }
1052    if let Some((path, source)) = candidates.into_iter().find(|(path, _)| is_file(path)) {
1053        return Ok(WorkerBinaryAvailability::Local {
1054            path,
1055            source: source.into(),
1056        });
1057    }
1058    if requirement == WorkerBinaryRequirement::LocalHost
1059        && let Some((path, source)) = select_native_worker(current, is_file)
1060    {
1061        return Ok(WorkerBinaryAvailability::Local {
1062            path,
1063            source: source.into(),
1064        });
1065    }
1066    if !controller_replaced
1067        && let Some((path, source)) = select_sibling_worker(current, &triple, is_file)
1068    {
1069        return Ok(WorkerBinaryAvailability::Local {
1070            path,
1071            source: source.into(),
1072        });
1073    }
1074    if let Some(template) = hel::hel_config::env_override("WORKER_URL") {
1075        let expected = hel::hel_config::env_override("WORKER_SHA256")
1076            .context("MJ_WORKER_URL requires MJ_WORKER_SHA256")?;
1077        validate_worker_sha256(&expected)?;
1078        return Ok(WorkerBinaryAvailability::Remote {
1079            url: template.replace("{target}", &triple),
1080            sha256: expected,
1081            triple,
1082        });
1083    }
1084    // Telling someone to install a worker beside a binary that is no longer
1085    // there sends them looking in the wrong place.
1086    ensure!(
1087        !controller_replaced,
1088        "the running mj binary was replaced or removed on disk ({}); restart the Mjolnir daemon so it runs the current build, then retry",
1089        display_path(current)
1090    );
1091    bail!(
1092        "no Linux worker for {triple}; install mj-worker-{triple} beside mj, set MJ_WORKER_DIR/MJ_WORKER_BINARY, or configure MJ_WORKER_URL and MJ_WORKER_SHA256"
1093    )
1094}
1095
1096/// Linux appends " (deleted)" to `/proc/<pid>/exe` for a removed image. That
1097/// marker belongs in a message but never in a decision, which `is_file` makes.
1098fn display_path(path: &Path) -> String {
1099    let text = path.to_string_lossy();
1100    text.strip_suffix(" (deleted)").unwrap_or(&text).to_owned()
1101}
1102
1103/// The architecture a configured template names outright, if it names one. A
1104/// container `platform` such as `linux/arm64` decides what the target runs
1105/// whatever the controller's own machine is, and it is the only architecture a
1106/// configured target template can state: the configured `AwsEc2` variant names
1107/// a launch template, whose instance type is only discoverable through the AWS
1108/// API.
1109fn template_architecture(template: &hel::hel_config::TargetTemplate) -> Option<&'static str> {
1110    use hel::hel_config::TargetTemplate as Template;
1111    let platform = match template {
1112        Template::LocalPodman { container }
1113        | Template::LocalDocker { container }
1114        | Template::AppleContainer { container }
1115        | Template::SshPodman { container, .. }
1116        | Template::SshDocker { container, .. } => container.platform.as_deref()?,
1117        Template::LocalBare | Template::SshBare { .. } | Template::AwsEc2 { .. } => return None,
1118    };
1119    // Platform strings appear as "linux/arm64", "arm64", or "linux/arm64/v8".
1120    platform.split('/').find_map(|part| match part.trim() {
1121        "x86_64" | "amd64" => Some("x86_64"),
1122        "aarch64" | "arm64" => Some("aarch64"),
1123        _ => None,
1124    })
1125}
1126
1127/// Architectures a resume must be able to serve, knowing only the configured
1128/// template. Provisioning learns the real answer by running `uname -m` on the
1129/// live target; a resume has no target yet, so this uses what is knowable
1130/// without one: an architecture the template names, else the controller's own
1131/// architecture for a target that runs on this machine, else either Linux
1132/// architecture for a remote target.
1133fn preflight_architectures(template: &hel::hel_config::TargetTemplate) -> Vec<&'static str> {
1134    use hel::hel_config::TargetTemplate as Template;
1135    if let Some(arch) = template_architecture(template) {
1136        return vec![arch];
1137    }
1138    match template {
1139        Template::LocalBare
1140        | Template::LocalPodman { .. }
1141        | Template::LocalDocker { .. }
1142        | Template::AppleContainer { .. } => vec![std::env::consts::ARCH],
1143        Template::SshBare { .. }
1144        | Template::SshPodman { .. }
1145        | Template::SshDocker { .. }
1146        | Template::AwsEc2 { .. } => {
1147            vec!["x86_64", "aarch64"]
1148        }
1149    }
1150}
1151
1152/// Whether this controller could produce a Linux worker binary for a target
1153/// that does not exist yet.
1154///
1155/// A resume compacts a cross-harness transcript before it provisions anything,
1156/// which costs minutes and paid model requests. Resolving the worker binary is
1157/// local and takes microseconds, so a resume that could never install a worker
1158/// must fail before spending any of that. This downloads nothing: a remote
1159/// source counts as available, because fetching it belongs to provisioning.
1160pub(super) fn preflight_worker_binary(template: &hel::hel_config::TargetTemplate) -> Result<()> {
1161    // Only a bare local target may run the controller's own host binary as
1162    // its worker; every other target needs a portable Linux worker.
1163    let requirement = if matches!(template, hel::hel_config::TargetTemplate::LocalBare) {
1164        WorkerBinaryRequirement::LocalHost
1165    } else {
1166        WorkerBinaryRequirement::PortableLinux
1167    };
1168    let mut failure = None;
1169    for arch in preflight_architectures(template) {
1170        match worker_binary_for_arch(arch, requirement) {
1171            Ok(_) => return Ok(()),
1172            Err(error) => failure = Some(error),
1173        }
1174    }
1175    match failure {
1176        // The message is the one provisioning would have printed later, so the
1177        // user reads the same fix, sooner.
1178        Some(error) => Err(error).context("preflight the worker binary before resuming"),
1179        None => Ok(()),
1180    }
1181}
1182
1183pub(super) fn worker_binary_for(
1184    locator: &hel_targets::TargetLocator,
1185    executor: &impl CommandExecutor,
1186) -> Result<PathBuf> {
1187    let arch = target_architecture(locator, executor)?;
1188    let requirement = if matches!(locator, hel_targets::TargetLocator::LocalBare { .. }) {
1189        WorkerBinaryRequirement::LocalHost
1190    } else {
1191        WorkerBinaryRequirement::PortableLinux
1192    };
1193    match worker_binary_for_arch(arch, requirement)? {
1194        WorkerBinaryAvailability::Local { path, .. } => Ok(path),
1195        WorkerBinaryAvailability::Remote {
1196            url,
1197            sha256,
1198            triple,
1199        } => download_worker(&url, &sha256, &triple),
1200    }
1201}
1202
1203fn target_architecture(
1204    locator: &hel_targets::TargetLocator,
1205    executor: &impl CommandExecutor,
1206) -> Result<&'static str> {
1207    let command = match locator {
1208        hel_targets::TargetLocator::LocalBare { .. } => CommandSpec::new("uname", ["-m"]),
1209        hel_targets::TargetLocator::LocalPodman { container_id, .. } => {
1210            CommandSpec::new("podman", ["exec", container_id, "uname", "-m"])
1211        }
1212        hel_targets::TargetLocator::LocalDocker { container_id } => {
1213            CommandSpec::new("docker", ["exec", container_id, "uname", "-m"])
1214        }
1215        hel_targets::TargetLocator::AppleContainer { container_id } => {
1216            CommandSpec::new("container", ["exec", container_id, "uname", "-m"])
1217        }
1218        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
1219        | hel_targets::TargetLocator::SshBare { ssh, .. } => ssh_command_spec(ssh, ["uname", "-m"]),
1220        hel_targets::TargetLocator::SshPodman {
1221            ssh, container_id, ..
1222        } => ssh_command_spec(ssh, ["podman", "exec", container_id, "uname", "-m"]),
1223        hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
1224            ssh_command_spec(ssh, ["docker", "exec", container_id, "uname", "-m"])
1225        }
1226    }
1227    .purpose("detect target architecture");
1228    let output = execute_checked(executor, command)?;
1229    match String::from_utf8(output.stdout)?.trim() {
1230        "x86_64" | "amd64" => Ok("x86_64"),
1231        "aarch64" | "arm64" => Ok("aarch64"),
1232        architecture => bail!("unsupported target architecture {architecture:?}"),
1233    }
1234}
1235
1236fn download_worker(url: &str, expected_sha256: &str, triple: &str) -> Result<PathBuf> {
1237    validate_worker_sha256(expected_sha256)?;
1238    let digest = expected_sha256.to_ascii_lowercase();
1239    let directory = data_dir().join("workers").join("pinned");
1240    let destination = directory.join(&digest).join("hel");
1241    std::fs::create_dir_all(destination.parent().unwrap_or(&directory))?;
1242    if destination.is_file() {
1243        let bytes = std::fs::read(&destination).with_context(|| {
1244            format!(
1245                "read cached worker for {triple} from {}",
1246                destination.display()
1247            )
1248        })?;
1249        if format!("{:x}", Sha256::digest(&bytes)).eq_ignore_ascii_case(expected_sha256) {
1250            return Ok(destination);
1251        }
1252        bail!(
1253            "content-addressed worker cache {} does not match {} checksum",
1254            destination.display(),
1255            expected_sha256
1256        );
1257    }
1258    let bytes = reqwest::blocking::Client::builder()
1259        .timeout(std::time::Duration::from_secs(120))
1260        .build()?
1261        .get(url)
1262        .send()?
1263        .error_for_status()?
1264        .bytes()?;
1265    let actual = format!("{:x}", Sha256::digest(&bytes));
1266    if !actual.eq_ignore_ascii_case(expected_sha256) {
1267        bail!("downloaded worker checksum mismatch: expected {expected_sha256}, got {actual}");
1268    }
1269    std::fs::create_dir_all(&directory)?;
1270    let mut temporary = tempfile::NamedTempFile::new_in(&directory)?;
1271    std::io::Write::write_all(&mut temporary, &bytes)?;
1272    temporary.as_file_mut().sync_all()?;
1273    #[cfg(unix)]
1274    {
1275        use std::os::unix::fs::PermissionsExt;
1276        std::fs::set_permissions(temporary.path(), std::fs::Permissions::from_mode(0o700))?;
1277    }
1278    publish_cached_worker(temporary, &directory, &digest)
1279}
1280
1281fn validate_worker_sha256(expected_sha256: &str) -> Result<()> {
1282    if expected_sha256.len() != 64 || !expected_sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
1283    {
1284        bail!("MJ_WORKER_SHA256 must be a 64-character hexadecimal digest");
1285    }
1286    Ok(())
1287}
1288
1289fn workspace_paths(
1290    locator: &hel_targets::TargetLocator,
1291    bundle: &ProjectBundle,
1292    session_id: &str,
1293) -> Result<(String, Vec<String>)> {
1294    let root = match locator {
1295        hel_targets::TargetLocator::LocalBare { .. } => {
1296            bail!("local bare projects use their selected directory")
1297        }
1298        hel_targets::TargetLocator::LocalPodman { .. }
1299        | hel_targets::TargetLocator::LocalDocker { .. }
1300        | hel_targets::TargetLocator::AppleContainer { .. }
1301        | hel_targets::TargetLocator::SshPodman { .. }
1302        | hel_targets::TargetLocator::SshDocker { .. } => "/workspace".to_string(),
1303        hel_targets::TargetLocator::AwsEc2 { workspace, .. }
1304        | hel_targets::TargetLocator::SshBare { workspace, .. } => workspace.clone(),
1305    };
1306    if matches!(locator, hel_targets::TargetLocator::AwsEc2 { .. }) {
1307        let expected = format!(".local/share/hel/workspaces/{session_id}");
1308        if root != expected {
1309            bail!("AWS workspace does not match session")
1310        }
1311    }
1312    let primary = bundle.primary().context("bundle primary is missing")?;
1313    let primary_path = format!("{root}/{}", primary.destination.to_string_lossy());
1314    let additional = bundle
1315        .repositories
1316        .iter()
1317        .filter(|repository| repository.id != bundle.primary_repo)
1318        .map(|repository| format!("{root}/{}", repository.destination.to_string_lossy()))
1319        .collect();
1320    Ok((primary_path, additional))
1321}
1322
1323// Package versions for ACP bridges and their harnesses. Keep these in lockstep with the global
1324// npm installs in containers/Containerfile.agent-dev; bridge_pins_match_containerfile() below
1325// fails the build when they drift.
1326// Codex 0.148 reuses pending MCP startups during runtime reconciliation. Older
1327// releases could cancel the first project-memory startup while immediately
1328// replacing it with an equivalent connection, leaving a false failed-tool
1329// event at the beginning of every session.
1330/// Stage shown after the worker is reachable and while its ACP bridge becomes
1331/// ready. Default launchers that can fetch their own harness name that work;
1332/// explicit executables and DSH only have a process to start.
1333pub(super) fn bridge_readiness_stage(profile: &HarnessProfile) -> ProvisionStage {
1334    if matches!(
1335        profile.kind,
1336        HarnessKind::Codex
1337            | HarnessKind::Claude
1338            | HarnessKind::Kimi
1339            | HarnessKind::Grok
1340            | HarnessKind::Muse
1341    ) {
1342        ProvisionStage::Installing(profile.kind)
1343    } else {
1344        ProvisionStage::Starting
1345    }
1346}
1347
1348pub(super) fn bridge_launch(
1349    harness: hel::hel_config::HarnessKind,
1350    policy: hel::hel_config::ExecutionPolicy,
1351) -> (String, Vec<String>) {
1352    match harness {
1353        hel::hel_config::HarnessKind::Muse => ("muse-acp".into(), Vec::new()),
1354        hel::hel_config::HarnessKind::Codex => (
1355            "sh".into(),
1356            vec![
1357                "-c".into(),
1358                format!("if command -v codex-acp >/dev/null 2>&1 && [ \"$(codex-acp --version 2>/dev/null)\" = \"{CODEX_ACP_PACKAGE} {CODEX_ACP_VERSION}\" ]; then exec codex-acp; fi; {}; exec npx -y {CODEX_ACP_PACKAGE}@{CODEX_ACP_VERSION}", ensure_node_script()),
1359            ],
1360        ),
1361        hel::hel_config::HarnessKind::Claude => (
1362            "sh".into(),
1363            vec![
1364                "-c".into(),
1365                format!("if command -v claude-agent-acp >/dev/null 2>&1; then exec claude-agent-acp; fi; {}; exec npx -y @agentclientprotocol/claude-agent-acp@{CLAUDE_ACP_VERSION}", ensure_node_script()),
1366            ],
1367        ),
1368        hel::hel_config::HarnessKind::Kimi => (
1369            "sh".into(),
1370            vec![
1371                "-c".into(),
1372                "if command -v kimi >/dev/null 2>&1; then exec kimi acp; elif [ -x \"$HOME/.kimi-code/bin/kimi\" ]; then exec \"$HOME/.kimi-code/bin/kimi\" acp; elif command -v curl >/dev/null 2>&1; then curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash && exec \"$HOME/.kimi-code/bin/kimi\" acp; else echo 'Mjolnir needs compatible Kimi Code or curl for its official installer; add the tool to PATH' >&2; exit 127; fi".into(),
1373            ],
1374        ),
1375        hel::hel_config::HarnessKind::Grok => {
1376            let acp = hel::hel_config::HarnessKind::Grok
1377                .bridge_args(policy)
1378                .join(" ");
1379            (
1380                "sh".into(),
1381                vec![
1382                    "-c".into(),
1383                    format!(
1384                        "if command -v grok >/dev/null 2>&1; then exec grok {acp}; elif [ -x \"$GROK_HOME/bin/grok\" ]; then exec \"$GROK_HOME/bin/grok\" {acp}; elif [ -x \"$HOME/.grok/bin/grok\" ]; then exec \"$HOME/.grok/bin/grok\" {acp}; elif command -v curl >/dev/null 2>&1; then curl -fsSL https://x.ai/cli/install.sh | bash && exec \"$HOME/.grok/bin/grok\" {acp}; else echo 'Mjolnir needs compatible Grok Build or curl for its official installer; add the tool to PATH' >&2; exit 127; fi"
1385                    ),
1386                ],
1387            )
1388        }
1389        hel::hel_config::HarnessKind::Deepseek => {
1390            let acp = hel::hel_config::HarnessKind::Deepseek
1391                .bridge_args(policy)
1392                .join(" ");
1393            (
1394                "sh".into(),
1395                vec![
1396                    "-c".into(),
1397                    format!(
1398                        "{}; if command -v dsh >/dev/null 2>&1 && [ \"$(dsh --version 2>/dev/null)\" = \"{DEEPSEEK_DSH_VERSION}\" ]; then exec dsh {acp}; fi; echo 'Mjolnir needs @deepseek-ai/dsh@{DEEPSEEK_DSH_VERSION} installed on PATH' >&2; exit 127",
1399                        ensure_node_22_script(),
1400                    ),
1401                ],
1402            )
1403        }
1404    }
1405}
1406
1407pub(super) fn preflight_harness(
1408    template: &hel::hel_config::TargetTemplate,
1409    profile: &HarnessProfile,
1410    executor: &impl CommandExecutor,
1411) -> Result<()> {
1412    use hel::hel_config::TargetTemplate;
1413    if !matches!(
1414        profile.kind,
1415        HarnessKind::Codex | HarnessKind::Claude | HarnessKind::Deepseek
1416    ) {
1417        return Ok(());
1418    }
1419    if !matches!(
1420        template,
1421        TargetTemplate::LocalBare | TargetTemplate::SshBare { .. }
1422    ) {
1423        return Ok(());
1424    }
1425    let script = "if ! command -v node >/dev/null 2>&1; then echo 'Node.js is missing from PATH; install Node.js 22 or newer in the target environment' >&2; exit 127; fi; if ! node -e 'process.exit(Number(process.versions.node.split(\".\")[0]) >= 22 ? 0 : 1)'; then echo 'Node.js 22 or newer is required in the target environment' >&2; exit 1; fi; if ! command -v npm >/dev/null 2>&1 || ! npm --version >/dev/null; then echo 'npm is missing or unusable; install npm in the target environment' >&2; exit 127; fi";
1426    let mut args = if profile.environment.contains_key("PATH") {
1427        vec![
1428            "-c".to_owned(),
1429            format!("export PATH=\"$1\"; {script}"),
1430            "mj-node-preflight".into(),
1431            profile.environment["PATH"].clone(),
1432        ]
1433    } else {
1434        vec!["-lc".to_owned(), script.to_owned()]
1435    };
1436    let (command, destination) = match template {
1437        TargetTemplate::LocalBare => (CommandSpec::new("sh", args), "local host".to_owned()),
1438        TargetTemplate::SshBare { ssh, .. } => {
1439            let ssh = super::backend_ssh(ssh);
1440            args.insert(0, "sh".into());
1441            (ssh_command_spec(&ssh, args), ssh.destination)
1442        }
1443        _ => unreachable!(),
1444    };
1445    execute_checked(executor, command.purpose("preflight managed harness Node.js and npm"))
1446        .with_context(|| format!("{} launch preflight failed on {destination}; Node.js 22+ and npm must be available on the target PATH", profile.kind.display_name()))?;
1447    Ok(())
1448}
1449
1450fn ensure_node_script() -> &'static str {
1451    "if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1 || ! command -v npx >/dev/null 2>&1; then echo 'Mjolnir needs Node.js, npm, and npx on PATH; install Node in the target environment' >&2; exit 127; fi"
1452}
1453
1454fn ensure_node_22_script() -> String {
1455    format!(
1456        "{}; if ! node -e 'process.exit(Number(process.versions.node.split(\".\")[0]) >= 22 ? 0 : 1)'; then echo 'DeepSeek Harness requires Node.js 22 or newer' >&2; exit 127; fi",
1457        ensure_node_script()
1458    )
1459}
1460
1461const MJ_CONTAINER_ENVIRONMENT: &str = "## Mjolnir disposable environment\n\nThis session runs in a disposable Mjolnir container. When the session closes, Mjolnir checkpoints everything in project workspace directories under `/workspace`, including committed work, staged and unstaged changes, and untracked files. Mjolnir then removes the container.\n\nEverything outside `/workspace`, including installed packages, `$HOME`, and `/tmp`, is ephemeral and will be lost. Keep durable results in the workspace or push them to a remote.\n\nNew workspaces start on their own session branch from the default network fetch remote’s default branch. Local unpublished commits and uncommitted files are not copied. Use normal git push to publish the current branch to the configured network push destination. Closing saves a checkpoint; it does not publish commits or update the original local checkout. Resumed sessions restore their saved work.\n";
1462
1463pub(super) fn stage_profile(
1464    profile: &hel::hel_config::HarnessProfile,
1465    destination: &Path,
1466) -> Result<()> {
1467    let harness = profile.kind;
1468    let source = profile.home.as_path();
1469    std::fs::create_dir_all(destination)?;
1470    let allowlist: &[&str] = match harness {
1471        hel::hel_config::HarnessKind::Muse => &[
1472            "auth.json",
1473            "settings.json",
1474            "trust.json",
1475            "AGENTS.md",
1476            "skills",
1477            "rules",
1478        ],
1479        hel::hel_config::HarnessKind::Codex => &[
1480            "auth.json",
1481            "config.toml",
1482            "AGENTS.md",
1483            "instructions.md",
1484            "rules",
1485            "skills",
1486        ],
1487        hel::hel_config::HarnessKind::Claude => &[
1488            ".claude.json",
1489            ".credentials.json",
1490            "settings.json",
1491            "CLAUDE.md",
1492            "skills",
1493            "plugins",
1494        ],
1495        hel::hel_config::HarnessKind::Kimi => &[
1496            "credentials",
1497            "config.toml",
1498            "device_id",
1499            "AGENTS.md",
1500            "SYSTEM.md",
1501            "mcp.json",
1502            "skills",
1503            "agents",
1504            "plugins",
1505        ],
1506        hel::hel_config::HarnessKind::Grok => &[
1507            "auth.json",
1508            "config.toml",
1509            "AGENTS.md",
1510            "agent_id",
1511            "skills",
1512            "plugins",
1513        ],
1514        hel::hel_config::HarnessKind::Deepseek => &[
1515            ".credentials.yaml",
1516            "settings.yaml",
1517            "AGENTS.md",
1518            "skills",
1519            ".agent-presets",
1520        ],
1521    };
1522    // Allowlist entries (and, within each, a copied directory's children) are
1523    // independent of one another, so copying them concurrently shortens the
1524    // stage step for profiles with large skills/plugins trees.
1525    allowlist.par_iter().try_for_each(|name| -> Result<()> {
1526        let from = source.join(name);
1527        if from.exists() {
1528            copy_profile_entry(&from, &destination.join(name))?;
1529        }
1530        Ok(())
1531    })?;
1532    Ok(())
1533}
1534
1535/// Add lifecycle guidance only for targets that Hel destroys as a whole.
1536fn append_hel_target_environment(
1537    harness: hel::hel_config::HarnessKind,
1538    destination: &Path,
1539    target: &hel_targets::TargetLocator,
1540) -> Result<()> {
1541    let environment = match target {
1542        hel_targets::TargetLocator::LocalPodman { .. }
1543        | hel_targets::TargetLocator::LocalDocker { .. }
1544        | hel_targets::TargetLocator::AppleContainer { .. }
1545        | hel_targets::TargetLocator::SshPodman { .. }
1546        | hel_targets::TargetLocator::SshDocker { .. } => MJ_CONTAINER_ENVIRONMENT.to_owned(),
1547        hel_targets::TargetLocator::AwsEc2 { workspace, .. } => format!(
1548            "## Mjolnir disposable environment\n\nThis session runs on a disposable Mjolnir EC2 instance. When the session closes, Mjolnir checkpoints everything in project workspace directories under `$HOME/{workspace}`, including committed work, staged and unstaged changes, and untracked files. Mjolnir then terminates the instance.\n\nEverything outside `$HOME/{workspace}`, including installed packages, the rest of `$HOME`, and `/tmp`, is ephemeral and will be lost. Keep durable results in the workspace or push them to a remote.\n\nNew workspaces start on their own session branch from the default network fetch remote’s default branch. Local unpublished commits and uncommitted files are not copied. Use normal git push to publish the current branch to the configured network push destination. Closing saves a checkpoint; it does not publish commits or update the original local checkout. Resumed sessions restore their saved work.\n"
1549        ),
1550        hel_targets::TargetLocator::LocalBare { .. }
1551        | hel_targets::TargetLocator::SshBare { .. } => return Ok(()),
1552    };
1553    let instructions = match harness {
1554        hel::hel_config::HarnessKind::Codex => "AGENTS.md",
1555        hel::hel_config::HarnessKind::Claude => "CLAUDE.md",
1556        hel::hel_config::HarnessKind::Kimi => "AGENTS.md",
1557        hel::hel_config::HarnessKind::Grok => "AGENTS.md",
1558        hel::hel_config::HarnessKind::Deepseek => "AGENTS.md",
1559        hel::hel_config::HarnessKind::Muse => "AGENTS.md",
1560    };
1561    let path = destination.join(instructions);
1562    let separator = match std::fs::read_to_string(&path) {
1563        Ok(contents) if !contents.is_empty() && !contents.ends_with('\n') => "\n\n",
1564        Ok(contents) if !contents.is_empty() => "\n",
1565        Ok(_) => "",
1566        Err(error) if error.kind() == std::io::ErrorKind::NotFound => "",
1567        Err(error) => return Err(error.into()),
1568    };
1569    use std::io::Write;
1570
1571    let mut file = std::fs::OpenOptions::new()
1572        .create(true)
1573        .append(true)
1574        .open(&path)
1575        .with_context(|| format!("open staged harness instructions {}", path.display()))?;
1576    file.write_all(separator.as_bytes())?;
1577    file.write_all(environment.as_bytes())?;
1578    Ok(())
1579}
1580
1581fn copy_profile_entry(source: &Path, destination: &Path) -> Result<()> {
1582    let metadata = std::fs::symlink_metadata(source)
1583        .with_context(|| format!("read staged profile entry metadata {}", source.display()))?;
1584    if metadata.file_type().is_symlink() {
1585        return Ok(());
1586    }
1587    if metadata.is_file() {
1588        if let Some(parent) = destination.parent() {
1589            std::fs::create_dir_all(parent)
1590                .with_context(|| format!("create staged profile directory {}", parent.display()))?;
1591        }
1592        std::fs::copy(source, destination).with_context(|| {
1593            format!(
1594                "copy staged profile file {} to {}",
1595                source.display(),
1596                destination.display()
1597            )
1598        })?;
1599        return Ok(());
1600    }
1601    if metadata.is_dir() {
1602        std::fs::create_dir_all(destination).with_context(|| {
1603            format!("create staged profile directory {}", destination.display())
1604        })?;
1605        let entries = std::fs::read_dir(source)
1606            .with_context(|| format!("list staged profile directory {}", source.display()))?
1607            .collect::<std::io::Result<Vec<_>>>()
1608            .with_context(|| {
1609                format!(
1610                    "read staged profile directory entries in {}",
1611                    source.display()
1612                )
1613            })?;
1614        // Sibling entries in one directory are independent, so recurse in
1615        // parallel; this is the level most likely to hold many files (e.g. a
1616        // skills or plugins tree).
1617        entries.par_iter().try_for_each(|entry| {
1618            copy_profile_entry(&entry.path(), &destination.join(entry.file_name()))
1619        })?;
1620        std::fs::set_permissions(destination, metadata.permissions()).with_context(|| {
1621            format!(
1622                "set permissions for staged profile directory {}",
1623                destination.display()
1624            )
1625        })?;
1626    }
1627    Ok(())
1628}
1629
1630// Container copies can create root-owned files even when exec defaults to a
1631// non-root image user. The worker directory was created by that user, so use
1632// its ownership for uploaded files before restricting their permissions.
1633pub(super) fn container_upload_ownership_args(
1634    container_id: &str,
1635    worker_root: &str,
1636    paths: &[&str],
1637) -> Vec<String> {
1638    let mut args = vec![
1639        "exec".into(),
1640        "--user".into(),
1641        "0".into(),
1642        container_id.into(),
1643        "sh".into(),
1644        "-c".into(),
1645        // GNU and BusyBox stat both support this numeric ownership format.
1646        r#"set -eu; owner=$(stat -c '%u:%g' -- "$1"); shift; chown -R "$owner" -- "$@""#.into(),
1647        "sh".into(),
1648        worker_root.into(),
1649    ];
1650    args.extend(paths.iter().map(|path| (*path).to_owned()));
1651    args
1652}
1653
1654#[allow(clippy::too_many_arguments)]
1655fn install_worker_files(
1656    executor: &impl CommandExecutor,
1657    locator: &hel_targets::TargetLocator,
1658    session_id: &str,
1659    worker_root: &str,
1660    profile_home: &str,
1661    worker_binary: &Path,
1662    launch_config: &Path,
1663    ownership: &Path,
1664    profile_stage: &Path,
1665) -> Result<()> {
1666    match locator {
1667        hel_targets::TargetLocator::LocalBare { .. } => {
1668            if profile_stage.is_dir() {
1669                std::fs::create_dir_all(profile_home).context("create isolated local profile")?;
1670                for entry in std::fs::read_dir(profile_stage)? {
1671                    let entry = entry?;
1672                    copy_profile_entry(
1673                        &entry.path(),
1674                        &Path::new(profile_home).join(entry.file_name()),
1675                    )?;
1676                }
1677            }
1678            for command in [
1679                CommandSpec::new("mkdir", ["-p", worker_root])
1680                    .purpose("create local bare worker directory"),
1681                CommandSpec::new(
1682                    "cp",
1683                    [
1684                        worker_binary.to_string_lossy().into_owned(),
1685                        format!("{worker_root}/hel"),
1686                    ],
1687                )
1688                .purpose("install local Mjolnir worker"),
1689                CommandSpec::new(
1690                    "cp",
1691                    [
1692                        launch_config.to_string_lossy().into_owned(),
1693                        format!("{worker_root}/launch.json"),
1694                    ],
1695                )
1696                .purpose("install local worker launch configuration"),
1697                CommandSpec::new(
1698                    "cp",
1699                    [
1700                        ownership.to_string_lossy().into_owned(),
1701                        format!("{worker_root}/ownership.json"),
1702                    ],
1703                )
1704                .purpose("install local worker ownership marker"),
1705                CommandSpec::new("chmod", ["700", &format!("{worker_root}/hel")])
1706                    .purpose("make local Mjolnir worker executable"),
1707            ] {
1708                execute_checked(executor, command)?;
1709            }
1710        }
1711        hel_targets::TargetLocator::LocalPodman { container_id, .. }
1712        | hel_targets::TargetLocator::LocalDocker { container_id }
1713        | hel_targets::TargetLocator::AppleContainer { container_id } => {
1714            let engine = match locator {
1715                hel_targets::TargetLocator::LocalPodman { .. } => "podman",
1716                hel_targets::TargetLocator::LocalDocker { .. } => "docker",
1717                hel_targets::TargetLocator::AppleContainer { .. } => "container",
1718                _ => unreachable!("matched local container target"),
1719            };
1720            for command in [
1721                CommandSpec::new(
1722                    engine,
1723                    [
1724                        "exec".into(),
1725                        container_id.clone(),
1726                        "mkdir".into(),
1727                        "-p".into(),
1728                        worker_root.into(),
1729                        profile_home.into(),
1730                    ],
1731                )
1732                .purpose("create target worker directories"),
1733                CommandSpec::new(
1734                    engine,
1735                    [
1736                        "cp".into(),
1737                        worker_binary.to_string_lossy().into_owned(),
1738                        format!("{container_id}:{worker_root}/hel"),
1739                    ],
1740                )
1741                .purpose("upload Mjolnir worker"),
1742                CommandSpec::new(
1743                    engine,
1744                    [
1745                        "cp".into(),
1746                        launch_config.to_string_lossy().into_owned(),
1747                        format!("{container_id}:{worker_root}/launch.json"),
1748                    ],
1749                )
1750                .purpose("upload worker launch configuration"),
1751                CommandSpec::new(
1752                    engine,
1753                    [
1754                        "cp".into(),
1755                        ownership.to_string_lossy().into_owned(),
1756                        format!("{container_id}:{worker_root}/ownership.json"),
1757                    ],
1758                )
1759                .purpose("upload worker ownership marker"),
1760                CommandSpec::new(
1761                    engine,
1762                    [
1763                        "cp".into(),
1764                        format!("{}/.", profile_stage.display()),
1765                        format!("{container_id}:{profile_home}"),
1766                    ],
1767                )
1768                .purpose("upload harness profile allowlist"),
1769                CommandSpec::new(
1770                    engine,
1771                    container_upload_ownership_args(
1772                        container_id,
1773                        worker_root,
1774                        &[
1775                            &format!("{worker_root}/hel"),
1776                            &format!("{worker_root}/launch.json"),
1777                            &format!("{worker_root}/ownership.json"),
1778                            profile_home,
1779                        ],
1780                    ),
1781                )
1782                .purpose("assign uploaded files to the worker user"),
1783                CommandSpec::new(
1784                    engine,
1785                    [
1786                        "exec".into(),
1787                        container_id.clone(),
1788                        "chmod".into(),
1789                        "700".into(),
1790                        format!("{worker_root}/hel"),
1791                    ],
1792                )
1793                .purpose("make Mjolnir worker executable"),
1794                CommandSpec::new(
1795                    engine,
1796                    [
1797                        "exec".into(),
1798                        container_id.clone(),
1799                        "chmod".into(),
1800                        "-R".into(),
1801                        "go-rwx".into(),
1802                        profile_home.into(),
1803                    ],
1804                )
1805                .purpose("restrict harness profile permissions"),
1806            ] {
1807                execute_checked(executor, command)?;
1808            }
1809        }
1810        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
1811        | hel_targets::TargetLocator::SshBare { ssh, .. } => {
1812            install_worker_over_ssh(
1813                executor,
1814                ssh,
1815                worker_root,
1816                profile_home,
1817                worker_binary,
1818                launch_config,
1819                ownership,
1820                profile_stage,
1821            )?;
1822        }
1823        hel_targets::TargetLocator::SshPodman {
1824            ssh, container_id, ..
1825        }
1826        | hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
1827            let engine = match locator {
1828                hel_targets::TargetLocator::SshPodman { .. } => "podman",
1829                hel_targets::TargetLocator::SshDocker { .. } => "docker",
1830                _ => unreachable!("matched remote container target"),
1831            };
1832            // The worker binary is 10-30 MB and identical across sessions, so
1833            // keep it in a content-addressed cache on the remote host and copy
1834            // it over the wire only once per unique binary.
1835            let digest = hel::hel_worker_launch::worker_executable_digest(worker_binary)?;
1836            // Home-relative, not "~/": ssh_command_spec single-quotes every
1837            // argument, so a tilde would stay literal in the remote shell
1838            // while scp expands it, and the two sides would disagree. Both
1839            // ssh commands (cwd is the login home) and scp resolve a relative
1840            // path against the remote home.
1841            let cache_dir = format!(".cache/mjolnir/workers/{digest}");
1842            let cached_worker = format!("{cache_dir}/hel");
1843            let cached = matches!(
1844                executor.execute(
1845                    &ssh_command_spec(ssh, ["test", "-f", &cached_worker])
1846                        .purpose("probe cached remote Mjolnir worker"),
1847                ),
1848                Ok(output) if output.status == 0
1849            );
1850            if !cached {
1851                execute_checked(
1852                    executor,
1853                    ssh_command_spec(ssh, ["mkdir", "-p", &cache_dir])
1854                        .purpose("create remote worker cache"),
1855                )?;
1856                let partial = format!("{cache_dir}/hel.partial-{session_id}");
1857                execute_checked(
1858                    executor,
1859                    scp_command_spec(ssh, worker_binary, &partial, false)
1860                        .purpose("upload remote container worker binary"),
1861                )?;
1862                // Rename within the cache directory so the final path only
1863                // ever names a complete upload.
1864                execute_checked(
1865                    executor,
1866                    ssh_command_spec(ssh, ["mv", &partial, &cached_worker])
1867                        .purpose("publish cached remote Mjolnir worker"),
1868                )?;
1869            }
1870            let upload = format!(".cache/mjolnir/uploads/{session_id}");
1871            execute_checked(
1872                executor,
1873                ssh_command_spec(ssh, ["mkdir", "-p", &upload])
1874                    .purpose("create remote upload staging"),
1875            )?;
1876            for (source, name) in [
1877                (launch_config, "launch.json"),
1878                (ownership, "ownership.json"),
1879            ] {
1880                execute_checked(
1881                    executor,
1882                    scp_command_spec(ssh, source, &format!("{upload}/{name}"), false)
1883                        .purpose("upload remote container worker file"),
1884                )?;
1885            }
1886            execute_checked(
1887                executor,
1888                scp_command_spec(ssh, profile_stage, &format!("{upload}/profile"), true)
1889                    .purpose("upload remote container profile allowlist"),
1890            )?;
1891            let remote = [
1892                vec![
1893                    engine.into(),
1894                    "exec".into(),
1895                    container_id.clone(),
1896                    "mkdir".into(),
1897                    "-p".into(),
1898                    worker_root.into(),
1899                    profile_home.into(),
1900                ],
1901                vec![
1902                    engine.into(),
1903                    "cp".into(),
1904                    cached_worker.clone(),
1905                    format!("{container_id}:{worker_root}/hel"),
1906                ],
1907                vec![
1908                    engine.into(),
1909                    "cp".into(),
1910                    format!("{upload}/launch.json"),
1911                    format!("{container_id}:{worker_root}/launch.json"),
1912                ],
1913                vec![
1914                    engine.into(),
1915                    "cp".into(),
1916                    format!("{upload}/ownership.json"),
1917                    format!("{container_id}:{worker_root}/ownership.json"),
1918                ],
1919                vec![
1920                    engine.into(),
1921                    "cp".into(),
1922                    format!("{upload}/profile/."),
1923                    format!("{container_id}:{profile_home}"),
1924                ],
1925                std::iter::once(engine.to_owned())
1926                    .chain(container_upload_ownership_args(
1927                        container_id,
1928                        worker_root,
1929                        &[
1930                            &format!("{worker_root}/hel"),
1931                            &format!("{worker_root}/launch.json"),
1932                            &format!("{worker_root}/ownership.json"),
1933                            profile_home,
1934                        ],
1935                    ))
1936                    .collect(),
1937                vec![
1938                    engine.into(),
1939                    "exec".into(),
1940                    container_id.clone(),
1941                    "chmod".into(),
1942                    "700".into(),
1943                    format!("{worker_root}/hel"),
1944                ],
1945                vec![
1946                    engine.into(),
1947                    "exec".into(),
1948                    container_id.clone(),
1949                    "chmod".into(),
1950                    "-R".into(),
1951                    "go-rwx".into(),
1952                    profile_home.into(),
1953                ],
1954                vec!["rm".into(), "-rf".into(), "--".into(), upload.clone()],
1955            ];
1956            for args in remote {
1957                execute_checked(
1958                    executor,
1959                    ssh_command_spec(ssh, args).purpose("install remote container worker"),
1960                )?;
1961            }
1962        }
1963    }
1964    Ok(())
1965}
1966
1967#[allow(clippy::too_many_arguments)]
1968fn install_worker_over_ssh(
1969    executor: &impl CommandExecutor,
1970    ssh: &SshTarget,
1971    worker_root: &str,
1972    profile_home: &str,
1973    worker_binary: &Path,
1974    launch_config: &Path,
1975    ownership: &Path,
1976    profile_stage: &Path,
1977) -> Result<()> {
1978    execute_checked(
1979        executor,
1980        ssh_command_spec(ssh, ["mkdir", "-p", worker_root, profile_home])
1981            .purpose("create SSH worker directories"),
1982    )?;
1983    for (source, remote, recursive) in [
1984        (worker_binary, format!("{worker_root}/hel"), false),
1985        (launch_config, format!("{worker_root}/launch.json"), false),
1986        (ownership, format!("{worker_root}/ownership.json"), false),
1987    ] {
1988        execute_checked(
1989            executor,
1990            scp_command_spec(ssh, source, &remote, recursive).purpose("upload SSH worker file"),
1991        )?;
1992    }
1993    let incoming_profile = format!("{profile_home}.incoming");
1994    execute_checked(
1995        executor,
1996        scp_command_spec(ssh, profile_stage, &incoming_profile, true)
1997            .purpose("upload SSH harness profile allowlist"),
1998    )?;
1999    execute_checked(
2000        executor,
2001        ssh_command_spec(
2002            ssh,
2003            ["cp", "-R", &format!("{incoming_profile}/."), profile_home],
2004        )
2005        .purpose("install SSH harness profile allowlist"),
2006    )?;
2007    execute_checked(
2008        executor,
2009        ssh_command_spec(ssh, ["rm", "-rf", "--", &incoming_profile])
2010            .purpose("remove SSH profile staging"),
2011    )?;
2012    execute_checked(
2013        executor,
2014        ssh_command_spec(ssh, ["chmod", "700", &format!("{worker_root}/hel")])
2015            .purpose("make SSH worker executable"),
2016    )?;
2017    execute_checked(
2018        executor,
2019        ssh_command_spec(ssh, ["chmod", "-R", "go-rwx", profile_home])
2020            .purpose("restrict SSH harness profile permissions"),
2021    )?;
2022    Ok(())
2023}
2024
2025/// Replace `{worker_root}/hel` with the controller's current worker binary.
2026///
2027/// Checkpoint export starts that path as a new process. A live daemon already
2028/// has the previous inode mapped, so this does not restart it. Writing through
2029/// `hel.next` and renaming avoids `ETXTBSY` on a running image.
2030pub(super) fn replace_installed_worker_binary(
2031    executor: &impl CommandExecutor,
2032    locator: &hel_targets::TargetLocator,
2033    session_id: &str,
2034    worker_binary: &Path,
2035) -> Result<()> {
2036    let plan = installed_worker_binary_replacement_plan(locator, session_id, worker_binary)?;
2037    for command in plan.commands {
2038        execute_checked(executor, command)?;
2039    }
2040    Ok(())
2041}
2042
2043pub(super) fn replace_installed_worker_launch_config(
2044    executor: &impl CommandExecutor,
2045    locator: &hel_targets::TargetLocator,
2046    session_id: &str,
2047    launch: &WorkerLaunchConfig,
2048) -> Result<()> {
2049    let plan = worker_launch_refresh_plan(locator, session_id, launch)?;
2050    for command in plan.replace.commands {
2051        execute_checked(executor, command)?;
2052    }
2053    Ok(())
2054}
2055
2056/// Prepare the exact managed harness using the current worker binary. Remote
2057/// targets receive a separately staged copy; local bare targets run the binary
2058/// directly with a private launch config. The running worker is not stopped or
2059/// replaced, so any failure here leaves the quiet session attachable on its
2060/// previous build.
2061pub(super) fn prepare_managed_harness_for_upgrade(
2062    executor: &impl CommandExecutor,
2063    locator: &hel_targets::TargetLocator,
2064    session_id: &str,
2065    worker_binary: &Path,
2066    launch: &WorkerLaunchConfig,
2067) -> Result<()> {
2068    if launch.harness_runtime != HarnessRuntimePolicy::Managed {
2069        return Ok(());
2070    }
2071    let worker_root = hel_targets::worker_root(locator, session_id)?;
2072    let staging_root = format!("{worker_root}/harness-prepare");
2073    let staging_binary = format!("{staging_root}/hel");
2074    let staging_config = format!("{staging_root}/launch.json");
2075    let staging = tempfile::tempdir().context("create managed harness upgrade staging")?;
2076    let local_config = staging.path().join("launch.json");
2077    launch.write(&local_config)?;
2078
2079    // Local bare workers already share the controller's filesystem. Running
2080    // the current binary against a private launch config is enough to prepare
2081    // the cache, and leaves the live worker root completely untouched.
2082    if matches!(locator, hel_targets::TargetLocator::LocalBare { .. }) {
2083        execute_checked(
2084            executor,
2085            CommandSpec::new(
2086                worker_binary.to_string_lossy().into_owned(),
2087                [
2088                    "worker".to_owned(),
2089                    "prepare-harness".to_owned(),
2090                    "--config".to_owned(),
2091                    local_config.to_string_lossy().into_owned(),
2092                ],
2093            )
2094            .purpose("prepare exact managed harness"),
2095        )?;
2096        return Ok(());
2097    }
2098
2099    let ssh = match locator {
2100        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2101        | hel_targets::TargetLocator::SshBare { ssh, .. } => ssh,
2102        _ => bail!("managed harness policy requires a local bare, SSH-bare, or EC2 target"),
2103    };
2104    let result = (|| {
2105        execute_checked(
2106            executor,
2107            ssh_command_spec(ssh, ["rm", "-rf", "--", &staging_root])
2108                .purpose("clear managed harness preparation staging"),
2109        )?;
2110        execute_checked(
2111            executor,
2112            ssh_command_spec(ssh, ["mkdir", "-p", &staging_root])
2113                .purpose("create managed harness preparation staging"),
2114        )?;
2115        execute_checked(
2116            executor,
2117            scp_command_spec(ssh, worker_binary, &staging_binary, false)
2118                .purpose("stage current worker for managed harness preparation"),
2119        )?;
2120        execute_checked(
2121            executor,
2122            scp_command_spec(ssh, &local_config, &staging_config, false)
2123                .purpose("stage managed harness launch configuration"),
2124        )?;
2125        execute_checked(
2126            executor,
2127            ssh_command_spec(ssh, ["chmod", "700", &staging_binary])
2128                .purpose("make managed harness preparation worker executable"),
2129        )?;
2130        execute_checked(
2131            executor,
2132            ssh_command_spec(
2133                ssh,
2134                [
2135                    staging_binary.as_str(),
2136                    "worker",
2137                    "prepare-harness",
2138                    "--config",
2139                    staging_config.as_str(),
2140                ],
2141            )
2142            .purpose("prepare exact managed harness"),
2143        )?;
2144        Ok(())
2145    })();
2146    let cleanup = execute_checked(
2147        executor,
2148        ssh_command_spec(ssh, ["rm", "-rf", "--", &staging_root])
2149            .purpose("remove managed harness preparation staging"),
2150    );
2151    match (result, cleanup) {
2152        (Ok(()), Ok(_)) => Ok(()),
2153        (Ok(()), Err(error)) => Err(error).context("clean managed harness preparation staging"),
2154        (Err(error), Ok(_)) => Err(error),
2155        (Err(error), Err(cleanup)) => {
2156            tracing::warn!(%cleanup, path = %staging_root, "managed harness preparation staging cleanup failed");
2157            Err(error)
2158        }
2159    }
2160}
2161
2162fn prepare_installed_managed_harness(
2163    executor: &impl CommandExecutor,
2164    locator: &hel_targets::TargetLocator,
2165    worker_root: &str,
2166    launch: &WorkerLaunchConfig,
2167) -> Result<()> {
2168    if launch.harness_runtime != HarnessRuntimePolicy::Managed {
2169        return Ok(());
2170    }
2171    let worker_binary = format!("{worker_root}/hel");
2172    let launch_config = format!("{worker_root}/launch.json");
2173    let command = match locator {
2174        hel_targets::TargetLocator::LocalBare { .. } => CommandSpec::new(
2175            worker_binary.clone(),
2176            [
2177                "worker",
2178                "prepare-harness",
2179                "--config",
2180                launch_config.as_str(),
2181            ],
2182        ),
2183        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2184        | hel_targets::TargetLocator::SshBare { ssh, .. } => ssh_command_spec(
2185            ssh,
2186            [
2187                worker_binary.as_str(),
2188                "worker",
2189                "prepare-harness",
2190                "--config",
2191                launch_config.as_str(),
2192            ],
2193        ),
2194        _ => bail!("managed harness policy requires a local bare, SSH-bare, or EC2 target"),
2195    };
2196    execute_checked(
2197        executor,
2198        command.purpose("prepare exact managed harness before worker startup"),
2199    )?;
2200    Ok(())
2201}
2202
2203fn installed_worker_binary_replacement_plan(
2204    locator: &hel_targets::TargetLocator,
2205    session_id: &str,
2206    worker_binary: &Path,
2207) -> Result<CommandPlan> {
2208    let worker_root = hel_targets::worker_root(locator, session_id)?;
2209    let installed = format!("{worker_root}/hel");
2210    let staged = format!("{worker_root}/hel.next");
2211    let commands = match locator {
2212        hel_targets::TargetLocator::LocalBare { .. } => vec![
2213            CommandSpec::new(
2214                "cp",
2215                [worker_binary.to_string_lossy().into_owned(), staged.clone()],
2216            )
2217            .purpose("stage replacement Mjolnir worker"),
2218            CommandSpec::new("mv", ["-f", &staged, &installed])
2219                .purpose("replace installed Mjolnir worker"),
2220            CommandSpec::new("chmod", ["700", &installed])
2221                .purpose("make replaced Mjolnir worker executable"),
2222        ],
2223        hel_targets::TargetLocator::LocalPodman { container_id, .. }
2224        | hel_targets::TargetLocator::LocalDocker { container_id }
2225        | hel_targets::TargetLocator::AppleContainer { container_id } => {
2226            let engine = match locator {
2227                hel_targets::TargetLocator::LocalPodman { .. } => "podman",
2228                hel_targets::TargetLocator::LocalDocker { .. } => "docker",
2229                hel_targets::TargetLocator::AppleContainer { .. } => "container",
2230                _ => unreachable!("matched local container target"),
2231            };
2232            vec![
2233                CommandSpec::new(
2234                    engine,
2235                    [
2236                        "cp".into(),
2237                        worker_binary.to_string_lossy().into_owned(),
2238                        format!("{container_id}:{staged}"),
2239                    ],
2240                )
2241                .purpose("stage replacement Mjolnir worker"),
2242                CommandSpec::new(
2243                    engine,
2244                    container_upload_ownership_args(container_id, &worker_root, &[&staged]),
2245                )
2246                .purpose("assign replacement worker to the worker user"),
2247                CommandSpec::new(
2248                    engine,
2249                    [
2250                        "exec".into(),
2251                        container_id.clone(),
2252                        "mv".into(),
2253                        "-f".into(),
2254                        staged,
2255                        installed.clone(),
2256                    ],
2257                )
2258                .purpose("replace installed Mjolnir worker"),
2259                CommandSpec::new(
2260                    engine,
2261                    [
2262                        "exec".into(),
2263                        container_id.clone(),
2264                        "chmod".into(),
2265                        "700".into(),
2266                        installed,
2267                    ],
2268                )
2269                .purpose("make replaced Mjolnir worker executable"),
2270            ]
2271        }
2272        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2273        | hel_targets::TargetLocator::SshBare { ssh, .. } => vec![
2274            scp_command_spec(ssh, worker_binary, &staged, false)
2275                .purpose("stage replacement Mjolnir worker"),
2276            ssh_command_spec(ssh, ["mv", "-f", "--", &staged, &installed])
2277                .purpose("replace installed Mjolnir worker"),
2278            ssh_command_spec(ssh, ["chmod", "700", &installed])
2279                .purpose("make replaced Mjolnir worker executable"),
2280        ],
2281        hel_targets::TargetLocator::SshPodman {
2282            ssh, container_id, ..
2283        }
2284        | hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
2285            let engine = match locator {
2286                hel_targets::TargetLocator::SshPodman { .. } => "podman",
2287                hel_targets::TargetLocator::SshDocker { .. } => "docker",
2288                _ => unreachable!("matched remote container target"),
2289            };
2290            let upload = format!(".cache/mjolnir/uploads/{session_id}-hel.next");
2291            vec![
2292                ssh_command_spec(ssh, ["mkdir", "-p", ".cache/mjolnir/uploads"])
2293                    .purpose("create remote replacement worker staging"),
2294                scp_command_spec(ssh, worker_binary, &upload, false)
2295                    .purpose("stage replacement Mjolnir worker"),
2296                ssh_command_spec(
2297                    ssh,
2298                    [engine, "cp", &upload, &format!("{container_id}:{staged}")],
2299                )
2300                .purpose("stage replacement Mjolnir worker"),
2301                ssh_command_spec(
2302                    ssh,
2303                    std::iter::once(engine.to_owned()).chain(container_upload_ownership_args(
2304                        container_id,
2305                        &worker_root,
2306                        &[&staged],
2307                    )),
2308                )
2309                .purpose("assign replacement worker to the worker user"),
2310                ssh_command_spec(
2311                    ssh,
2312                    [
2313                        engine,
2314                        "exec",
2315                        container_id,
2316                        "mv",
2317                        "-f",
2318                        "--",
2319                        &staged,
2320                        &installed,
2321                    ],
2322                )
2323                .purpose("replace installed Mjolnir worker"),
2324                ssh_command_spec(
2325                    ssh,
2326                    [engine, "exec", container_id, "chmod", "700", &installed],
2327                )
2328                .purpose("make replaced Mjolnir worker executable"),
2329                ssh_command_spec(ssh, ["rm", "-f", "--", &upload])
2330                    .purpose("remove remote replacement worker staging"),
2331            ]
2332        }
2333    };
2334    Ok(CommandPlan {
2335        description: format!("replace stale Mjolnir worker for session {session_id}"),
2336        commands,
2337    })
2338}
2339
2340fn installed_file_digest_command(
2341    locator: &hel_targets::TargetLocator,
2342    path: &str,
2343    purpose: &str,
2344) -> CommandSpec {
2345    match locator {
2346        hel_targets::TargetLocator::LocalBare { .. } => CommandSpec::new("sha256sum", [path]),
2347        hel_targets::TargetLocator::LocalPodman { container_id, .. } => {
2348            CommandSpec::new("podman", ["exec", container_id, "sha256sum", path])
2349        }
2350        hel_targets::TargetLocator::LocalDocker { container_id } => {
2351            CommandSpec::new("docker", ["exec", container_id, "sha256sum", path])
2352        }
2353        hel_targets::TargetLocator::AppleContainer { container_id } => {
2354            CommandSpec::new("container", ["exec", container_id, "sha256sum", path])
2355        }
2356        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2357        | hel_targets::TargetLocator::SshBare { ssh, .. } => {
2358            ssh_command_spec(ssh, ["sha256sum", path])
2359        }
2360        hel_targets::TargetLocator::SshPodman {
2361            ssh, container_id, ..
2362        } => ssh_command_spec(ssh, ["podman", "exec", container_id, "sha256sum", path]),
2363        hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
2364            ssh_command_spec(ssh, ["docker", "exec", container_id, "sha256sum", path])
2365        }
2366    }
2367    .purpose(purpose)
2368}
2369
2370fn worker_launch_refresh_plan(
2371    locator: &hel_targets::TargetLocator,
2372    session_id: &str,
2373    launch: &WorkerLaunchConfig,
2374) -> Result<WorkerLaunchRefreshPlan> {
2375    let worker_root = hel_targets::worker_root(locator, session_id)?;
2376    let installed = format!("{worker_root}/launch.json");
2377    let staged = format!("{installed}.next");
2378    let staged_arg = hel_targets::join_remote_command(std::slice::from_ref(&staged));
2379    let installed_arg = hel_targets::join_remote_command(std::slice::from_ref(&installed));
2380    let script = format!("umask 077; cat > {staged_arg} && mv -f -- {staged_arg} {installed_arg}");
2381    let body = serde_json::to_vec_pretty(launch).context("serialize worker launch config")?;
2382    let expected_sha256 = format!("{:x}", Sha256::digest(&body));
2383    let replace = match locator {
2384        hel_targets::TargetLocator::LocalBare { .. } => CommandSpec::new("sh", ["-c", &script]),
2385        hel_targets::TargetLocator::LocalPodman { container_id, .. } => {
2386            CommandSpec::new("podman", ["exec", "-i", container_id, "sh", "-c", &script])
2387        }
2388        hel_targets::TargetLocator::LocalDocker { container_id } => {
2389            CommandSpec::new("docker", ["exec", "-i", container_id, "sh", "-c", &script])
2390        }
2391        hel_targets::TargetLocator::AppleContainer { container_id } => CommandSpec::new(
2392            "container",
2393            ["exec", "-i", container_id, "sh", "-c", &script],
2394        ),
2395        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2396        | hel_targets::TargetLocator::SshBare { ssh, .. } => {
2397            ssh_command_spec(ssh, ["sh", "-c", &script])
2398        }
2399        hel_targets::TargetLocator::SshPodman {
2400            ssh, container_id, ..
2401        } => ssh_command_spec(
2402            ssh,
2403            ["podman", "exec", "-i", container_id, "sh", "-c", &script],
2404        ),
2405        hel_targets::TargetLocator::SshDocker { ssh, container_id } => ssh_command_spec(
2406            ssh,
2407            ["docker", "exec", "-i", container_id, "sh", "-c", &script],
2408        ),
2409    }
2410    .purpose("replace stale Mjolnir worker launch config")
2411    .with_sensitive_stdin(body);
2412    Ok(WorkerLaunchRefreshPlan {
2413        expected_sha256,
2414        installed_digest: installed_file_digest_command(
2415            locator,
2416            &installed,
2417            "identify installed Mjolnir worker launch config",
2418        ),
2419        replace: CommandPlan {
2420            description: format!("replace stale Mjolnir launch config for session {session_id}"),
2421            commands: vec![replace],
2422        },
2423    })
2424}
2425
2426/// Prepare a local refresh without hashing the controller binary. Digesting
2427/// happens only after recovery has proved that the worker needs a restart.
2428fn worker_binary_refresh_plan(
2429    locator: &hel_targets::TargetLocator,
2430    session_id: &str,
2431) -> Result<Option<WorkerBinaryRefresh>> {
2432    let worker_root = hel_targets::worker_root(locator, session_id)?;
2433    let installed = format!("{worker_root}/hel");
2434    // Remote targets defer source selection to the recovery task: choosing the
2435    // binary needs the target's architecture, and probing it (plus hashing the
2436    // remote binary) is blocking ssh work that must not run on this UI/event
2437    // path. Building the refresh here stays cheap.
2438    if matches!(
2439        locator,
2440        hel_targets::TargetLocator::AwsEc2 { .. }
2441            | hel_targets::TargetLocator::SshBare { .. }
2442            | hel_targets::TargetLocator::SshPodman { .. }
2443            | hel_targets::TargetLocator::SshDocker { .. }
2444    ) {
2445        return Ok(Some(WorkerBinaryRefresh::Remote(
2446            RemoteWorkerBinaryRefresh {
2447                locator: locator.clone(),
2448                session_id: session_id.to_owned(),
2449                installed_digest: installed_file_digest_command(
2450                    locator,
2451                    &installed,
2452                    "identify installed Mjolnir worker binary",
2453                ),
2454            },
2455        )));
2456    }
2457    // Local: resolve the source now. Resolving a deleted running executable
2458    // materializes /proc/self/exe and can copy hundreds of megabytes; target
2459    // lists are assembled on UI/event loops, so leave refresh disabled until
2460    // the next controller start rather than doing that work here.
2461    if PINNED_WORKER_BINARY_SOURCES.get().is_none()
2462        && !std::env::current_exe().is_ok_and(|path| path.is_file())
2463    {
2464        return Ok(None);
2465    }
2466    let requirement = if matches!(locator, hel_targets::TargetLocator::LocalBare { .. }) {
2467        WorkerBinaryRequirement::LocalHost
2468    } else {
2469        WorkerBinaryRequirement::PortableLinux
2470    };
2471    let source = match worker_binary_for_arch(std::env::consts::ARCH, requirement) {
2472        Ok(WorkerBinaryAvailability::Local { path, .. }) => path,
2473        Ok(WorkerBinaryAvailability::Remote { .. }) | Err(_) => return Ok(None),
2474    };
2475    Ok(Some(WorkerBinaryRefresh::Prepared(
2476        WorkerBinaryRefreshPlan {
2477            replace: installed_worker_binary_replacement_plan(locator, session_id, &source)?,
2478            source,
2479            installed_digest: installed_file_digest_command(
2480                locator,
2481                &installed,
2482                "identify installed Mjolnir worker binary",
2483            ),
2484        },
2485    )))
2486}
2487
2488/// Refresh a remote worker binary during recovery: pick the worker binary for
2489/// the target's own architecture, and copy it over the installed one only when
2490/// their digests differ. This runs inside the recovery task, where blocking
2491/// ssh work is allowed; it must never be called from a UI/event loop.
2492///
2493/// The digest gate is what stops a redeploy loop: once the right binary is
2494/// installed, its digest matches the source and nothing is copied again, even
2495/// though recovery may still restart the worker.
2496pub(crate) fn refresh_remote_worker_binary_if_stale(
2497    executor: &impl CommandExecutor,
2498    refresh: &RemoteWorkerBinaryRefresh,
2499) -> Result<()> {
2500    let source = worker_binary_for(&refresh.locator, executor)
2501        .context("resolve the worker binary for the recovering target")?;
2502    replace_remote_worker_binary_if_stale(
2503        executor,
2504        &refresh.locator,
2505        &refresh.session_id,
2506        &refresh.installed_digest,
2507        &source,
2508    )
2509    .map(|_| ())
2510}
2511
2512/// Copy `source` over the installed remote worker only when the installed
2513/// digest differs from `source`'s. Returns whether a copy ran. Split from the
2514/// resolver above so the digest gate is testable without resolving a real
2515/// worker binary for a target architecture.
2516fn replace_remote_worker_binary_if_stale(
2517    executor: &impl CommandExecutor,
2518    locator: &hel_targets::TargetLocator,
2519    session_id: &str,
2520    installed_digest: &CommandSpec,
2521    source: &Path,
2522) -> Result<bool> {
2523    let expected = hel::hel_worker_launch::worker_executable_digest(source)?;
2524    let installed = executor
2525        .execute(installed_digest)
2526        .context("read the installed remote worker digest")?;
2527    let matches = installed.status == 0
2528        && String::from_utf8_lossy(&installed.stdout)
2529            .split_whitespace()
2530            .next()
2531            .is_some_and(|digest| digest.eq_ignore_ascii_case(&expected));
2532    if matches {
2533        return Ok(false);
2534    }
2535    installed_worker_binary_replacement_plan(locator, session_id, source)?
2536        .execute(executor)
2537        .context("replace stale remote relay worker binary")?;
2538    Ok(true)
2539}
2540
2541/// Stop the detached worker daemon at `worker_root` without deleting its files.
2542///
2543/// The script signals the worker's process group so a wedged ACP child dies
2544/// with it. Checkpoint then restarts the daemon against the same relay root.
2545pub(super) fn stop_worker(
2546    executor: &impl CommandExecutor,
2547    locator: &hel_targets::TargetLocator,
2548    worker_root: &str,
2549) -> Result<()> {
2550    execute_checked(executor, stop_worker_command(locator, worker_root))?;
2551    Ok(())
2552}
2553
2554/// Restore a stopped Podman target before signaling its worker. Checkpoint
2555/// recovery uses this instead of assuming every persisted target is running.
2556pub(super) fn stop_worker_after_target_recovery(
2557    executor: &impl CommandExecutor,
2558    locator: &hel_targets::TargetLocator,
2559    session_id: &str,
2560    worker_root: &str,
2561) -> Result<()> {
2562    let target = hel_targets::target_recovery_plan(locator, session_id)?;
2563    hel_targets::ensure_recovery_target_running(executor, target.as_ref())
2564        .context("restore Mjolnir worker target")?;
2565    stop_worker(executor, locator, worker_root)
2566}
2567
2568fn stop_worker_command(locator: &hel_targets::TargetLocator, worker_root: &str) -> CommandSpec {
2569    let script = hel_targets::stop_worker_daemon_script(worker_root);
2570    match locator {
2571        hel_targets::TargetLocator::LocalBare { .. } => CommandSpec::new("sh", ["-c", &script]),
2572        hel_targets::TargetLocator::LocalPodman { container_id, .. } => {
2573            CommandSpec::new("podman", ["exec", container_id, "sh", "-c", &script])
2574        }
2575        hel_targets::TargetLocator::LocalDocker { container_id } => {
2576            CommandSpec::new("docker", ["exec", container_id, "sh", "-c", &script])
2577        }
2578        hel_targets::TargetLocator::AppleContainer { container_id } => {
2579            CommandSpec::new("container", ["exec", container_id, "sh", "-c", &script])
2580        }
2581        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2582        | hel_targets::TargetLocator::SshBare { ssh, .. } => {
2583            ssh_command_spec(ssh, ["sh", "-c", &script])
2584        }
2585        hel_targets::TargetLocator::SshPodman {
2586            ssh, container_id, ..
2587        } => ssh_command_spec(ssh, ["podman", "exec", container_id, "sh", "-c", &script]),
2588        hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
2589            ssh_command_spec(ssh, ["docker", "exec", container_id, "sh", "-c", &script])
2590        }
2591    }
2592    .purpose("stop Mjolnir worker daemon")
2593}
2594
2595fn worker_liveness_command(locator: &hel_targets::TargetLocator, worker_root: &str) -> CommandSpec {
2596    let script = hel_targets::worker_daemon_liveness_script(worker_root);
2597    match locator {
2598        hel_targets::TargetLocator::LocalBare { .. } => CommandSpec::new("sh", ["-c", &script]),
2599        hel_targets::TargetLocator::LocalPodman { container_id, .. } => {
2600            CommandSpec::new("podman", ["exec", container_id, "sh", "-c", &script])
2601        }
2602        hel_targets::TargetLocator::LocalDocker { container_id } => {
2603            CommandSpec::new("docker", ["exec", container_id, "sh", "-c", &script])
2604        }
2605        hel_targets::TargetLocator::AppleContainer { container_id } => {
2606            CommandSpec::new("container", ["exec", container_id, "sh", "-c", &script])
2607        }
2608        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2609        | hel_targets::TargetLocator::SshBare { ssh, .. } => {
2610            ssh_command_spec(ssh, ["sh", "-c", &script])
2611        }
2612        hel_targets::TargetLocator::SshPodman {
2613            ssh, container_id, ..
2614        } => ssh_command_spec(ssh, ["podman", "exec", container_id, "sh", "-c", &script]),
2615        hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
2616            ssh_command_spec(ssh, ["docker", "exec", container_id, "sh", "-c", &script])
2617        }
2618    }
2619    .purpose("probe Mjolnir worker daemon liveness")
2620}
2621
2622pub(super) fn start_worker(
2623    executor: &impl CommandExecutor,
2624    locator: &hel_targets::TargetLocator,
2625    worker_root: &str,
2626) -> Result<()> {
2627    execute_checked(executor, start_worker_command(locator, worker_root))?;
2628    Ok(())
2629}
2630
2631fn start_worker_command(locator: &hel_targets::TargetLocator, worker_root: &str) -> CommandSpec {
2632    let binary = format!("{worker_root}/hel");
2633    let config = format!("{worker_root}/launch.json");
2634    // These files describe the worker's previous life. Clear them as part of
2635    // the launch, before the new daemon can be probed: a stale exit record
2636    // aborts startup, while a stale socket makes a recovering daemon look
2637    // ready and invites the reconnect actor to kill it as unresponsive.
2638    let clear_stale_runtime = format!(
2639        "rm -f {} {}; ",
2640        hel_targets::join_remote_command(&[format!("{worker_root}/worker-exit.json")]),
2641        hel_targets::join_remote_command(&[format!("{worker_root}/control.sock")]),
2642    );
2643    let detached_script = format!(
2644        "{clear_stale_runtime}nohup {} >{} 2>&1 </dev/null &",
2645        hel_targets::join_remote_command(&[
2646            binary.clone(),
2647            "worker".into(),
2648            "run".into(),
2649            "--root".into(),
2650            worker_root.into(),
2651            "--config".into(),
2652            config.clone(),
2653        ]),
2654        hel_targets::join_remote_command(&[format!("{worker_root}/worker.log")]),
2655    );
2656    // Redirect daemon output to worker.log in every launch mode; an
2657    // unexplained dead worker is undebuggable without it.
2658    let exec_script = format!(
2659        "{clear_stale_runtime}exec {} >{} 2>&1",
2660        hel_targets::join_remote_command(&[
2661            binary.clone(),
2662            "worker".into(),
2663            "run".into(),
2664            "--root".into(),
2665            worker_root.into(),
2666            "--config".into(),
2667            config.clone(),
2668        ]),
2669        hel_targets::join_remote_command(&[format!("{worker_root}/worker.log")]),
2670    );
2671    match locator {
2672        hel_targets::TargetLocator::LocalBare { .. } => {
2673            CommandSpec::new("sh", ["-c", &detached_script])
2674        }
2675        hel_targets::TargetLocator::LocalPodman { container_id, .. } => CommandSpec::new(
2676            "podman",
2677            ["exec", "--detach", container_id, "sh", "-c", &exec_script],
2678        ),
2679        hel_targets::TargetLocator::LocalDocker { container_id } => CommandSpec::new(
2680            "docker",
2681            ["exec", "--detach", container_id, "sh", "-c", &exec_script],
2682        ),
2683        hel_targets::TargetLocator::AppleContainer { container_id } => CommandSpec::new(
2684            "container",
2685            ["exec", "--detach", container_id, "sh", "-c", &exec_script],
2686        ),
2687        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2688        | hel_targets::TargetLocator::SshBare { ssh, .. } => {
2689            ssh_command_spec(ssh, ["sh", "-c", &detached_script])
2690        }
2691        hel_targets::TargetLocator::SshPodman {
2692            ssh, container_id, ..
2693        } => ssh_command_spec(
2694            ssh,
2695            [
2696                "podman",
2697                "exec",
2698                "--detach",
2699                container_id,
2700                "sh",
2701                "-c",
2702                &exec_script,
2703            ],
2704        ),
2705        hel_targets::TargetLocator::SshDocker { ssh, container_id } => ssh_command_spec(
2706            ssh,
2707            [
2708                "docker",
2709                "exec",
2710                "--detach",
2711                container_id,
2712                "sh",
2713                "-c",
2714                &exec_script,
2715            ],
2716        ),
2717    }
2718    .purpose("start detached Mjolnir worker")
2719    // Everything before this moves data into the target and reports as Sync.
2720    // Start begins here, with the daemon launch.
2721    .stage(ProvisionStage::Starting)
2722}
2723
2724/// Enrich an opaque handshake failure by running the installed worker binary
2725/// directly in the target. This surfaces loader errors (for example a
2726/// glibc-linked worker inside an older-glibc container) that a detached start
2727/// swallows.
2728pub(super) fn worker_probe_diagnosis(
2729    executor: &impl CommandExecutor,
2730    locator: &hel_targets::TargetLocator,
2731    worker_root: &str,
2732    error: anyhow::Error,
2733) -> anyhow::Error {
2734    let error = match worker_binary_probe_failure(executor, locator, worker_root) {
2735        Some(failure) => error.context(failure),
2736        None => error,
2737    };
2738    match worker_last_words(executor, locator, worker_root) {
2739        Some(last_words) => error.context(last_words),
2740        None => error,
2741    }
2742}
2743
2744fn worker_binary_probe_failure(
2745    executor: &impl CommandExecutor,
2746    locator: &hel_targets::TargetLocator,
2747    worker_root: &str,
2748) -> Option<String> {
2749    let binary = format!("{worker_root}/hel");
2750    let command = match locator {
2751        hel_targets::TargetLocator::LocalBare { .. } => {
2752            CommandSpec::new(binary.clone(), ["--version"])
2753        }
2754        hel_targets::TargetLocator::LocalPodman { container_id, .. } => {
2755            CommandSpec::new("podman", ["exec", container_id, &binary, "--version"])
2756        }
2757        hel_targets::TargetLocator::LocalDocker { container_id } => {
2758            CommandSpec::new("docker", ["exec", container_id, &binary, "--version"])
2759        }
2760        hel_targets::TargetLocator::AppleContainer { container_id } => {
2761            CommandSpec::new("container", ["exec", container_id, &binary, "--version"])
2762        }
2763        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2764        | hel_targets::TargetLocator::SshBare { ssh, .. } => {
2765            ssh_command_spec(ssh, [binary.as_str(), "--version"])
2766        }
2767        hel_targets::TargetLocator::SshPodman {
2768            ssh, container_id, ..
2769        } => ssh_command_spec(
2770            ssh,
2771            ["podman", "exec", container_id, binary.as_str(), "--version"],
2772        ),
2773        hel_targets::TargetLocator::SshDocker { ssh, container_id } => ssh_command_spec(
2774            ssh,
2775            ["docker", "exec", container_id, binary.as_str(), "--version"],
2776        ),
2777    }
2778    .purpose("probe installed worker binary");
2779    match executor.execute(&command) {
2780        Ok(output) if output.status == 0 => None,
2781        Ok(output) => {
2782            let stderr = String::from_utf8_lossy(&output.stderr);
2783            let stdout = String::from_utf8_lossy(&output.stdout);
2784            let detail = if !stderr.trim().is_empty() {
2785                stderr.trim()
2786            } else if !stdout.trim().is_empty() {
2787                stdout.trim()
2788            } else {
2789                "the process exited unsuccessfully without output"
2790            };
2791            Some(format!(
2792                "worker binary {binary} fails to run in the target: {detail}; \
2793                 if this is a loader/glibc error, provide a musl worker \
2794                 (cargo build --release --target <arch>-unknown-linux-musl \
2795                  -p brokk-mj-worker --bin mj-worker, \
2796                 or set MJ_WORKER_BINARY/MJ_WORKER_DIR)"
2797            ))
2798        }
2799        Err(probe_error) => Some(format!("worker probe failed: {probe_error:#}")),
2800    }
2801}
2802
2803/// Fetch the dead worker's structured exit record and log tail from the
2804/// target, so unreachable-worker errors carry the root cause.
2805pub(super) fn worker_last_words(
2806    executor: &impl CommandExecutor,
2807    locator: &hel_targets::TargetLocator,
2808    worker_root: &str,
2809) -> Option<String> {
2810    let script = format!(
2811        "if [ -f {root}/worker-exit.json ]; then echo '{marker}'; cat {root}/worker-exit.json; fi; if [ -f {root}/worker.log ]; then echo '--- worker.log (tail) ---'; tail -n 20 {root}/worker.log; fi",
2812        root = worker_root,
2813        marker = WORKER_EXIT_RECORD_MARKER
2814    );
2815    let command = match locator {
2816        hel_targets::TargetLocator::LocalBare { .. } => CommandSpec::new("sh", ["-c", &script]),
2817        hel_targets::TargetLocator::LocalPodman { container_id, .. } => {
2818            CommandSpec::new("podman", ["exec", container_id, "sh", "-c", &script])
2819        }
2820        hel_targets::TargetLocator::LocalDocker { container_id } => {
2821            CommandSpec::new("docker", ["exec", container_id, "sh", "-c", &script])
2822        }
2823        hel_targets::TargetLocator::AppleContainer { container_id } => {
2824            CommandSpec::new("container", ["exec", container_id, "sh", "-c", &script])
2825        }
2826        hel_targets::TargetLocator::AwsEc2 { ssh, .. }
2827        | hel_targets::TargetLocator::SshBare { ssh, .. } => {
2828            ssh_command_spec(ssh, ["sh", "-c", &script])
2829        }
2830        hel_targets::TargetLocator::SshPodman {
2831            ssh, container_id, ..
2832        } => ssh_command_spec(ssh, ["podman", "exec", container_id, "sh", "-c", &script]),
2833        hel_targets::TargetLocator::SshDocker { ssh, container_id } => {
2834            ssh_command_spec(ssh, ["docker", "exec", container_id, "sh", "-c", &script])
2835        }
2836    }
2837    .purpose("collect worker last words");
2838    let output = match executor.execute(&command) {
2839        Ok(output) => output,
2840        Err(error) => {
2841            tracing::debug!(
2842                worker_root,
2843                %error,
2844                "could not collect worker diagnostics"
2845            );
2846            return None;
2847        }
2848    };
2849    if output.status != 0 {
2850        tracing::debug!(
2851            worker_root,
2852            status = output.status,
2853            "worker diagnostic probe returned a failure"
2854        );
2855        return None;
2856    }
2857    let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
2858    (!text.is_empty()).then(|| format!("worker diagnostics:\n{text}"))
2859}
2860
2861#[cfg(test)]
2862mod tests {
2863    use super::*;
2864
2865    use anyhow::Result;
2866
2867    use hel::hel_config::ExecutionPolicy;
2868    use hel::hel_targets::{self, CommandExecutor, CommandOutput, CommandSpec, SshTarget};
2869
2870    use sha2::{Digest, Sha256};
2871    use std::cell::RefCell;
2872    use std::collections::BTreeMap;
2873
2874    use std::path::{Path, PathBuf};
2875
2876    #[cfg(unix)]
2877    #[test]
2878    fn node_preflight_checks_missing_old_and_supported_tools_on_profile_path() {
2879        use std::os::unix::fs::PermissionsExt;
2880        let directory = tempfile::tempdir().unwrap();
2881        let profile = HarnessProfile {
2882            enabled: true,
2883            kind: HarnessKind::Codex,
2884            home: directory.path().into(),
2885            environment: std::collections::BTreeMap::from([(
2886                "PATH".into(),
2887                directory.path().to_string_lossy().into_owned(),
2888            )]),
2889            context_window_bytes: None,
2890        };
2891        let check = || {
2892            preflight_harness(
2893                &hel::hel_config::TargetTemplate::LocalBare,
2894                &profile,
2895                &ProcessExecutor,
2896            )
2897        };
2898        let write_tool = |name: &str, body: &str| {
2899            let path = directory.path().join(name);
2900            std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap();
2901            std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
2902        };
2903        assert!(format!("{:#}", check().unwrap_err()).contains("Node.js is missing"));
2904        write_tool("node", "exit 1");
2905        assert!(format!("{:#}", check().unwrap_err()).contains("Node.js 22 or newer is required"));
2906        write_tool("node", "exit 0");
2907        assert!(format!("{:#}", check().unwrap_err()).contains("npm is missing or unusable"));
2908        write_tool("npm", "exit 0");
2909        check().unwrap();
2910    }
2911
2912    #[test]
2913    fn a_stored_setup_token_reaches_only_claude_workers_that_do_not_set_their_own() {
2914        use hel::hel_config::HarnessKind;
2915        use hel::hel_credentials::{CLAUDE_OAUTH_TOKEN_ENV, write_claude_oauth_token};
2916
2917        let directory = tempfile::tempdir().unwrap();
2918        let token_path = directory.path().join("profiles/claude/claude-oauth-token");
2919        let missing = directory.path().join("profiles/absent/claude-oauth-token");
2920        write_claude_oauth_token(&token_path, b"sk-ant-oat01-stored").unwrap();
2921
2922        let mut claude = BTreeMap::new();
2923        apply_claude_setup_token(&mut claude, HarnessKind::Claude, &token_path);
2924        assert_eq!(
2925            claude.get(CLAUDE_OAUTH_TOKEN_ENV).map(String::as_str),
2926            Some("sk-ant-oat01-stored")
2927        );
2928
2929        // Every other harness ignores the variable, so it must not appear.
2930        for kind in HarnessKind::ALL
2931            .into_iter()
2932            .filter(|kind| *kind != HarnessKind::Claude)
2933        {
2934            let mut environment = BTreeMap::new();
2935            apply_claude_setup_token(&mut environment, kind, &token_path);
2936            assert!(environment.is_empty(), "{kind:?} must not read the token");
2937        }
2938
2939        // A profile that sets the variable itself stays authoritative.
2940        let mut overridden = BTreeMap::from([(
2941            CLAUDE_OAUTH_TOKEN_ENV.to_owned(),
2942            "profile-token".to_owned(),
2943        )]);
2944        apply_claude_setup_token(&mut overridden, HarnessKind::Claude, &token_path);
2945        assert_eq!(
2946            overridden.get(CLAUDE_OAUTH_TOKEN_ENV).map(String::as_str),
2947            Some("profile-token")
2948        );
2949
2950        // A profile with no stored token launches exactly as before.
2951        let mut without = BTreeMap::new();
2952        apply_claude_setup_token(&mut without, HarnessKind::Claude, &missing);
2953        assert!(without.is_empty());
2954    }
2955
2956    #[test]
2957    fn packaged_worker_names_match_release_archives() {
2958        let directory = Path::new("/opt/hel/bin");
2959        assert_eq!(
2960            packaged_worker_binary_path(directory, "x86_64-unknown-linux-musl"),
2961            directory.join("mj-worker-x86_64-unknown-linux-musl")
2962        );
2963        assert_eq!(
2964            packaged_worker_binary_path(directory, "aarch64-unknown-linux-musl"),
2965            directory.join("mj-worker-aarch64-unknown-linux-musl")
2966        );
2967    }
2968
2969    #[test]
2970    fn pinned_snapshot_keeps_native_and_portable_sources_stable() {
2971        let directory = tempfile::tempdir().unwrap();
2972        let native = directory.path().join("native-worker");
2973        let x86 = directory.path().join("x86-worker");
2974        let arm = directory.path().join("arm-worker");
2975        std::fs::write(&native, b"native bytes").unwrap();
2976        std::fs::write(&x86, b"x86 bytes").unwrap();
2977        std::fs::write(&arm, b"arm bytes").unwrap();
2978        let cache = directory.path().join("cache");
2979        let snapshot = WorkerBinarySourceSnapshot::capture(&cache, |arch, requirement| {
2980            let path = match requirement {
2981                WorkerBinaryRequirement::LocalHost => &native,
2982                WorkerBinaryRequirement::PortableLinux if arch == "x86_64" => &x86,
2983                WorkerBinaryRequirement::PortableLinux => &arm,
2984            };
2985            Ok(WorkerBinaryAvailability::Local {
2986                path: path.clone(),
2987                source: format!("{arch}-{requirement:?}"),
2988            })
2989        });
2990
2991        let native = snapshot
2992            .resolve(std::env::consts::ARCH, WorkerBinaryRequirement::LocalHost)
2993            .unwrap();
2994        let x86 = snapshot
2995            .resolve("x86_64", WorkerBinaryRequirement::PortableLinux)
2996            .unwrap();
2997        let arm = snapshot
2998            .resolve("aarch64", WorkerBinaryRequirement::PortableLinux)
2999            .unwrap();
3000        let WorkerBinaryAvailability::Local { path: native, .. } = native else {
3001            panic!("native source should be local");
3002        };
3003        let WorkerBinaryAvailability::Local { path: x86, .. } = x86 else {
3004            panic!("x86 source should be local");
3005        };
3006        let WorkerBinaryAvailability::Local { path: arm, .. } = arm else {
3007            panic!("arm source should be local");
3008        };
3009        assert_eq!(std::fs::read(native).unwrap(), b"native bytes");
3010        assert_eq!(std::fs::read(x86).unwrap(), b"x86 bytes");
3011        assert_eq!(std::fs::read(arm).unwrap(), b"arm bytes");
3012    }
3013
3014    #[test]
3015    fn pinned_snapshot_survives_source_replacement_and_missing_candidate_install() {
3016        let directory = tempfile::tempdir().unwrap();
3017        let source = directory.path().join("worker");
3018        std::fs::write(&source, b"before").unwrap();
3019        let cache = directory.path().join("cache");
3020        let resolve_source = |_: &str, _: WorkerBinaryRequirement| {
3021            Ok(WorkerBinaryAvailability::Local {
3022                path: source.clone(),
3023                source: "test source".into(),
3024            })
3025        };
3026        let pinned = WorkerBinarySourceSnapshot::capture(&cache, resolve_source);
3027
3028        std::fs::write(&source, b"in-place mutation").unwrap();
3029        let WorkerBinaryAvailability::Local { path, .. } = pinned
3030            .resolve("x86_64", WorkerBinaryRequirement::PortableLinux)
3031            .unwrap()
3032        else {
3033            panic!("source should be local");
3034        };
3035        assert_eq!(std::fs::read(path).unwrap(), b"before");
3036
3037        let replacement = directory.path().join("replacement");
3038        std::fs::write(&replacement, b"after").unwrap();
3039        std::fs::rename(replacement, &source).unwrap();
3040        let WorkerBinaryAvailability::Local { path, .. } = pinned
3041            .resolve("x86_64", WorkerBinaryRequirement::PortableLinux)
3042            .unwrap()
3043        else {
3044            panic!("source should be local");
3045        };
3046        assert_eq!(std::fs::read(path).unwrap(), b"before");
3047        let fresh_replaced = WorkerBinarySourceSnapshot::capture(&cache, resolve_source);
3048        let WorkerBinaryAvailability::Local { path, .. } = fresh_replaced
3049            .resolve("x86_64", WorkerBinaryRequirement::PortableLinux)
3050            .unwrap()
3051        else {
3052            panic!("source should be local");
3053        };
3054        assert_eq!(std::fs::read(path).unwrap(), b"after");
3055
3056        let missing = directory.path().join("missing-worker");
3057        let missing_snapshot = WorkerBinarySourceSnapshot::capture(&cache, {
3058            let missing = missing.clone();
3059            move |_: &str, _: WorkerBinaryRequirement| {
3060                if missing.is_file() {
3061                    Ok(WorkerBinaryAvailability::Local {
3062                        path: missing.clone(),
3063                        source: "new source".into(),
3064                    })
3065                } else {
3066                    Err(anyhow::anyhow!("candidate is unavailable"))
3067                }
3068            }
3069        });
3070        std::fs::write(&missing, b"now installed").unwrap();
3071        assert!(
3072            missing_snapshot
3073                .resolve("x86_64", WorkerBinaryRequirement::PortableLinux)
3074                .is_err()
3075        );
3076        let fresh_snapshot = WorkerBinarySourceSnapshot::capture(&cache, {
3077            let missing = missing.clone();
3078            move |_: &str, _: WorkerBinaryRequirement| {
3079                Ok(WorkerBinaryAvailability::Local {
3080                    path: missing.clone(),
3081                    source: "new source".into(),
3082                })
3083            }
3084        });
3085        assert!(
3086            fresh_snapshot
3087                .resolve("x86_64", WorkerBinaryRequirement::PortableLinux)
3088                .is_ok()
3089        );
3090
3091        let remote_url = std::cell::RefCell::new("https://old.example/{target}".to_owned());
3092        let remote_snapshot = WorkerBinarySourceSnapshot::capture(
3093            &directory.path().join("remote-cache"),
3094            |arch, _| {
3095                Ok(WorkerBinaryAvailability::Remote {
3096                    url: remote_url.borrow().replace("{target}", arch),
3097                    sha256: "a".repeat(64),
3098                    triple: format!("{arch}-unknown-linux-musl"),
3099                })
3100            },
3101        );
3102        *remote_url.borrow_mut() = "https://new.example/{target}".into();
3103        let WorkerBinaryAvailability::Remote { url, .. } = remote_snapshot
3104            .resolve("x86_64", WorkerBinaryRequirement::PortableLinux)
3105            .unwrap()
3106        else {
3107            panic!("source should be remote");
3108        };
3109        assert_eq!(url, "https://old.example/x86_64");
3110
3111        let blocked_cache = directory.path().join("blocked-cache");
3112        std::fs::write(&blocked_cache, b"not a directory").unwrap();
3113        let failed_snapshot = WorkerBinarySourceSnapshot::capture(&blocked_cache, resolve_source);
3114        assert!(
3115            failed_snapshot
3116                .resolve("x86_64", WorkerBinaryRequirement::PortableLinux)
3117                .is_err()
3118        );
3119    }
3120
3121    #[test]
3122    fn dev_checkout_prefers_the_dedicated_musl_worker() {
3123        let controller = PathBuf::from("target/debug/mj");
3124        let musl = PathBuf::from("target/worker/x86_64-unknown-linux-musl/debug/mj-worker");
3125        let shared_target_worker =
3126            PathBuf::from("target/x86_64-unknown-linux-musl/debug/mj-worker");
3127        let legacy = PathBuf::from("target/x86_64-unknown-linux-musl/debug/mj");
3128        let present = [
3129            controller.clone(),
3130            musl.clone(),
3131            shared_target_worker,
3132            legacy,
3133        ];
3134        let selected = select_sibling_worker(&controller, "x86_64-unknown-linux-musl", |path| {
3135            present.iter().any(|p| p == path)
3136        });
3137        assert_eq!(
3138            selected,
3139            Some((musl, "isolated development musl worker")),
3140            "the dedicated worker must win over legacy artifacts"
3141        );
3142    }
3143
3144    #[test]
3145    fn local_bare_may_use_a_native_worker_beside_the_controller() {
3146        let controller = PathBuf::from("target/debug/mj");
3147        let worker = PathBuf::from("target/debug/mj-worker");
3148        let selected = worker_binary_prerequisite_for_current(
3149            std::env::consts::ARCH,
3150            WorkerBinaryRequirement::LocalHost,
3151            &controller,
3152            &|path| path == controller || path == worker,
3153        )
3154        .unwrap();
3155        assert_eq!(
3156            selected,
3157            WorkerBinaryAvailability::Local {
3158                path: worker,
3159                source: "native worker beside mj".into(),
3160            }
3161        );
3162    }
3163
3164    #[test]
3165    fn local_bare_prefers_the_isolated_native_development_worker() {
3166        let controller = PathBuf::from("target/debug/mj");
3167        let worker = PathBuf::from("target/worker/debug/mj-worker");
3168        let packaged = PathBuf::from("target/debug/mj-worker");
3169        let selected = worker_binary_prerequisite_for_current(
3170            std::env::consts::ARCH,
3171            WorkerBinaryRequirement::LocalHost,
3172            &controller,
3173            &|path| path == controller || path == worker || path == packaged,
3174        )
3175        .unwrap();
3176        assert_eq!(
3177            selected,
3178            WorkerBinaryAvailability::Local {
3179                path: worker,
3180                source: "isolated native development worker".into(),
3181            }
3182        );
3183    }
3184
3185    #[cfg(target_os = "linux")]
3186    #[test]
3187    fn replaced_dev_controller_still_finds_its_musl_sibling() {
3188        let controller = PathBuf::from("target/debug/mj (deleted)");
3189        let musl = PathBuf::from("target/x86_64-unknown-linux-musl/debug/mj");
3190        let selected = select_sibling_worker(&controller, "x86_64-unknown-linux-musl", |path| {
3191            path == musl
3192        });
3193
3194        assert_eq!(selected, Some((musl, "development musl sibling")));
3195    }
3196
3197    #[cfg(target_os = "linux")]
3198    #[test]
3199    fn replaced_dev_controller_never_selects_the_new_glibc_controller_as_its_worker() {
3200        let controller = PathBuf::from("target/debug/mj (deleted)");
3201        let replacement = PathBuf::from("target/debug/mj");
3202        let selected = select_sibling_worker(&controller, "x86_64-unknown-linux-musl", |path| {
3203            path == replacement
3204        });
3205
3206        assert_eq!(selected, None);
3207    }
3208
3209    /// A configured container template for the preflight tests. Only the
3210    /// platform matters here; the rest is the smallest valid template.
3211    fn container_template(platform: Option<&str>) -> hel::hel_config::ContainerTemplate {
3212        hel::hel_config::ContainerTemplate {
3213            image: "example.invalid/mj-test:latest".into(),
3214            pull_policy: Default::default(),
3215            platform: platform.map(str::to_owned),
3216            cpus: None,
3217            memory: None,
3218            environment: BTreeMap::new(),
3219            workspace_storage: Default::default(),
3220        }
3221    }
3222
3223    fn ssh_connection() -> hel::hel_config::SshConnection {
3224        hel::hel_config::SshConnection {
3225            host: "builder".into(),
3226            user: Some("dev".into()),
3227            identity_file: None,
3228            extra_args: Vec::new(),
3229        }
3230    }
3231
3232    #[test]
3233    fn recovery_workspace_uses_the_launch_directory_for_bare_targets_only() {
3234        let cwd = PathBuf::from("/workspace/session/project");
3235        let local = worker_workspace_for_recovery(
3236            &hel_targets::TargetLocator::LocalBare {
3237                worker_root: "/workspace/session/worker".into(),
3238            },
3239            &cwd,
3240        )
3241        .expect("local bare targets need a workspace probe");
3242        assert_eq!(local.directory, cwd);
3243        assert_eq!(local.target, hel::hel_state::ManagedWorktreeTarget::Local);
3244
3245        let remote = worker_workspace_for_recovery(
3246            &hel_targets::TargetLocator::SshBare {
3247                ssh: SshTarget {
3248                    destination: "dev@builder".into(),
3249                    ssh_args: vec!["-oBatchMode=yes".into()],
3250                },
3251                workspace: "/workspace/session".into(),
3252            },
3253            &cwd,
3254        )
3255        .expect("SSH bare targets need a workspace probe");
3256        assert_eq!(remote.directory, cwd);
3257        assert_eq!(
3258            remote.target,
3259            hel::hel_state::ManagedWorktreeTarget::Ssh {
3260                destination: "dev@builder".into(),
3261                ssh_args: vec!["-oBatchMode=yes".into()],
3262            }
3263        );
3264
3265        assert!(
3266            worker_workspace_for_recovery(
3267                &hel_targets::TargetLocator::LocalPodman {
3268                    container_id: "container".into(),
3269                    workspace_storage: Default::default(),
3270                },
3271                &cwd,
3272            )
3273            .is_none()
3274        );
3275        assert!(
3276            worker_workspace_for_recovery(
3277                &hel_targets::TargetLocator::AwsEc2 {
3278                    profile: "default".into(),
3279                    region: "us-east-1".into(),
3280                    instance_id: "i-test".into(),
3281                    ssh: SshTarget {
3282                        destination: "dev@builder".into(),
3283                        ssh_args: Vec::new(),
3284                    },
3285                    workspace: "/workspace/session".into(),
3286                },
3287                &cwd,
3288            )
3289            .is_none()
3290        );
3291    }
3292
3293    #[test]
3294    fn preflight_reads_the_architecture_a_template_names() {
3295        use hel::hel_config::TargetTemplate;
3296
3297        for (platform, expected) in [
3298            ("linux/arm64", "aarch64"),
3299            ("linux/arm64/v8", "aarch64"),
3300            ("linux/amd64", "x86_64"),
3301            ("aarch64", "aarch64"),
3302        ] {
3303            assert_eq!(
3304                preflight_architectures(&TargetTemplate::LocalPodman {
3305                    container: container_template(Some(platform)),
3306                }),
3307                vec![expected],
3308                "platform {platform}"
3309            );
3310        }
3311        // A named platform decides a remote container target too, so a resume
3312        // onto an arm64 container never asks about the host's architecture.
3313        assert_eq!(
3314            preflight_architectures(&TargetTemplate::SshPodman {
3315                ssh: ssh_connection(),
3316                container: container_template(Some("linux/arm64")),
3317            }),
3318            vec!["aarch64"]
3319        );
3320    }
3321
3322    #[test]
3323    fn preflight_uses_the_host_architecture_for_a_local_target() {
3324        use hel::hel_config::TargetTemplate;
3325
3326        for template in [
3327            TargetTemplate::LocalBare,
3328            TargetTemplate::LocalPodman {
3329                container: container_template(None),
3330            },
3331            TargetTemplate::LocalDocker {
3332                container: container_template(None),
3333            },
3334            TargetTemplate::AppleContainer {
3335                container: container_template(None),
3336            },
3337        ] {
3338            assert_eq!(
3339                preflight_architectures(&template),
3340                vec![std::env::consts::ARCH],
3341                "{template:?}"
3342            );
3343        }
3344    }
3345
3346    #[test]
3347    fn preflight_accepts_either_linux_architecture_for_a_remote_target() {
3348        use hel::hel_config::TargetTemplate;
3349
3350        // Nothing in the configuration says what a remote machine runs, so the
3351        // preflight passes as long as one architecture could be served; the
3352        // real architecture is read from the live target during provisioning.
3353        for template in [
3354            TargetTemplate::SshBare {
3355                ssh: ssh_connection(),
3356                permissions: hel::hel_config::PermissionMode::Yolo,
3357                workspace_prefix: PathBuf::from(".local/share/hel/workspaces"),
3358            },
3359            TargetTemplate::SshPodman {
3360                ssh: ssh_connection(),
3361                container: container_template(None),
3362            },
3363            TargetTemplate::AwsEc2 {
3364                aws_profile: None,
3365                region: "us-east-1".into(),
3366                launch_template: "lt-mj".into(),
3367                launch_template_version: None,
3368                ssh_user: "dev".into(),
3369                address_source: Default::default(),
3370                identity_file: None,
3371                ssh_args: Vec::new(),
3372            },
3373        ] {
3374            assert_eq!(
3375                preflight_architectures(&template),
3376                vec!["x86_64", "aarch64"],
3377                "{template:?}"
3378            );
3379        }
3380    }
3381
3382    #[test]
3383    fn dev_checkout_still_finds_a_hel_named_sibling() {
3384        let controller = PathBuf::from("target/debug/hel");
3385        let musl = PathBuf::from("target/x86_64-unknown-linux-musl/debug/hel");
3386        let present = [controller.clone(), musl.clone()];
3387        let selected = select_sibling_worker(&controller, "x86_64-unknown-linux-musl", |path| {
3388            present.iter().any(|p| p == path)
3389        });
3390        assert_eq!(selected, Some((musl, "development musl sibling")));
3391    }
3392
3393    /// An architecture no host builds for, so the lookup cannot take one of
3394    /// the "native mj binary" shortcuts and reaches the end on any machine.
3395    const FOREIGN_ARCH: &str = "riscv64";
3396
3397    /// A rebuilt or renamed checkout leaves a running daemon pointing at a
3398    /// path that holds nothing. Searching beside that path finds nothing and
3399    /// blames the user for a worker that may well be installed correctly.
3400    #[test]
3401    fn a_replaced_controller_is_reported_instead_of_a_missing_worker() {
3402        let stale = PathBuf::from("/src/.backup-vHXvCs/target/debug/mj (deleted)");
3403        let probed = RefCell::new(Vec::new());
3404
3405        let error = worker_binary_prerequisite_for_current(
3406            FOREIGN_ARCH,
3407            WorkerBinaryRequirement::PortableLinux,
3408            &stale,
3409            &|path| {
3410                probed.borrow_mut().push(path.to_path_buf());
3411                false
3412            },
3413        )
3414        .unwrap_err();
3415
3416        let detail = format!("{error:#}");
3417        assert!(
3418            detail.contains("was replaced or removed on disk"),
3419            "{detail}"
3420        );
3421        assert!(detail.contains("restart the Mjolnir daemon"), "{detail}");
3422        // The path is named without the kernel's deletion marker.
3423        assert!(
3424            detail.contains("/src/.backup-vHXvCs/target/debug/mj)"),
3425            "{detail}"
3426        );
3427        assert!(!detail.contains("(deleted)"), "{detail}");
3428        assert_eq!(
3429            probed.into_inner(),
3430            vec![stale],
3431            "nothing beside a path that no longer exists is worth probing"
3432        );
3433    }
3434
3435    /// The guard is about a controller path that no longer exists and nothing
3436    /// else: a controller still on disk keeps its whole sibling lookup, and
3437    /// keeps the plain "no Linux worker" answer when that lookup comes up
3438    /// empty. A present controller is never its own portable worker, so with
3439    /// nothing installed beside it the lookup ends in that plain answer.
3440    #[test]
3441    fn a_present_controller_still_looks_beside_itself() {
3442        let controller = PathBuf::from("/opt/brokk/mj");
3443        let probed = RefCell::new(Vec::new());
3444
3445        let error = worker_binary_prerequisite_for_current(
3446            FOREIGN_ARCH,
3447            WorkerBinaryRequirement::PortableLinux,
3448            &controller,
3449            &|path| {
3450                probed.borrow_mut().push(path.to_path_buf());
3451                path == controller
3452            },
3453        )
3454        .unwrap_err();
3455
3456        let probed = probed.into_inner();
3457        assert!(
3458            probed
3459                .iter()
3460                .any(|path| path.ends_with("mj-worker-riscv64-unknown-linux-musl")),
3461            "the packaged worker name must still be probed: {probed:?}"
3462        );
3463        let detail = format!("{error:#}");
3464        assert!(
3465            detail.contains("no Linux worker for riscv64-unknown-linux-musl"),
3466            "{detail}"
3467        );
3468        assert!(!detail.contains("restart the Mjolnir daemon"), "{detail}");
3469
3470        // With nothing beside it either, a present controller still gets the
3471        // generic message; only a replaced one is told to restart.
3472        let root = PathBuf::from("/");
3473        let error = worker_binary_prerequisite_for_current(
3474            FOREIGN_ARCH,
3475            WorkerBinaryRequirement::PortableLinux,
3476            &root,
3477            &|path| path == root,
3478        )
3479        .unwrap_err();
3480        let detail = format!("{error:#}");
3481        assert!(
3482            detail.contains("no Linux worker for riscv64-unknown-linux-musl"),
3483            "{detail}"
3484        );
3485        assert!(!detail.contains("restart the Mjolnir daemon"), "{detail}");
3486    }
3487
3488    const WORKER_BINARY_OVERRIDE_CHILD: &str = "MJ_WORKER_BINARY_OVERRIDE_CHILD";
3489
3490    /// The override names a worker outright, so it does not care where the
3491    /// controller lives or whether that path still exists.
3492    #[test]
3493    fn a_replaced_controller_still_honors_the_worker_binary_override() {
3494        // MJ_WORKER_BINARY is process-global and other tests resolve worker
3495        // binaries, so set it only in an exact child test.
3496        if std::env::var_os(WORKER_BINARY_OVERRIDE_CHILD).is_none() {
3497            let directory = tempfile::tempdir().unwrap();
3498            let worker = directory.path().join("mj-worker");
3499            std::fs::write(&worker, b"worker").unwrap();
3500            let test_name = format!(
3501                "{}::a_replaced_controller_still_honors_the_worker_binary_override",
3502                module_path!()
3503                    .strip_prefix("mj_controller::")
3504                    .unwrap_or(module_path!())
3505            );
3506            let output = std::process::Command::new(std::env::current_exe().unwrap())
3507                .args(["--exact", &test_name, "--nocapture"])
3508                .env(WORKER_BINARY_OVERRIDE_CHILD, "1")
3509                .env("MJ_WORKER_BINARY", &worker)
3510                .output()
3511                .unwrap();
3512            assert!(
3513                output.status.success(),
3514                "isolated worker override test failed\nstdout:\n{}\nstderr:\n{}",
3515                String::from_utf8_lossy(&output.stdout),
3516                String::from_utf8_lossy(&output.stderr)
3517            );
3518            return;
3519        }
3520
3521        let stale = PathBuf::from("/src/.backup-vHXvCs/target/debug/mj (deleted)");
3522        let availability = worker_binary_prerequisite_for_current(
3523            FOREIGN_ARCH,
3524            WorkerBinaryRequirement::PortableLinux,
3525            &stale,
3526            &|path| path.is_file(),
3527        )
3528        .unwrap();
3529
3530        match availability {
3531            WorkerBinaryAvailability::Local { source, .. } => {
3532                assert_eq!(source, "MJ_WORKER_BINARY");
3533            }
3534            other => panic!("expected the override to resolve, got {other:?}"),
3535        }
3536    }
3537
3538    #[test]
3539    fn sibling_lookup_falls_back_to_the_legacy_hel_name_beside_an_mj_controller() {
3540        let controller = PathBuf::from("/opt/brokk/mj");
3541        let legacy = PathBuf::from("/opt/brokk/hel");
3542        let selected = select_sibling_worker(&controller, "x86_64-unknown-linux-musl", |path| {
3543            path == legacy
3544        });
3545        assert_eq!(selected, Some((legacy, "beside the running executable")));
3546    }
3547
3548    #[test]
3549    fn worker_diagnosis_surfaces_a_loader_failure_from_the_installed_binary() {
3550        struct FailedProbe;
3551
3552        impl CommandExecutor for FailedProbe {
3553            fn execute(&self, _command: &CommandSpec) -> Result<CommandOutput> {
3554                Ok(CommandOutput {
3555                    status: 1,
3556                    stdout: Vec::new(),
3557                    stderr: b"libc.so.6: version `GLIBC_2.39' not found\n".to_vec(),
3558                })
3559            }
3560        }
3561
3562        let failure = worker_binary_probe_failure(
3563            &FailedProbe,
3564            &hel_targets::TargetLocator::LocalBare {
3565                worker_root: "/worker/root".into(),
3566            },
3567            "/worker/root",
3568        )
3569        .expect("an unsuccessful --version probe should explain the dead worker");
3570
3571        assert!(failure.contains("GLIBC_2.39"), "{failure}");
3572        assert!(failure.contains("provide a musl worker"), "{failure}");
3573    }
3574
3575    /// A worker that died leaves an exit record behind. Starting a new worker
3576    /// must clear it first, or the startup connect loop reads the previous
3577    /// death as this worker's and gives up on a healthy daemon.
3578    #[test]
3579    fn starting_a_worker_clears_stale_runtime_files_before_launching() {
3580        struct RecordingExecutor {
3581            commands: RefCell<Vec<CommandSpec>>,
3582        }
3583
3584        impl CommandExecutor for RecordingExecutor {
3585            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
3586                self.commands.borrow_mut().push(command.clone());
3587                Ok(CommandOutput {
3588                    status: 0,
3589                    stdout: Vec::new(),
3590                    stderr: Vec::new(),
3591                })
3592            }
3593        }
3594
3595        for locator in [
3596            hel_targets::TargetLocator::LocalBare {
3597                worker_root: "/worker/root".into(),
3598            },
3599            hel_targets::TargetLocator::LocalPodman {
3600                container_id: "container-1".into(),
3601                workspace_storage: Default::default(),
3602            },
3603        ] {
3604            let executor = RecordingExecutor {
3605                commands: RefCell::new(Vec::new()),
3606            };
3607            start_worker(&executor, &locator, "/worker/root").unwrap();
3608
3609            let commands = executor.commands.borrow();
3610            let script = commands
3611                .iter()
3612                .flat_map(|command| command.args.iter())
3613                .find(|argument| argument.contains("worker-exit.json"))
3614                .unwrap_or_else(|| {
3615                    panic!("no launch script cleared the exit record: {commands:?}")
3616                });
3617            let cleared = script.find("rm -f").expect("the exit record is removed");
3618            let launched = script.find("worker").expect("the daemon is launched");
3619            assert!(
3620                script.contains("control.sock"),
3621                "the stale relay endpoint must be cleared before startup: {script}"
3622            );
3623            assert!(
3624                cleared < launched,
3625                "stale runtime files must be cleared before the daemon starts: {script}"
3626            );
3627        }
3628    }
3629    #[test]
3630    fn stopping_a_worker_runs_the_daemon_stop_script() {
3631        struct RecordingExecutor {
3632            commands: RefCell<Vec<CommandSpec>>,
3633        }
3634
3635        impl CommandExecutor for RecordingExecutor {
3636            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
3637                self.commands.borrow_mut().push(command.clone());
3638                Ok(CommandOutput {
3639                    status: 0,
3640                    stdout: Vec::new(),
3641                    stderr: Vec::new(),
3642                })
3643            }
3644        }
3645
3646        let locator = hel_targets::TargetLocator::SshBare {
3647            ssh: SshTarget {
3648                destination: "user@example.test".into(),
3649                ssh_args: Vec::new(),
3650            },
3651            workspace: "/workspace".into(),
3652        };
3653        let executor = RecordingExecutor {
3654            commands: RefCell::new(Vec::new()),
3655        };
3656        stop_worker(&executor, &locator, "/worker/root").unwrap();
3657
3658        let commands = executor.commands.borrow();
3659        assert_eq!(commands.len(), 1);
3660        assert_eq!(commands[0].purpose, "stop Mjolnir worker daemon");
3661        assert!(
3662            commands[0]
3663                .args
3664                .last()
3665                .is_some_and(|remote| remote.starts_with("'sh' '-c' ")),
3666            "raw SSH worker management must not source login profiles: {commands:?}"
3667        );
3668        let script = commands[0]
3669            .args
3670            .iter()
3671            .find(|argument| argument.contains("worker run --root"))
3672            .unwrap_or_else(|| panic!("stop script missing from {commands:?}"));
3673        assert!(
3674            script.contains("hel_match=\"hel worker run --root $hel_root\""),
3675            "stop must match only this session's worker: {script}"
3676        );
3677        assert!(
3678            script.contains("hel_match_home=\"hel worker run --root $HOME/$hel_root\""),
3679            "stop must also match a login-home-absolute --root: {script}"
3680        );
3681        assert!(
3682            !script.contains("grep -F"),
3683            "leftover detection must not grep the match string: {script}"
3684        );
3685    }
3686    #[test]
3687    fn checkpoint_worker_stop_restores_a_stopped_podman_target_first() {
3688        struct RecordingExecutor {
3689            commands: RefCell<Vec<CommandSpec>>,
3690            outputs: RefCell<Vec<CommandOutput>>,
3691        }
3692
3693        impl CommandExecutor for RecordingExecutor {
3694            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
3695                self.commands.borrow_mut().push(command.clone());
3696                Ok(self.outputs.borrow_mut().remove(0))
3697            }
3698        }
3699
3700        let session = "0123456789abcdef0123456789abcdef";
3701        let container_id = hel_targets::resource_name(session).unwrap();
3702        let inspection = |status: &str| CommandOutput {
3703            status: 0,
3704            stdout: serde_json::to_vec(&serde_json::json!([{
3705                "Config": { "Labels": {
3706                    (hel_targets::MANAGED_LABEL): "true",
3707                    (hel_targets::SESSION_LABEL): session,
3708                }},
3709                "State": { "Status": status },
3710            }]))
3711            .unwrap(),
3712            stderr: Vec::new(),
3713        };
3714        let executor = RecordingExecutor {
3715            commands: RefCell::new(Vec::new()),
3716            outputs: RefCell::new(vec![
3717                CommandOutput {
3718                    status: 0,
3719                    stdout: Vec::new(),
3720                    stderr: Vec::new(),
3721                },
3722                inspection("exited"),
3723                CommandOutput {
3724                    status: 0,
3725                    stdout: Vec::new(),
3726                    stderr: Vec::new(),
3727                },
3728                inspection("running"),
3729                CommandOutput {
3730                    status: 0,
3731                    stdout: Vec::new(),
3732                    stderr: Vec::new(),
3733                },
3734            ]),
3735        };
3736        let locator = hel_targets::TargetLocator::LocalPodman {
3737            container_id,
3738            workspace_storage: Default::default(),
3739        };
3740
3741        stop_worker_after_target_recovery(&executor, &locator, session, "/worker/root").unwrap();
3742
3743        let commands = executor.commands.borrow();
3744        let purposes = commands
3745            .iter()
3746            .map(|command| command.purpose.as_str())
3747            .collect::<Vec<_>>();
3748        assert_eq!(
3749            purposes,
3750            [
3751                "check for Mjolnir session container",
3752                "inspect Mjolnir session container",
3753                "start stopped Mjolnir session container",
3754                "inspect Mjolnir session container",
3755                "stop Mjolnir worker daemon",
3756            ]
3757        );
3758    }
3759
3760    struct PodmanInstallExecutor {
3761        commands: RefCell<Vec<CommandSpec>>,
3762        worker_cached: bool,
3763    }
3764    impl CommandExecutor for PodmanInstallExecutor {
3765        fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
3766            self.commands.borrow_mut().push(command.clone());
3767            let probing_cache = command
3768                .args
3769                .iter()
3770                .any(|argument| argument.contains("'test' '-f'"));
3771            let status = if probing_cache && !self.worker_cached {
3772                1
3773            } else {
3774                0
3775            };
3776            Ok(CommandOutput {
3777                status,
3778                stdout: Vec::new(),
3779                stderr: Vec::new(),
3780            })
3781        }
3782    }
3783    struct PodmanInstallFixture {
3784        _root: tempfile::TempDir,
3785        worker_binary: PathBuf,
3786        launch_config: PathBuf,
3787        ownership: PathBuf,
3788        profile_stage: PathBuf,
3789        locator: hel_targets::TargetLocator,
3790        digest: String,
3791    }
3792    fn podman_install_fixture() -> PodmanInstallFixture {
3793        let root = tempfile::tempdir().unwrap();
3794        let worker_binary = root.path().join("hel");
3795        std::fs::write(&worker_binary, b"worker-binary-bytes").unwrap();
3796        let launch_config = root.path().join("launch.json");
3797        std::fs::write(&launch_config, b"{}").unwrap();
3798        let ownership = root.path().join("ownership.json");
3799        std::fs::write(&ownership, b"{}").unwrap();
3800        let profile_stage = root.path().join("profile");
3801        std::fs::create_dir_all(&profile_stage).unwrap();
3802        let digest = format!("{:x}", Sha256::digest(b"worker-binary-bytes"));
3803        PodmanInstallFixture {
3804            _root: root,
3805            worker_binary,
3806            launch_config,
3807            ownership,
3808            profile_stage,
3809            locator: hel_targets::TargetLocator::SshPodman {
3810                ssh: SshTarget {
3811                    destination: "user@example.test".into(),
3812                    ssh_args: Vec::new(),
3813                },
3814                container_id: "container-1".into(),
3815                workspace_storage: Default::default(),
3816            },
3817            digest,
3818        }
3819    }
3820    fn run_podman_install(worker_cached: bool) -> (Vec<CommandSpec>, PodmanInstallFixture) {
3821        let fixture = podman_install_fixture();
3822        let executor = PodmanInstallExecutor {
3823            commands: RefCell::new(Vec::new()),
3824            worker_cached,
3825        };
3826        install_worker_files(
3827            &executor,
3828            &fixture.locator,
3829            "0123456789abcdef0123456789abcdef",
3830            "/workspace/.hel/worker",
3831            "/workspace/.hel/profile",
3832            &fixture.worker_binary,
3833            &fixture.launch_config,
3834            &fixture.ownership,
3835            &fixture.profile_stage,
3836        )
3837        .unwrap();
3838        let commands = executor.commands.borrow().clone();
3839        (commands, fixture)
3840    }
3841    fn rendered(commands: &[CommandSpec]) -> Vec<String> {
3842        commands
3843            .iter()
3844            .map(|command| format!("{} {}", command.program, command.args.join(" ")))
3845            .collect()
3846    }
3847    #[test]
3848    fn ssh_podman_install_caches_the_worker_binary_on_a_cache_miss() {
3849        let (commands, fixture) = run_podman_install(false);
3850        let lines = rendered(&commands);
3851        let digest = &fixture.digest;
3852        let cache_dir = format!(".cache/mjolnir/workers/{digest}");
3853        let session = "0123456789abcdef0123456789abcdef";
3854
3855        assert!(
3856            lines
3857                .iter()
3858                .any(|line| line.starts_with("ssh") && line.contains("'test' '-f'")),
3859            "expected a cache probe, got {lines:#?}"
3860        );
3861        assert!(
3862            !lines.iter().any(|line| line.contains('~')),
3863            "remote staging paths must be home-relative: ssh arguments are \
3864                 single-quoted so a tilde stays literal in the remote shell while \
3865                 scp expands it, got {lines:#?}"
3866        );
3867        assert!(
3868            lines.iter().any(|line| line.starts_with("ssh")
3869                && line.contains(&format!("'mkdir' '-p' '{cache_dir}'"))),
3870            "expected the cache directory to be created, got {lines:#?}"
3871        );
3872        let partial = format!("{cache_dir}/hel.partial-{session}");
3873        assert!(
3874            lines.iter().any(|line| line
3875                == &format!(
3876                    "scp {} user@example.test:{partial}",
3877                    fixture.worker_binary.display()
3878                )),
3879            "expected the worker to be uploaded to the partial cache path, got {lines:#?}"
3880        );
3881        assert!(
3882            lines.iter().any(|line| line.starts_with("ssh")
3883                && line.contains(&format!("'mv' '{partial}' '{cache_dir}/hel'"))),
3884            "expected an atomic rename into the cache, got {lines:#?}"
3885        );
3886        assert!(
3887            lines.iter().any(|line| line.contains("'podman' 'cp'")
3888                && line.contains(&format!("'{cache_dir}/hel'"))),
3889            "expected podman cp to read the cached worker, got {lines:#?}"
3890        );
3891        assert!(
3892            !lines.iter().any(|line| line.starts_with("scp")
3893                && line.ends_with(&format!(
3894                    "user@example.test:.cache/mjolnir/uploads/{session}/hel"
3895                ))),
3896            "the worker must not be staged in the per-session upload directory, got {lines:#?}"
3897        );
3898    }
3899    #[test]
3900    fn ssh_podman_install_skips_the_worker_upload_on_a_cache_hit() {
3901        let (commands, fixture) = run_podman_install(true);
3902        let lines = rendered(&commands);
3903        let digest = &fixture.digest;
3904        let cache_dir = format!(".cache/mjolnir/workers/{digest}");
3905        let session = "0123456789abcdef0123456789abcdef";
3906
3907        assert!(
3908            !lines.iter().any(|line| line.starts_with("scp")
3909                && line.contains(&fixture.worker_binary.display().to_string())),
3910            "a cached worker must not be re-uploaded, got {lines:#?}"
3911        );
3912        assert!(
3913            !lines.iter().any(|line| line.contains("'mv'")),
3914            "a cache hit must not rename anything, got {lines:#?}"
3915        );
3916        assert!(
3917            lines.iter().any(|line| line.contains("'podman' 'cp'")
3918                && line.contains(&format!("'{cache_dir}/hel'"))),
3919            "expected podman cp to read the cached worker, got {lines:#?}"
3920        );
3921        for name in ["launch.json", "ownership.json"] {
3922            assert!(
3923                lines.iter().any(|line| line.starts_with("scp")
3924                    && line.ends_with(&format!(
3925                        "user@example.test:.cache/mjolnir/uploads/{session}/{name}"
3926                    ))),
3927                "expected {name} to still be uploaded per session, got {lines:#?}"
3928            );
3929        }
3930    }
3931
3932    #[test]
3933    fn ssh_docker_install_uses_docker_for_remote_container_operations() {
3934        let mut fixture = podman_install_fixture();
3935        fixture.locator = hel_targets::TargetLocator::SshDocker {
3936            ssh: SshTarget {
3937                destination: "user@example.test".into(),
3938                ssh_args: Vec::new(),
3939            },
3940            container_id: "container-1".into(),
3941        };
3942        let executor = PodmanInstallExecutor {
3943            commands: RefCell::new(Vec::new()),
3944            worker_cached: true,
3945        };
3946        install_worker_files(
3947            &executor,
3948            &fixture.locator,
3949            "0123456789abcdef0123456789abcdef",
3950            "/workspace/.hel/worker",
3951            "/workspace/.hel/profile",
3952            &fixture.worker_binary,
3953            &fixture.launch_config,
3954            &fixture.ownership,
3955            &fixture.profile_stage,
3956        )
3957        .unwrap();
3958
3959        let lines = rendered(&executor.commands.borrow());
3960        assert!(
3961            lines.iter().any(|line| line.contains("'docker' 'cp'")),
3962            "expected Docker to copy the cached worker, got {lines:#?}"
3963        );
3964        assert!(
3965            lines.iter().any(|line| line.contains("'docker' 'exec'")),
3966            "expected Docker to prepare the worker directories, got {lines:#?}"
3967        );
3968        assert!(
3969            !lines.iter().any(|line| line.contains("'podman'")),
3970            "Docker installation accidentally used Podman: {lines:#?}"
3971        );
3972    }
3973
3974    #[test]
3975    #[ignore = "requires Docker and the locally installed agent-dev image"]
3976    fn docker_uploads_and_replacements_are_usable_by_the_non_root_worker() {
3977        let fixture = podman_install_fixture();
3978        let session = hel::hel_state::new_session_id().unwrap();
3979        let container_id = hel_targets::resource_name(&session).unwrap();
3980        let locator = hel_targets::TargetLocator::LocalDocker {
3981            container_id: container_id.clone(),
3982        };
3983        execute_checked(
3984            &ProcessExecutor,
3985            CommandSpec::new(
3986                "docker",
3987                [
3988                    "run",
3989                    "--pull=never",
3990                    "-d",
3991                    "--name",
3992                    &container_id,
3993                    "ghcr.io/brokkai/mjolnir/agent-dev:latest",
3994                    "sleep",
3995                    "infinity",
3996                ],
3997            ),
3998        )
3999        .unwrap();
4000        let result = (|| -> Result<()> {
4001            let root = hel_targets::worker_root(&locator, &session)?;
4002            let profile = format!("{root}/profile");
4003            std::fs::write(fixture.profile_stage.join("credential"), "private")?;
4004            install_worker_files(
4005                &ProcessExecutor,
4006                &locator,
4007                &session,
4008                &root,
4009                &profile,
4010                &fixture.worker_binary,
4011                &fixture.launch_config,
4012                &fixture.ownership,
4013                &fixture.profile_stage,
4014            )?;
4015            replace_installed_worker_binary(
4016                &ProcessExecutor,
4017                &locator,
4018                &session,
4019                &fixture.worker_binary,
4020            )?;
4021            execute_checked(
4022                &ProcessExecutor,
4023                CommandSpec::new(
4024                    "docker",
4025                    [
4026                        "exec",
4027                        &container_id,
4028                        "sh",
4029                        "-c",
4030                        "test \"$(id -u)\" != 0 && test -x \"$1/hel\" && test -r \"$1/launch.json\" && test -r \"$1/ownership.json\" && test -r \"$1/profile/credential\" && test -w \"$1/profile/credential\"",
4031                        "sh",
4032                        &root,
4033                    ],
4034                ),
4035            )?;
4036            Ok(())
4037        })();
4038        let cleanup = execute_checked(
4039            &ProcessExecutor,
4040            CommandSpec::new("docker", ["rm", "-f", &container_id]),
4041        );
4042        result.unwrap();
4043        cleanup.unwrap();
4044    }
4045
4046    #[test]
4047    fn replacing_an_installed_podman_worker_writes_through_a_next_path() {
4048        struct RecordingExecutor {
4049            commands: RefCell<Vec<CommandSpec>>,
4050        }
4051        impl CommandExecutor for RecordingExecutor {
4052            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
4053                self.commands.borrow_mut().push(command.clone());
4054                Ok(CommandOutput {
4055                    status: 0,
4056                    stdout: Vec::new(),
4057                    stderr: Vec::new(),
4058                })
4059            }
4060        }
4061
4062        let session = "0123456789abcdef0123456789abcdef";
4063        let container_id = hel_targets::resource_name(session).unwrap();
4064        let locator = hel_targets::TargetLocator::LocalPodman {
4065            container_id: container_id.clone(),
4066            workspace_storage: Default::default(),
4067        };
4068        let executor = RecordingExecutor {
4069            commands: RefCell::new(Vec::new()),
4070        };
4071        replace_installed_worker_binary(&executor, &locator, session, Path::new("/controller/hel"))
4072            .unwrap();
4073
4074        let mut lines = rendered(&executor.commands.borrow());
4075        let ownership = lines.remove(1);
4076        assert!(ownership.starts_with(&format!("podman exec --user 0 {container_id} sh -c")));
4077        assert!(ownership.contains("chown -R"));
4078        assert!(ownership.ends_with(&format!("/var/lib/hel/workers/{session}/hel.next")));
4079        assert_eq!(
4080            lines,
4081            vec![
4082                format!(
4083                    "podman cp /controller/hel {container_id}:/var/lib/hel/workers/{session}/hel.next"
4084                ),
4085                format!(
4086                    "podman exec {container_id} mv -f /var/lib/hel/workers/{session}/hel.next /var/lib/hel/workers/{session}/hel"
4087                ),
4088                format!("podman exec {container_id} chmod 700 /var/lib/hel/workers/{session}/hel"),
4089            ]
4090        );
4091    }
4092    #[test]
4093    fn default_bridges_pin_command_capable_adapter_versions() {
4094        let (codex_command, codex_arguments) = bridge_launch(
4095            hel::hel_config::HarnessKind::Codex,
4096            ExecutionPolicy::Unconstrained,
4097        );
4098        assert_eq!(codex_command, "sh");
4099        assert_eq!(codex_arguments[0], "-c");
4100        assert!(codex_arguments[1].contains("@brokkai/codex-acp@1.11.1"));
4101        assert!(codex_arguments[1].contains("codex-acp --version"));
4102
4103        let (claude_command, claude_arguments) = bridge_launch(
4104            hel::hel_config::HarnessKind::Claude,
4105            ExecutionPolicy::Unconstrained,
4106        );
4107        assert_eq!(claude_command, "sh");
4108        assert_eq!(claude_arguments[0], "-c");
4109        assert!(claude_arguments[1].contains("@agentclientprotocol/claude-agent-acp@0.73.0"));
4110
4111        let (deepseek_command, deepseek_arguments) = bridge_launch(
4112            hel::hel_config::HarnessKind::Deepseek,
4113            ExecutionPolicy::Unconstrained,
4114        );
4115        assert_eq!(deepseek_command, "sh");
4116        assert_eq!(deepseek_arguments[0], "-c");
4117        assert!(deepseek_arguments[1].contains("@deepseek-ai/dsh@0.1.2-rc.1"));
4118        assert!(deepseek_arguments[1].contains("dsh --profile acp"));
4119        assert!(deepseek_arguments[1].contains("dsh --version"));
4120        assert!(!deepseek_arguments[1].contains("npx -y -p @deepseek-ai/dsh"));
4121        assert!(deepseek_arguments[1].contains("Mjolnir needs @deepseek-ai/dsh"));
4122        assert!(!deepseek_arguments[1].contains("Hel"));
4123    }
4124
4125    #[test]
4126    fn readiness_stage_names_only_install_capable_default_harnesses() {
4127        let profile = |kind| hel::hel_config::HarnessProfile {
4128            enabled: true,
4129            kind,
4130            home: PathBuf::from("/profiles/test"),
4131            environment: BTreeMap::new(),
4132            context_window_bytes: None,
4133        };
4134
4135        for harness in [
4136            HarnessKind::Codex,
4137            HarnessKind::Claude,
4138            HarnessKind::Kimi,
4139            HarnessKind::Grok,
4140        ] {
4141            assert_eq!(
4142                bridge_readiness_stage(&profile(harness)),
4143                ProvisionStage::Installing(harness)
4144            );
4145        }
4146        assert_eq!(
4147            bridge_readiness_stage(&profile(HarnessKind::Deepseek)),
4148            ProvisionStage::Starting
4149        );
4150    }
4151    #[test]
4152    fn codex_execution_environment_follows_the_target_policy() {
4153        let mut podman_environment =
4154            BTreeMap::from([("INITIAL_AGENT_MODE".to_owned(), "read-only".to_owned())]);
4155        hel::hel_config::HarnessKind::Codex
4156            .configure_execution_environment(
4157                ExecutionPolicy::Unconstrained,
4158                &mut podman_environment,
4159            )
4160            .unwrap();
4161        assert_eq!(
4162            podman_environment
4163                .get("INITIAL_AGENT_MODE")
4164                .map(String::as_str),
4165            Some("agent-full-access")
4166        );
4167
4168        let mut bare_environment =
4169            BTreeMap::from([("INITIAL_AGENT_MODE".to_owned(), "read-only".to_owned())]);
4170        hel::hel_config::HarnessKind::Codex
4171            .configure_execution_environment(
4172                ExecutionPolicy::ConfiguredApprovals,
4173                &mut bare_environment,
4174            )
4175            .unwrap();
4176        assert_eq!(
4177            bare_environment
4178                .get("INITIAL_AGENT_MODE")
4179                .map(String::as_str),
4180            Some("agent"),
4181            "Codex uses guardian on raw localhost"
4182        );
4183    }
4184    #[test]
4185    fn only_raw_targets_without_an_explicit_path_request_login_path_discovery() {
4186        let raw = hel_targets::TargetLocator::LocalBare {
4187            worker_root: "/worker".into(),
4188        };
4189        let managed = hel_targets::TargetLocator::LocalPodman {
4190            container_id: "container".into(),
4191            workspace_storage: Default::default(),
4192        };
4193
4194        let mut environment = BTreeMap::new();
4195        configure_login_path_discovery(&mut environment, &raw);
4196        assert_eq!(
4197            environment.get(DISCOVER_LOGIN_PATH_ENV).map(String::as_str),
4198            Some("1")
4199        );
4200
4201        let mut explicit = BTreeMap::from([
4202            ("PATH".into(), "/configured/bin".into()),
4203            (DISCOVER_LOGIN_PATH_ENV.into(), "stale".into()),
4204        ]);
4205        configure_login_path_discovery(&mut explicit, &raw);
4206        assert_eq!(
4207            explicit.get("PATH").map(String::as_str),
4208            Some("/configured/bin")
4209        );
4210        assert!(!explicit.contains_key(DISCOVER_LOGIN_PATH_ENV));
4211
4212        let mut managed_environment =
4213            BTreeMap::from([(DISCOVER_LOGIN_PATH_ENV.into(), "stale".into())]);
4214        configure_login_path_discovery(&mut managed_environment, &managed);
4215        assert!(!managed_environment.contains_key(DISCOVER_LOGIN_PATH_ENV));
4216    }
4217    #[test]
4218    fn bare_targets_use_managed_harnesses_but_containers_stay_ambient() {
4219        let ssh = SshTarget {
4220            destination: "user@example.test".into(),
4221            ssh_args: Vec::new(),
4222        };
4223        let targets = [
4224            (
4225                hel_targets::TargetLocator::LocalBare {
4226                    worker_root: "/worker".into(),
4227                },
4228                HarnessRuntimePolicy::Managed,
4229            ),
4230            (
4231                hel_targets::TargetLocator::LocalPodman {
4232                    container_id: "container".into(),
4233                    workspace_storage: Default::default(),
4234                },
4235                HarnessRuntimePolicy::Ambient,
4236            ),
4237            (
4238                hel_targets::TargetLocator::SshBare {
4239                    ssh: ssh.clone(),
4240                    workspace: "/workspace/session".into(),
4241                },
4242                HarnessRuntimePolicy::Managed,
4243            ),
4244            (
4245                hel_targets::TargetLocator::AwsEc2 {
4246                    profile: "profile".into(),
4247                    region: "us-east-1".into(),
4248                    instance_id: "i-test".into(),
4249                    ssh,
4250                    workspace: "/workspace/session".into(),
4251                },
4252                HarnessRuntimePolicy::Managed,
4253            ),
4254        ];
4255
4256        for (target, expected) in targets {
4257            assert_eq!(harness_runtime_policy(&target), expected, "{target:?}");
4258        }
4259    }
4260    #[test]
4261    fn grok_sandbox_environment_follows_the_target_policy() {
4262        let mut isolated = BTreeMap::from([("GROK_SANDBOX".to_owned(), "strict".to_owned())]);
4263        hel::hel_config::HarnessKind::Grok
4264            .configure_execution_environment(ExecutionPolicy::Unconstrained, &mut isolated)
4265            .unwrap();
4266        assert_eq!(
4267            isolated.get("GROK_SANDBOX").map(String::as_str),
4268            Some("off")
4269        );
4270
4271        let mut local = BTreeMap::from([("GROK_SANDBOX".to_owned(), "strict".to_owned())]);
4272        hel::hel_config::HarnessKind::Grok
4273            .configure_execution_environment(ExecutionPolicy::ConfiguredApprovals, &mut local)
4274            .unwrap();
4275        assert_eq!(
4276            local.get("GROK_SANDBOX").map(String::as_str),
4277            Some("strict"),
4278            "raw localhost must preserve the profile's configured sandbox"
4279        );
4280    }
4281    #[test]
4282    fn bridge_fallback_pins_match_the_agent_dev_containerfile() {
4283        const CONTAINERFILE: &str = include_str!("../../../containers/Containerfile.agent-dev");
4284
4285        let codex = format!("codex-acp@{CODEX_ACP_VERSION}");
4286        assert!(
4287            CONTAINERFILE.contains(&codex),
4288            "containers/Containerfile.agent-dev must install {codex}. The image and the \
4289                 bridge_launch() npx fallbacks have to stay in lockstep, otherwise a container \
4290                 session and an npx session run different adapter versions."
4291        );
4292
4293        let claude = format!("claude-agent-acp@{CLAUDE_ACP_VERSION}");
4294        assert!(
4295            CONTAINERFILE.contains(&claude),
4296            "containers/Containerfile.agent-dev must install {claude}. The image and the \
4297                 bridge_launch() npx fallbacks have to stay in lockstep, otherwise a container \
4298                 session and an npx session run different adapter versions."
4299        );
4300
4301        let deepseek = format!("@deepseek-ai/dsh@{DEEPSEEK_DSH_VERSION}");
4302        assert!(
4303            CONTAINERFILE.contains(&deepseek),
4304            "containers/Containerfile.agent-dev must install {deepseek}"
4305        );
4306        assert!(!CONTAINERFILE.contains("dsh-acp-server"));
4307    }
4308    #[test]
4309    fn kimi_default_bridge_is_non_login_and_uses_bash_for_the_official_installer() {
4310        let (command, arguments) = bridge_launch(
4311            hel::hel_config::HarnessKind::Kimi,
4312            ExecutionPolicy::Unconstrained,
4313        );
4314        assert_eq!(command, "sh");
4315        assert_eq!(arguments[0], "-c");
4316        assert!(arguments[1].contains("install.sh | bash &&"));
4317        assert!(arguments[1].contains("$HOME/.kimi-code/bin/kimi"));
4318        assert!(arguments[1].contains("Mjolnir needs compatible Kimi Code"));
4319        assert!(!arguments[1].contains("Hel"));
4320    }
4321    #[test]
4322    fn grok_default_bridge_is_non_login_and_uses_bash_for_the_official_installer() {
4323        let (command, arguments) = bridge_launch(
4324            hel::hel_config::HarnessKind::Grok,
4325            ExecutionPolicy::ConfiguredApprovals,
4326        );
4327        assert_eq!(command, "sh");
4328        assert_eq!(arguments[0], "-c");
4329        let script = &arguments[1];
4330        assert!(script.contains("https://x.ai/cli/install.sh | bash &&"));
4331        assert!(script.contains("command -v grok"));
4332        assert!(script.contains("[ -x \"$GROK_HOME/bin/grok\" ]"));
4333        assert!(script.contains("[ -x \"$HOME/.grok/bin/grok\" ]"));
4334        assert!(script.contains("exit 127"));
4335        assert!(script.contains("exec grok agent stdio"));
4336        assert!(!script.contains("--always-approve"));
4337        assert!(script.contains("Mjolnir needs compatible Grok Build"));
4338        assert!(!script.contains("Hel"));
4339    }
4340    #[test]
4341    fn node_bootstrap_errors_name_mjolnir() {
4342        let script = ensure_node_script();
4343        assert!(script.contains("Mjolnir needs Node.js, npm, and npx"));
4344        assert!(!script.contains("sudo"));
4345        assert!(!script.contains("apt-get"));
4346        assert!(!script.contains("Hel"));
4347    }
4348    #[test]
4349    fn grok_default_bridge_adds_the_always_approve_flag_when_unrestricted() {
4350        let (_, arguments) = bridge_launch(
4351            hel::hel_config::HarnessKind::Grok,
4352            ExecutionPolicy::Unconstrained,
4353        );
4354        let script = &arguments[1];
4355        assert!(script.contains("exec grok agent --always-approve stdio"));
4356        assert!(script.contains("exec \"$GROK_HOME/bin/grok\" agent --always-approve stdio"));
4357        assert!(script.contains("exec \"$HOME/.grok/bin/grok\" agent --always-approve stdio"));
4358    }
4359    #[test]
4360    fn kimi_uses_runtime_aware_memory_delivery_only_on_staged_targets() {
4361        let local = hel_targets::TargetLocator::LocalBare {
4362            worker_root: "/worker".into(),
4363        };
4364        let podman = hel_targets::TargetLocator::LocalPodman {
4365            container_id: "container".into(),
4366            workspace_storage: Default::default(),
4367        };
4368
4369        assert_eq!(
4370            project_memory_mcp_delivery(hel::hel_config::HarnessKind::Kimi, &local),
4371            ProjectMemoryMcpDelivery::Acp
4372        );
4373        assert_eq!(
4374            project_memory_mcp_delivery(hel::hel_config::HarnessKind::Kimi, &podman),
4375            ProjectMemoryMcpDelivery::HarnessProfile
4376        );
4377        assert_eq!(
4378            project_memory_mcp_delivery(hel::hel_config::HarnessKind::Codex, &podman),
4379            ProjectMemoryMcpDelivery::Acp
4380        );
4381    }
4382    #[test]
4383    fn stage_grok_profile_copies_authentication_and_agent_identity() {
4384        let home = tempfile::tempdir().unwrap();
4385        std::fs::write(
4386            home.path().join("auth.json"),
4387            "{\"https://auth.x.ai::1\":{}}",
4388        )
4389        .unwrap();
4390        std::fs::write(home.path().join("agent_id"), "stable-agent-id").unwrap();
4391        std::fs::write(home.path().join("config.toml"), "model = \"grok-4.6\"\n").unwrap();
4392        // Native session storage is checkpointed, never staged.
4393        std::fs::create_dir(home.path().join("sessions")).unwrap();
4394        std::fs::write(home.path().join("sessions/session_search.sqlite"), "x").unwrap();
4395        let staged = tempfile::tempdir().unwrap();
4396        let profile = hel::hel_config::HarnessProfile {
4397            enabled: true,
4398            kind: hel::hel_config::HarnessKind::Grok,
4399            home: home.path().to_path_buf(),
4400            environment: BTreeMap::new(),
4401            context_window_bytes: None,
4402        };
4403
4404        stage_profile(&profile, staged.path()).unwrap();
4405
4406        assert_eq!(
4407            std::fs::read_to_string(staged.path().join("agent_id")).unwrap(),
4408            "stable-agent-id"
4409        );
4410        assert!(staged.path().join("auth.json").is_file());
4411        assert!(staged.path().join("config.toml").is_file());
4412        assert!(!staged.path().join("sessions").exists());
4413    }
4414    #[test]
4415    fn stage_claude_profile_preserves_rollout_identity() {
4416        let home = tempfile::tempdir().unwrap();
4417        let identity = r#"{
4418                "machineID": "stable-machine",
4419                "userID": "stable-user",
4420                "cachedGrowthBookFeatures": {
4421                    "tengu_velvet_mallet_fable_5": true
4422                }
4423            }"#;
4424        std::fs::write(home.path().join(".claude.json"), identity).unwrap();
4425        let staged = tempfile::tempdir().unwrap();
4426        let profile = hel::hel_config::HarnessProfile {
4427            enabled: true,
4428            kind: hel::hel_config::HarnessKind::Claude,
4429            home: home.path().to_path_buf(),
4430            environment: BTreeMap::new(),
4431            context_window_bytes: None,
4432        };
4433
4434        stage_profile(&profile, staged.path()).unwrap();
4435
4436        assert_eq!(
4437            std::fs::read_to_string(staged.path().join(".claude.json")).unwrap(),
4438            identity
4439        );
4440    }
4441    #[test]
4442    fn stage_kimi_profile_preserves_device_identity() {
4443        let home = tempfile::tempdir().unwrap();
4444        std::fs::write(home.path().join("config.toml"), "default_model = \"k3\"\n").unwrap();
4445        std::fs::write(home.path().join("device_id"), "stable-device-id").unwrap();
4446        std::fs::create_dir(home.path().join("credentials")).unwrap();
4447        std::fs::write(
4448            home.path().join("credentials/kimi-code.json"),
4449            "{\"access_token\":\"secret\"}",
4450        )
4451        .unwrap();
4452        let staged = tempfile::tempdir().unwrap();
4453        let profile = hel::hel_config::HarnessProfile {
4454            enabled: true,
4455            kind: hel::hel_config::HarnessKind::Kimi,
4456            home: home.path().to_path_buf(),
4457            environment: BTreeMap::new(),
4458            context_window_bytes: None,
4459        };
4460
4461        stage_profile(&profile, staged.path()).unwrap();
4462
4463        assert_eq!(
4464            std::fs::read_to_string(staged.path().join("device_id")).unwrap(),
4465            "stable-device-id"
4466        );
4467        assert!(staged.path().join("credentials/kimi-code.json").is_file());
4468    }
4469    #[test]
4470    fn staged_kimi_profile_binds_project_memory_to_the_target_runtime() {
4471        let home = tempfile::tempdir().unwrap();
4472        let original = serde_json::json!({
4473            "mcpServers": {
4474                "user-server": {
4475                    "command": "user-mcp",
4476                    "args": ["serve"]
4477                }
4478            },
4479            "userSetting": true
4480        });
4481        let original_body = serde_json::to_vec_pretty(&original).unwrap();
4482        std::fs::write(home.path().join("mcp.json"), &original_body).unwrap();
4483        let staged = tempfile::tempdir().unwrap();
4484        let profile = hel::hel_config::HarnessProfile {
4485            enabled: true,
4486            kind: hel::hel_config::HarnessKind::Kimi,
4487            home: home.path().to_path_buf(),
4488            environment: BTreeMap::new(),
4489            context_window_bytes: None,
4490        };
4491        stage_profile(&profile, staged.path()).unwrap();
4492        let memory = ProjectMemoryLaunchConfig {
4493            project_key: "project".into(),
4494            root: "/var/lib/hel/profiles/session/projects/project/memory".into(),
4495            baseline_root: PathBuf::new(),
4496            repository_roots: BTreeMap::new(),
4497            mcp_delivery: ProjectMemoryMcpDelivery::HarnessProfile,
4498        };
4499
4500        configure_kimi_project_memory_mcp(staged.path(), "/var/lib/hel/workers/session", &memory)
4501            .unwrap();
4502
4503        let configured: serde_json::Value =
4504            serde_json::from_slice(&std::fs::read(staged.path().join("mcp.json")).unwrap())
4505                .unwrap();
4506        assert_eq!(configured["userSetting"], true);
4507        assert_eq!(
4508            configured["mcpServers"]["user-server"]["command"],
4509            "user-mcp"
4510        );
4511        assert_eq!(
4512            configured["mcpServers"]["mj-project-memory"],
4513            serde_json::json!({
4514                "transport": "stdio",
4515                "command": "/var/lib/hel/workers/session/hel",
4516                "args": [
4517                    "worker",
4518                    "memory-mcp",
4519                    "--root",
4520                    "/var/lib/hel/profiles/session/projects/project/memory"
4521                ],
4522                "runtime_id": "local"
4523            })
4524        );
4525        assert_eq!(
4526            std::fs::read(home.path().join("mcp.json")).unwrap(),
4527            original_body,
4528            "the controller-side Kimi profile must remain unchanged"
4529        );
4530    }
4531
4532    #[test]
4533    fn staged_kimi_project_memory_resolves_ssh_paths_from_target_home() {
4534        let staged = tempfile::tempdir().unwrap();
4535        let memory = ProjectMemoryLaunchConfig {
4536            project_key: "project".into(),
4537            root: ".local/share/hel/profiles/session/projects/project/memory".into(),
4538            baseline_root: PathBuf::new(),
4539            repository_roots: BTreeMap::new(),
4540            mcp_delivery: ProjectMemoryMcpDelivery::HarnessProfile,
4541        };
4542
4543        configure_kimi_project_memory_mcp(
4544            staged.path(),
4545            ".local/share/hel/workers/session",
4546            &memory,
4547        )
4548        .unwrap();
4549
4550        let configured: serde_json::Value =
4551            serde_json::from_slice(&std::fs::read(staged.path().join("mcp.json")).unwrap())
4552                .unwrap();
4553        let server = &configured["mcpServers"]["mj-project-memory"];
4554        assert_eq!(server["command"], "sh");
4555        assert_eq!(server["runtime_id"], "local");
4556        assert_eq!(
4557            server["args"],
4558            serde_json::json!([
4559                "-c",
4560                "exec \"$HOME/$1\" worker memory-mcp --root \"$HOME/$2\"",
4561                "mj-project-memory",
4562                ".local/share/hel/workers/session/hel",
4563                ".local/share/hel/profiles/session/projects/project/memory"
4564            ])
4565        );
4566    }
4567    #[test]
4568    fn stage_deepseek_profile_copies_only_portable_configuration() {
4569        let home = tempfile::tempdir().unwrap();
4570        std::fs::write(
4571            home.path().join(".credentials.yaml"),
4572            "version: 1\nrefs: {}\n",
4573        )
4574        .unwrap();
4575        std::fs::write(home.path().join("settings.yaml"), "models: {}\n").unwrap();
4576        std::fs::create_dir(home.path().join("sessions")).unwrap();
4577        std::fs::write(home.path().join("sessions/native-session"), "private state").unwrap();
4578        std::fs::create_dir(home.path().join("profiles")).unwrap();
4579        let staged = tempfile::tempdir().unwrap();
4580        let profile = hel::hel_config::HarnessProfile {
4581            enabled: true,
4582            kind: hel::hel_config::HarnessKind::Deepseek,
4583            home: home.path().to_path_buf(),
4584            environment: BTreeMap::new(),
4585            context_window_bytes: None,
4586        };
4587
4588        stage_profile(&profile, staged.path()).unwrap();
4589
4590        assert!(staged.path().join(".credentials.yaml").is_file());
4591        assert!(staged.path().join("settings.yaml").is_file());
4592        assert!(!staged.path().join("sessions").exists());
4593        assert!(!staged.path().join("profiles").exists());
4594    }
4595    #[test]
4596    fn disposable_container_guidance_reaches_each_harness_without_touching_home() {
4597        let target = hel_targets::TargetLocator::LocalPodman {
4598            container_id: "container".into(),
4599            workspace_storage: Default::default(),
4600        };
4601        for (kind, instructions) in [
4602            (hel::hel_config::HarnessKind::Codex, "AGENTS.md"),
4603            (hel::hel_config::HarnessKind::Claude, "CLAUDE.md"),
4604            (hel::hel_config::HarnessKind::Kimi, "AGENTS.md"),
4605            (hel::hel_config::HarnessKind::Grok, "AGENTS.md"),
4606            (hel::hel_config::HarnessKind::Deepseek, "AGENTS.md"),
4607            (hel::hel_config::HarnessKind::Muse, "AGENTS.md"),
4608        ] {
4609            let home = tempfile::tempdir().unwrap();
4610            let original = "# Controller instructions\n\nKeep this source unchanged.\n";
4611            let source_instructions = home.path().join(instructions);
4612            std::fs::write(&source_instructions, original).unwrap();
4613            let staged = tempfile::tempdir().unwrap();
4614            let profile = hel::hel_config::HarnessProfile {
4615                enabled: true,
4616                kind,
4617                home: home.path().to_path_buf(),
4618                environment: std::collections::BTreeMap::new(),
4619                context_window_bytes: None,
4620            };
4621
4622            stage_profile(&profile, staged.path()).unwrap();
4623            append_hel_target_environment(kind, staged.path(), &target).unwrap();
4624
4625            let guidance = std::fs::read_to_string(staged.path().join(instructions)).unwrap();
4626            assert_eq!(
4627                guidance,
4628                format!("{original}\n{MJ_CONTAINER_ENVIRONMENT}"),
4629                "{instructions} receives the section in the staged profile"
4630            );
4631            assert!(guidance.contains("## Mjolnir disposable environment"));
4632            assert!(!guidance.contains("## Hel disposable environment"));
4633            assert_eq!(
4634                std::fs::read_to_string(source_instructions).unwrap(),
4635                original,
4636                "{instructions} in the controller-side home stays untouched"
4637            );
4638        }
4639    }
4640    #[test]
4641    fn kimi_guidance_uses_agents_md_without_mutating_the_system_override() {
4642        let home = tempfile::tempdir().unwrap();
4643        let system_override = "# Custom Kimi system prompt\n";
4644        std::fs::write(home.path().join("SYSTEM.md"), system_override).unwrap();
4645        let staged = tempfile::tempdir().unwrap();
4646        let profile = hel::hel_config::HarnessProfile {
4647            enabled: true,
4648            kind: hel::hel_config::HarnessKind::Kimi,
4649            home: home.path().to_path_buf(),
4650            environment: std::collections::BTreeMap::new(),
4651            context_window_bytes: None,
4652        };
4653
4654        stage_profile(&profile, staged.path()).unwrap();
4655        append_hel_target_environment(
4656            profile.kind,
4657            staged.path(),
4658            &hel_targets::TargetLocator::LocalPodman {
4659                container_id: "container".into(),
4660                workspace_storage: Default::default(),
4661            },
4662        )
4663        .unwrap();
4664
4665        assert_eq!(
4666            std::fs::read_to_string(staged.path().join("AGENTS.md")).unwrap(),
4667            MJ_CONTAINER_ENVIRONMENT
4668        );
4669        assert_eq!(
4670            std::fs::read_to_string(staged.path().join("SYSTEM.md")).unwrap(),
4671            system_override
4672        );
4673        assert!(!home.path().join("AGENTS.md").exists());
4674        assert_eq!(
4675            std::fs::read_to_string(home.path().join("SYSTEM.md")).unwrap(),
4676            system_override
4677        );
4678    }
4679
4680    #[test]
4681    fn ec2_guidance_names_its_real_workspace_and_ssh_bare_gets_none() {
4682        let ec2 = tempfile::tempdir().unwrap();
4683        append_hel_target_environment(
4684            hel::hel_config::HarnessKind::Codex,
4685            ec2.path(),
4686            &hel_targets::TargetLocator::AwsEc2 {
4687                profile: "profile".into(),
4688                region: "region".into(),
4689                instance_id: "instance".into(),
4690                ssh: hel_targets::SshTarget {
4691                    destination: "host".into(),
4692                    ssh_args: Vec::new(),
4693                },
4694                workspace: ".local/share/hel/workspaces/session".into(),
4695            },
4696        )
4697        .unwrap();
4698        let guidance = std::fs::read_to_string(ec2.path().join("AGENTS.md")).unwrap();
4699        assert_eq!(
4700            guidance,
4701            "## Mjolnir disposable environment\n\nThis session runs on a disposable Mjolnir EC2 instance. When the session closes, Mjolnir checkpoints everything in project workspace directories under `$HOME/.local/share/hel/workspaces/session`, including committed work, staged and unstaged changes, and untracked files. Mjolnir then terminates the instance.\n\nEverything outside `$HOME/.local/share/hel/workspaces/session`, including installed packages, the rest of `$HOME`, and `/tmp`, is ephemeral and will be lost. Keep durable results in the workspace or push them to a remote.\n\nNew workspaces start on their own session branch from the default network fetch remote’s default branch. Local unpublished commits and uncommitted files are not copied. Use normal git push to publish the current branch to the configured network push destination. Closing saves a checkpoint; it does not publish commits or update the original local checkout. Resumed sessions restore their saved work.\n"
4702        );
4703        assert!(!guidance.contains("## Hel disposable environment"));
4704
4705        let ssh_bare = tempfile::tempdir().unwrap();
4706        append_hel_target_environment(
4707            hel::hel_config::HarnessKind::Codex,
4708            ssh_bare.path(),
4709            &hel_targets::TargetLocator::SshBare {
4710                ssh: hel_targets::SshTarget {
4711                    destination: "host".into(),
4712                    ssh_args: Vec::new(),
4713                },
4714                workspace: ".local/share/hel/workspaces/session".into(),
4715            },
4716        )
4717        .unwrap();
4718        assert!(!ssh_bare.path().join("AGENTS.md").exists());
4719    }
4720
4721    #[test]
4722    fn project_memory_replicas_are_session_private() {
4723        let key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
4724        assert_eq!(
4725            project_memory_replica_slug(key, "session-a"),
4726            "hel-0123456789abcdef-session-a"
4727        );
4728        assert_ne!(
4729            project_memory_replica_slug(key, "session-a"),
4730            project_memory_replica_slug(key, "session-b")
4731        );
4732    }
4733
4734    /// Returns a fixed digest line for every command and records what it ran,
4735    /// so a remote refresh can be driven without a real ssh host.
4736    struct DigestExecutor {
4737        installed_line: String,
4738        commands: RefCell<Vec<CommandSpec>>,
4739    }
4740
4741    impl CommandExecutor for DigestExecutor {
4742        fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
4743            self.commands.borrow_mut().push(command.clone());
4744            Ok(CommandOutput {
4745                status: 0,
4746                stdout: self.installed_line.clone().into_bytes(),
4747                stderr: Vec::new(),
4748            })
4749        }
4750    }
4751
4752    // The SshBare worker_root guard requires the workspace to end in the exact
4753    // session ID, so build the locator around the session under test.
4754    fn ssh_bare_locator(session_id: &str) -> hel_targets::TargetLocator {
4755        hel_targets::TargetLocator::SshBare {
4756            ssh: SshTarget {
4757                destination: "user@host.test".into(),
4758                ssh_args: Vec::new(),
4759            },
4760            workspace: format!("/srv/mj/{session_id}"),
4761        }
4762    }
4763
4764    #[test]
4765    fn remote_upgrade_prepares_managed_harness_without_touching_running_worker() {
4766        let session = "session-remote";
4767        let executor = DigestExecutor {
4768            installed_line: String::new(),
4769            commands: RefCell::new(Vec::new()),
4770        };
4771        let launch = WorkerLaunchConfig {
4772            session_id: session.into(),
4773            harness: HarnessKind::Codex,
4774            bridge_command: "ignored".into(),
4775            bridge_args: Vec::new(),
4776            harness_runtime: HarnessRuntimePolicy::Managed,
4777            environment: BTreeMap::new(),
4778            cwd: "/srv/mj/session-remote/project".into(),
4779            additional_directories: Vec::new(),
4780            native_session_id: None,
4781            project_memory: None,
4782            execution_policy: ExecutionPolicy::ConfiguredApprovals,
4783        };
4784
4785        prepare_managed_harness_for_upgrade(
4786            &executor,
4787            &ssh_bare_locator(session),
4788            session,
4789            Path::new("/controller/hel"),
4790            &launch,
4791        )
4792        .unwrap();
4793
4794        let commands = executor.commands.borrow();
4795        let purposes = commands
4796            .iter()
4797            .map(|command| command.purpose.as_str())
4798            .collect::<Vec<_>>();
4799        assert_eq!(
4800            purposes,
4801            vec![
4802                "clear managed harness preparation staging",
4803                "create managed harness preparation staging",
4804                "stage current worker for managed harness preparation",
4805                "stage managed harness launch configuration",
4806                "make managed harness preparation worker executable",
4807                "prepare exact managed harness",
4808                "remove managed harness preparation staging",
4809            ]
4810        );
4811        assert!(commands.iter().all(|command| {
4812            !command.purpose.contains("stop Mjolnir worker")
4813                && !command.purpose.contains("start Mjolnir worker")
4814                && !command
4815                    .purpose
4816                    .contains("install the current Mjolnir worker binary")
4817        }));
4818        let prepare = commands
4819            .iter()
4820            .find(|command| command.purpose == "prepare exact managed harness")
4821            .unwrap();
4822        let rendered = format!("{} {}", prepare.program, prepare.args.join(" "));
4823        assert!(rendered.contains("worker' 'prepare-harness' '--config'"));
4824    }
4825
4826    #[test]
4827    fn local_upgrade_preflight_uses_current_binary_and_preserves_launch_policy() {
4828        struct ConfigRecordingExecutor {
4829            command: RefCell<Option<CommandSpec>>,
4830            launch: RefCell<Option<WorkerLaunchConfig>>,
4831        }
4832
4833        impl CommandExecutor for ConfigRecordingExecutor {
4834            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
4835                let config_path = command
4836                    .args
4837                    .get(3)
4838                    .context("local prepare command did not include its config path")?;
4839                *self.command.borrow_mut() = Some(command.clone());
4840                *self.launch.borrow_mut() = Some(WorkerLaunchConfig::read(Path::new(config_path))?);
4841                Ok(CommandOutput {
4842                    status: 0,
4843                    stdout: Vec::new(),
4844                    stderr: Vec::new(),
4845                })
4846            }
4847        }
4848
4849        struct FailingExecutor {
4850            purposes: RefCell<Vec<String>>,
4851        }
4852
4853        impl CommandExecutor for FailingExecutor {
4854            fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
4855                self.purposes.borrow_mut().push(command.purpose.clone());
4856                Err(anyhow::anyhow!("managed harness installation failed"))
4857            }
4858        }
4859
4860        let executor = ConfigRecordingExecutor {
4861            command: RefCell::new(None),
4862            launch: RefCell::new(None),
4863        };
4864        let launch = WorkerLaunchConfig {
4865            session_id: "session-local".into(),
4866            harness: HarnessKind::Codex,
4867            bridge_command: "ignored".into(),
4868            bridge_args: Vec::new(),
4869            harness_runtime: HarnessRuntimePolicy::Managed,
4870            environment: BTreeMap::from([("CODEX_HOME".into(), "/configured/profile/home".into())]),
4871            cwd: "/workspace/project".into(),
4872            additional_directories: Vec::new(),
4873            native_session_id: None,
4874            project_memory: None,
4875            execution_policy: ExecutionPolicy::ConfiguredApprovals,
4876        };
4877        let locator = hel_targets::TargetLocator::LocalBare {
4878            worker_root: "/worker/session-local".into(),
4879        };
4880
4881        prepare_managed_harness_for_upgrade(
4882            &executor,
4883            &locator,
4884            "session-local",
4885            Path::new("/controller/hel"),
4886            &launch,
4887        )
4888        .unwrap();
4889
4890        {
4891            let command = executor.command.borrow();
4892            let command = command.as_ref().unwrap();
4893            assert_eq!(command.purpose, "prepare exact managed harness");
4894            assert_eq!(command.program, "/controller/hel");
4895            assert_eq!(
4896                &command.args[..3],
4897                ["worker", "prepare-harness", "--config"]
4898            );
4899            assert!(!command.args[3].contains("/worker/session-local"));
4900        }
4901
4902        let prepared = executor.launch.borrow();
4903        let prepared = prepared.as_ref().unwrap();
4904        assert_eq!(
4905            prepared.environment.get("CODEX_HOME").map(String::as_str),
4906            Some("/configured/profile/home")
4907        );
4908        assert_eq!(
4909            prepared.execution_policy,
4910            ExecutionPolicy::ConfiguredApprovals
4911        );
4912
4913        let failing = FailingExecutor {
4914            purposes: RefCell::new(Vec::new()),
4915        };
4916        let error = prepare_managed_harness_for_upgrade(
4917            &failing,
4918            &locator,
4919            "session-local",
4920            Path::new("/controller/hel"),
4921            &launch,
4922        )
4923        .unwrap_err();
4924        assert!(
4925            error
4926                .to_string()
4927                .contains("managed harness installation failed")
4928        );
4929        assert_eq!(
4930            failing.purposes.borrow().as_slice(),
4931            ["prepare exact managed harness"]
4932        );
4933    }
4934
4935    #[test]
4936    fn initial_bare_provision_prepares_the_harness_from_installed_files() {
4937        let session = "session-remote";
4938        let executor = DigestExecutor {
4939            installed_line: String::new(),
4940            commands: RefCell::new(Vec::new()),
4941        };
4942        let mut launch = WorkerLaunchConfig {
4943            session_id: session.into(),
4944            harness: HarnessKind::Kimi,
4945            bridge_command: "ignored".into(),
4946            bridge_args: Vec::new(),
4947            harness_runtime: HarnessRuntimePolicy::Managed,
4948            environment: BTreeMap::new(),
4949            cwd: "/srv/mj/session-remote/project".into(),
4950            additional_directories: Vec::new(),
4951            native_session_id: None,
4952            project_memory: None,
4953            execution_policy: ExecutionPolicy::ConfiguredApprovals,
4954        };
4955
4956        let locator = ssh_bare_locator(session);
4957        prepare_installed_managed_harness(&executor, &locator, "/worker/root", &launch).unwrap();
4958        let commands = executor.commands.borrow();
4959        assert_eq!(commands.len(), 1);
4960        assert_eq!(
4961            commands[0].purpose,
4962            "prepare exact managed harness before worker startup"
4963        );
4964        let rendered = format!("{} {}", commands[0].program, commands[0].args.join(" "));
4965        assert!(rendered.contains("'/worker/root/hel' 'worker' 'prepare-harness'"));
4966        drop(commands);
4967
4968        let local = hel_targets::TargetLocator::LocalBare {
4969            worker_root: "/worker/session-remote".into(),
4970        };
4971        prepare_installed_managed_harness(&executor, &local, "/worker/session-remote", &launch)
4972            .unwrap();
4973        let commands = executor.commands.borrow();
4974        assert_eq!(commands.len(), 2);
4975        assert_eq!(commands[1].program, "/worker/session-remote/hel");
4976        assert_eq!(
4977            commands[1].args,
4978            vec![
4979                "worker".to_owned(),
4980                "prepare-harness".to_owned(),
4981                "--config".to_owned(),
4982                "/worker/session-remote/launch.json".to_owned(),
4983            ]
4984        );
4985        drop(commands);
4986
4987        launch.harness_runtime = HarnessRuntimePolicy::Ambient;
4988        prepare_installed_managed_harness(&executor, &locator, "/worker/root", &launch).unwrap();
4989        assert_eq!(executor.commands.borrow().len(), 2);
4990    }
4991
4992    #[test]
4993    fn a_remote_worker_with_a_mismatched_binary_is_replaced_before_restart() {
4994        let directory = tempfile::tempdir().unwrap();
4995        let source = directory.path().join("worker");
4996        std::fs::write(&source, b"fresh musl worker").unwrap();
4997        let executor = DigestExecutor {
4998            installed_line: format!("{}  /root/hel\n", "0".repeat(64)),
4999            commands: RefCell::new(Vec::new()),
5000        };
5001        let replaced = replace_remote_worker_binary_if_stale(
5002            &executor,
5003            &ssh_bare_locator("session-remote"),
5004            "session-remote",
5005            &CommandSpec::new("true", Vec::<String>::new()),
5006            &source,
5007        )
5008        .unwrap();
5009        assert!(replaced, "a stale remote binary must be replaced");
5010        assert!(
5011            executor.commands.borrow().len() > 1,
5012            "the digest probe must be followed by replacement commands"
5013        );
5014    }
5015
5016    #[test]
5017    fn a_remote_worker_already_current_is_restarted_without_recopying() {
5018        let directory = tempfile::tempdir().unwrap();
5019        let source = directory.path().join("worker");
5020        std::fs::write(&source, b"fresh musl worker").unwrap();
5021        let current = hel::hel_worker_launch::worker_executable_digest(&source).unwrap();
5022        let executor = DigestExecutor {
5023            installed_line: format!("{current}  /root/hel\n"),
5024            commands: RefCell::new(Vec::new()),
5025        };
5026        let replaced = replace_remote_worker_binary_if_stale(
5027            &executor,
5028            &ssh_bare_locator("session-remote"),
5029            "session-remote",
5030            &CommandSpec::new("true", Vec::<String>::new()),
5031            &source,
5032        )
5033        .unwrap();
5034        assert!(!replaced, "a current remote binary must not be recopied");
5035        assert_eq!(
5036            executor.commands.borrow().len(),
5037            1,
5038            "only the digest probe runs when the binary is already current"
5039        );
5040    }
5041
5042    #[test]
5043    fn a_remote_recovery_plan_defers_binary_refresh_to_the_recovery_task() {
5044        let locator = ssh_bare_locator("session-remote");
5045        let refresh = worker_binary_refresh_plan(&locator, "session-remote")
5046            .unwrap()
5047            .expect("a remote target now gets a binary refresh");
5048        match refresh {
5049            WorkerBinaryRefresh::Remote(remote) => {
5050                assert_eq!(remote.session_id, "session-remote");
5051                assert_eq!(remote.locator, locator);
5052            }
5053            WorkerBinaryRefresh::Prepared(_) => {
5054                panic!("a remote target must defer, not prepare, its binary refresh")
5055            }
5056        }
5057    }
5058}