megacode-agent-sdk 0.30.0

Drive a MEGA harness agent session from a Rust program.
Documentation
//! The verified cache (spec slice-ar R12.3–R12.4):
//! `<cache>/megacode-agent-sdk/<version>/<target>/harness`.
//!
//! Every ancestor of `<cache>` is a real folder owned by this user or root
//! and not writable by group or others unless sticky; `<cache>` and each
//! folder below it is a real folder owned by this user and not writable by
//! group or others; the file is a regular file owned by this user with no
//! group or other write. With those rules no other user can change the
//! file, or move a folder on its path, between the check and the run.

use std::fs;
use std::io::{self, Read, Seek, Write};
use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};

use sha2::{Digest, Sha256};

use super::hex;
use crate::error::Error;
use crate::process::BINARY_ENV;

/// Where one version's executable for one target lives.
#[derive(Debug, Clone)]
pub(crate) struct Layout {
    /// `<cache>`.
    pub root: PathBuf,
    /// `<cache>` and each folder below it, top down, the last holding the
    /// file.
    pub folders: Vec<PathBuf>,
    pub file: PathBuf,
}

impl Layout {
    pub(crate) fn new(root: &Path, version: &str, target: &str) -> Layout {
        let sdk = root.join("megacode-agent-sdk");
        let ver = sdk.join(version);
        let tgt = ver.join(target);
        Layout {
            root: root.to_path_buf(),
            file: tgt.join("harness"),
            folders: vec![root.to_path_buf(), sdk, ver, tgt],
        }
    }

    fn dir(&self) -> &Path {
        self.folders.last().expect("four folders")
    }
}

/// What the cache holds.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Cached {
    /// The file, its digest the pinned one.
    Verified,
    /// No file (or a folder on its path not made yet).
    Missing,
    /// A file with another digest.
    Mismatch,
}

fn euid() -> u32 {
    // SAFETY: `geteuid` has no preconditions and cannot fail.
    unsafe { libc::geteuid() }
}

fn refuse_folder(folder: &Path, why: &str) -> Error {
    Error::BinaryUnavailable(format!(
        "the cache folder {} {why}: it must be a real folder (not a symlink), owned by you, \
         not writable by group or others. Fix the folder, or name a harness with \
         Options::binary or {BINARY_ENV}",
        folder.display()
    ))
}

fn refuse_ancestor(folder: &Path, why: &str) -> Error {
    Error::BinaryUnavailable(format!(
        "the folder {} above the cache {why}: every folder above the cache must be a real \
         folder (not a symlink), owned by you or root, not writable by group or others unless \
         sticky (as /tmp is). Fix the folder, or name a harness with Options::binary or \
         {BINARY_ENV}",
        folder.display()
    ))
}

fn refuse_file(file: &Path, why: &str) -> Error {
    Error::BinaryUnavailable(format!(
        "the cached harness {} {why}: it must be a regular file (not a symlink), owned by you, \
         not writable by group or others. Remove it, or name a harness with Options::binary or \
         {BINARY_ENV}",
        file.display()
    ))
}

/// Checks every ancestor of `<cache>`, top down.
fn check_ancestors(root: &Path) -> Result<(), Error> {
    if !root.is_absolute() {
        return Err(refuse_folder(root, "is not an absolute path"));
    }
    let me = euid();
    let mut above: Vec<&Path> = root.ancestors().skip(1).collect();
    above.reverse();
    for folder in above {
        let m = fs::symlink_metadata(folder)
            .map_err(|e| refuse_ancestor(folder, &format!("cannot be read ({e})")))?;
        if m.file_type().is_symlink() {
            return Err(refuse_ancestor(folder, "is a symlink"));
        }
        if !m.is_dir() {
            return Err(refuse_ancestor(folder, "is not a folder"));
        }
        if m.uid() != me && m.uid() != 0 {
            return Err(refuse_ancestor(folder, "is owned by another user"));
        }
        if m.mode() & 0o022 != 0 && m.mode() & 0o1000 == 0 {
            return Err(refuse_ancestor(folder, "is writable by group or others"));
        }
    }
    Ok(())
}

/// Checks one of the cache's own folders; `false` when it does not exist.
fn check_folder(folder: &Path) -> Result<bool, Error> {
    let m = match fs::symlink_metadata(folder) {
        Ok(m) => m,
        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
        Err(e) => return Err(refuse_folder(folder, &format!("cannot be read ({e})"))),
    };
    if m.file_type().is_symlink() {
        return Err(refuse_folder(folder, "is a symlink"));
    }
    if !m.is_dir() {
        return Err(refuse_folder(folder, "is not a folder"));
    }
    if m.uid() != euid() {
        return Err(refuse_folder(folder, "is owned by another user"));
    }
    if m.mode() & 0o022 != 0 {
        return Err(refuse_folder(folder, "is writable by group or others"));
    }
    Ok(true)
}

/// R12.4: opens the cached file, checks it on the open file, and hashes it.
pub(crate) fn check(layout: &Layout, executable_sha256: &str) -> Result<Cached, Error> {
    check_ancestors(&layout.root)?;
    for folder in &layout.folders {
        if !check_folder(folder)? {
            return Ok(Cached::Missing);
        }
    }
    let file = &layout.file;
    let mut open = fs::OpenOptions::new();
    open.read(true)
        .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC);
    let mut f = match open.open(file) {
        Ok(f) => f,
        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Cached::Missing),
        Err(e) if e.raw_os_error() == Some(libc::ELOOP) => {
            return Err(refuse_file(file, "is a symlink"));
        }
        Err(e) => return Err(refuse_file(file, &format!("cannot be opened ({e})"))),
    };
    let m = f
        .metadata()
        .map_err(|e| refuse_file(file, &format!("cannot be read ({e})")))?;
    if !m.is_file() {
        return Err(refuse_file(file, "is not a regular file"));
    }
    if m.uid() != euid() {
        return Err(refuse_file(file, "is owned by another user"));
    }
    if m.mode() & 0o022 != 0 {
        return Err(refuse_file(file, "is writable by group or others"));
    }
    let digest = hash(&mut f).map_err(|e| refuse_file(file, &format!("cannot be read ({e})")))?;
    Ok(if digest == executable_sha256 {
        Cached::Verified
    } else {
        Cached::Mismatch
    })
}

fn hash(f: &mut impl Read) -> io::Result<String> {
    let mut h = Sha256::new();
    let mut buf = vec![0u8; 256 * 1024];
    loop {
        let n = f.read(&mut buf)?;
        if n == 0 {
            return Ok(hex(&h.finalize()));
        }
        h.update(&buf[..n]);
    }
}

/// R12.3 writing: makes the missing folders (`0700`), writes `executable`
/// to `.harness.<pid>.<random>.tmp` (created exclusively), checks it
/// against the pinned digest, makes it `0755`, and renames it into place.
/// The temporary file is removed on any failure.
pub(crate) fn install(
    layout: &Layout,
    executable: &[u8],
    executable_sha256: &str,
) -> Result<(), Error> {
    check_ancestors(&layout.root)?;
    for folder in &layout.folders {
        if check_folder(folder)? {
            continue;
        }
        match fs::DirBuilder::new().mode(0o700).create(folder) {
            Ok(()) => {}
            // Another program made it first; it is checked as any other.
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
            Err(e) => return Err(refuse_folder(folder, &format!("cannot be made ({e})"))),
        }
        if !check_folder(folder)? {
            return Err(refuse_folder(folder, "vanished after it was made"));
        }
    }
    sweep_stale(layout.dir());
    let tmp = layout.dir().join(format!(
        ".harness.{}.{:016x}.tmp",
        std::process::id(),
        crate::process::unique()
    ));
    let written = write_checked(&tmp, executable, executable_sha256)
        .and_then(|()| fs::rename(&tmp, &layout.file));
    written.map_err(|e| {
        let _ = fs::remove_file(&tmp);
        Error::BinaryUnavailable(format!(
            "cannot write the harness into {} ({e}); nothing was cached",
            layout.dir().display()
        ))
    })
}

/// A temporary file older than this was left by a program that stopped
/// mid-write; a live writer's file is far younger.
const STALE: std::time::Duration = std::time::Duration::from_secs(60 * 60);

/// Removes stale `.harness.*.tmp` regular files from `dir`, each by name;
/// nothing else is touched.
fn sweep_stale(dir: &Path) {
    let Ok(entries) = fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if !(name.starts_with(".harness.") && name.ends_with(".tmp")) {
            continue;
        }
        let path = entry.path();
        let stale = fs::symlink_metadata(&path).is_ok_and(|m| {
            m.is_file()
                && m.modified()
                    .is_ok_and(|t| t.elapsed().is_ok_and(|age| age > STALE))
        });
        if stale {
            let _ = fs::remove_file(&path);
        }
    }
}

fn write_checked(tmp: &Path, executable: &[u8], executable_sha256: &str) -> io::Result<()> {
    let mut f = fs::OpenOptions::new()
        .read(true)
        .write(true)
        .create_new(true)
        .mode(0o700)
        .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
        .open(tmp)?;
    f.write_all(executable)?;
    f.sync_all()?;
    f.rewind()?;
    let digest = hash(&mut f)?;
    if digest != executable_sha256 {
        return Err(io::Error::other(format!(
            "the written file's SHA-256 is {digest}, not the pinned {executable_sha256}"
        )));
    }
    f.set_permissions(fs::Permissions::from_mode(0o755))?;
    f.sync_all()
}