Skip to main content

bot_forge/planning/
plan.rs

1//! Serializable plan types and target-platform matching.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::config::schema::{
8    AptMirrorDef, CertificatePreflightDef, CheckSpec, EnvironmentMutation, InstallSpec,
9    NetworkPolicy,
10};
11use crate::error::ForgeError;
12use crate::model::{Agent, InstallKind};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16/// Immutable, hashed description of one installation execution.
17pub struct ExecutionPlan {
18    /// Profile whose closure was resolved.
19    pub profile: String,
20    /// Target platform used for selector matching.
21    pub target: TargetPlatform,
22    /// Digest of the canonical configuration input.
23    pub config_hash: String,
24    /// Digest of this complete plan with the hash field initially empty.
25    pub plan_hash: String,
26    /// Frozen policy consumed by execution and scheduling.
27    pub policy: PlanPolicy,
28    /// Platform-applicable certificate prerequisite executed before ordinary installation work.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub certificate_preflight: Option<CertificatePreflightDef>,
31    /// Resolved components in deterministic order.
32    pub components: Vec<ResolvedComponent>,
33    /// Components requested by the profile but unavailable on the target platform.
34    ///
35    /// These entries are presentation metadata only. They are intentionally excluded from
36    /// serialized plans and execution graphs so an unsupported component can never be run.
37    #[serde(skip)]
38    pub unsupported_components: Vec<UnsupportedComponent>,
39    /// Executable dependency graph in deterministic order.
40    pub nodes: Vec<ExecutionNode>,
41    /// Platform-applicable environment mutations.
42    pub environment: Vec<EnvironmentMutation>,
43    /// Optional APT mirror configuration carried into execution.
44    pub apt_mirror: Option<AptMirrorDef>,
45    /// Configuration provenance retained for plan explanation.
46    pub origins: BTreeMap<String, String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50/// A profile component retained for display when no target-platform contract matches.
51pub struct UnsupportedComponent {
52    /// Canonical component identifier.
53    pub id: String,
54    /// Optional human-readable component name.
55    pub display_name: Option<String>,
56    /// Tool or skill lifecycle kind.
57    pub kind: InstallKind,
58    /// Whether interactive selection would otherwise allow omission.
59    pub optional: bool,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63/// Resource and policy limits copied into an execution plan.
64pub struct PlanPolicy {
65    /// Network access allowed during execution.
66    pub network: NetworkPolicy,
67    /// Maximum number of concurrently scheduled nodes.
68    pub max_parallel: usize,
69    /// Maximum number of concurrent network transfers.
70    pub max_downloads: usize,
71    /// Optional upper bound on memory tokens, in MiB.
72    pub max_memory_mib: Option<u64>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76/// Operating-system, architecture, and ABI selector used for component matching.
77pub struct TargetPlatform {
78    /// Canonical operating-system name.
79    pub os: String,
80    /// Canonical architecture name.
81    pub arch: String,
82    /// ABI selector, or `native` when the OS contract has no separate ABI dimension.
83    pub abi: String,
84}
85
86impl TargetPlatform {
87    /// Detect the host target using Rust's compile-time platform constants.
88    pub fn host() -> Self {
89        let os = std::env::consts::OS.to_string();
90        let arch = normalize_arch(std::env::consts::ARCH).to_string();
91        let abi = if os == "windows" && cfg!(target_env = "gnu") {
92            "gnu"
93        } else if os == "windows" {
94            "msvc"
95        } else if os == "linux" && cfg!(target_env = "musl") {
96            "musl"
97        } else if os == "linux" {
98            "gnu"
99        } else {
100            "native"
101        };
102        Self {
103            os,
104            arch,
105            abi: abi.to_string(),
106        }
107    }
108
109    /// Return the canonical platform selector used by configuration constraints.
110    pub fn selector(&self) -> String {
111        if self.abi == "native" {
112            format!("{}-{}", self.os, self.arch)
113        } else {
114            format!("{}-{}-{}", self.os, self.arch, self.abi)
115        }
116    }
117
118    /// Reject targets outside the release-supported platform matrix.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`crate::error::ForgeError::Config`] for an unsupported operating-system,
123    /// architecture, or ABI combination.
124    pub fn validate_supported(&self) -> Result<(), ForgeError> {
125        let supported = matches!(
126            (self.os.as_str(), self.arch.as_str(), self.abi.as_str()),
127            ("linux", "x86_64" | "aarch64", "gnu")
128                | ("windows", "x86_64" | "aarch64", "msvc")
129                | ("macos", "x86_64" | "aarch64", "native")
130        );
131        if supported {
132            Ok(())
133        } else {
134            Err(ForgeError::Config(format!(
135                "this release does not support target platform: {}; supported targets are Linux GNU, macOS, and Windows MSVC on x86_64/aarch64",
136                self.selector()
137            )))
138        }
139    }
140
141    /// Test whether a component selector matches this target.
142    pub(crate) fn matches(&self, selector: &str) -> bool {
143        selector == "*"
144            || selector == format!("{}-*", self.os)
145            || selector == self.selector()
146            || selector == format!("{}-{}", self.os, self.arch)
147    }
148}
149
150fn normalize_arch(arch: &str) -> &str {
151    match arch {
152        "amd64" | "x64" => "x86_64",
153        "arm64" => "aarch64",
154        value => value,
155    }
156}
157
158#[cfg(test)]
159mod platform_tests {
160    use crate::planning::TargetPlatform;
161
162    #[test]
163    fn host_abi_matches_the_compilation_target() {
164        let target = TargetPlatform::host();
165        if cfg!(target_os = "linux") && cfg!(target_env = "musl") {
166            assert_eq!(target.abi, "musl");
167        } else if (cfg!(target_os = "linux") || cfg!(windows)) && cfg!(target_env = "gnu") {
168            assert_eq!(target.abi, "gnu");
169        } else if cfg!(windows) {
170            assert_eq!(target.abi, "msvc");
171        } else {
172            assert_eq!(target.abi, "native");
173        }
174    }
175
176    #[test]
177    fn release_contract_rejects_unpublished_target_families() {
178        for target in [
179            TargetPlatform {
180                os: "linux".into(),
181                arch: "x86_64".into(),
182                abi: "musl".into(),
183            },
184            TargetPlatform {
185                os: "windows".into(),
186                arch: "x86_64".into(),
187                abi: "gnu".into(),
188            },
189        ] {
190            assert!(target.validate_supported().is_err());
191        }
192    }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196/// Component after profile, dependency, platform, and variant resolution.
197pub struct ResolvedComponent {
198    /// Canonical component identifier.
199    pub id: String,
200    /// Optional human-readable component name.
201    pub display_name: Option<String>,
202    /// Version supplied by the selected component or platform variant.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub version: Option<String>,
205    /// Whether interactive selection may omit the component.
206    pub optional: bool,
207    /// Per-component insecure hosts forwarded only to supported installers.
208    pub allow_insecure_hosts: Vec<String>,
209    /// Tool or skill lifecycle kind.
210    pub kind: InstallKind,
211    /// Selected variant identifier, when target resolution chose one.
212    pub variant: Option<String>,
213    /// Stable reasons that pulled the component into the dependency closure.
214    pub requested_by: Vec<String>,
215    /// Canonical component IDs that must complete first.
216    pub dependencies: Vec<String>,
217    /// Capability names supplied by this component.
218    pub provides: Vec<String>,
219    /// Component or capability names that cannot coexist with this selection.
220    pub conflicts: Vec<String>,
221    /// Typed pre-install detection contract.
222    pub detect: Option<CheckSpec>,
223    /// Typed installation contract selected for the target.
224    pub install: Option<InstallSpec>,
225    /// Typed post-install verification contract.
226    pub verify: Option<CheckSpec>,
227    /// Canonical upstream source, when the backend has one.
228    pub source: Option<String>,
229    /// Pinned upstream revision, when the backend has one.
230    pub revision: Option<String>,
231    /// Agent destinations for a skill component.
232    pub agents: Vec<Agent>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236/// One executable node in the plan DAG.
237pub struct ExecutionNode {
238    /// Stable node identity referenced by dependency edges.
239    pub id: String,
240    /// Resolved component associated with this node.
241    pub component: String,
242    /// Lifecycle operation represented by the node.
243    pub kind: NodeKind,
244    /// Node IDs that must complete successfully before this node can run.
245    pub dependencies: Vec<String>,
246    /// Capacities and named locks acquired before running the node.
247    pub resources: Vec<ResourceClaim>,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "kebab-case")]
252/// Lifecycle operation performed by an execution node.
253pub enum NodeKind {
254    /// Observe whether a component already satisfies its detection contract.
255    Detect,
256    /// Fetch network-backed input into managed local state.
257    Fetch,
258    /// Perform the component's typed installation transaction.
259    Acquire,
260    /// Validate the installed result against its verification contract.
261    Verify,
262    /// Persist a verified immutable artifact.
263    Store,
264    /// Switch a managed target to the selected artifact.
265    Activate,
266    /// Commit installation metadata to persistent state.
267    Record,
268    /// Apply selected environment mutations.
269    Environment,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273/// Quantity of a named capacity or lock resource required by a node.
274pub struct ResourceClaim {
275    /// Named capacity or lock key.
276    pub key: String,
277    /// Number of units required atomically.
278    pub units: u32,
279}