bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
//! Serializable plan types and target-platform matching.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use crate::config::schema::{
    AptMirrorDef, CertificatePreflightDef, CheckSpec, EnvironmentMutation, InstallSpec,
    NetworkPolicy,
};
use crate::error::ForgeError;
use crate::model::{Agent, InstallKind};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
/// Immutable, hashed description of one installation execution.
pub struct ExecutionPlan {
    /// Profile whose closure was resolved.
    pub profile: String,
    /// Target platform used for selector matching.
    pub target: TargetPlatform,
    /// Digest of the canonical configuration input.
    pub config_hash: String,
    /// Digest of this complete plan with the hash field initially empty.
    pub plan_hash: String,
    /// Frozen policy consumed by execution and scheduling.
    pub policy: PlanPolicy,
    /// Platform-applicable certificate prerequisite executed before ordinary installation work.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub certificate_preflight: Option<CertificatePreflightDef>,
    /// Resolved components in deterministic order.
    pub components: Vec<ResolvedComponent>,
    /// Components requested by the profile but unavailable on the target platform.
    ///
    /// These entries are presentation metadata only. They are intentionally excluded from
    /// serialized plans and execution graphs so an unsupported component can never be run.
    #[serde(skip)]
    pub unsupported_components: Vec<UnsupportedComponent>,
    /// Executable dependency graph in deterministic order.
    pub nodes: Vec<ExecutionNode>,
    /// Platform-applicable environment mutations.
    pub environment: Vec<EnvironmentMutation>,
    /// Optional APT mirror configuration carried into execution.
    pub apt_mirror: Option<AptMirrorDef>,
    /// Configuration provenance retained for plan explanation.
    pub origins: BTreeMap<String, String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// A profile component retained for display when no target-platform contract matches.
pub struct UnsupportedComponent {
    /// Canonical component identifier.
    pub id: String,
    /// Optional human-readable component name.
    pub display_name: Option<String>,
    /// Tool or skill lifecycle kind.
    pub kind: InstallKind,
    /// Whether interactive selection would otherwise allow omission.
    pub optional: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// Resource and policy limits copied into an execution plan.
pub struct PlanPolicy {
    /// Network access allowed during execution.
    pub network: NetworkPolicy,
    /// Maximum number of concurrently scheduled nodes.
    pub max_parallel: usize,
    /// Maximum number of concurrent network transfers.
    pub max_downloads: usize,
    /// Optional upper bound on memory tokens, in MiB.
    pub max_memory_mib: Option<u64>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// Operating-system, architecture, and ABI selector used for component matching.
pub struct TargetPlatform {
    /// Canonical operating-system name.
    pub os: String,
    /// Canonical architecture name.
    pub arch: String,
    /// ABI selector, or `native` when the OS contract has no separate ABI dimension.
    pub abi: String,
}

impl TargetPlatform {
    /// Detect the host target using Rust's compile-time platform constants.
    pub fn host() -> Self {
        let os = std::env::consts::OS.to_string();
        let arch = normalize_arch(std::env::consts::ARCH).to_string();
        let abi = if os == "windows" && cfg!(target_env = "gnu") {
            "gnu"
        } else if os == "windows" {
            "msvc"
        } else if os == "linux" && cfg!(target_env = "musl") {
            "musl"
        } else if os == "linux" {
            "gnu"
        } else {
            "native"
        };
        Self {
            os,
            arch,
            abi: abi.to_string(),
        }
    }

    /// Return the canonical platform selector used by configuration constraints.
    pub fn selector(&self) -> String {
        if self.abi == "native" {
            format!("{}-{}", self.os, self.arch)
        } else {
            format!("{}-{}-{}", self.os, self.arch, self.abi)
        }
    }

    /// Reject targets outside the release-supported platform matrix.
    ///
    /// # Errors
    ///
    /// Returns [`crate::error::ForgeError::Config`] for an unsupported operating-system,
    /// architecture, or ABI combination.
    pub fn validate_supported(&self) -> Result<(), ForgeError> {
        let supported = matches!(
            (self.os.as_str(), self.arch.as_str(), self.abi.as_str()),
            ("linux", "x86_64" | "aarch64", "gnu")
                | ("windows", "x86_64" | "aarch64", "msvc")
                | ("macos", "x86_64" | "aarch64", "native")
        );
        if supported {
            Ok(())
        } else {
            Err(ForgeError::Config(format!(
                "this release does not support target platform: {}; supported targets are Linux GNU, macOS, and Windows MSVC on x86_64/aarch64",
                self.selector()
            )))
        }
    }

    /// Test whether a component selector matches this target.
    pub(crate) fn matches(&self, selector: &str) -> bool {
        selector == "*"
            || selector == format!("{}-*", self.os)
            || selector == self.selector()
            || selector == format!("{}-{}", self.os, self.arch)
    }
}

fn normalize_arch(arch: &str) -> &str {
    match arch {
        "amd64" | "x64" => "x86_64",
        "arm64" => "aarch64",
        value => value,
    }
}

#[cfg(test)]
mod platform_tests {
    use crate::planning::TargetPlatform;

    #[test]
    fn host_abi_matches_the_compilation_target() {
        let target = TargetPlatform::host();
        if cfg!(target_os = "linux") && cfg!(target_env = "musl") {
            assert_eq!(target.abi, "musl");
        } else if (cfg!(target_os = "linux") || cfg!(windows)) && cfg!(target_env = "gnu") {
            assert_eq!(target.abi, "gnu");
        } else if cfg!(windows) {
            assert_eq!(target.abi, "msvc");
        } else {
            assert_eq!(target.abi, "native");
        }
    }

    #[test]
    fn release_contract_rejects_unpublished_target_families() {
        for target in [
            TargetPlatform {
                os: "linux".into(),
                arch: "x86_64".into(),
                abi: "musl".into(),
            },
            TargetPlatform {
                os: "windows".into(),
                arch: "x86_64".into(),
                abi: "gnu".into(),
            },
        ] {
            assert!(target.validate_supported().is_err());
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// Component after profile, dependency, platform, and variant resolution.
pub struct ResolvedComponent {
    /// Canonical component identifier.
    pub id: String,
    /// Optional human-readable component name.
    pub display_name: Option<String>,
    /// Version supplied by the selected component or platform variant.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
    /// Whether interactive selection may omit the component.
    pub optional: bool,
    /// Per-component insecure hosts forwarded only to supported installers.
    pub allow_insecure_hosts: Vec<String>,
    /// Tool or skill lifecycle kind.
    pub kind: InstallKind,
    /// Selected variant identifier, when target resolution chose one.
    pub variant: Option<String>,
    /// Stable reasons that pulled the component into the dependency closure.
    pub requested_by: Vec<String>,
    /// Canonical component IDs that must complete first.
    pub dependencies: Vec<String>,
    /// Capability names supplied by this component.
    pub provides: Vec<String>,
    /// Component or capability names that cannot coexist with this selection.
    pub conflicts: Vec<String>,
    /// Typed pre-install detection contract.
    pub detect: Option<CheckSpec>,
    /// Typed installation contract selected for the target.
    pub install: Option<InstallSpec>,
    /// Typed post-install verification contract.
    pub verify: Option<CheckSpec>,
    /// Canonical upstream source, when the backend has one.
    pub source: Option<String>,
    /// Pinned upstream revision, when the backend has one.
    pub revision: Option<String>,
    /// Agent destinations for a skill component.
    pub agents: Vec<Agent>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// One executable node in the plan DAG.
pub struct ExecutionNode {
    /// Stable node identity referenced by dependency edges.
    pub id: String,
    /// Resolved component associated with this node.
    pub component: String,
    /// Lifecycle operation represented by the node.
    pub kind: NodeKind,
    /// Node IDs that must complete successfully before this node can run.
    pub dependencies: Vec<String>,
    /// Capacities and named locks acquired before running the node.
    pub resources: Vec<ResourceClaim>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Lifecycle operation performed by an execution node.
pub enum NodeKind {
    /// Observe whether a component already satisfies its detection contract.
    Detect,
    /// Fetch network-backed input into managed local state.
    Fetch,
    /// Perform the component's typed installation transaction.
    Acquire,
    /// Validate the installed result against its verification contract.
    Verify,
    /// Persist a verified immutable artifact.
    Store,
    /// Switch a managed target to the selected artifact.
    Activate,
    /// Commit installation metadata to persistent state.
    Record,
    /// Apply selected environment mutations.
    Environment,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// Quantity of a named capacity or lock resource required by a node.
pub struct ResourceClaim {
    /// Named capacity or lock key.
    pub key: String,
    /// Number of units required atomically.
    pub units: u32,
}