Skip to main content

mj_controller/hel_controller/
worker_binary.rs

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