Skip to main content

mj_controller/hel_controller/
worker_binary.rs

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