Skip to main content

falsegreen_agent/
runtime.rs

1//! Pinned llama.cpp runtime catalog and integrity-preserving installation.
2
3use std::fs::{self, File, OpenOptions};
4use std::io::Write;
5use std::path::{Component, Path, PathBuf};
6use std::thread;
7use std::time::{Duration, Instant};
8
9use flate2::read::GzDecoder;
10use fs2::FileExt;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13use uuid::Uuid;
14
15use crate::artifact::{ArtifactError, ArtifactFetcher, ArtifactSpec, hash_file};
16use crate::hardware::{
17    Architecture, HardwareProfile, OperatingSystem, RuntimeBackend, RuntimePlatform,
18};
19
20/// Official llama.cpp nightly built from the source commit recorded by the NeoHorse qualification.
21pub const LLAMA_CPP_RELEASE: &str = "b10630";
22pub const LLAMA_CPP_COMMIT: &str = "d222767c7a6516559a3f49e7721b6c6b1acc87b4";
23pub const LLAMA_CPP_VERSION: &str = "0.3.0-dev";
24
25const MANIFEST_FILE: &str = ".falsegreen-runtime-manifest.json";
26const MAX_EXTRACTED_BYTES: u64 = 4 * 1024 * 1024 * 1024;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30enum ArchiveFormat {
31    TarGz,
32    Zip,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36struct RuntimeArtifact {
37    spec: ArtifactSpec,
38    platform: RuntimePlatform,
39    backend: RuntimeBackend,
40    archive_format: ArchiveFormat,
41    server_relative_path: PathBuf,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct RuntimeInstall {
46    pub root: PathBuf,
47    pub server_executable: PathBuf,
48    pub artifact_file_name: String,
49    pub artifact_sha256: String,
50    pub platform: RuntimePlatform,
51    pub backend: RuntimeBackend,
52    pub release: String,
53    pub commit: String,
54    pub reused: bool,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct RuntimeCacheInspection {
59    pub archive_path: PathBuf,
60    pub install_root: PathBuf,
61    pub server_executable: PathBuf,
62    pub artifact_file_name: String,
63    pub artifact_sha256: String,
64    pub artifact_size_bytes: u64,
65    pub archive_present: bool,
66    pub archive_digest_verified: Option<bool>,
67    pub install_present: bool,
68    pub install_verified: bool,
69}
70
71#[derive(Debug, Clone)]
72pub struct LlamaRuntimeManager {
73    cache_root: PathBuf,
74    fetcher: ArtifactFetcher,
75    lock_timeout: Duration,
76}
77
78impl LlamaRuntimeManager {
79    #[must_use]
80    pub fn new(cache_root: impl Into<PathBuf>, fetcher: ArtifactFetcher) -> Self {
81        Self {
82            cache_root: cache_root.into(),
83            fetcher,
84            lock_timeout: Duration::from_secs(60),
85        }
86    }
87
88    #[must_use]
89    pub const fn with_lock_timeout(mut self, timeout: Duration) -> Self {
90        self.lock_timeout = timeout;
91        self
92    }
93
94    pub fn ensure(
95        &self,
96        hardware: &HardwareProfile,
97        preferred_backend: Option<RuntimeBackend>,
98    ) -> Result<RuntimeInstall, RuntimeError> {
99        self.ensure_for(
100            hardware.platform,
101            preferred_backend.unwrap_or_else(|| hardware.recommended_backend()),
102        )
103    }
104
105    pub fn ensure_for(
106        &self,
107        platform: RuntimePlatform,
108        backend: RuntimeBackend,
109    ) -> Result<RuntimeInstall, RuntimeError> {
110        let artifact = runtime_artifact(platform, backend)?;
111        let archive_path = self
112            .cache_root
113            .join("downloads")
114            .join("llama.cpp")
115            .join(LLAMA_CPP_RELEASE)
116            .join(&artifact.spec.file_name);
117        self.fetcher.ensure_file(&artifact.spec, &archive_path)?;
118
119        let install_parent = self
120            .cache_root
121            .join("runtimes")
122            .join("llama.cpp")
123            .join(LLAMA_CPP_RELEASE);
124        fs::create_dir_all(&install_parent)
125            .map_err(|source| io_error("create runtime cache", &install_parent, source))?;
126        let install_name = format!("{}-{}", platform.cache_key(), backend.cache_key());
127        let install_root = install_parent.join(install_name);
128        let lock_path = install_root.with_extension("lock");
129        let _lock = DirectoryLock::acquire(&lock_path, self.lock_timeout)?;
130
131        if install_root.exists() {
132            match validate_install(&install_root, &artifact) {
133                Ok(server_executable) => {
134                    return Ok(RuntimeInstall {
135                        root: install_root,
136                        server_executable,
137                        artifact_file_name: artifact.spec.file_name.clone(),
138                        artifact_sha256: artifact.spec.sha256.clone(),
139                        platform,
140                        backend,
141                        release: LLAMA_CPP_RELEASE.to_owned(),
142                        commit: LLAMA_CPP_COMMIT.to_owned(),
143                        reused: true,
144                    });
145                }
146                Err(RuntimeError::InvalidInstall(_)) => quarantine_directory(&install_root)?,
147                Err(error) => return Err(error),
148            }
149        }
150
151        let staging = install_parent.join(format!(".staging-{}", Uuid::new_v4()));
152        let mut staging_guard = StagingDirectory::create(staging)?;
153        extract_archive(&archive_path, staging_guard.path(), artifact.archive_format)?;
154        let server_executable = staging_guard.path().join(&artifact.server_relative_path);
155        if !server_executable.is_file() {
156            return Err(RuntimeError::InvalidInstall(format!(
157                "archive did not contain expected server executable {}",
158                artifact.server_relative_path.display()
159            )));
160        }
161        make_executable(&server_executable)?;
162        let entries = collect_entries(staging_guard.path())?;
163        let manifest = InstallManifest {
164            schema_version: 1,
165            release: LLAMA_CPP_RELEASE.to_owned(),
166            commit: LLAMA_CPP_COMMIT.to_owned(),
167            platform,
168            backend,
169            archive_sha256: artifact.spec.sha256.to_ascii_lowercase(),
170            server_relative_path: artifact.server_relative_path.clone(),
171            entries,
172        };
173        write_manifest(staging_guard.path(), &manifest)?;
174        staging_guard.publish(&install_root)?;
175        let server_executable = install_root.join(&artifact.server_relative_path);
176        validate_install(&install_root, &artifact)?;
177
178        Ok(RuntimeInstall {
179            root: install_root,
180            server_executable,
181            artifact_file_name: artifact.spec.file_name,
182            artifact_sha256: artifact.spec.sha256,
183            platform,
184            backend,
185            release: LLAMA_CPP_RELEASE.to_owned(),
186            commit: LLAMA_CPP_COMMIT.to_owned(),
187            reused: false,
188        })
189    }
190
191    /// Inspect pinned runtime cache paths without creating, quarantining, or downloading data.
192    pub fn inspect_for(
193        &self,
194        platform: RuntimePlatform,
195        backend: RuntimeBackend,
196        verify_archive_digest: bool,
197    ) -> Result<RuntimeCacheInspection, RuntimeError> {
198        let artifact = runtime_artifact(platform, backend)?;
199        let archive_path = self
200            .cache_root
201            .join("downloads")
202            .join("llama.cpp")
203            .join(LLAMA_CPP_RELEASE)
204            .join(&artifact.spec.file_name);
205        let install_root = self
206            .cache_root
207            .join("runtimes")
208            .join("llama.cpp")
209            .join(LLAMA_CPP_RELEASE)
210            .join(format!("{}-{}", platform.cache_key(), backend.cache_key()));
211        let archive_present = fs::symlink_metadata(&archive_path)
212            .is_ok_and(|metadata| metadata.file_type().is_file());
213        let archive_digest_verified = (archive_present && verify_archive_digest)
214            .then(|| crate::artifact::verify_file(&archive_path, &artifact.spec).is_ok());
215        let install_present =
216            fs::symlink_metadata(&install_root).is_ok_and(|metadata| metadata.file_type().is_dir());
217        let validated_server = install_present
218            .then(|| validate_install(&install_root, &artifact))
219            .transpose();
220        let (server_executable, install_verified) = match validated_server {
221            Ok(Some(path)) => (path, true),
222            Ok(None) | Err(_) => (install_root.join(&artifact.server_relative_path), false),
223        };
224        Ok(RuntimeCacheInspection {
225            archive_path,
226            install_root,
227            server_executable,
228            artifact_file_name: artifact.spec.file_name,
229            artifact_sha256: artifact.spec.sha256,
230            artifact_size_bytes: artifact.spec.size_bytes,
231            archive_present,
232            archive_digest_verified,
233            install_present,
234            install_verified,
235        })
236    }
237}
238
239#[derive(Debug, Error)]
240pub enum RuntimeError {
241    #[error(transparent)]
242    Artifact(#[from] ArtifactError),
243    #[error("no pinned llama-server artifact for {platform} with {backend:?} backend")]
244    UnsupportedTarget {
245        platform: RuntimePlatform,
246        backend: RuntimeBackend,
247    },
248    #[error("runtime archive contains an unsafe or unsupported entry: {0}")]
249    UnsafeArchive(String),
250    #[error("managed runtime installation is not trustworthy: {0}")]
251    InvalidInstall(String),
252    #[error("timed out waiting for runtime install lock {path} after {seconds} seconds")]
253    LockTimeout { path: PathBuf, seconds: u64 },
254    #[error("could not {operation} at {path}: {source}")]
255    Io {
256        operation: &'static str,
257        path: PathBuf,
258        #[source]
259        source: std::io::Error,
260    },
261    #[error("could not decode runtime archive: {0}")]
262    Archive(String),
263    #[error("could not serialize runtime manifest: {0}")]
264    Manifest(#[from] serde_json::Error),
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268struct InstallManifest {
269    schema_version: u32,
270    release: String,
271    commit: String,
272    platform: RuntimePlatform,
273    backend: RuntimeBackend,
274    archive_sha256: String,
275    server_relative_path: PathBuf,
276    entries: Vec<InstalledEntry>,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(tag = "kind", rename_all = "snake_case")]
281enum InstalledEntry {
282    File {
283        path: PathBuf,
284        sha256: String,
285        size_bytes: u64,
286    },
287    Symlink {
288        path: PathBuf,
289        target: PathBuf,
290    },
291}
292
293fn runtime_artifact(
294    platform: RuntimePlatform,
295    backend: RuntimeBackend,
296) -> Result<RuntimeArtifact, RuntimeError> {
297    let (name, digest, size, format) = match (platform.os, platform.architecture, backend) {
298        (OperatingSystem::Linux, Architecture::X86_64, RuntimeBackend::Cpu) => (
299            "llama-b10630-bin-ubuntu-x64.tar.gz",
300            "ab5e2c472ae317a8e2210272a25569a74c054a05e196feef200bfef270d2204b",
301            16_292_161,
302            ArchiveFormat::TarGz,
303        ),
304        (OperatingSystem::Linux, Architecture::X86_64, RuntimeBackend::Rocm) => (
305            "llama-b10630-bin-ubuntu-rocm-7.14-x64.tar.gz",
306            "a44a8649b44398465ad49bccd1eda39e7019caade63f918c220e05e3572aaaee",
307            213_952_218,
308            ArchiveFormat::TarGz,
309        ),
310        (OperatingSystem::Linux, Architecture::X86_64, RuntimeBackend::Vulkan) => (
311            "llama-b10630-bin-ubuntu-vulkan-x64.tar.gz",
312            "d2d3da1bab8c3b85dfd130a8bcbec9ca83abc584000ee688d92c296550666353",
313            32_914_728,
314            ArchiveFormat::TarGz,
315        ),
316        (OperatingSystem::Linux, Architecture::Aarch64, RuntimeBackend::Cpu) => (
317            "llama-b10630-bin-ubuntu-arm64.tar.gz",
318            "783bd5dd828cdb9a26a26b85ce9adc2951f2be0c37e0b9593059116431a8e771",
319            13_042_152,
320            ArchiveFormat::TarGz,
321        ),
322        (OperatingSystem::Linux, Architecture::Aarch64, RuntimeBackend::Vulkan) => (
323            "llama-b10630-bin-ubuntu-vulkan-arm64.tar.gz",
324            "6dee633ff7f9f5e2279eeda15bd464b5b3894af5bd602b3a2d7c7da27cae7e57",
325            26_779_926,
326            ArchiveFormat::TarGz,
327        ),
328        (OperatingSystem::Macos, Architecture::X86_64, RuntimeBackend::Metal) => (
329            "llama-b10630-bin-macos-x64.tar.gz",
330            "49100754ff4211e6c0e558fe3e9384d59eca51ce1c70673a632f788c6b58d800",
331            11_034_413,
332            ArchiveFormat::TarGz,
333        ),
334        (OperatingSystem::Macos, Architecture::Aarch64, RuntimeBackend::Metal) => (
335            "llama-b10630-bin-macos-arm64.tar.gz",
336            "3394752534fc7cb62bf861868b750a704ead264ac9fac7083e7d49e835a19a10",
337            10_963_541,
338            ArchiveFormat::TarGz,
339        ),
340        (OperatingSystem::Windows, Architecture::X86_64, RuntimeBackend::Cpu) => (
341            "llama-b10630-bin-win-cpu-x64.zip",
342            "45fe4ffb81386872ee3cbb1ed83473df1ed3ce68edcb3d2d0bd31c5a16c4f416",
343            18_067_881,
344            ArchiveFormat::Zip,
345        ),
346        (OperatingSystem::Windows, Architecture::X86_64, RuntimeBackend::Rocm) => (
347            "llama-b10630-bin-win-rocm-7.14-x64.zip",
348            "fb067cc91e8a09885d3dbb9dc9ebc67279c1e11a078a0ebf8aa05c4cc8b6b0bb",
349            196_227_720,
350            ArchiveFormat::Zip,
351        ),
352        (OperatingSystem::Windows, Architecture::X86_64, RuntimeBackend::Vulkan) => (
353            "llama-b10630-bin-win-vulkan-x64.zip",
354            "09e9afa74194522465110df724250dae40393f66b42a8ce8901c4dab6ed17c1f",
355            34_403_736,
356            ArchiveFormat::Zip,
357        ),
358        (OperatingSystem::Windows, Architecture::Aarch64, RuntimeBackend::Cpu) => (
359            "llama-b10630-bin-win-cpu-arm64.zip",
360            "c7e0e822fc3cc967e4111b6ab0685ffa11ee2f62923b6c35527e601f8c9e6da4",
361            11_846_985,
362            ArchiveFormat::Zip,
363        ),
364        _ => return Err(RuntimeError::UnsupportedTarget { platform, backend }),
365    };
366    let server_relative_path = if platform.os == OperatingSystem::Windows {
367        PathBuf::from("llama-server.exe")
368    } else {
369        PathBuf::from(format!("llama-{LLAMA_CPP_RELEASE}/llama-server"))
370    };
371    Ok(RuntimeArtifact {
372        spec: ArtifactSpec {
373            id: "llama.cpp".to_owned(),
374            version: LLAMA_CPP_RELEASE.to_owned(),
375            file_name: name.to_owned(),
376            url: format!(
377                "https://github.com/ggml-org/llama.cpp/releases/download/{LLAMA_CPP_RELEASE}/{name}"
378            ),
379            sha256: digest.to_owned(),
380            size_bytes: size,
381        },
382        platform,
383        backend,
384        archive_format: format,
385        server_relative_path,
386    })
387}
388
389fn extract_archive(
390    archive_path: &Path,
391    destination: &Path,
392    format: ArchiveFormat,
393) -> Result<(), RuntimeError> {
394    match format {
395        ArchiveFormat::TarGz => extract_tar_gz(archive_path, destination),
396        ArchiveFormat::Zip => extract_zip(archive_path, destination),
397    }
398}
399
400fn extract_tar_gz(archive_path: &Path, destination: &Path) -> Result<(), RuntimeError> {
401    let file = File::open(archive_path)
402        .map_err(|source| io_error("open runtime archive", archive_path, source))?;
403    let mut archive = tar::Archive::new(GzDecoder::new(file));
404    let entries = archive
405        .entries()
406        .map_err(|error| RuntimeError::Archive(error.to_string()))?;
407    let mut extracted = 0_u64;
408    for entry in entries {
409        let mut entry = entry.map_err(|error| RuntimeError::Archive(error.to_string()))?;
410        let relative = entry
411            .path()
412            .map_err(|error| RuntimeError::Archive(error.to_string()))?
413            .into_owned();
414        validate_relative_path(&relative)?;
415        let kind = entry.header().entry_type();
416        if kind.is_file() {
417            extracted = bounded_add(extracted, entry.size(), &relative)?;
418        } else if kind.is_symlink() {
419            let target = entry
420                .link_name()
421                .map_err(|error| RuntimeError::Archive(error.to_string()))?
422                .ok_or_else(|| {
423                    RuntimeError::UnsafeArchive(format!(
424                        "symlink {} has no target",
425                        relative.display()
426                    ))
427                })?;
428            validate_symlink_target(&relative, &target)?;
429        } else if !kind.is_dir() {
430            return Err(RuntimeError::UnsafeArchive(format!(
431                "{} has unsupported tar entry type",
432                relative.display()
433            )));
434        }
435        let unpacked = entry
436            .unpack_in(destination)
437            .map_err(|error| RuntimeError::Archive(error.to_string()))?;
438        if !unpacked {
439            return Err(RuntimeError::UnsafeArchive(relative.display().to_string()));
440        }
441    }
442    Ok(())
443}
444
445fn extract_zip(archive_path: &Path, destination: &Path) -> Result<(), RuntimeError> {
446    let file = File::open(archive_path)
447        .map_err(|source| io_error("open runtime archive", archive_path, source))?;
448    let mut archive =
449        zip::ZipArchive::new(file).map_err(|error| RuntimeError::Archive(error.to_string()))?;
450    let mut extracted = 0_u64;
451    for index in 0..archive.len() {
452        let mut entry = archive
453            .by_index(index)
454            .map_err(|error| RuntimeError::Archive(error.to_string()))?;
455        let relative = entry.enclosed_name().ok_or_else(|| {
456            RuntimeError::UnsafeArchive(format!("unsafe zip path {}", entry.name()))
457        })?;
458        validate_relative_path(&relative)?;
459        if entry
460            .unix_mode()
461            .is_some_and(|mode| mode & 0o170_000 == 0o120_000)
462        {
463            return Err(RuntimeError::UnsafeArchive(format!(
464                "zip symlinks are not accepted: {}",
465                relative.display()
466            )));
467        }
468        let output_path = destination.join(&relative);
469        if entry.is_dir() {
470            fs::create_dir_all(&output_path)
471                .map_err(|source| io_error("create archive directory", &output_path, source))?;
472            continue;
473        }
474        extracted = bounded_add(extracted, entry.size(), &relative)?;
475        if let Some(parent) = output_path.parent() {
476            fs::create_dir_all(parent)
477                .map_err(|source| io_error("create archive directory", parent, source))?;
478        }
479        let mut output = OpenOptions::new()
480            .create_new(true)
481            .write(true)
482            .open(&output_path)
483            .map_err(|source| io_error("create extracted file", &output_path, source))?;
484        std::io::copy(&mut entry, &mut output)
485            .map_err(|source| io_error("extract runtime file", &output_path, source))?;
486    }
487    Ok(())
488}
489
490fn bounded_add(total: u64, size: u64, path: &Path) -> Result<u64, RuntimeError> {
491    let total = total.checked_add(size).ok_or_else(|| {
492        RuntimeError::UnsafeArchive(format!("expanded size overflow at {}", path.display()))
493    })?;
494    if total > MAX_EXTRACTED_BYTES {
495        return Err(RuntimeError::UnsafeArchive(format!(
496            "expanded archive exceeds {MAX_EXTRACTED_BYTES} bytes"
497        )));
498    }
499    Ok(total)
500}
501
502fn validate_relative_path(path: &Path) -> Result<(), RuntimeError> {
503    if path.as_os_str().is_empty()
504        || path
505            .components()
506            .any(|component| !matches!(component, Component::Normal(_) | Component::CurDir))
507    {
508        return Err(RuntimeError::UnsafeArchive(path.display().to_string()));
509    }
510    Ok(())
511}
512
513fn validate_symlink_target(path: &Path, target: &Path) -> Result<(), RuntimeError> {
514    if target.is_absolute() {
515        return Err(RuntimeError::UnsafeArchive(format!(
516            "{} points outside the install root",
517            path.display()
518        )));
519    }
520    let mut depth = path.parent().map_or(0, |parent| {
521        parent
522            .components()
523            .filter(|component| matches!(component, Component::Normal(_)))
524            .count()
525    });
526    for component in target.components() {
527        match component {
528            Component::Normal(_) => depth += 1,
529            Component::CurDir => {}
530            Component::ParentDir if depth > 0 => depth -= 1,
531            _ => {
532                return Err(RuntimeError::UnsafeArchive(format!(
533                    "{} has unsafe symlink target {}",
534                    path.display(),
535                    target.display()
536                )));
537            }
538        }
539    }
540    Ok(())
541}
542
543fn collect_entries(root: &Path) -> Result<Vec<InstalledEntry>, RuntimeError> {
544    fn walk(
545        root: &Path,
546        directory: &Path,
547        entries: &mut Vec<InstalledEntry>,
548    ) -> Result<(), RuntimeError> {
549        let children = fs::read_dir(directory)
550            .map_err(|source| io_error("inspect runtime install", directory, source))?;
551        for child in children {
552            let child =
553                child.map_err(|source| io_error("inspect runtime install", directory, source))?;
554            let path = child.path();
555            let relative = path
556                .strip_prefix(root)
557                .expect("walked path remains beneath root")
558                .to_path_buf();
559            if relative == Path::new(MANIFEST_FILE) {
560                continue;
561            }
562            let metadata = fs::symlink_metadata(&path)
563                .map_err(|source| io_error("inspect runtime entry", &path, source))?;
564            if metadata.is_dir() {
565                walk(root, &path, entries)?;
566            } else if metadata.is_file() {
567                let (sha256, size_bytes) = hash_file(&path)?;
568                entries.push(InstalledEntry::File {
569                    path: relative,
570                    sha256,
571                    size_bytes,
572                });
573            } else if metadata.file_type().is_symlink() {
574                let target = fs::read_link(&path)
575                    .map_err(|source| io_error("read runtime symlink", &path, source))?;
576                validate_symlink_target(&relative, &target)?;
577                entries.push(InstalledEntry::Symlink {
578                    path: relative,
579                    target,
580                });
581            } else {
582                return Err(RuntimeError::InvalidInstall(format!(
583                    "unsupported installed entry {}",
584                    relative.display()
585                )));
586            }
587        }
588        Ok(())
589    }
590
591    let mut entries = Vec::new();
592    walk(root, root, &mut entries)?;
593    entries.sort_by(|left, right| entry_path(left).cmp(entry_path(right)));
594    Ok(entries)
595}
596
597fn entry_path(entry: &InstalledEntry) -> &Path {
598    match entry {
599        InstalledEntry::File { path, .. } | InstalledEntry::Symlink { path, .. } => path,
600    }
601}
602
603fn write_manifest(root: &Path, manifest: &InstallManifest) -> Result<(), RuntimeError> {
604    let path = root.join(MANIFEST_FILE);
605    let bytes = serde_json::to_vec_pretty(manifest)?;
606    let mut file = OpenOptions::new()
607        .create_new(true)
608        .write(true)
609        .open(&path)
610        .map_err(|source| io_error("create runtime manifest", &path, source))?;
611    file.write_all(&bytes)
612        .and_then(|()| file.sync_all())
613        .map_err(|source| io_error("write runtime manifest", &path, source))
614}
615
616fn validate_install(root: &Path, artifact: &RuntimeArtifact) -> Result<PathBuf, RuntimeError> {
617    let root_metadata = fs::symlink_metadata(root).map_err(|error| {
618        RuntimeError::InvalidInstall(format!("cannot inspect install root: {error}"))
619    })?;
620    if !root_metadata.is_dir() || root_metadata.file_type().is_symlink() {
621        return Err(RuntimeError::InvalidInstall(format!(
622            "{} is not a directory",
623            root.display()
624        )));
625    }
626    let manifest_path = root.join(MANIFEST_FILE);
627    let bytes = fs::read(&manifest_path)
628        .map_err(|error| RuntimeError::InvalidInstall(format!("cannot read manifest: {error}")))?;
629    let manifest: InstallManifest = serde_json::from_slice(&bytes)
630        .map_err(|error| RuntimeError::InvalidInstall(format!("invalid manifest: {error}")))?;
631    if manifest.schema_version != 1
632        || manifest.release != LLAMA_CPP_RELEASE
633        || manifest.commit != LLAMA_CPP_COMMIT
634        || manifest.platform != artifact.platform
635        || manifest.backend != artifact.backend
636        || !manifest
637            .archive_sha256
638            .eq_ignore_ascii_case(&artifact.spec.sha256)
639        || manifest.server_relative_path != artifact.server_relative_path
640    {
641        return Err(RuntimeError::InvalidInstall(
642            "manifest identity does not match the pinned runtime".to_owned(),
643        ));
644    }
645    let current =
646        collect_entries(root).map_err(|error| RuntimeError::InvalidInstall(error.to_string()))?;
647    if current != manifest.entries {
648        return Err(RuntimeError::InvalidInstall(
649            "installed runtime files differ from the integrity manifest".to_owned(),
650        ));
651    }
652    let executable = root.join(&manifest.server_relative_path);
653    if !executable.is_file() {
654        return Err(RuntimeError::InvalidInstall(
655            "server executable is missing".to_owned(),
656        ));
657    }
658    Ok(executable)
659}
660
661#[cfg(unix)]
662fn make_executable(path: &Path) -> Result<(), RuntimeError> {
663    use std::os::unix::fs::PermissionsExt;
664
665    let mut permissions = fs::metadata(path)
666        .map_err(|source| io_error("inspect server executable", path, source))?
667        .permissions();
668    permissions.set_mode(permissions.mode() | 0o700);
669    fs::set_permissions(path, permissions)
670        .map_err(|source| io_error("mark server executable", path, source))
671}
672
673#[cfg(not(unix))]
674fn make_executable(_path: &Path) -> Result<(), RuntimeError> {
675    Ok(())
676}
677
678fn quarantine_directory(path: &Path) -> Result<(), RuntimeError> {
679    let file_name = path
680        .file_name()
681        .and_then(|name| name.to_str())
682        .ok_or_else(|| RuntimeError::InvalidInstall(path.display().to_string()))?;
683    let target = path.with_file_name(format!(".{file_name}.invalid-{}", Uuid::new_v4()));
684    fs::rename(path, &target).map_err(|source| io_error("quarantine invalid runtime", path, source))
685}
686
687fn io_error(operation: &'static str, path: &Path, source: std::io::Error) -> RuntimeError {
688    RuntimeError::Io {
689        operation,
690        path: path.to_path_buf(),
691        source,
692    }
693}
694
695struct DirectoryLock {
696    file: File,
697}
698
699impl DirectoryLock {
700    fn acquire(path: &Path, timeout: Duration) -> Result<Self, RuntimeError> {
701        let started = Instant::now();
702        match fs::symlink_metadata(path) {
703            Ok(metadata) if metadata.file_type().is_symlink() => {
704                return Err(RuntimeError::InvalidInstall(format!(
705                    "runtime lock must not be a symlink: {}",
706                    path.display()
707                )));
708            }
709            Ok(_) => {}
710            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
711            Err(source) => return Err(io_error("inspect runtime install lock", path, source)),
712        }
713        let file = OpenOptions::new()
714            .create(true)
715            .read(true)
716            .write(true)
717            .truncate(false)
718            .open(path)
719            .map_err(|source| io_error("open runtime install lock", path, source))?;
720        loop {
721            match FileExt::try_lock_exclusive(&file) {
722                Ok(()) => return Ok(Self { file }),
723                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
724                    if started.elapsed() >= timeout {
725                        return Err(RuntimeError::LockTimeout {
726                            path: path.to_path_buf(),
727                            seconds: timeout.as_secs(),
728                        });
729                    }
730                    thread::sleep(Duration::from_millis(50));
731                }
732                Err(source) => return Err(io_error("lock runtime install", path, source)),
733            }
734        }
735    }
736}
737
738impl Drop for DirectoryLock {
739    fn drop(&mut self) {
740        let _ = FileExt::unlock(&self.file);
741    }
742}
743
744struct StagingDirectory {
745    path: PathBuf,
746}
747
748impl StagingDirectory {
749    fn create(path: PathBuf) -> Result<Self, RuntimeError> {
750        fs::create_dir(&path)
751            .map_err(|source| io_error("create runtime staging directory", &path, source))?;
752        Ok(Self { path })
753    }
754
755    fn path(&self) -> &Path {
756        &self.path
757    }
758
759    fn publish(&mut self, destination: &Path) -> Result<(), RuntimeError> {
760        fs::rename(&self.path, destination)
761            .map_err(|source| io_error("publish runtime installation", destination, source))?;
762        self.path = PathBuf::new();
763        Ok(())
764    }
765}
766
767impl Drop for StagingDirectory {
768    fn drop(&mut self) {
769        if !self.path.as_os_str().is_empty() {
770            let _ = fs::remove_dir_all(&self.path);
771        }
772    }
773}
774
775#[cfg(test)]
776mod tests {
777    use std::io::Write;
778
779    use super::{
780        Architecture, ArchiveFormat, InstallManifest, LLAMA_CPP_COMMIT, LLAMA_CPP_RELEASE,
781        OperatingSystem, RuntimeArtifact, RuntimeBackend, RuntimeError, RuntimePlatform,
782        collect_entries, extract_zip, make_executable, runtime_artifact, validate_install,
783        validate_symlink_target, write_manifest,
784    };
785    use crate::artifact::ArtifactSpec;
786
787    #[test]
788    fn catalog_is_platform_and_backend_specific() {
789        let linux = RuntimePlatform {
790            os: OperatingSystem::Linux,
791            architecture: Architecture::X86_64,
792        };
793        let cpu = runtime_artifact(linux, RuntimeBackend::Cpu).expect("cpu");
794        let rocm = runtime_artifact(linux, RuntimeBackend::Rocm).expect("rocm");
795        assert_ne!(cpu.spec.sha256, rocm.spec.sha256);
796        assert!(rocm.spec.file_name.contains("rocm-7.14"));
797
798        let unsupported = RuntimePlatform {
799            os: OperatingSystem::Macos,
800            architecture: Architecture::Aarch64,
801        };
802        assert!(matches!(
803            runtime_artifact(unsupported, RuntimeBackend::Rocm),
804            Err(RuntimeError::UnsupportedTarget { .. })
805        ));
806
807        let windows = RuntimePlatform {
808            os: OperatingSystem::Windows,
809            architecture: Architecture::X86_64,
810        };
811        assert_eq!(
812            runtime_artifact(windows, RuntimeBackend::Cpu)
813                .expect("windows cpu")
814                .server_relative_path,
815            std::path::PathBuf::from("llama-server.exe")
816        );
817    }
818
819    #[test]
820    fn symlink_validation_allows_internal_links_only() {
821        assert!(
822            validate_symlink_target(
823                std::path::Path::new("root/lib.so"),
824                std::path::Path::new("lib.so.1")
825            )
826            .is_ok()
827        );
828        assert!(
829            validate_symlink_target(
830                std::path::Path::new("root/lib.so"),
831                std::path::Path::new("../../escape")
832            )
833            .is_err()
834        );
835        assert!(
836            validate_symlink_target(
837                std::path::Path::new("root/lib.so"),
838                std::path::Path::new("/absolute")
839            )
840            .is_err()
841        );
842    }
843
844    #[test]
845    fn zip_extraction_rejects_parent_traversal() {
846        let directory = tempfile::tempdir().expect("tempdir");
847        let archive_path = directory.path().join("unsafe.zip");
848        let file = std::fs::File::create(&archive_path).expect("archive");
849        let mut archive = zip::ZipWriter::new(file);
850        archive
851            .start_file(
852                "../escape",
853                zip::write::SimpleFileOptions::default()
854                    .compression_method(zip::CompressionMethod::Stored),
855            )
856            .expect("entry");
857        archive.write_all(b"escape").expect("bytes");
858        archive.finish().expect("finish");
859        let target = directory.path().join("target");
860        std::fs::create_dir(&target).expect("target");
861
862        assert!(matches!(
863            extract_zip(&archive_path, &target),
864            Err(RuntimeError::UnsafeArchive(_))
865        ));
866        assert!(!directory.path().join("escape").exists());
867        assert_eq!(ArchiveFormat::Zip, ArchiveFormat::Zip);
868    }
869
870    #[test]
871    fn install_manifest_detects_tampering_before_reuse() {
872        let directory = tempfile::tempdir().expect("tempdir");
873        let root = directory.path().join("runtime");
874        let release_root = root.join(format!("llama-{LLAMA_CPP_RELEASE}"));
875        std::fs::create_dir_all(&release_root).expect("release root");
876        let executable = release_root.join("llama-server");
877        std::fs::write(&executable, b"trusted executable").expect("executable");
878        make_executable(&executable).expect("permissions");
879        let platform = RuntimePlatform {
880            os: OperatingSystem::Linux,
881            architecture: Architecture::X86_64,
882        };
883        let artifact = RuntimeArtifact {
884            spec: ArtifactSpec {
885                id: "llama.cpp".to_owned(),
886                version: LLAMA_CPP_RELEASE.to_owned(),
887                file_name: "runtime.tar.gz".to_owned(),
888                url: "https://example.invalid/runtime.tar.gz".to_owned(),
889                sha256: "a".repeat(64),
890                size_bytes: 1,
891            },
892            platform,
893            backend: RuntimeBackend::Cpu,
894            archive_format: ArchiveFormat::TarGz,
895            server_relative_path: executable
896                .strip_prefix(&root)
897                .expect("relative")
898                .to_path_buf(),
899        };
900        let manifest = InstallManifest {
901            schema_version: 1,
902            release: LLAMA_CPP_RELEASE.to_owned(),
903            commit: LLAMA_CPP_COMMIT.to_owned(),
904            platform,
905            backend: RuntimeBackend::Cpu,
906            archive_sha256: artifact.spec.sha256.clone(),
907            server_relative_path: artifact.server_relative_path.clone(),
908            entries: collect_entries(&root).expect("entries"),
909        };
910        write_manifest(&root, &manifest).expect("manifest");
911        assert!(validate_install(&root, &artifact).is_ok());
912
913        std::fs::write(&executable, b"tampered executable").expect("tamper");
914        assert!(matches!(
915            validate_install(&root, &artifact),
916            Err(RuntimeError::InvalidInstall(_))
917        ));
918    }
919}