shine-core 2.0.0

Reusable lifecycle runtime and domain core for Shine applications
Documentation
use super::{AppCategory, CoreRuntime, PresetSourceKind, SysInstall, SysItem};
use crate::permission::PermissionDeclarationV1;
use crate::plan::{FilesystemAccessV1, PermissionSetV1, PermissionV1};
use crate::trust::{TrustCapabilityV1, TrustDecisionV1, TrustRequirementV1, evaluate_trust};
use anyhow::{Context, Result, bail};
use std::collections::BTreeSet;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExternalCodeRequirementReport {
    pub requirements: Vec<TrustRequirementV1>,
}

impl<H> CoreRuntime<H> {
    pub async fn external_code_requirements(
        &self,
        target: &str,
    ) -> Result<ExternalCodeRequirementReport> {
        if let Some(category_name) = target.strip_prefix("app/") {
            let category = self
                .app_categories(Some(category_name))?
                .into_iter()
                .find(|category| category.name == category_name)
                .with_context(|| format!("app preset category not found: {category_name}"))?;
            return Ok(ExternalCodeRequirementReport {
                requirements: self.app_external_code_requirements(&category)?,
            });
        }
        if let Some(item_id) = target.strip_prefix("sys/") {
            let os_id = match self.context().platform {
                super::RuntimePlatform::Macos => "macos",
                super::RuntimePlatform::Linux => "ubuntu",
                super::RuntimePlatform::Windows => "windows",
            };
            let loaded = self.load_sys_preset(os_id).await?;
            let item = loaded
                .manifest
                .items
                .iter()
                .find(|item| item.id == item_id)
                .with_context(|| format!("unknown sys item `{item_id}` for {os_id}"))?;
            return Ok(ExternalCodeRequirementReport {
                requirements: self.sys_external_code_requirements(os_id, item)?,
            });
        }
        bail!("trust target must be canonical app/<category> or sys/<item>: {target}")
    }

    pub(crate) fn app_external_code_requirements(
        &self,
        category: &AppCategory,
    ) -> Result<Vec<TrustRequirementV1>> {
        let target = format!("app/{}", category.name);
        let prefix = format!("app/{}/", category.name);
        let permissions = declared_permissions(category.permissions.as_ref())?;
        let generator_paths = category
            .files
            .iter()
            .filter_map(|file| file.generator.as_ref())
            .map(|generator| {
                format!(
                    "app/{}/{}",
                    category.name,
                    generator.script.to_string_lossy().replace('\\', "/")
                )
            })
            .collect::<Vec<_>>();
        let mut explicit_paths = generator_paths.clone();
        if let Some(artifact) = &category.artifact {
            explicit_paths.push(format!(
                "app/{}/{}",
                category.name,
                artifact.script.replace('\\', "/")
            ));
            if let Some(teardown) = &artifact.teardown {
                explicit_paths.push(format!(
                    "app/{}/{}",
                    category.name,
                    teardown.replace('\\', "/")
                ));
            }
        }
        let category_paths = code_paths(self, &prefix, &permissions, explicit_paths);
        let mut output = Vec::new();

        if (!category.post_install.is_empty() || !category.post_upgrade.is_empty())
            && any_external(self, category_paths.iter().copied())
        {
            output.push(self.requirement(
                &target,
                TrustCapabilityV1::AppHook,
                category_paths.iter().copied(),
                permissions.clone(),
            )?);
        }

        if !generator_paths.is_empty() && any_external(self, category_paths.iter().copied()) {
            output.push(self.requirement(
                &target,
                TrustCapabilityV1::AppGenerator,
                category_paths.iter().copied(),
                permissions.clone(),
            )?);
        }

        if category.artifact.is_some() && any_external(self, category_paths.iter().copied()) {
            output.push(self.requirement(
                &target,
                TrustCapabilityV1::AppArtifact,
                category_paths.iter().copied(),
                permissions,
            )?);
        }
        Ok(output)
    }

    pub(crate) fn sys_external_code_requirements(
        &self,
        os_id: &str,
        item: &SysItem,
    ) -> Result<Vec<TrustRequirementV1>> {
        let target = format!("sys/{}", item.id);
        let prefix = format!("sys/{os_id}/");
        let permissions = declared_permissions(item.permissions.as_ref())?;
        let mut explicit_paths = Vec::new();
        if let Some(SysInstall::Script { path, .. }) = &item.install {
            explicit_paths.push(format!("sys/{os_id}/{}", path.replace('\\', "/")));
        }
        for integration in &item.shell {
            for path in [
                integration.source.as_deref(),
                integration.fragment.as_deref(),
            ]
            .into_iter()
            .flatten()
            {
                explicit_paths.push(format!("sys/{os_id}/{}", path.replace('\\', "/")));
            }
        }
        let category_paths = code_paths(self, &prefix, &permissions, explicit_paths);
        let mut output = Vec::new();
        if matches!(&item.install, Some(SysInstall::Script { .. }))
            && any_external(self, category_paths.iter().copied())
        {
            output.push(self.requirement(
                &target,
                TrustCapabilityV1::SysBootstrapScript,
                category_paths.iter().copied(),
                permissions.clone(),
            )?);
        }

        let executable_integration = item.shell.iter().any(|integration| {
            !integration.eval_argv.is_empty()
                || integration.source.is_some()
                || integration.fragment.is_some()
        });
        if (executable_integration || external_profile_base(self, os_id))
            && (any_external(self, category_paths.iter().copied())
                || (executable_integration && self.context().is_external_presets))
        {
            output.push(self.requirement(
                &target,
                TrustCapabilityV1::SysProfileCode,
                category_paths.iter().copied(),
                permissions,
            )?);
        }
        Ok(output)
    }

    pub(crate) fn trust_decision(&self, requirement: &TrustRequirementV1) -> TrustDecisionV1 {
        evaluate_trust(&self.context().trust_grants, requirement)
    }

    pub(crate) fn app_capability_trusted(
        &self,
        category: &AppCategory,
        capability: TrustCapabilityV1,
    ) -> Result<bool> {
        let requirements = self.app_external_code_requirements(category)?;
        Ok(requirements
            .iter()
            .filter(|requirement| requirement.capability == capability)
            .all(|requirement| self.trust_decision(requirement) == TrustDecisionV1::Trusted))
    }

    pub(crate) fn sys_capability_trusted(
        &self,
        os_id: &str,
        item: &SysItem,
        capability: TrustCapabilityV1,
    ) -> Result<bool> {
        let requirements = self.sys_external_code_requirements(os_id, item)?;
        Ok(requirements
            .iter()
            .filter(|requirement| requirement.capability == capability)
            .all(|requirement| self.trust_decision(requirement) == TrustDecisionV1::Trusted))
    }

    fn requirement<'a>(
        &self,
        target: &str,
        capability: TrustCapabilityV1,
        paths: impl IntoIterator<Item = &'a str>,
        permissions: PermissionSetV1,
    ) -> Result<TrustRequirementV1> {
        let paths = paths.into_iter().collect::<BTreeSet<_>>();
        Ok(TrustRequirementV1 {
            target: target.to_string(),
            capability,
            code_digest: self.presets().code_digest_v1(paths)?,
            permissions,
        })
    }
}

fn declared_permissions(declaration: Option<&PermissionDeclarationV1>) -> Result<PermissionSetV1> {
    Ok(declaration
        .map(PermissionDeclarationV1::permission_set)
        .transpose()?
        .unwrap_or_default())
}

fn is_external<H>(runtime: &CoreRuntime<H>, logical: &str) -> bool {
    runtime
        .presets()
        .origin(logical)
        .is_some_and(|origin| origin.source_kind != PresetSourceKind::Embedded)
}

fn any_external<'a, H>(runtime: &CoreRuntime<H>, paths: impl IntoIterator<Item = &'a str>) -> bool {
    paths.into_iter().any(|path| is_external(runtime, path))
}

fn external_profile_base<H>(runtime: &CoreRuntime<H>, os_id: &str) -> bool {
    let ext = if os_id == "windows" { "ps1" } else { "sh" };
    ["pre", "post"].into_iter().any(|phase| {
        let logical = format!("sys/{os_id}/profile/base.{phase}.{ext}");
        runtime.presets().get(&logical).is_some() && is_external(runtime, &logical)
    })
}

fn code_paths<'a, H>(
    runtime: &'a CoreRuntime<H>,
    prefix: &str,
    permissions: &PermissionSetV1,
    explicit_paths: impl IntoIterator<Item = String>,
) -> Vec<&'a str> {
    let mut selected = explicit_paths.into_iter().collect::<BTreeSet<_>>();
    selected.insert(format!("{prefix}shine.toml"));
    for permission in permissions.iter() {
        if let PermissionV1::Filesystem {
            access: FilesystemAccessV1::Execute,
            path,
        } = permission
            && let Some(relative) = path.strip_prefix("preset:")
        {
            selected.insert(format!("{prefix}{relative}"));
        }
    }
    runtime
        .presets()
        .files()
        .keys()
        .filter(|path| path.starts_with(prefix))
        .filter(|path| selected.contains(path.as_str()) || is_code_support_file(path))
        .map(String::as_str)
        .collect()
}

fn is_code_support_file(path: &str) -> bool {
    let name = path.rsplit('/').next().unwrap_or(path);
    if matches!(name, "package.json" | "bun.lock" | "bun.lockb") {
        return true;
    }
    matches!(
        name.rsplit_once('.').map(|(_, extension)| extension),
        Some(
            "sh" | "bash"
                | "zsh"
                | "fish"
                | "ps1"
                | "cmd"
                | "bat"
                | "ts"
                | "js"
                | "mts"
                | "mjs"
                | "cjs"
        )
    )
}