Skip to main content

runner_manager_agent/
oci.rs

1//! Rootless Podman-compatible provider for dependency-isolated Linux runners.
2//! The runtime is installed by the operator; this adapter never starts a native
3//! runner for an isolated policy and never grants a host filesystem mount.
4
5use std::collections::BTreeMap;
6use std::io::Write;
7use std::path::Path;
8use std::process::{Child, Command, Stdio};
9use std::sync::Mutex;
10
11use runner_manager_domain::attempt::{FailureReason, IsolationProviderFailure, RunnerAttempt};
12use runner_manager_domain::execution::{
13    AttemptExecution, Backend, ExecutionPolicy, ImageReference,
14};
15use runner_manager_domain::model::{AttemptId, HostId};
16use runner_manager_domain::policy::ScalePolicy;
17use runner_manager_github::jit::EncodedJitConfig;
18
19use crate::lifecycle::{
20    EnvironmentIdentity, EnvironmentState, ExecutionProvider, NativeProcesses, OneTimeJitHandoff,
21    PreparedEnvironment, ProcessStartFailure, ProviderCapability, ProviderDestroy, ProviderStop,
22    ResolvedEnvironment,
23};
24
25const HOST_LABEL: &str = "io.runner-manager.host";
26const ATTEMPT_LABEL: &str = "io.runner-manager.attempt";
27const GENERATION_LABEL: &str = "io.runner-manager.generation";
28const IMAGE_LABEL: &str = "io.runner-manager.image";
29const BOOTSTRAP: &str = "IFS= read -r jit || exit 43; export ACTIONS_RUNNER_INPUT_JITCONFIG=\"$jit\"; unset jit; cd /runner || exit 44; exec ./bin/Runner.Listener run";
30const OCI_RUNTIME_ENV: &str = "RUNNER_MANAGER_OCI_RUNTIME";
31const BOUNDED_STORAGE_SCHEMA: u64 = 1;
32const BOUNDED_STORAGE_MODE: &str = "exclusive-filesystem-pool";
33const QUOTA_PROBE_MIB: u64 = 1024;
34
35#[derive(Debug)]
36pub struct OciProcesses {
37    host: HostId,
38    native: NativeProcesses,
39    runtime: String,
40    runtime_configuration_valid: bool,
41    attached: Mutex<BTreeMap<AttemptId, Child>>,
42}
43
44impl OciProcesses {
45    #[must_use]
46    pub fn new(host: HostId) -> Self {
47        let (runtime, runtime_configuration_valid) = match std::env::var(OCI_RUNTIME_ENV) {
48            Ok(runtime) if operator_runtime_is_trusted(Path::new(&runtime)) => (runtime, true),
49            Ok(_) => ("/runner-manager-invalid-oci-runtime".into(), false),
50            Err(std::env::VarError::NotPresent) => ("podman".into(), true),
51            Err(std::env::VarError::NotUnicode(_)) => {
52                ("/runner-manager-invalid-oci-runtime".into(), false)
53            }
54        };
55        Self {
56            host,
57            native: NativeProcesses::new(),
58            runtime,
59            runtime_configuration_valid,
60            attached: Mutex::new(BTreeMap::new()),
61        }
62    }
63
64    fn command(&self) -> Command {
65        let mut command = Command::new(&self.runtime);
66        command.stdin(Stdio::null()).stderr(Stdio::null());
67        command
68    }
69
70    fn output(&self, args: &[&str]) -> Result<String, FailureReason> {
71        let output = self
72            .command()
73            .args(args)
74            .output()
75            .map_err(|_| provider_failure())?;
76        if !output.status.success() || output.stdout.len() > 64 * 1024 {
77            return Err(provider_failure());
78        }
79        String::from_utf8(output.stdout).map_err(|_| provider_failure())
80    }
81
82    fn status(&self, args: &[&str]) -> Result<(), FailureReason> {
83        let status = self
84            .command()
85            .args(args)
86            .stdout(Stdio::null())
87            .status()
88            .map_err(|_| provider_failure())?;
89        if status.success() {
90            Ok(())
91        } else {
92            Err(provider_failure())
93        }
94    }
95
96    fn rootless_ready(&self) -> ProviderCapability {
97        self.rootless_ready_for(QUOTA_PROBE_MIB)
98    }
99
100    fn rootless_ready_for(&self, requested_mib: u64) -> ProviderCapability {
101        if !cfg!(target_os = "linux") {
102            return ProviderCapability::Unsupported;
103        }
104        if !self.runtime_configuration_valid {
105            return ProviderCapability::PermissionDenied;
106        }
107        let info = match self.command().args(["info", "--format=json"]).output() {
108            Ok(output) if output.status.success() && output.stdout.len() <= 64 * 1024 => {
109                match String::from_utf8(output.stdout) {
110                    Ok(info) => info,
111                    Err(_) => return ProviderCapability::Degraded,
112                }
113            }
114            Ok(_) => return ProviderCapability::PermissionDenied,
115            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
116                return ProviderCapability::NotInstalled;
117            }
118            Err(_) => return ProviderCapability::Degraded,
119        };
120        let Ok(info) = serde_json::from_str::<serde_json::Value>(&info) else {
121            return ProviderCapability::Degraded;
122        };
123        if info
124            .pointer("/host/security/rootless")
125            .and_then(serde_json::Value::as_bool)
126            != Some(true)
127        {
128            return ProviderCapability::PermissionDenied;
129        }
130        let mappings = ["/host/idMappings/uidmap", "/host/idMappings/gidmap"];
131        if mappings.iter().any(|path| {
132            info.pointer(path)
133                .and_then(serde_json::Value::as_array)
134                .is_none_or(|ranges| {
135                    !ranges.iter().any(|range| {
136                        range
137                            .get("size")
138                            .and_then(serde_json::Value::as_u64)
139                            .is_some_and(|size| size >= 65536)
140                    })
141                })
142        }) {
143            return ProviderCapability::PermissionDenied;
144        }
145        if info
146            .pointer("/host/cgroupVersion")
147            .and_then(serde_json::Value::as_str)
148            != Some("v2")
149        {
150            return ProviderCapability::Degraded;
151        }
152        let Some(controllers) = info
153            .pointer("/host/cgroupControllers")
154            .and_then(serde_json::Value::as_array)
155        else {
156            return ProviderCapability::Degraded;
157        };
158        if ["cpu", "memory", "pids"].iter().any(|needed| {
159            !controllers
160                .iter()
161                .any(|controller| controller.as_str() == Some(needed))
162        }) {
163            return ProviderCapability::Degraded;
164        }
165        let Some(graph_root) = info
166            .pointer("/store/graphRoot")
167            .and_then(serde_json::Value::as_str)
168        else {
169            return ProviderCapability::Degraded;
170        };
171        let Some(run_root) = info
172            .pointer("/store/runRoot")
173            .and_then(serde_json::Value::as_str)
174        else {
175            return ProviderCapability::Degraded;
176        };
177        if !graph_root.starts_with('/')
178            || !run_root.starts_with('/')
179            || graph_root.starts_with("/mnt/")
180            || run_root.starts_with("/mnt/")
181        {
182            return ProviderCapability::PermissionDenied;
183        }
184        let backing_filesystem = info
185            .pointer("/store/graphStatus/Backing Filesystem")
186            .and_then(serde_json::Value::as_str);
187        let storage_size = format!("size={requested_mib}m");
188        let quota_probe =
189            match self.output(&["--storage-opt", &storage_size, "info", "--format=json"]) {
190                Ok(output) => output,
191                Err(_) => return ProviderCapability::DiskQuotaUnavailable,
192            };
193        // Native Podman writable-layer quotas require XFS project quotas. XFS
194        // alone is insufficient: rootless quota setup can still fail with
195        // EPERM, which the command above catches before allocation or JIT.
196        if backing_filesystem == Some("xfs") {
197            return ProviderCapability::Ready;
198        }
199        // Managed WSL can instead use an operator-provisioned runtime helper.
200        // The helper routes every `size=...` create into an exclusive finite
201        // filesystem and attests that contract in the quota-probe response.
202        // Plain Podman on extfs has no attestation and remains fail-closed.
203        let Ok(quota_info) = serde_json::from_str::<serde_json::Value>(&quota_probe) else {
204            return ProviderCapability::DiskQuotaUnavailable;
205        };
206        if bounded_storage_attested(&quota_info, graph_root, run_root, requested_mib) {
207            ProviderCapability::Ready
208        } else {
209            ProviderCapability::DiskQuotaUnavailable
210        }
211    }
212
213    fn capability_or(&self, fallback: FailureReason) -> FailureReason {
214        let capability = self.rootless_ready();
215        if capability == ProviderCapability::Ready {
216            fallback
217        } else {
218            capability.refusal()
219        }
220    }
221
222    fn image_ready(&self, image: &ImageReference) -> Result<(), FailureReason> {
223        let Some((_, expected)) = image.as_str().rsplit_once("@sha256:") else {
224            return Err(image_failure());
225        };
226        self.status(&["pull", "--quiet", image.as_str()])
227            .map_err(|_| self.capability_or(image_failure()))?;
228        let digests = self
229            .output(&[
230                "image",
231                "inspect",
232                "--format",
233                "{{json .RepoDigests}}",
234                image.as_str(),
235            ])
236            .map_err(|_| self.capability_or(image_failure()))?;
237        let values: Vec<String> =
238            serde_json::from_str(digests.trim()).map_err(|_| image_failure())?;
239        if !values
240            .iter()
241            .any(|value| value.ends_with(&format!("@sha256:{expected}")))
242        {
243            return Err(image_failure());
244        }
245        Ok(())
246    }
247
248    fn isolated_identity(
249        &self,
250        attempt: &RunnerAttempt,
251        id: String,
252    ) -> Result<EnvironmentIdentity, FailureReason> {
253        let AttemptExecution::Isolated {
254            provider_kind: Backend::Oci,
255            resolved_image,
256            generation,
257            ..
258        } = attempt.execution()
259        else {
260            return Err(ownership_failure());
261        };
262        Ok(EnvironmentIdentity::Isolated {
263            host: self.host,
264            attempt: attempt.id,
265            provider_kind: Backend::Oci,
266            environment_id: id,
267            resolved_image: resolved_image.clone(),
268            generation: generation.clone(),
269        })
270    }
271
272    fn environment_id<'a>(&self, attempt: &'a RunnerAttempt) -> Result<&'a str, FailureReason> {
273        let AttemptExecution::Isolated {
274            provider_kind: Backend::Oci,
275            environment_id: Some(id),
276            ..
277        } = attempt.execution()
278        else {
279            return Err(ownership_failure());
280        };
281        Ok(id)
282    }
283
284    fn labels(&self, id: &str) -> Result<BTreeMap<String, String>, FailureReason> {
285        let labels = self.output(&[
286            "container",
287            "inspect",
288            "--format",
289            "{{json .Config.Labels}}",
290            id,
291        ])?;
292        serde_json::from_str(labels.trim()).map_err(|_| ownership_failure())
293    }
294
295    fn exists(&self, id: &str) -> Result<bool, FailureReason> {
296        let status = self
297            .command()
298            .args(["container", "exists", id])
299            .stdout(Stdio::null())
300            .status()
301            .map_err(|_| provider_failure())?;
302        match status.code() {
303            Some(0) => Ok(true),
304            Some(1) => Ok(false),
305            _ => Err(provider_failure()),
306        }
307    }
308
309    fn check_ownership(&self, attempt: &RunnerAttempt) -> Result<bool, FailureReason> {
310        let id = self.environment_id(attempt)?;
311        let labels = self.labels(id)?;
312        let AttemptExecution::Isolated { resolved_image, .. } = attempt.execution() else {
313            return Err(ownership_failure());
314        };
315        Ok(self.matches_attempt_labels(&labels, attempt)
316            && labels
317                .get(IMAGE_LABEL)
318                .is_some_and(|value| value == resolved_image.as_str()))
319    }
320
321    fn matches_attempt_labels(
322        &self,
323        labels: &BTreeMap<String, String>,
324        attempt: &RunnerAttempt,
325    ) -> bool {
326        let AttemptExecution::Isolated { generation, .. } = attempt.execution() else {
327            return false;
328        };
329        labels.get(HOST_LABEL) == Some(&self.host.to_string())
330            && labels.get(ATTEMPT_LABEL) == Some(&attempt.id.to_string())
331            && labels.get(GENERATION_LABEL) == Some(generation)
332    }
333
334    fn state(&self, attempt: &RunnerAttempt) -> Result<EnvironmentState, FailureReason> {
335        if !self.exists(self.environment_id(attempt)?)? {
336            return Ok(EnvironmentState::Missing);
337        }
338        if !self.check_ownership(attempt)? {
339            return Err(ownership_failure());
340        }
341        let status = self.output(&[
342            "container",
343            "inspect",
344            "--format",
345            "{{.State.Status}}",
346            self.environment_id(attempt)?,
347        ])?;
348        match status.trim() {
349            "running" => Ok(EnvironmentState::Running),
350            "created" | "configured" => Ok(EnvironmentState::Starting),
351            "exited" | "stopped" => Ok(EnvironmentState::Exited),
352            _ => Err(provider_failure()),
353        }
354    }
355
356    fn reap_attached(&self, attempt: AttemptId) {
357        let child = self
358            .attached
359            .lock()
360            .unwrap_or_else(std::sync::PoisonError::into_inner)
361            .remove(&attempt);
362        if let Some(mut child) = child {
363            if child.try_wait().ok().flatten().is_none() {
364                let _ = child.kill();
365            }
366            let _ = child.wait();
367        }
368    }
369}
370
371#[cfg(unix)]
372fn operator_runtime_is_trusted(path: &Path) -> bool {
373    use std::os::unix::fs::{MetadataExt, PermissionsExt};
374
375    if !path.is_absolute() {
376        return false;
377    }
378    let Ok(canonical) = std::fs::canonicalize(path) else {
379        return false;
380    };
381    if canonical == Path::new("/mnt") || canonical.starts_with("/mnt/") {
382        return false;
383    }
384    let Ok(metadata) = canonical.metadata() else {
385        return false;
386    };
387    let mode = metadata.permissions().mode();
388    metadata.is_file() && metadata.uid() == 0 && mode & 0o111 != 0 && mode & 0o022 == 0
389}
390
391#[cfg(not(unix))]
392fn operator_runtime_is_trusted(_path: &Path) -> bool {
393    false
394}
395
396fn bounded_storage_attested(
397    info: &serde_json::Value,
398    graph_root: &str,
399    run_root: &str,
400    requested_mib: u64,
401) -> bool {
402    info.pointer("/host/security/rootless")
403        .and_then(serde_json::Value::as_bool)
404        == Some(true)
405        && info
406            .pointer("/store/graphRoot")
407            .and_then(serde_json::Value::as_str)
408            == Some(graph_root)
409        && info
410            .pointer("/store/runRoot")
411            .and_then(serde_json::Value::as_str)
412            == Some(run_root)
413        && info
414            .pointer("/runnerManagerStorage/schema")
415            .and_then(serde_json::Value::as_u64)
416            == Some(BOUNDED_STORAGE_SCHEMA)
417        && info
418            .pointer("/runnerManagerStorage/mode")
419            .and_then(serde_json::Value::as_str)
420            == Some(BOUNDED_STORAGE_MODE)
421        && info
422            .pointer("/runnerManagerStorage/requestedMiB")
423            .and_then(serde_json::Value::as_u64)
424            == Some(requested_mib)
425        && info
426            .pointer("/runnerManagerStorage/hardCap")
427            .and_then(serde_json::Value::as_bool)
428            == Some(true)
429}
430
431fn provider_failure() -> FailureReason {
432    FailureReason::IsolationProvider(IsolationProviderFailure::RuntimeOperationFailed)
433}
434fn image_failure() -> FailureReason {
435    FailureReason::IsolationProvider(IsolationProviderFailure::ImageUnavailableOrIncompatible)
436}
437fn ownership_failure() -> FailureReason {
438    FailureReason::IsolationProvider(IsolationProviderFailure::OwnershipMismatch)
439}
440
441#[cfg(test)]
442fn retained_after_restart(state: EnvironmentState) -> bool {
443    matches!(state, EnvironmentState::Starting | EnvironmentState::Exited)
444}
445
446impl ExecutionProvider for OciProcesses {
447    fn probe(&self, policy: &ScalePolicy) -> ProviderCapability {
448        if policy.execution_policy().is_native() {
449            return ProviderCapability::Ready;
450        }
451        let ExecutionPolicy::Isolated {
452            backend,
453            image,
454            resources,
455        } = policy.execution_policy()
456        else {
457            return ProviderCapability::Unsupported;
458        };
459        if !matches!(backend, Backend::Auto | Backend::Oci) {
460            return ProviderCapability::Unsupported;
461        }
462        let capability = self.rootless_ready_for(u64::from(resources.disk_mib));
463        if capability != ProviderCapability::Ready {
464            return capability;
465        }
466        if self.image_ready(image).is_err() {
467            let capability = self.rootless_ready_for(u64::from(resources.disk_mib));
468            if capability == ProviderCapability::Ready {
469                ProviderCapability::ImageUnavailableOrIncompatible
470            } else {
471                capability
472            }
473        } else {
474            ProviderCapability::Ready
475        }
476    }
477
478    fn resolve(&self, policy: &ScalePolicy) -> Result<Option<ResolvedEnvironment>, FailureReason> {
479        let ExecutionPolicy::Isolated {
480            backend,
481            image,
482            resources,
483        } = policy.execution_policy()
484        else {
485            return Ok(None);
486        };
487        if !matches!(backend, Backend::Auto | Backend::Oci) {
488            return Err(ProviderCapability::Unsupported.refusal());
489        }
490        let capability = self.rootless_ready_for(u64::from(resources.disk_mib));
491        if capability != ProviderCapability::Ready {
492            return Err(capability.refusal());
493        }
494        self.image_ready(image)?;
495        Ok(Some(ResolvedEnvironment {
496            provider_kind: Backend::Oci,
497            image: image.clone(),
498        }))
499    }
500
501    fn prepare(
502        &self,
503        attempt: &RunnerAttempt,
504        policy: &ScalePolicy,
505    ) -> Result<PreparedEnvironment, FailureReason> {
506        let ExecutionPolicy::Isolated { resources, .. } = policy.execution_policy() else {
507            return ExecutionProvider::prepare(&self.native, attempt, policy);
508        };
509        let AttemptExecution::Isolated {
510            provider_kind: Backend::Oci,
511            resolved_image,
512            generation,
513            ..
514        } = attempt.execution()
515        else {
516            return Err(ownership_failure());
517        };
518        // Managed WSL must keep runner bytes in the distribution's Linux FS.
519        if attempt
520            .runtime_path()
521            .to_str()
522            .is_none_or(|path| !path.starts_with('/') || path.starts_with("/mnt/"))
523        {
524            return Err(FailureReason::IsolationProvider(
525                IsolationProviderFailure::UnsafeRuntimePath,
526            ));
527        }
528        let capability = self.rootless_ready_for(u64::from(resources.disk_mib));
529        if capability != ProviderCapability::Ready {
530            return Err(capability.refusal());
531        }
532        let name = format!(
533            "rm-{}-{}",
534            attempt.id,
535            &generation[..generation.len().min(8)]
536        );
537        let cpu = format!("{:.3}", f64::from(resources.cpu_millis) / 1000.0);
538        let memory = format!("{}m", resources.memory_mib);
539        let disk = format!("size={}m", resources.disk_mib);
540        let host_label = format!("{HOST_LABEL}={}", self.host);
541        let attempt_label = format!("{ATTEMPT_LABEL}={}", attempt.id);
542        let generation_label = format!("{GENERATION_LABEL}={generation}");
543        let image_label = format!("{IMAGE_LABEL}={}", resolved_image.as_str());
544        let id = self
545            .output(&[
546                "create",
547                "--interactive",
548                "--name",
549                &name,
550                "--pull=never",
551                "--entrypoint=sh",
552                "--log-driver=none",
553                "--image-volume=ignore",
554                "--http-proxy=false",
555                "--userns=keep-id",
556                "--user=0",
557                "--cap-drop=all",
558                "--cap-add=CHOWN",
559                "--cap-add=SETUID",
560                "--cap-add=SETGID",
561                "--cap-add=DAC_OVERRIDE",
562                "--cap-add=FOWNER",
563                "--security-opt=no-new-privileges",
564                "--network=slirp4netns",
565                "--pids-limit=128",
566                "--cpus",
567                &cpu,
568                "--memory",
569                &memory,
570                "--storage-opt",
571                &disk,
572                "--label",
573                &host_label,
574                "--label",
575                &attempt_label,
576                "--label",
577                &generation_label,
578                "--label",
579                &image_label,
580                resolved_image.as_str(),
581                "-c",
582                BOOTSTRAP,
583            ])
584            .map_err(|reason| self.capability_or(reason))?
585            .trim()
586            .to_owned();
587        if id.is_empty() || !id.bytes().all(|byte| byte.is_ascii_hexdigit()) {
588            return Err(self.capability_or(provider_failure()));
589        }
590        let identity = self.isolated_identity(attempt, id.clone())?;
591        // The package copy is host-to-guest only; no host path is mounted.
592        let source = format!("{}/.", attempt.runtime_path().display());
593        let destination = format!("{id}:/runner");
594        if self.status(&["cp", &source, &destination]).is_err() {
595            // Roll back before returning without a journalled environment ID.
596            // An unavailable runtime leaves a labelled orphan for enumeration.
597            if self
598                .labels(&id)
599                .is_ok_and(|labels| self.matches_attempt_labels(&labels, attempt))
600            {
601                let _ = self.status(&["rm", "--force", &id]);
602            }
603            return Err(self.capability_or(provider_failure()));
604        }
605        Ok(PreparedEnvironment::isolated(attempt.id, identity))
606    }
607
608    fn start(
609        &self,
610        prepared: PreparedEnvironment,
611        attempt: &RunnerAttempt,
612        handoff: OneTimeJitHandoff<'_>,
613    ) -> Result<EnvironmentIdentity, ProcessStartFailure> {
614        if attempt.execution().is_native() {
615            return ExecutionProvider::start(&self.native, prepared, attempt, handoff);
616        }
617        let Some(identity @ EnvironmentIdentity::Isolated { .. }) = prepared.identity().cloned()
618        else {
619            return Err(ProcessStartFailure::before_spawn(ownership_failure()));
620        };
621        let EnvironmentIdentity::Isolated { environment_id, .. } = &identity else {
622            unreachable!()
623        };
624        if self.environment_id(attempt).ok() != Some(environment_id.as_str())
625            || self.check_ownership(attempt) != Ok(true)
626        {
627            return Err(ProcessStartFailure::before_spawn(ownership_failure()));
628        }
629        let config = handoff.consume();
630        let payload = config.expose();
631        if payload.is_empty()
632            || payload.len() > 64 * 1024
633            || payload
634                .bytes()
635                .any(|byte| byte == b'\n' || byte == b'\r' || byte == 0)
636        {
637            return Err(ProcessStartFailure::before_spawn(
638                FailureReason::IsolationProvider(IsolationProviderFailure::JitHandoffRejected),
639            ));
640        }
641        let mut command = self.command();
642        let mut child = command
643            .args([
644                "start",
645                "--attach",
646                "--interactive",
647                "--sig-proxy=false",
648                environment_id,
649            ])
650            .stdin(Stdio::piped())
651            .stdout(Stdio::null())
652            .stderr(Stdio::null())
653            .spawn()
654            .map_err(|_| ProcessStartFailure::before_spawn(provider_failure()))?;
655        let write = child.stdin.take().is_some_and(|mut stdin| {
656            stdin.write_all(payload.as_bytes()).is_ok() && stdin.write_all(b"\n").is_ok()
657        });
658        if !write {
659            let _ = child.kill();
660            let _ = child.wait();
661            return Err(ProcessStartFailure::before_spawn(provider_failure()));
662        }
663        self.attached
664            .lock()
665            .unwrap_or_else(std::sync::PoisonError::into_inner)
666            .insert(attempt.id, child);
667        Ok(identity)
668    }
669
670    fn inspect(&self, attempt: &RunnerAttempt) -> Result<EnvironmentState, FailureReason> {
671        if attempt.execution().is_native() {
672            return ExecutionProvider::inspect(&self.native, attempt);
673        }
674        self.state(attempt)
675    }
676
677    fn recover(&self, attempt: &RunnerAttempt) -> Result<EnvironmentState, FailureReason> {
678        let AttemptExecution::Isolated {
679            environment_id: None,
680            generation,
681            ..
682        } = attempt.execution()
683        else {
684            return self.inspect(attempt);
685        };
686        let filters = [
687            format!("label={HOST_LABEL}={}", self.host),
688            format!("label={ATTEMPT_LABEL}={}", attempt.id),
689            format!("label={GENERATION_LABEL}={generation}"),
690        ];
691        let ids = self.output(&[
692            "ps",
693            "--all",
694            "--no-trunc",
695            "--filter",
696            &filters[0],
697            "--filter",
698            &filters[1],
699            "--filter",
700            &filters[2],
701            "--format",
702            "{{.ID}}",
703        ])?;
704        if ids.trim().is_empty() {
705            Ok(EnvironmentState::Missing)
706        } else {
707            Ok(EnvironmentState::Starting)
708        }
709    }
710
711    fn stop(&self, attempt: &RunnerAttempt) -> Result<ProviderStop, FailureReason> {
712        if attempt.execution().is_native() {
713            return ExecutionProvider::stop(&self.native, attempt);
714        }
715        if !self.exists(self.environment_id(attempt)?)? {
716            return Ok(ProviderStop::Stopped);
717        }
718        if self.state(attempt)? == EnvironmentState::Exited {
719            return Ok(ProviderStop::Stopped);
720        }
721        if !self.check_ownership(attempt)? {
722            return Err(ownership_failure());
723        }
724        self.status(&["stop", "--time=10", self.environment_id(attempt)?])
725            .map_err(|reason| self.capability_or(reason))?;
726        Ok(ProviderStop::Stopped)
727    }
728
729    fn destroy(&self, attempt: &RunnerAttempt) -> Result<ProviderDestroy, FailureReason> {
730        if attempt.execution().is_native() {
731            return ExecutionProvider::destroy(&self.native, attempt);
732        }
733        if !self.exists(self.environment_id(attempt)?)? {
734            self.reap_attached(attempt.id);
735            return Ok(ProviderDestroy::Destroyed);
736        }
737        if !self.check_ownership(attempt)? {
738            return Err(ownership_failure());
739        }
740        self.status(&["rm", "--force", self.environment_id(attempt)?])
741            .map_err(|reason| self.capability_or(reason))?;
742        self.reap_attached(attempt.id);
743        Ok(ProviderDestroy::Destroyed)
744    }
745
746    fn owns(&self, attempt: &RunnerAttempt) -> Result<bool, FailureReason> {
747        if attempt.execution().is_native() {
748            Ok(true)
749        } else {
750            if !self.exists(self.environment_id(attempt)?)? {
751                return Ok(false);
752            }
753            self.check_ownership(attempt)
754        }
755    }
756
757    fn enumerate_owned(&self, host: HostId) -> Vec<EnvironmentIdentity> {
758        // Recovery discovery must still work when a capability has degraded
759        // since creation. A failed readiness probe is not evidence of absence.
760        if host != self.host {
761            return Vec::new();
762        }
763        let filter = format!("label={HOST_LABEL}={host}");
764        let Ok(ids) = self.output(&[
765            "ps",
766            "--all",
767            "--no-trunc",
768            "--filter",
769            &filter,
770            "--format",
771            "{{.ID}}",
772        ]) else {
773            return Vec::new();
774        };
775        ids.lines()
776            .filter_map(|id| {
777                if id.is_empty() || !id.bytes().all(|byte| byte.is_ascii_hexdigit()) {
778                    return None;
779                }
780                let labels = self.labels(id).ok()?;
781                if labels.get(HOST_LABEL)? != &host.to_string() {
782                    return None;
783                }
784                let attempt =
785                    AttemptId::from_uuid(uuid::Uuid::parse_str(labels.get(ATTEMPT_LABEL)?).ok()?);
786                let generation = labels.get(GENERATION_LABEL)?.clone();
787                let resolved_image = ImageReference::new(labels.get(IMAGE_LABEL)?.clone()).ok()?;
788                Some(EnvironmentIdentity::Isolated {
789                    host,
790                    attempt,
791                    provider_kind: Backend::Oci,
792                    environment_id: id.to_owned(),
793                    resolved_image,
794                    generation,
795                })
796            })
797            .collect()
798    }
799
800    fn spawn(
801        &self,
802        attempt: &RunnerAttempt,
803        config: &EncodedJitConfig,
804    ) -> Result<u32, ProcessStartFailure> {
805        if !attempt.execution().is_native() {
806            return Err(ProcessStartFailure::before_spawn(ownership_failure()));
807        }
808        self.native.spawn(attempt, config)
809    }
810    fn is_alive(&self, attempt: &RunnerAttempt) -> Result<bool, FailureReason> {
811        if !attempt.execution().is_native() {
812            return Err(ownership_failure());
813        }
814        self.native.is_alive(attempt)
815    }
816    fn recovered_pid(&self, attempt: &RunnerAttempt) -> Result<Option<u32>, FailureReason> {
817        if !attempt.execution().is_native() {
818            return Err(ownership_failure());
819        }
820        self.native.recovered_pid(attempt)
821    }
822    fn completed_successfully(&self, attempt: &RunnerAttempt) -> bool {
823        if attempt.execution().is_native() {
824            return self.native.completed_successfully(attempt);
825        }
826        self.attached
827            .lock()
828            .unwrap_or_else(std::sync::PoisonError::into_inner)
829            .get_mut(&attempt.id)
830            .and_then(|child| child.try_wait().ok().flatten())
831            .is_some_and(|status| status.success())
832    }
833    fn record_terminate_intent(&self, attempt: &RunnerAttempt) -> Result<(), FailureReason> {
834        if !attempt.execution().is_native() {
835            return Err(ownership_failure());
836        }
837        self.native.record_terminate_intent(attempt)
838    }
839    fn has_terminate_intent(&self, attempt: &RunnerAttempt) -> bool {
840        attempt.execution().is_native() && self.native.has_terminate_intent(attempt)
841    }
842    fn terminate(&self, attempt: &RunnerAttempt) -> Result<(), FailureReason> {
843        if !attempt.execution().is_native() {
844            return Err(ownership_failure());
845        }
846        self.native.terminate(attempt)
847    }
848}
849
850#[cfg(test)]
851mod boundary_tests {
852    use super::*;
853    use runner_manager_domain::model::Clock;
854    use runner_manager_testkit::{clock::FakeClock, fixtures};
855
856    #[test]
857    fn restart_recovery_accepts_only_retained_container_states() {
858        assert!(retained_after_restart(EnvironmentState::Starting));
859        assert!(retained_after_restart(EnvironmentState::Exited));
860        assert!(!retained_after_restart(EnvironmentState::Running));
861        assert!(!retained_after_restart(EnvironmentState::Missing));
862    }
863
864    #[test]
865    fn isolated_attempt_cannot_use_native_compatibility_methods() {
866        let root = tempfile::tempdir().unwrap();
867        let policy = fixtures::policy().build();
868        let mut attempt = RunnerAttempt::allocate(
869            AttemptId::new_random(),
870            policy.id,
871            root.path(),
872            FakeClock::default().now(),
873        );
874        attempt
875            .allocate_execution(AttemptExecution::Isolated {
876                provider_kind: Backend::Oci,
877                environment_id: None,
878                resolved_image: ImageReference::new(format!(
879                    "registry.example/runner@sha256:{}",
880                    "a".repeat(64)
881                ))
882                .unwrap(),
883                generation: "generation-1".into(),
884            })
885            .unwrap();
886        let provider = OciProcesses::new(HostId::from_u128(1));
887        let config = EncodedJitConfig::new("test-jit");
888        let failure = provider.spawn(&attempt, &config).unwrap_err();
889        assert_eq!(failure.reason, ownership_failure());
890        assert!(failure.live_pid.is_none());
891        assert_eq!(provider.is_alive(&attempt), Err(ownership_failure()));
892        assert_eq!(provider.recovered_pid(&attempt), Err(ownership_failure()));
893        assert_eq!(
894            provider.record_terminate_intent(&attempt),
895            Err(ownership_failure())
896        );
897        assert!(!provider.has_terminate_intent(&attempt));
898        assert_eq!(provider.terminate(&attempt), Err(ownership_failure()));
899    }
900}
901
902#[cfg(all(test, target_os = "linux"))]
903mod tests {
904    use super::*;
905    use runner_manager_domain::attempt::{AttemptOutcome, AttemptState};
906    use runner_manager_domain::execution::ResourceLimits;
907    use runner_manager_domain::model::Clock;
908    use runner_manager_domain::store::{SqliteStore, Store};
909    use runner_manager_testkit::{clock::FakeClock, fixtures};
910    use std::io::Read;
911    use std::os::unix::fs::PermissionsExt;
912    use std::path::{Path, PathBuf};
913    use std::time::{Duration, Instant};
914
915    fn fixture(info: &str) -> (tempfile::TempDir, OciProcesses) {
916        fixture_with_quota(info, true)
917    }
918
919    fn fixture_with_quota(info: &str, quota_ok: bool) -> (tempfile::TempDir, OciProcesses) {
920        let root = tempfile::tempdir().unwrap();
921        let path = root.path().join("podman");
922        let quota_gate = if quota_ok {
923            ""
924        } else {
925            "if [ \"$1\" = \"--storage-opt\" ]; then exit 1; fi\n"
926        };
927        let script = format!(
928            "#!/bin/sh\n{quota_gate}printf '%s' '{}'\n",
929            info.replace('\'', "'\\''")
930        );
931        std::fs::write(&path, script).unwrap();
932        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
933        let mut provider = OciProcesses::new(HostId::from_u128(1));
934        provider.runtime = path.to_string_lossy().into_owned();
935        (root, provider)
936    }
937
938    fn info(rootless: bool, graph_root: &str, subuids: u64) -> String {
939        serde_json::json!({
940            "host": {
941                "security": {"rootless": rootless},
942                "cgroupVersion": "v2",
943                "cgroupControllers": ["cpu", "memory", "pids"],
944                "idMappings": {
945                    "uidmap": [{"size": 1}, {"size": subuids}],
946                    "gidmap": [{"size": 1}, {"size": subuids}]
947                }
948            },
949            "store": {
950                "graphRoot": graph_root,
951                "runRoot": "/run/user/1000/containers",
952                "graphStatus": {"Backing Filesystem": "xfs"}
953            }
954        })
955        .to_string()
956    }
957
958    #[test]
959    fn rootless_probe_refuses_rootful_and_drvfs_storage() {
960        let (_root, provider) = fixture(&info(false, "/home/ivan/.local/share/containers", 65536));
961        assert_eq!(
962            provider.rootless_ready(),
963            ProviderCapability::PermissionDenied
964        );
965        let (_root, provider) = fixture(&info(true, "/mnt/c/containers", 65536));
966        assert_eq!(
967            provider.rootless_ready(),
968            ProviderCapability::PermissionDenied
969        );
970        let (_root, provider) = fixture(&info(true, "/home/ivan/.local/share/containers", 1));
971        assert_eq!(
972            provider.rootless_ready(),
973            ProviderCapability::PermissionDenied
974        );
975        let (_root, provider) = fixture(&info(true, "/home/ivan/.local/share/containers", 65536));
976        assert_eq!(provider.rootless_ready(), ProviderCapability::Ready);
977        let no_pids = info(true, "/home/ivan/.local/share/containers", 65536)
978            .replace("\"pids\"", "\"missing\"");
979        let (_root, provider) = fixture(&no_pids);
980        assert_eq!(provider.rootless_ready(), ProviderCapability::Degraded);
981        let extfs = info(true, "/home/ivan/.local/share/containers", 65536).replace(
982            "\"Backing Filesystem\":\"xfs\"",
983            "\"Backing Filesystem\":\"extfs\"",
984        );
985        let (_root, provider) = fixture(&extfs);
986        assert_eq!(
987            provider.rootless_ready(),
988            ProviderCapability::DiskQuotaUnavailable
989        );
990        let mut bounded: serde_json::Value = serde_json::from_str(&extfs).unwrap();
991        bounded["runnerManagerStorage"] = serde_json::json!({
992            "schema": BOUNDED_STORAGE_SCHEMA,
993            "mode": BOUNDED_STORAGE_MODE,
994            "requestedMiB": QUOTA_PROBE_MIB,
995            "hardCap": true
996        });
997        let (_root, provider) = fixture(&bounded.to_string());
998        assert_eq!(provider.rootless_ready(), ProviderCapability::Ready);
999        assert_eq!(
1000            provider.rootless_ready_for(2048),
1001            ProviderCapability::DiskQuotaUnavailable,
1002            "the helper must attest the exact requested size"
1003        );
1004        bounded["runnerManagerStorage"]["hardCap"] = serde_json::json!(false);
1005        let (_root, provider) = fixture(&bounded.to_string());
1006        assert_eq!(
1007            provider.rootless_ready(),
1008            ProviderCapability::DiskQuotaUnavailable
1009        );
1010        let (_root, provider) = fixture_with_quota(
1011            &info(true, "/home/ivan/.local/share/containers", 65536),
1012            false,
1013        );
1014        assert_eq!(
1015            provider.rootless_ready(),
1016            ProviderCapability::DiskQuotaUnavailable
1017        );
1018    }
1019
1020    #[test]
1021    fn missing_operator_runtime_is_not_treated_as_ready() {
1022        let mut provider = OciProcesses::new(HostId::from_u128(1));
1023        provider.runtime = "/a/path/that/cannot/be/podman".into();
1024        assert_eq!(provider.rootless_ready(), ProviderCapability::NotInstalled);
1025    }
1026
1027    #[test]
1028    fn recovery_discovers_owned_container_when_capability_degrades() {
1029        let root = tempfile::tempdir().unwrap();
1030        let path = root.path().join("podman");
1031        let host = HostId::from_u128(1);
1032        let attempt = AttemptId::new_random();
1033        let generation = "generation-1";
1034        let image = format!("registry.example/runner@sha256:{}", "a".repeat(64));
1035        let id = "b".repeat(64);
1036        let degraded_info = info(true, "/home/ivan/.local/share/containers", 65536)
1037            .replace("\"pids\"", "\"missing\"");
1038        let labels = BTreeMap::from([
1039            (HOST_LABEL, host.to_string()),
1040            (ATTEMPT_LABEL, attempt.to_string()),
1041            (GENERATION_LABEL, generation.to_owned()),
1042            (IMAGE_LABEL, image),
1043        ]);
1044        let labels = serde_json::to_string(&labels).unwrap();
1045        let script = format!(
1046            "#!/bin/sh\ncase \"$1\" in\n  info) printf '%s' '{degraded_info}' ;;\n  ps) printf '%s\\n' '{id}' ;;\n  container) printf '%s' '{labels}' ;;\n  *) exit 1 ;;\nesac\n"
1047        );
1048        std::fs::write(&path, script).unwrap();
1049        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
1050        let mut provider = OciProcesses::new(host);
1051        provider.runtime = path.to_string_lossy().into_owned();
1052        assert_eq!(provider.rootless_ready(), ProviderCapability::Degraded);
1053        let owned = provider.enumerate_owned(host);
1054        assert_eq!(owned.len(), 1);
1055        assert!(matches!(
1056            &owned[0],
1057            EnvironmentIdentity::Isolated {
1058                host: found_host,
1059                attempt: found_attempt,
1060                environment_id,
1061                generation: found_generation,
1062                ..
1063            } if *found_host == host && *found_attempt == attempt
1064                && environment_id == &id && found_generation == generation
1065        ));
1066    }
1067
1068    #[test]
1069    fn native_start_still_delegates_to_the_existing_process_supervisor() {
1070        let root = tempfile::tempdir().unwrap();
1071        let runtime = root.path().join("runtime");
1072        let bin = runtime.join("bin");
1073        std::fs::create_dir_all(&bin).unwrap();
1074        let listener = bin.join("Runner.Listener");
1075        std::fs::write(&listener, "#!/bin/sh\nsleep 10\n").unwrap();
1076        std::fs::set_permissions(&listener, std::fs::Permissions::from_mode(0o700)).unwrap();
1077        let policy = fixtures::policy()
1078            .repository("octo/repo")
1079            .autoscale("home", 1)
1080            .active()
1081            .build();
1082        let attempt = RunnerAttempt::allocate(
1083            AttemptId::new_random(),
1084            policy.id,
1085            &runtime,
1086            FakeClock::default().now(),
1087        );
1088        let provider = OciProcesses::new(HostId::from_u128(1));
1089        let prepared = provider.prepare(&attempt, &policy).unwrap();
1090        let config = EncodedJitConfig::new("native-test-jit");
1091        let identity = provider
1092            .start(prepared, &attempt, OneTimeJitHandoff::new(&config))
1093            .unwrap();
1094        assert!(matches!(identity, EnvironmentIdentity::NativeProcess(_)));
1095        provider.stop(&attempt).unwrap();
1096    }
1097
1098    const LIVE_IMAGE_ENV: &str = "RUNNER_MANAGER_OCI_ACCEPTANCE_IMAGE";
1099    const LIVE_HOST: u128 = 0x00ac_ce7a_ce00_0000_0000_0000_0000_0001;
1100    const LIVE_PACKAGE: &str = "runner-manager-oci-conflict";
1101
1102    fn live_runtime() -> String {
1103        std::env::var(OCI_RUNTIME_ENV).unwrap_or_else(|_| "podman".into())
1104    }
1105
1106    fn podman(args: &[&str]) -> std::process::Output {
1107        Command::new(live_runtime())
1108            .args(args)
1109            .output()
1110            .unwrap_or_else(|error| panic!("podman {args:?} did not execute: {error}"))
1111    }
1112
1113    fn podman_ok(args: &[&str]) -> String {
1114        let output = podman(args);
1115        assert!(
1116            output.status.success(),
1117            "podman {args:?} failed: {}",
1118            String::from_utf8_lossy(&output.stderr)
1119        );
1120        String::from_utf8(output.stdout).expect("podman emitted UTF-8")
1121    }
1122
1123    fn write_listener(runtime: &Path) {
1124        let bin = runtime.join("bin");
1125        std::fs::create_dir_all(&bin).unwrap();
1126        let listener = bin.join("Runner.Listener");
1127        // The real runner consumes the same environment variable. This fixture
1128        // proves the provider's stdin-to-bootstrap handoff, then removes it
1129        // before the long-lived child exists so the sentinel is not retained
1130        // in the process environment or writable layer.
1131        std::fs::write(
1132            &listener,
1133            "#!/bin/sh\n\
1134             test -n \"${ACTIONS_RUNNER_INPUT_JITCONFIG:-}\" || exit 70\n\
1135             unset ACTIONS_RUNNER_INPUT_JITCONFIG\n\
1136             : > /tmp/runner-manager-jit-received\n\
1137             exec sleep 600\n",
1138        )
1139        .unwrap();
1140        std::fs::set_permissions(&listener, std::fs::Permissions::from_mode(0o700)).unwrap();
1141    }
1142
1143    fn build_conflicting_package(runtime: &Path, version: &str) -> PathBuf {
1144        let root = runtime.join(format!("package-{version}"));
1145        let debian = root.join("DEBIAN");
1146        let bin = root.join("usr/local/bin");
1147        std::fs::create_dir_all(&debian).unwrap();
1148        std::fs::create_dir_all(&bin).unwrap();
1149        std::fs::write(
1150            debian.join("control"),
1151            format!(
1152                "Package: {LIVE_PACKAGE}\nVersion: {version}\nArchitecture: all\n\
1153                 Maintainer: Runner Manager acceptance <nobody@example.invalid>\n\
1154                 Description: isolated package database acceptance fixture\n"
1155            ),
1156        )
1157        .unwrap();
1158        let tool = bin.join("runner-manager-conflict");
1159        std::fs::write(&tool, format!("#!/bin/sh\nprintf '%s\\n' '{version}'\n")).unwrap();
1160        std::fs::set_permissions(&tool, std::fs::Permissions::from_mode(0o755)).unwrap();
1161        let package = runtime.join("conflict.deb");
1162        let output = Command::new("dpkg-deb")
1163            .args(["--build", "--root-owner-group"])
1164            .arg(&root)
1165            .arg(&package)
1166            .output()
1167            .expect("dpkg-deb must be installed by the native acceptance job");
1168        assert!(
1169            output.status.success(),
1170            "dpkg-deb failed: {}",
1171            String::from_utf8_lossy(&output.stderr)
1172        );
1173        package
1174    }
1175
1176    fn isolated_policy(image: ImageReference) -> ScalePolicy {
1177        let mut policy = fixtures::policy()
1178            .repository("octo/oci-native-acceptance")
1179            .autoscale("isolated", 3)
1180            .active()
1181            .build();
1182        policy
1183            .set_execution_policy(ExecutionPolicy::Isolated {
1184                backend: Backend::Oci,
1185                image,
1186                resources: ResourceLimits {
1187                    cpu_millis: 1000,
1188                    memory_mib: 512,
1189                    disk_mib: 1024,
1190                },
1191            })
1192            .unwrap();
1193        policy
1194    }
1195
1196    fn prepare_live_attempt(
1197        provider: &OciProcesses,
1198        policy: &ScalePolicy,
1199        image: &ImageReference,
1200        version: Option<&str>,
1201        generation: &str,
1202        production_quota_path: bool,
1203    ) -> (
1204        tempfile::TempDir,
1205        RunnerAttempt,
1206        PreparedEnvironment,
1207        String,
1208    ) {
1209        let runtime = tempfile::tempdir().unwrap();
1210        write_listener(runtime.path());
1211        if let Some(version) = version {
1212            build_conflicting_package(runtime.path(), version);
1213        }
1214        let mut attempt = RunnerAttempt::allocate(
1215            AttemptId::new_random(),
1216            policy.id,
1217            runtime.path(),
1218            FakeClock::default().now(),
1219        );
1220        attempt
1221            .allocate_execution(AttemptExecution::Isolated {
1222                provider_kind: Backend::Oci,
1223                environment_id: None,
1224                resolved_image: image.clone(),
1225                generation: generation.into(),
1226            })
1227            .unwrap();
1228        if production_quota_path {
1229            let prepared = provider.prepare(&attempt, policy).unwrap();
1230            let EnvironmentIdentity::Isolated { environment_id, .. } =
1231                prepared.identity().cloned().expect("isolated identity")
1232            else {
1233                panic!("OCI prepare returned a native identity")
1234            };
1235            return (runtime, attempt, prepared, environment_id);
1236        }
1237        let ExecutionPolicy::Isolated { resources, .. } = policy.execution_policy() else {
1238            panic!("structural acceptance needs an isolated policy")
1239        };
1240        let name = format!("rm-{}-{}", attempt.id, generation);
1241        let cpu = format!("{:.3}", f64::from(resources.cpu_millis) / 1000.0);
1242        let memory = format!("{}m", resources.memory_mib);
1243        let host_label = format!("{HOST_LABEL}={}", provider.host);
1244        let attempt_label = format!("{ATTEMPT_LABEL}={}", attempt.id);
1245        let generation_label = format!("{GENERATION_LABEL}={generation}");
1246        let image_label = format!("{IMAGE_LABEL}={}", image.as_str());
1247        // Rootless Podman cannot initialize XFS project quotas because project
1248        // IDs are not namespaced and an unprivileged user cannot create the
1249        // backing block-device node. The acceptance first proves the production
1250        // provider refuses that missing hard cap. This second, explicitly
1251        // structural fixture omits only --storage-opt so the remaining real
1252        // container boundary can still be measured on the hard-bounded loop FS.
1253        let environment_id = provider
1254            .output(&[
1255                "create",
1256                "--interactive",
1257                "--name",
1258                &name,
1259                "--pull=never",
1260                "--entrypoint=sh",
1261                "--log-driver=none",
1262                "--image-volume=ignore",
1263                "--http-proxy=false",
1264                "--userns=keep-id",
1265                "--user=0",
1266                "--cap-drop=all",
1267                "--cap-add=CHOWN",
1268                "--cap-add=SETUID",
1269                "--cap-add=SETGID",
1270                "--cap-add=DAC_OVERRIDE",
1271                "--cap-add=FOWNER",
1272                "--security-opt=no-new-privileges",
1273                "--network=slirp4netns",
1274                "--pids-limit=128",
1275                "--cpus",
1276                &cpu,
1277                "--memory",
1278                &memory,
1279                "--label",
1280                &host_label,
1281                "--label",
1282                &attempt_label,
1283                "--label",
1284                &generation_label,
1285                "--label",
1286                &image_label,
1287                image.as_str(),
1288                "-c",
1289                BOOTSTRAP,
1290            ])
1291            .unwrap()
1292            .trim()
1293            .to_owned();
1294        assert!(!environment_id.is_empty());
1295        let source = format!("{}/.", attempt.runtime_path().display());
1296        let destination = format!("{environment_id}:/runner");
1297        provider.status(&["cp", &source, &destination]).unwrap();
1298        let identity = provider
1299            .isolated_identity(&attempt, environment_id.clone())
1300            .unwrap();
1301        let prepared = PreparedEnvironment::isolated(attempt.id, identity);
1302        (runtime, attempt, prepared, environment_id)
1303    }
1304
1305    fn start_live_attempt(
1306        provider: &OciProcesses,
1307        attempt: &mut RunnerAttempt,
1308        prepared: PreparedEnvironment,
1309        environment_id: &str,
1310        sentinel: &str,
1311    ) {
1312        attempt
1313            .prepared_environment(environment_id.to_owned())
1314            .unwrap();
1315        let config = EncodedJitConfig::new(sentinel);
1316        provider
1317            .start(prepared, attempt, OneTimeJitHandoff::new(&config))
1318            .unwrap();
1319        let deadline = Instant::now() + Duration::from_secs(20);
1320        loop {
1321            let received = podman(&[
1322                "exec",
1323                environment_id,
1324                "test",
1325                "-f",
1326                "/tmp/runner-manager-jit-received",
1327            ]);
1328            if received.status.success() {
1329                break;
1330            }
1331            assert!(Instant::now() < deadline, "listener did not consume JIT");
1332            std::thread::sleep(Duration::from_millis(100));
1333        }
1334    }
1335
1336    fn inspect_controls(environment_id: &str, image: &ImageReference, sentinel: &str) {
1337        let raw = podman_ok(&["container", "inspect", environment_id]);
1338        assert!(
1339            !raw.contains(sentinel),
1340            "JIT sentinel reached inspect metadata"
1341        );
1342        assert!(!raw.contains("/run/podman/podman.sock"));
1343        assert!(!raw.contains("/var/run/docker.sock"));
1344        let inspect: serde_json::Value = serde_json::from_str(&raw).unwrap();
1345        let item = &inspect[0];
1346        assert_eq!(
1347            item.pointer("/Mounts")
1348                .and_then(|v| v.as_array())
1349                .map(Vec::len),
1350            Some(0)
1351        );
1352        assert!(
1353            item.pointer("/HostConfig/Devices")
1354                .is_none_or(|value| value.is_null() || value.as_array().is_some_and(Vec::is_empty)),
1355            "host devices were attached: {}",
1356            item["HostConfig"]["Devices"]
1357        );
1358        assert!(
1359            item.pointer("/HostConfig/Binds")
1360                .is_none_or(|value| value.is_null() || value.as_array().is_some_and(Vec::is_empty)),
1361            "host bind mounts were attached: {}",
1362            item["HostConfig"]["Binds"]
1363        );
1364        assert_eq!(item["HostConfig"]["PidsLimit"].as_i64(), Some(128));
1365        assert_eq!(
1366            item["HostConfig"]["Memory"].as_u64(),
1367            Some(512 * 1024 * 1024)
1368        );
1369        assert_eq!(
1370            item["Config"]["Labels"][IMAGE_LABEL].as_str(),
1371            Some(image.as_str())
1372        );
1373        let command = item["Config"]["CreateCommand"]
1374            .as_array()
1375            .expect("Podman records create command");
1376        for forbidden in ["--device", "--mount", "--volume", "-v", "--privileged"] {
1377            assert!(
1378                !command.iter().any(|part| part.as_str() == Some(forbidden)),
1379                "forbidden create option {forbidden} was present"
1380            );
1381        }
1382        let socket_check = podman(&[
1383            "exec",
1384            environment_id,
1385            "sh",
1386            "-c",
1387            "test ! -S /run/podman/podman.sock && test ! -S /var/run/docker.sock",
1388        ]);
1389        assert!(
1390            socket_check.status.success(),
1391            "a host runtime socket is visible"
1392        );
1393        let logs = podman(&["logs", environment_id]);
1394        assert!(!String::from_utf8_lossy(&logs.stdout).contains(sentinel));
1395        assert!(!String::from_utf8_lossy(&logs.stderr).contains(sentinel));
1396        let history = podman_ok(&["history", "--no-trunc", image.as_str()]);
1397        assert!(
1398            !history.contains(sentinel),
1399            "JIT sentinel reached image history"
1400        );
1401    }
1402
1403    fn assert_export_excludes(environment_id: &str, sentinel: &str) {
1404        let mut child = Command::new(live_runtime())
1405            .args(["export", environment_id])
1406            .stdout(Stdio::piped())
1407            .stderr(Stdio::piped())
1408            .spawn()
1409            .expect("podman export starts");
1410        let mut bytes = Vec::new();
1411        child
1412            .stdout
1413            .take()
1414            .unwrap()
1415            .read_to_end(&mut bytes)
1416            .unwrap();
1417        let output = child.wait_with_output().unwrap();
1418        assert!(
1419            output.status.success(),
1420            "podman export failed: {}",
1421            String::from_utf8_lossy(&output.stderr)
1422        );
1423        assert!(
1424            !bytes
1425                .windows(sentinel.len())
1426                .any(|window| window == sentinel.as_bytes()),
1427            "JIT sentinel reached the container root filesystem"
1428        );
1429    }
1430
1431    fn wait_for_container_file(environment_id: &str, path: &str, deadline: Instant) -> bool {
1432        loop {
1433            if podman(&["exec", environment_id, "test", "-f", path])
1434                .status
1435                .success()
1436            {
1437                return true;
1438            }
1439            if Instant::now() >= deadline {
1440                return false;
1441            }
1442            std::thread::sleep(Duration::from_millis(250));
1443        }
1444    }
1445
1446    fn assert_live_jit_absent(environment_id: &str, image: &ImageReference, jit: &str) {
1447        let inspect = podman_ok(&["container", "inspect", environment_id]);
1448        assert!(
1449            !inspect.contains(jit),
1450            "JIT reached container inspect metadata"
1451        );
1452
1453        let top = podman(&["top", environment_id, "pid,args"]);
1454        assert!(top.status.success(), "container process listing failed");
1455        assert!(
1456            !top.stdout
1457                .windows(jit.len())
1458                .any(|window| window == jit.as_bytes()),
1459            "JIT reached the container process listing"
1460        );
1461
1462        let logs = podman(&["logs", environment_id]);
1463        assert!(
1464            !logs
1465                .stdout
1466                .windows(jit.len())
1467                .any(|window| window == jit.as_bytes())
1468        );
1469        assert!(
1470            !logs
1471                .stderr
1472                .windows(jit.len())
1473                .any(|window| window == jit.as_bytes())
1474        );
1475        let history = podman_ok(&["history", "--no-trunc", image.as_str()]);
1476        assert!(!history.contains(jit), "JIT reached image history");
1477    }
1478
1479    fn stop_and_destroy(provider: &OciProcesses, attempt: &RunnerAttempt) {
1480        assert_eq!(provider.stop(attempt).unwrap(), ProviderStop::Stopped);
1481        assert_eq!(
1482            provider.destroy(attempt).unwrap(),
1483            ProviderDestroy::Destroyed
1484        );
1485        assert_eq!(
1486            provider.inspect(attempt).unwrap(),
1487            EnvironmentState::Missing
1488        );
1489    }
1490
1491    fn run_live_rootless_acceptance(production_quota_path: bool) {
1492        let image = ImageReference::new(
1493            std::env::var(LIVE_IMAGE_ENV)
1494                .unwrap_or_else(|_| panic!("{LIVE_IMAGE_ENV} must name a pinned image digest")),
1495        )
1496        .unwrap();
1497        let provider = OciProcesses::new(HostId::from_u128(LIVE_HOST));
1498        let policy = isolated_policy(image.clone());
1499
1500        if production_quota_path {
1501            assert_eq!(provider.probe(&policy), ProviderCapability::Ready);
1502            let resolution = provider.resolve(&policy).unwrap().unwrap();
1503            assert_eq!(resolution.provider_kind, Backend::Oci);
1504            assert_eq!(resolution.image, image);
1505        } else {
1506            assert_eq!(
1507                provider.probe(&policy),
1508                ProviderCapability::DiskQuotaUnavailable
1509            );
1510            assert_eq!(
1511                provider.resolve(&policy),
1512                Err(ProviderCapability::DiskQuotaUnavailable.refusal()),
1513                "the production provider must fail before image allocation or JIT"
1514            );
1515            provider.image_ready(&image).unwrap();
1516        }
1517        assert!(
1518            !Command::new("dpkg-query")
1519                .args(["-W", LIVE_PACKAGE])
1520                .status()
1521                .unwrap()
1522                .success(),
1523            "acceptance package unexpectedly exists on the host"
1524        );
1525
1526        let (_runtime_one, mut one, prepared_one, id_one) = prepare_live_attempt(
1527            &provider,
1528            &policy,
1529            &image,
1530            Some("1.0"),
1531            "native-one",
1532            production_quota_path,
1533        );
1534        let sentinel_one = "rm-jit-sentinel-native-one-8eeb09c26b8a";
1535        start_live_attempt(&provider, &mut one, prepared_one, &id_one, sentinel_one);
1536        inspect_controls(&id_one, &image, sentinel_one);
1537        podman_ok(&["exec", &id_one, "dpkg", "-i", "/runner/conflict.deb"]);
1538        assert_eq!(
1539            podman_ok(&[
1540                "exec",
1541                &id_one,
1542                "dpkg-query",
1543                "-W",
1544                "-f=${Version}",
1545                LIVE_PACKAGE,
1546            ])
1547            .trim(),
1548            "1.0"
1549        );
1550        assert_eq!(
1551            podman_ok(&["exec", &id_one, "/usr/local/bin/runner-manager-conflict"]).trim(),
1552            "1.0"
1553        );
1554
1555        let (_runtime_two, mut two, prepared_two, id_two) = prepare_live_attempt(
1556            &provider,
1557            &policy,
1558            &image,
1559            Some("2.0"),
1560            "native-two",
1561            production_quota_path,
1562        );
1563        let sentinel_two = "rm-jit-sentinel-native-two-62b2db40633b";
1564        start_live_attempt(&provider, &mut two, prepared_two, &id_two, sentinel_two);
1565        inspect_controls(&id_two, &image, sentinel_two);
1566        podman_ok(&["exec", &id_two, "dpkg", "-i", "/runner/conflict.deb"]);
1567        assert_eq!(
1568            podman_ok(&[
1569                "exec",
1570                &id_two,
1571                "dpkg-query",
1572                "-W",
1573                "-f=${Version}",
1574                LIVE_PACKAGE,
1575            ])
1576            .trim(),
1577            "2.0"
1578        );
1579        assert_eq!(
1580            podman_ok(&["exec", &id_one, "/usr/local/bin/runner-manager-conflict"]).trim(),
1581            "1.0",
1582            "the second package install mutated its sibling"
1583        );
1584        assert_export_excludes(&id_one, sentinel_one);
1585        assert_export_excludes(&id_two, sentinel_two);
1586
1587        // Prove the loopback store's hard upper bound with real writes. A
1588        // fallocate-only probe may fail because an overlay does not implement
1589        // allocation, which says nothing about the backing filesystem's cap.
1590        // `dd` writes zeroes through fuse-overlayfs until ext4/XFS returns
1591        // ENOSPC; `df` records that the same filesystem reached zero available
1592        // bytes. Remove the partial file before Podman needs to journal the
1593        // process exit or destroy the container.
1594        let oversize_mib = if production_quota_path {
1595            "1100"
1596        } else {
1597            "7168"
1598        };
1599        let exhaustion = podman(&[
1600            "exec",
1601            &id_one,
1602            "sh",
1603            "-c",
1604            "set +e; printf 'RM_FS_BEFORE '; df -B1 --output=size,used,avail / | tail -n 1; dd if=/dev/zero of=/backing-store-cap bs=1M count=\"$1\" conv=fsync status=none; status=$?; printf 'RM_FS_FULL '; df -B1 --output=size,used,avail / | tail -n 1; rm -f /backing-store-cap; sync; exit \"$status\"",
1605            "sh",
1606            oversize_mib,
1607        ]);
1608        assert!(
1609            !exhaustion.status.success(),
1610            "the backing-store hard cap allowed an oversized allocation"
1611        );
1612        let exhaustion_stdout = String::from_utf8(exhaustion.stdout).unwrap();
1613        let exhaustion_stderr = String::from_utf8(exhaustion.stderr).unwrap();
1614        assert!(
1615            exhaustion_stderr.contains("No space left on device"),
1616            "the write failed without ENOSPC: {exhaustion_stderr}"
1617        );
1618        let df_line = |marker: &str| {
1619            exhaustion_stdout
1620                .lines()
1621                .find_map(|line| line.strip_prefix(marker))
1622                .map(|line| {
1623                    line.split_whitespace()
1624                        .map(|value| value.parse().expect("df emitted integer byte counts"))
1625                        .collect::<Vec<u64>>()
1626                })
1627                .unwrap_or_else(|| {
1628                    panic!("the write did not emit {marker:?} df evidence: {exhaustion_stdout}")
1629                })
1630        };
1631        let before = df_line("RM_FS_BEFORE ");
1632        let full = df_line("RM_FS_FULL ");
1633        assert_eq!(
1634            before.len(),
1635            3,
1636            "unexpected initial df evidence: {before:?}"
1637        );
1638        assert_eq!(full.len(), 3, "unexpected full df evidence: {full:?}");
1639        assert!(
1640            full[1] >= before[1] + 512 * 1024 * 1024,
1641            "the ENOSPC probe did not perform substantial writes: before={before:?} full={full:?}"
1642        );
1643        assert!(
1644            full[2] <= 1024 * 1024,
1645            "ENOSPC left more than 1 MiB available: {full:?}"
1646        );
1647        stop_and_destroy(&provider, &one);
1648        stop_and_destroy(&provider, &two);
1649
1650        let (_runtime_fresh, mut fresh, prepared_fresh, id_fresh) = prepare_live_attempt(
1651            &provider,
1652            &policy,
1653            &image,
1654            None,
1655            "native-fresh",
1656            production_quota_path,
1657        );
1658        start_live_attempt(
1659            &provider,
1660            &mut fresh,
1661            prepared_fresh,
1662            &id_fresh,
1663            "rm-jit-sentinel-native-fresh-c1b9f470d09e",
1664        );
1665        assert!(
1666            !podman(&["exec", &id_fresh, "dpkg-query", "-W", LIVE_PACKAGE])
1667                .status
1668                .success(),
1669            "a fresh sibling inherited the earlier package database"
1670        );
1671        assert!(
1672            !podman(&[
1673                "exec",
1674                &id_fresh,
1675                "test",
1676                "-e",
1677                "/usr/local/bin/runner-manager-conflict",
1678            ])
1679            .status
1680            .success(),
1681            "a fresh sibling inherited an earlier writable-layer file"
1682        );
1683        stop_and_destroy(&provider, &fresh);
1684
1685        // Simulate the crash gap after create and before the environment ID is
1686        // journalled. Recovery discovers and quarantines exactly the labelled
1687        // orphan; after the durable identity is restored, normal destroy owns
1688        // and removes it.
1689        let (_runtime_orphan, mut orphan, prepared_orphan, orphan_id) = prepare_live_attempt(
1690            &provider,
1691            &policy,
1692            &image,
1693            None,
1694            "native-orphan",
1695            production_quota_path,
1696        );
1697        assert_eq!(
1698            provider.recover(&orphan).unwrap(),
1699            EnvironmentState::Starting
1700        );
1701        let owned = provider.enumerate_owned(HostId::from_u128(LIVE_HOST));
1702        assert_eq!(owned.len(), 1, "recovery did not isolate one owned orphan");
1703        assert!(matches!(
1704            &owned[0],
1705            EnvironmentIdentity::Isolated { environment_id, .. } if environment_id == &orphan_id
1706        ));
1707        orphan.prepared_environment(orphan_id).unwrap();
1708        assert_eq!(
1709            provider.destroy(&orphan).unwrap(),
1710            ProviderDestroy::Destroyed
1711        );
1712        assert!(
1713            provider
1714                .enumerate_owned(HostId::from_u128(LIVE_HOST))
1715                .is_empty()
1716        );
1717        drop(prepared_orphan);
1718
1719        assert!(
1720            !Command::new("dpkg-query")
1721                .args(["-W", LIVE_PACKAGE])
1722                .status()
1723                .unwrap()
1724                .success(),
1725            "container package installation mutated the host package database"
1726        );
1727    }
1728
1729    const RESTART_FIXTURE_ENV: &str = "RUNNER_MANAGER_OCI_RESTART_FIXTURE";
1730
1731    fn restart_fixture_root() -> PathBuf {
1732        let root = PathBuf::from(std::env::var(RESTART_FIXTURE_ENV).unwrap_or_else(|_| {
1733            panic!("{RESTART_FIXTURE_ENV} must name the durable fixture root")
1734        }));
1735        assert!(root.is_absolute() && !root.starts_with("/mnt"));
1736        root
1737    }
1738
1739    fn restart_attempt(
1740        root: &Path,
1741        policy: &ScalePolicy,
1742        image: &ImageReference,
1743        suffix: &str,
1744        id: u128,
1745        generation: &str,
1746    ) -> RunnerAttempt {
1747        let runtime = root.join("runtimes").join(suffix);
1748        std::fs::create_dir_all(&runtime).unwrap();
1749        write_listener(&runtime);
1750        let mut attempt = RunnerAttempt::allocate(
1751            AttemptId::from_u128(id),
1752            policy.id,
1753            runtime,
1754            FakeClock::default().now(),
1755        );
1756        attempt
1757            .allocate_execution(AttemptExecution::Isolated {
1758                provider_kind: Backend::Oci,
1759                environment_id: None,
1760                resolved_image: image.clone(),
1761                generation: generation.into(),
1762            })
1763            .unwrap();
1764        attempt
1765    }
1766
1767    fn prepare_restart_resource(
1768        provider: &OciProcesses,
1769        policy: &ScalePolicy,
1770        attempt: &mut RunnerAttempt,
1771    ) -> (PreparedEnvironment, String) {
1772        attempt.begin_prepare(FakeClock::default().now()).unwrap();
1773        let prepared = provider.prepare(attempt, policy).unwrap();
1774        let EnvironmentIdentity::Isolated { environment_id, .. } =
1775            prepared.identity().cloned().expect("isolated identity")
1776        else {
1777            panic!("OCI prepare returned a native identity")
1778        };
1779        (prepared, environment_id)
1780    }
1781
1782    #[test]
1783    #[ignore = "seeds real managed-WSL resources for the terminate/restart acceptance"]
1784    fn live_rootless_managed_wsl_restart_seed() {
1785        let root = restart_fixture_root();
1786        std::fs::create_dir_all(&root).unwrap();
1787        let journal = SqliteStore::open(root.join("attempts.sqlite3")).unwrap();
1788        let image = ImageReference::new(
1789            std::env::var(LIVE_IMAGE_ENV)
1790                .unwrap_or_else(|_| panic!("{LIVE_IMAGE_ENV} must name a pinned image digest")),
1791        )
1792        .unwrap();
1793        let provider = OciProcesses::new(HostId::from_u128(LIVE_HOST));
1794        let policy = isolated_policy(image.clone());
1795        let nonce = std::env::var("RUNNER_MANAGER_OCI_RESTART_NONCE").expect(
1796            "RUNNER_MANAGER_OCI_RESTART_NONCE must contain the non-secret acceptance nonce",
1797        );
1798        assert!(nonce.starts_with("rm-reboot-nonce-") && nonce.len() == 48);
1799        assert_eq!(provider.probe(&policy), ProviderCapability::Ready);
1800
1801        let clock = FakeClock::default();
1802        let mut prepared = restart_attempt(
1803            &root,
1804            &policy,
1805            &image,
1806            "prepared",
1807            0xd101,
1808            "restart-prepared",
1809        );
1810        let (_prepared_handle, prepared_id) =
1811            prepare_restart_resource(&provider, &policy, &mut prepared);
1812        prepared.prepared_environment(prepared_id).unwrap();
1813        prepared.mark_prepared(clock.now()).unwrap();
1814        journal.record_attempt(&prepared).unwrap();
1815
1816        let mut running =
1817            restart_attempt(&root, &policy, &image, "running", 0xd102, "restart-running");
1818        let (running_handle, running_id) =
1819            prepare_restart_resource(&provider, &policy, &mut running);
1820        running.prepared_environment(running_id.clone()).unwrap();
1821        running.mark_prepared(clock.now()).unwrap();
1822        running.jit_received(clock.now()).unwrap();
1823        provider
1824            .start(
1825                running_handle,
1826                &running,
1827                OneTimeJitHandoff::new(&EncodedJitConfig::new(&nonce)),
1828            )
1829            .unwrap();
1830        running.started_isolated(clock.now()).unwrap();
1831        assert!(wait_for_container_file(
1832            &running_id,
1833            "/tmp/runner-manager-jit-received",
1834            Instant::now() + Duration::from_secs(60),
1835        ));
1836        journal.record_attempt(&running).unwrap();
1837        inspect_controls(&running_id, &image, &nonce);
1838        assert_export_excludes(&running_id, &nonce);
1839
1840        let mut deferred = restart_attempt(
1841            &root,
1842            &policy,
1843            &image,
1844            "cleanup-deferred",
1845            0xd103,
1846            "restart-deferred",
1847        );
1848        let (_deferred_handle, deferred_id) =
1849            prepare_restart_resource(&provider, &policy, &mut deferred);
1850        deferred.prepared_environment(deferred_id).unwrap();
1851        deferred.mark_prepared(clock.now()).unwrap();
1852        deferred.jit_received(clock.now()).unwrap();
1853        deferred.started_isolated(clock.now()).unwrap();
1854        deferred
1855            .conclude(AttemptOutcome::Orphaned, clock.now())
1856            .unwrap();
1857        deferred.begin_destroy(clock.now()).unwrap();
1858        deferred.defer_cleanup(clock.now()).unwrap();
1859        journal.record_attempt(&deferred).unwrap();
1860
1861        let mut orphaned = restart_attempt(
1862            &root,
1863            &policy,
1864            &image,
1865            "orphaned",
1866            0xd104,
1867            "restart-orphaned",
1868        );
1869        let (_orphaned_handle, orphaned_id) =
1870            prepare_restart_resource(&provider, &policy, &mut orphaned);
1871        orphaned.prepared_environment(orphaned_id).unwrap();
1872        orphaned.mark_prepared(clock.now()).unwrap();
1873        orphaned
1874            .conclude(AttemptOutcome::Orphaned, clock.now())
1875            .unwrap();
1876        journal.record_attempt(&orphaned).unwrap();
1877
1878        // Crash gap: the provider resource exists, but its ID was never put in
1879        // the journal. Recovery may adopt only the resource carrying this exact
1880        // attempt and generation label.
1881        let mut orphan = restart_attempt(
1882            &root,
1883            &policy,
1884            &image,
1885            "crash-gap-orphan",
1886            0xd105,
1887            "restart-orphan",
1888        );
1889        let (_orphan_handle, _orphan_id) =
1890            prepare_restart_resource(&provider, &policy, &mut orphan);
1891        journal.record_attempt(&orphan).unwrap();
1892
1893        let attempts = journal.uncleaned_ephemeral_attempts().unwrap();
1894        assert_eq!(attempts.len(), 5);
1895        assert!(attempts.iter().all(RunnerAttempt::counts_against_capacity));
1896        assert_eq!(
1897            provider.enumerate_owned(HostId::from_u128(LIVE_HOST)).len(),
1898            5
1899        );
1900        assert!(
1901            !std::fs::read(root.join("attempts.sqlite3"))
1902                .unwrap()
1903                .windows(nonce.len())
1904                .any(|window| window == nonce.as_bytes()),
1905            "the non-secret JIT stand-in reached the durable attempt journal"
1906        );
1907        std::fs::write(root.join("registration-count"), "0\n").unwrap();
1908        std::fs::write(
1909            root.join("seed-complete"),
1910            "prepared starting cleanup_deferred orphaned crash_gap\n",
1911        )
1912        .unwrap();
1913        std::fs::write(
1914            root.join("security-scan-complete"),
1915            "inspect logs history export journal: nonce absent; github registrations=0\n",
1916        )
1917        .unwrap();
1918    }
1919
1920    #[test]
1921    #[ignore = "recovers real managed-WSL resources after the distribution restarts"]
1922    fn live_rootless_managed_wsl_restart_recover() {
1923        let root = restart_fixture_root();
1924        assert!(root.join("seed-complete").is_file());
1925        let journal = SqliteStore::open(root.join("attempts.sqlite3")).unwrap();
1926        let provider = OciProcesses::new(HostId::from_u128(LIVE_HOST));
1927        let nonce = std::env::var("RUNNER_MANAGER_OCI_RESTART_NONCE")
1928            .expect("RUNNER_MANAGER_OCI_RESTART_NONCE must survive in the Windows manifest");
1929        let mut attempts = journal.uncleaned_ephemeral_attempts().unwrap();
1930        assert_eq!(attempts.len(), 5, "the durable journal lost an attempt");
1931        assert!(attempts.iter().all(RunnerAttempt::counts_against_capacity));
1932        let image = match attempts[0].execution() {
1933            AttemptExecution::Isolated { resolved_image, .. } => resolved_image.clone(),
1934            _ => unreachable!(),
1935        };
1936        let owned = provider.enumerate_owned(HostId::from_u128(LIVE_HOST));
1937        assert_eq!(owned.len(), 5, "the remounted provider lost a resource");
1938
1939        for identity in &owned {
1940            let EnvironmentIdentity::Isolated { environment_id, .. } = identity else {
1941                unreachable!()
1942            };
1943            let inspect = podman_ok(&["container", "inspect", environment_id]);
1944            assert!(
1945                !inspect.contains(&nonce),
1946                "nonce reached durable inspect metadata"
1947            );
1948            let logs = podman(&["logs", environment_id]);
1949            assert!(
1950                !logs
1951                    .stdout
1952                    .windows(nonce.len())
1953                    .any(|window| window == nonce.as_bytes())
1954            );
1955            assert!(
1956                !logs
1957                    .stderr
1958                    .windows(nonce.len())
1959                    .any(|window| window == nonce.as_bytes())
1960            );
1961            assert_export_excludes(environment_id, &nonce);
1962        }
1963        let history = podman_ok(&["history", "--no-trunc", image.as_str()]);
1964        assert!(
1965            !history.contains(&nonce),
1966            "nonce reached durable image history"
1967        );
1968        assert!(
1969            !std::fs::read(root.join("attempts.sqlite3"))
1970                .unwrap()
1971                .windows(nonce.len())
1972                .any(|window| window == nonce.as_bytes()),
1973            "nonce reached the durable attempt journal"
1974        );
1975
1976        let clock = FakeClock::default();
1977        for attempt in &mut attempts {
1978            if attempt.state() == AttemptState::Preparing {
1979                let AttemptExecution::Isolated {
1980                    resolved_image,
1981                    generation,
1982                    ..
1983                } = attempt.execution()
1984                else {
1985                    unreachable!()
1986                };
1987                let mut wrong_generation = RunnerAttempt::allocate(
1988                    attempt.id,
1989                    attempt.policy_id,
1990                    attempt.runtime_path(),
1991                    clock.now(),
1992                );
1993                wrong_generation
1994                    .allocate_execution(AttemptExecution::Isolated {
1995                        provider_kind: Backend::Oci,
1996                        environment_id: None,
1997                        resolved_image: resolved_image.clone(),
1998                        generation: format!("{generation}-wrong"),
1999                    })
2000                    .unwrap();
2001                assert_eq!(
2002                    provider.recover(&wrong_generation).unwrap(),
2003                    EnvironmentState::Missing,
2004                    "a different generation adopted the crash-gap resource"
2005                );
2006                assert_eq!(
2007                    provider.recover(attempt).unwrap(),
2008                    EnvironmentState::Starting
2009                );
2010                let identity = owned
2011                    .iter()
2012                    .find(|identity| {
2013                        matches!(identity, EnvironmentIdentity::Isolated {
2014                            attempt: found_attempt,
2015                            generation: found_generation,
2016                            ..
2017                        } if found_attempt == &attempt.id && found_generation == generation)
2018                    })
2019                    .cloned()
2020                    .expect("the exact crash-gap identity is enumerable");
2021                let EnvironmentIdentity::Isolated { environment_id, .. } = identity else {
2022                    unreachable!()
2023                };
2024                attempt.prepared_environment(environment_id).unwrap();
2025                attempt.mark_prepared(clock.now()).unwrap();
2026            }
2027
2028            let state_after_restart = provider.inspect(attempt).unwrap();
2029            assert!(
2030                retained_after_restart(state_after_restart),
2031                "the remounted provider did not retain a recoverable resource: {state_after_restart:?}"
2032            );
2033            match attempt.state() {
2034                AttemptState::Prepared => {
2035                    attempt
2036                        .conclude(AttemptOutcome::Orphaned, clock.now())
2037                        .unwrap();
2038                    attempt.begin_destroy(clock.now()).unwrap();
2039                }
2040                AttemptState::Starting => {
2041                    attempt
2042                        .conclude(AttemptOutcome::Orphaned, clock.now())
2043                        .unwrap();
2044                    attempt.begin_destroy(clock.now()).unwrap();
2045                }
2046                AttemptState::Orphaned | AttemptState::CleanupDeferred => {
2047                    attempt.begin_destroy(clock.now()).unwrap()
2048                }
2049                state => panic!("unexpected restart state {state}"),
2050            }
2051            assert_eq!(
2052                provider.destroy(attempt).unwrap(),
2053                ProviderDestroy::Destroyed
2054            );
2055            attempt.clean_isolated(clock.now()).unwrap();
2056            journal.record_attempt(attempt).unwrap();
2057        }
2058
2059        assert!(journal.uncleaned_ephemeral_attempts().unwrap().is_empty());
2060        assert!(
2061            provider
2062                .enumerate_owned(HostId::from_u128(LIVE_HOST))
2063                .is_empty()
2064        );
2065        assert_eq!(
2066            std::fs::read_to_string(root.join("registration-count")).unwrap(),
2067            "0\n",
2068            "restart recovery attempted a second JIT registration"
2069        );
2070        std::fs::write(
2071            root.join("recovery-complete"),
2072            "adopted destroyed capacity=0 registrations=0\n",
2073        )
2074        .unwrap();
2075    }
2076
2077    #[test]
2078    #[ignore = "requires the CI-provisioned native rootless Podman/XFS project-quota fixture"]
2079    fn live_rootless_native_acceptance() {
2080        run_live_rootless_acceptance(false);
2081    }
2082
2083    #[test]
2084    #[ignore = "requires a root-owned bounded-storage helper fixture"]
2085    fn live_rootless_bounded_storage_acceptance() {
2086        run_live_rootless_acceptance(true);
2087    }
2088
2089    #[test]
2090    #[ignore = "requires a queued same-repository Actions job and a real JIT config on stdin"]
2091    fn live_rootless_github_jit_acceptance() {
2092        let image = ImageReference::new(
2093            std::env::var(LIVE_IMAGE_ENV)
2094                .unwrap_or_else(|_| panic!("{LIVE_IMAGE_ENV} must name a pinned image digest")),
2095        )
2096        .unwrap();
2097        let runner_root = PathBuf::from(
2098            std::env::var("RUNNER_MANAGER_OCI_JIT_RUNNER_DIR")
2099                .expect("RUNNER_MANAGER_OCI_JIT_RUNNER_DIR must name the extracted runner package"),
2100        );
2101        assert!(runner_root.is_absolute());
2102        assert!(!runner_root.starts_with("/mnt"));
2103        assert!(runner_root.join("bin/Runner.Listener").is_file());
2104
2105        let mut jit = String::new();
2106        std::io::stdin()
2107            .read_to_string(&mut jit)
2108            .expect("JIT stdin could not be read");
2109        while matches!(jit.as_bytes().last(), Some(b'\n' | b'\r')) {
2110            jit.pop();
2111        }
2112        assert!(!jit.is_empty(), "JIT stdin was empty");
2113        assert!(
2114            jit.len() <= 64 * 1024,
2115            "JIT stdin exceeded the provider limit"
2116        );
2117        assert!(
2118            !jit.bytes()
2119                .any(|byte| byte == b'\n' || byte == b'\r' || byte == 0),
2120            "JIT stdin contained a forbidden delimiter"
2121        );
2122
2123        let provider = OciProcesses::new(HostId::from_u128(LIVE_HOST));
2124        let policy = isolated_policy(image.clone());
2125        assert_eq!(provider.probe(&policy), ProviderCapability::Ready);
2126        let resolution = provider.resolve(&policy).unwrap().unwrap();
2127        assert_eq!(resolution.provider_kind, Backend::Oci);
2128        assert_eq!(resolution.image, image);
2129
2130        let mut attempt = RunnerAttempt::allocate(
2131            AttemptId::new_random(),
2132            policy.id,
2133            &runner_root,
2134            FakeClock::default().now(),
2135        );
2136        attempt
2137            .allocate_execution(AttemptExecution::Isolated {
2138                provider_kind: Backend::Oci,
2139                environment_id: None,
2140                resolved_image: image.clone(),
2141                generation: "github-jit".into(),
2142            })
2143            .unwrap();
2144        let prepared = provider.prepare(&attempt, &policy).unwrap();
2145        let EnvironmentIdentity::Isolated { environment_id, .. } =
2146            prepared.identity().cloned().expect("isolated identity")
2147        else {
2148            panic!("OCI prepare returned a native identity")
2149        };
2150        attempt
2151            .prepared_environment(environment_id.clone())
2152            .unwrap();
2153        let config = EncodedJitConfig::new(std::mem::take(&mut jit));
2154        provider
2155            .start(prepared, &attempt, OneTimeJitHandoff::new(&config))
2156            .unwrap();
2157
2158        let started = wait_for_container_file(
2159            &environment_id,
2160            "/tmp/runner-manager-actions-job-started",
2161            Instant::now() + Duration::from_secs(300),
2162        );
2163        assert!(
2164            started,
2165            "the queued Actions job was not claimed within five minutes"
2166        );
2167        assert_live_jit_absent(&environment_id, &image, config.expose());
2168        assert!(
2169            podman(&[
2170                "exec",
2171                &environment_id,
2172                "touch",
2173                "/tmp/runner-manager-provider-inspected",
2174            ])
2175            .status
2176            .success(),
2177            "could not release the Actions job after provider-side inspection"
2178        );
2179
2180        let completed = wait_for_container_file(
2181            &environment_id,
2182            "/tmp/runner-manager-actions-job-complete",
2183            Instant::now() + Duration::from_secs(600),
2184        );
2185        assert!(
2186            completed,
2187            "the Actions marker step did not complete within ten minutes"
2188        );
2189        let exit_deadline = Instant::now() + Duration::from_secs(120);
2190        while provider.inspect(&attempt).unwrap() != EnvironmentState::Exited {
2191            assert!(
2192                Instant::now() < exit_deadline,
2193                "the one-time runner did not exit after its job"
2194            );
2195            std::thread::sleep(Duration::from_millis(250));
2196        }
2197        assert_export_excludes(&environment_id, config.expose());
2198        assert_eq!(
2199            provider.destroy(&attempt).unwrap(),
2200            ProviderDestroy::Destroyed
2201        );
2202        assert_eq!(
2203            provider.inspect(&attempt).unwrap(),
2204            EnvironmentState::Missing
2205        );
2206        eprintln!("live GitHub JIT job completed through the OCI provider; evidence is redacted");
2207    }
2208
2209    #[test]
2210    #[ignore = "requires a real rootless Podman fixture with unavailable writable-layer quota"]
2211    fn live_rootless_quota_refusal_is_typed_before_jit() {
2212        let provider = OciProcesses::new(HostId::from_u128(1));
2213        assert_eq!(
2214            provider.rootless_ready(),
2215            ProviderCapability::DiskQuotaUnavailable
2216        );
2217    }
2218}