Skip to main content

lean_ctx/core/intelligence_runtime/
configuration.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Persist explicit user consent only after the selected runtime passes health.
3
4use std::path::Path;
5
6use crate::core::config::{Config, IntelligenceRuntimeConfig};
7
8use super::{InstallError, Result, health};
9
10pub const DISCLOSURE: &str = "Optional Intelligence Runtime: proprietary, runs locally. Its commercial features are licensed through LeanCTX | Hive or a commercial licence; the public reference runtime works without either. Staging packages are not production releases.";
11
12/// Verified setup snapshot: discovery never starts proprietary code or grants consent.
13pub struct SetupRuntime {
14    config: IntelligenceRuntimeConfig,
15    observed_config: IntelligenceRuntimeConfig,
16    version: Option<String>,
17}
18
19impl SetupRuntime {
20    pub fn available(&self) -> bool {
21        self.version.is_some()
22    }
23
24    pub fn channel_available(&self) -> bool {
25        !self.config.channel_url.is_empty()
26    }
27
28    pub fn enabled(&self) -> bool {
29        self.available() && self.config.enabled && self.config.accept_proprietary
30    }
31
32    pub fn status(&self) -> &'static str {
33        if self.enabled() {
34            "configured"
35        } else if self.available() {
36            "available"
37        } else if self.channel_available() {
38            if self.config.manifest_sha256.is_empty() {
39                "download_available"
40            } else {
41                "repair_available"
42            }
43        } else {
44            "not_configured"
45        }
46    }
47
48    pub(crate) fn report(&self) -> serde_json::Value {
49        // Reported here, once and without content, instead of as an
50        // unavailable semantic check on every admission: a configured
51        // runtime whose package does not offer semantic detectors is a
52        // property of the installation.
53        // The install-side fields are the ones `runtime health` prints, from
54        // the same function: what the authenticated package and its
55        // installed model set offer. `semantic_categories` are the categories
56        // that set examines; a category that is not listed is not covered.
57        let mut report = serde_json::json!({"schema_version": 1, "status": self.status(),
58            "version": self.version, "disclosure": DISCLOSURE,
59            "semantic_detectors": "not_configured",
60            "semantic_descriptor_sha256_prefix": null,
61            "semantic_versions": [], "semantic_categories": [],
62            "runtime_started": false, "release_approved": false});
63        if self.enabled() {
64            let authenticated = super::trust_key(&self.config.trust_key_hex)
65                .and_then(|key| {
66                    super::install::verified_active(
67                        Path::new(&self.config.root),
68                        &self.config.manifest_sha256,
69                        &key,
70                    )
71                })
72                .ok();
73            if let Some(package) = authenticated {
74                let state = super::install::semantic_state(&package);
75                let versions = super::install::semantic_set(&package)
76                    .map_or_else(Vec::new, |set| set.versions);
77                for (key, value) in health::semantic_report(&package, &state, versions) {
78                    report[key] = value;
79                }
80            } else {
81                report["semantic_detectors"] = "unverified".into();
82            }
83        }
84        for (key, value) in health::semantic_worker_report(&self.config) {
85            report[key] = value;
86        }
87        report
88    }
89
90    /// The caller must obtain explicit consent for this captured, trusted selection.
91    pub fn enable(&self) -> Result<serde_json::Value> {
92        if !self.available() {
93            return Err(InstallError::State);
94        }
95        let _guard = super::install::configuration_guard(Path::new(&self.config.root))?;
96        activate_checked(
97            Path::new(&self.config.root),
98            &self.config.manifest_sha256,
99            &self.config.trust_key_hex,
100            Some(self),
101        )
102    }
103
104    /// Called only after explicit consent; discovery itself never uses the network.
105    pub fn install_and_enable(&self) -> Result<serde_json::Value> {
106        if !self.channel_available() {
107            return Err(InstallError::Policy);
108        }
109        if global_runtime()? != self.observed_config {
110            return Err(InstallError::Configuration);
111        }
112        let root = Path::new(&self.config.root);
113        if self.config != self.observed_config {
114            super::install::prepare_parent_chain(root.parent().ok_or(InstallError::Directory)?)?;
115        }
116        let _guard = super::install::configuration_guard(root)?;
117        if global_runtime()? != self.observed_config {
118            return Err(InstallError::Configuration);
119        }
120        let expected = super::install::active_manifest(root)?;
121        let result = super::catalog::sync(
122            [&self.config.channel_url, &self.config.channel_signature_url],
123            &super::trust_key(&self.config.channel_root_key_hex)?,
124            root,
125            &expected,
126        )?;
127        let selected = result["receipt"]["manifest_sha256"]
128            .as_str()
129            .ok_or(InstallError::State)?;
130        let key = result["artifact_key_hex"]
131            .as_str()
132            .ok_or(InstallError::State)?;
133        let activation = activate_checked(root, selected, key, Some(self))?;
134        Ok(
135            serde_json::json!({"status": "installed_and_activated", "installation": result,
136            "activation": activation, "release_approved": false}),
137        )
138    }
139
140    /// Restore only the retained, authenticated predecessor of this snapshot.
141    pub(crate) fn rollback_and_enable(&self) -> Result<serde_json::Value> {
142        if self.config.manifest_sha256.is_empty() || self.config.staging {
143            return Err(InstallError::Policy);
144        }
145        let root = Path::new(&self.config.root);
146        let _guard = super::install::configuration_guard(root)?;
147        if global_runtime()? != self.observed_config {
148            return Err(InstallError::Configuration);
149        }
150        let (result, key) =
151            super::install::rollback_configured(root, &self.config.manifest_sha256)?;
152        let selected = result["receipt"]["manifest_sha256"]
153            .as_str()
154            .ok_or(InstallError::State)?;
155        // A failed health/configuration check leaves the old binding mismatched
156        // and therefore unusable. Retrying reauthenticates the signed rollback
157        // relationship and completes activation without another selection change.
158        let activation = activate_checked(root, selected, &key, Some(self))?;
159        Ok(
160            serde_json::json!({"status": "rolled_back_and_activated", "rollback": result,
161            "activation": activation, "release_approved": false}),
162        )
163    }
164}
165
166pub fn inspect_setup() -> Result<SetupRuntime> {
167    // Never merge project configuration, search PATH, download or start a child.
168    let observed_config = global_runtime()?;
169    let config = if observed_config == IntelligenceRuntimeConfig::default() {
170        super::bootstrap::config()?
171    } else {
172        observed_config.clone()
173    };
174    let version = if config == IntelligenceRuntimeConfig::default() {
175        None
176    } else {
177        if !super::bootstrap::permits_config(&config) {
178            return Err(InstallError::Policy);
179        }
180        let has_channel = !config.channel_url.is_empty()
181            || !config.channel_signature_url.is_empty()
182            || !config.channel_root_key_hex.is_empty();
183        if has_channel {
184            super::channel::validate_urls(&[&config.channel_url, &config.channel_signature_url])?;
185            super::trust_key(&config.channel_root_key_hex)?;
186            if !Path::new(&config.root).is_absolute() {
187                return Err(InstallError::Directory);
188            }
189        }
190        if config.manifest_sha256.is_empty() && config.trust_key_hex.is_empty() {
191            if !has_channel || config.enabled || config.accept_proprietary {
192                return Err(InstallError::Policy);
193            }
194            None
195        } else {
196            let key = super::trust_key(&config.trust_key_hex)?;
197            match super::install::verified_active(
198                Path::new(&config.root),
199                &config.manifest_sha256,
200                &key,
201            ) {
202                Ok(package) => Some(package.receipt.version),
203                // Offer only a new consented, authenticated download; never enable
204                // this unverified selection. This also recovers an interrupted update.
205                Err(_) if has_channel => None,
206                Err(error) => return Err(error),
207            }
208        }
209    };
210    Ok(SetupRuntime {
211        config,
212        observed_config,
213        version,
214    })
215}
216
217fn global_runtime() -> Result<IntelligenceRuntimeConfig> {
218    Config::try_load_global()
219        .map(|config| config.intelligence_runtime)
220        .map_err(|_| InstallError::Configuration)
221}
222
223pub(super) fn activate(root: &Path, selected: &str, encoded: &str) -> Result<serde_json::Value> {
224    activate_checked(root, selected, encoded, None)
225}
226
227fn activate_checked(
228    root: &Path,
229    selected: &str,
230    encoded: &str,
231    expected: Option<&SetupRuntime>,
232) -> Result<serde_json::Value> {
233    let observed = global_runtime()?;
234    if expected.is_some_and(|expected| observed != expected.observed_config) {
235        return Err(InstallError::Configuration);
236    }
237    let key = super::trust_key(encoded)?;
238    let checked = health::check(root, selected, &key)?;
239    let staging = checked["receipt"]["staging_only"]
240        .as_bool()
241        .ok_or(InstallError::State)?;
242    let production = super::bootstrap::production_key(root)?.is_some();
243    if staging == production {
244        return Err(InstallError::Policy);
245    }
246    let previous = if production {
247        let mut compiled = super::bootstrap::config()?;
248        if !super::bootstrap::permits_config(&compiled) {
249            return Err(InstallError::Policy);
250        }
251        compiled.license_configuration = expected
252            .map(|expected| expected.config.license_configuration.clone())
253            .or_else(|| {
254                (!observed.staging && super::bootstrap::permits_config(&observed))
255                    .then(|| observed.license_configuration.clone())
256            })
257            .unwrap_or_default();
258        // A channel refresh must not erase the device's existing license binding.
259        if observed.root == compiled.root && super::bootstrap::permits_config(&observed) {
260            observed.clone()
261        } else {
262            compiled
263        }
264    } else if let Some(expected) = expected {
265        expected.config.clone()
266    } else if Path::new(&observed.root) == root {
267        observed.clone()
268    } else {
269        IntelligenceRuntimeConfig::default()
270    };
271    let root = root.to_str().ok_or(InstallError::Directory)?.to_owned();
272    Config::try_update_global(|config| {
273        if config.intelligence_runtime != observed {
274            return Err(crate::core::error::LeanCtxError::Config(
275                "runtime setup configuration changed".into(),
276            ));
277        }
278        config.intelligence_runtime = IntelligenceRuntimeConfig {
279            enabled: true,
280            accept_proprietary: true,
281            staging,
282            root,
283            manifest_sha256: selected.to_owned(),
284            trust_key_hex: encoded.to_owned(),
285            ..previous
286        };
287        Ok(())
288    })
289    .map_err(|_| InstallError::Configuration)?;
290    Ok(serde_json::json!({"status": "activated", "health": checked,
291        "staging_only": staging, "release_approved": false}))
292}
293
294pub(super) fn deactivate() -> Result<serde_json::Value> {
295    Config::update_global(|config| config.intelligence_runtime.enabled = false)
296        .map_err(|_| InstallError::Configuration)?;
297    Ok(serde_json::json!({"status": "disabled", "release_approved": false}))
298}