runx-runtime 0.6.19

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use super::ScaffoldError;
use super::ids::{now_iso8601, random_uuid_v4};

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum InitAction {
    Project,
    Global,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunxInitOptions {
    pub action: InitAction,
    pub project_dir: PathBuf,
    pub global_home_dir: PathBuf,
    pub official_cache_dir: PathBuf,
    pub prefetch_official: bool,
    pub generated: InitGeneratedValues,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InitGeneratedValues {
    pub project_id: String,
    pub installation_id: String,
    pub created_at: String,
}

impl InitGeneratedValues {
    #[must_use]
    pub fn generate() -> Self {
        Self {
            project_id: format!("proj_{}", random_uuid_v4()),
            installation_id: format!("inst_{}", random_uuid_v4()),
            created_at: now_iso8601(),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunxProjectState {
    pub version: u8,
    pub project_id: String,
    pub created_at: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunxInstallState {
    pub version: u8,
    pub installation_id: String,
    pub created_at: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RunxInitResult {
    pub action: InitAction,
    pub created: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project_dir: Option<PathBuf>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub global_home_dir: Option<PathBuf>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub installation_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub official_cache_dir: Option<PathBuf>,
}

pub fn runx_init(options: &RunxInitOptions) -> Result<RunxInitResult, ScaffoldError> {
    match options.action {
        InitAction::Project => {
            let ensured = ensure_runx_project_state(
                &options.project_dir,
                &options.generated.project_id,
                &options.generated.created_at,
            )?;
            let skills_dir = options.project_dir.join("skills");
            let tools_dir = options.project_dir.join("tools");
            fs::create_dir_all(&skills_dir).map_err(|source| {
                ScaffoldError::io("creating skills directory", skills_dir, source)
            })?;
            fs::create_dir_all(&tools_dir).map_err(|source| {
                ScaffoldError::io("creating tools directory", tools_dir, source)
            })?;
            Ok(RunxInitResult {
                action: InitAction::Project,
                created: ensured.created,
                project_dir: Some(options.project_dir.clone()),
                project_id: Some(ensured.state.project_id),
                global_home_dir: None,
                installation_id: None,
                official_cache_dir: None,
            })
        }
        InitAction::Global => {
            let ensured = ensure_runx_install_state(
                &options.global_home_dir,
                &options.generated.installation_id,
                &options.generated.created_at,
            )?;
            if options.prefetch_official {
                fs::create_dir_all(&options.official_cache_dir).map_err(|source| {
                    ScaffoldError::io(
                        "creating official skills cache",
                        &options.official_cache_dir,
                        source,
                    )
                })?;
            }
            Ok(RunxInitResult {
                action: InitAction::Global,
                created: ensured.created,
                project_dir: None,
                project_id: None,
                global_home_dir: Some(options.global_home_dir.clone()),
                installation_id: Some(ensured.state.installation_id),
                official_cache_dir: options
                    .prefetch_official
                    .then(|| options.official_cache_dir.clone()),
            })
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnsuredProjectState {
    pub state: RunxProjectState,
    pub created: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnsuredInstallState {
    pub state: RunxInstallState,
    pub created: bool,
}

pub fn ensure_runx_project_state(
    project_dir: &Path,
    project_id: &str,
    created_at: &str,
) -> Result<EnsuredProjectState, ScaffoldError> {
    if let Some(existing) = read_runx_project_state(project_dir)? {
        return Ok(EnsuredProjectState {
            state: existing,
            created: false,
        });
    }
    let state = RunxProjectState {
        version: 1,
        project_id: project_id.to_owned(),
        created_at: created_at.to_owned(),
    };
    fs::create_dir_all(project_dir)
        .map_err(|source| ScaffoldError::io("creating project directory", project_dir, source))?;
    write_state_json(&project_dir.join("project.json"), &state)?;
    Ok(EnsuredProjectState {
        state,
        created: true,
    })
}

pub fn ensure_runx_install_state(
    global_home_dir: &Path,
    installation_id: &str,
    created_at: &str,
) -> Result<EnsuredInstallState, ScaffoldError> {
    if let Some(existing) = read_runx_install_state(global_home_dir)? {
        return Ok(EnsuredInstallState {
            state: existing,
            created: false,
        });
    }
    let state = RunxInstallState {
        version: 1,
        installation_id: installation_id.to_owned(),
        created_at: created_at.to_owned(),
    };
    fs::create_dir_all(global_home_dir).map_err(|source| {
        ScaffoldError::io("creating global home directory", global_home_dir, source)
    })?;
    write_state_json(&global_home_dir.join("install.json"), &state)?;
    Ok(EnsuredInstallState {
        state,
        created: true,
    })
}

fn read_runx_project_state(project_dir: &Path) -> Result<Option<RunxProjectState>, ScaffoldError> {
    let path = project_dir.join("project.json");
    match fs::read_to_string(&path) {
        Ok(contents) => {
            let state: RunxProjectState = serde_json::from_str(&contents)
                .map_err(|source| ScaffoldError::json("reading project state", &path, source))?;
            validate_project_state(path, state).map(Some)
        }
        Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(source) => Err(ScaffoldError::io("reading project state", path, source)),
    }
}

fn read_runx_install_state(
    global_home_dir: &Path,
) -> Result<Option<RunxInstallState>, ScaffoldError> {
    let path = global_home_dir.join("install.json");
    match fs::read_to_string(&path) {
        Ok(contents) => {
            let state: RunxInstallState = serde_json::from_str(&contents)
                .map_err(|source| ScaffoldError::json("reading install state", &path, source))?;
            validate_install_state(path, state).map(Some)
        }
        Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(source) => Err(ScaffoldError::io("reading install state", path, source)),
    }
}

fn validate_project_state(
    path: PathBuf,
    state: RunxProjectState,
) -> Result<RunxProjectState, ScaffoldError> {
    if state.version != 1 || state.project_id.is_empty() || state.created_at.is_empty() {
        return Err(ScaffoldError::InvalidState {
            path,
            message: "expected version 1, project_id, and created_at".to_owned(),
        });
    }
    Ok(state)
}

fn validate_install_state(
    path: PathBuf,
    state: RunxInstallState,
) -> Result<RunxInstallState, ScaffoldError> {
    if state.version != 1 || state.installation_id.is_empty() || state.created_at.is_empty() {
        return Err(ScaffoldError::InvalidState {
            path,
            message: "expected version 1, installation_id, and created_at".to_owned(),
        });
    }
    Ok(state)
}

fn write_state_json<T: Serialize>(path: &Path, state: &T) -> Result<(), ScaffoldError> {
    let contents = serde_json::to_string_pretty(state)
        .map_err(|source| ScaffoldError::json("serializing state", path, source))?;
    fs::write(path, format!("{contents}\n"))
        .map_err(|source| ScaffoldError::io("writing state", path, source))?;
    set_private_file_mode(path)
}

#[cfg(unix)]
fn set_private_file_mode(path: &Path) -> Result<(), ScaffoldError> {
    use std::os::unix::fs::PermissionsExt;

    let permissions = fs::Permissions::from_mode(0o600);
    fs::set_permissions(path, permissions)
        .map_err(|source| ScaffoldError::io("setting state permissions", path, source))
}

#[cfg(not(unix))]
fn set_private_file_mode(_path: &Path) -> Result<(), ScaffoldError> {
    Ok(())
}