Skip to main content

lean_ctx/core/intelligence_runtime/
cli.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Explicit staging entry points; setup uses the same install authority.
3
4use std::collections::BTreeMap;
5use std::path::Path;
6
7use crate::core::config::{Config, IntelligenceRuntimeConfig};
8
9use super::{
10    InstallError, Result, RuntimeKind, install, manifest::MAX_ARCHIVE, read_regular, verify_kind,
11};
12
13pub fn run(args: &[String]) -> Result<String> {
14    let operation = args
15        .first()
16        .map(String::as_str)
17        .ok_or(InstallError::Usage)?;
18    if !matches!(
19        operation,
20        "verify"
21            | "install"
22            | "download"
23            | "sync"
24            | "rotate-root"
25            | "rollback"
26            | "health"
27            | "invoke"
28            | "activate"
29            | "deactivate"
30            | "status"
31            | "activate-configured"
32            | "sync-configured"
33            | "rollback-configured"
34    ) {
35        return Err(InstallError::Usage);
36    }
37    let mut flags = BTreeMap::new();
38    let mut arguments = args[1..].iter();
39    let mut staging = false;
40    let mut accepted = false;
41    while let Some(flag) = arguments.next() {
42        match flag.as_str() {
43            "--staging" if !staging => staging = true,
44            "--accept-proprietary" if !accepted => accepted = true,
45            "--archive"
46            | "--manifest"
47            | "--signature"
48            | "--manifest-sha256"
49            | "--trust-key-hex"
50            | "--root"
51            | "--expected-active"
52            | "--request"
53            | "--archive-url"
54            | "--manifest-url"
55            | "--signature-url"
56            | "--channel-url"
57            | "--transition"
58            | "--semantic-models"
59            | "--kind"
60            | "--channel-signature-url" => {
61                let value = arguments.next().ok_or(InstallError::Usage)?;
62                if value.starts_with("--") || flags.insert(flag.as_str(), value.as_str()).is_some()
63                {
64                    return Err(InstallError::Usage);
65                }
66            }
67            _ => return Err(InstallError::Usage),
68        }
69    }
70    // The companions of a v4 package are read from local files, with the
71    // package they belong to, and by no other operation.
72    let companions = flags.get("--semantic-models").copied().map(Path::new);
73    if companions.is_some() && operation != "install" {
74        return Err(InstallError::Usage);
75    }
76    // `--kind cockpit` selects the Cockpit package kind; it never counts as a
77    // package flag. Its stores are named apart, and each kind refuses the other's.
78    let kind = match flags.remove("--kind") {
79        None | Some("intelligence") => RuntimeKind::Intelligence,
80        Some("cockpit") => RuntimeKind::Cockpit,
81        Some(_) => return Err(InstallError::Usage),
82    };
83    if let Some(root) = flags.get("--root") {
84        let cockpit_store = Path::new(root)
85            .file_name()
86            .and_then(|name| name.to_str())
87            .is_some_and(|name| name.starts_with(RuntimeKind::Cockpit.store_name()));
88        if cockpit_store != (kind == RuntimeKind::Cockpit) {
89            return Err(InstallError::Usage);
90        }
91    }
92    if kind == RuntimeKind::Cockpit {
93        match operation {
94            "status" | "sync-configured" | "activate" | "deactivate" => {
95                return serde_json::to_string_pretty(&super::cockpit::run_cli(
96                    operation, staging, accepted, &flags,
97                )?)
98                .map_err(|_| InstallError::State);
99            }
100            "verify" | "install" | "download" | "sync" | "rotate-root" if companions.is_none() => {}
101            _ => return Err(InstallError::Usage),
102        }
103    }
104    let configured_operation = matches!(
105        operation,
106        "status" | "sync-configured" | "rollback-configured" | "activate-configured" | "deactivate"
107    );
108    if !staging && !configured_operation {
109        return Err(InstallError::Usage);
110    }
111    if configured_operation && !matches!(operation, "status" | "deactivate") {
112        let observed = Config::try_load_global()
113            .map_err(|_| InstallError::Configuration)?
114            .intelligence_runtime;
115        let configured = if observed == IntelligenceRuntimeConfig::default() {
116            super::bootstrap::config()?
117        } else {
118            observed
119        };
120        if configured != IntelligenceRuntimeConfig::default()
121            && (configured.staging != staging || !super::bootstrap::permits_config(&configured))
122        {
123            return Err(InstallError::Usage);
124        }
125    }
126    if staging
127        && let Some(root) = flags.get("--root")
128        && super::bootstrap::production_key(Path::new(root))?.is_some()
129    {
130        return Err(InstallError::Usage);
131    }
132    if !matches!(operation, "verify" | "deactivate" | "status") && !accepted {
133        return Err(InstallError::Usage);
134    }
135    let get = |name| flags.get(name).copied().ok_or(InstallError::Usage);
136    if operation == "rotate-root" {
137        if flags.len() != 4 {
138            return Err(InstallError::Usage);
139        }
140        let proof = serde_json::from_slice(&read_regular(Path::new(get("--transition")?), 4096)?)
141            .map_err(|_| InstallError::Manifest)?;
142        let result = install::rotate_root(
143            Path::new(get("--root")?),
144            get("--expected-active")?,
145            &super::trust_key(get("--trust-key-hex")?)?,
146            proof,
147        )?;
148        return serde_json::to_string_pretty(&result).map_err(|_| InstallError::State);
149    }
150    if operation == "sync" {
151        if flags.len() != 5 {
152            return Err(InstallError::Usage);
153        }
154        let result = super::catalog::sync_kind(
155            kind,
156            [get("--channel-url")?, get("--channel-signature-url")?],
157            &super::trust_key(get("--trust-key-hex")?)?,
158            Path::new(get("--root")?),
159            get("--expected-active")?,
160        )?;
161        return serde_json::to_string_pretty(&result).map_err(|_| InstallError::State);
162    }
163    if matches!(
164        operation,
165        "status" | "activate-configured" | "sync-configured" | "rollback-configured"
166    ) {
167        if !flags.is_empty() {
168            return Err(InstallError::Usage);
169        }
170        let candidate = super::configuration::inspect_setup()?;
171        let result = if operation == "status" {
172            candidate.report()
173        } else if operation == "sync-configured" {
174            candidate.install_and_enable()?
175        } else if operation == "rollback-configured" {
176            candidate.rollback_and_enable()?
177        } else {
178            candidate.enable()?
179        };
180        return serde_json::to_string_pretty(&result).map_err(|_| InstallError::State);
181    }
182    if operation == "deactivate" {
183        if !flags.is_empty() {
184            return Err(InstallError::Usage);
185        }
186        let result = super::configuration::deactivate()?;
187        return serde_json::to_string_pretty(&result).map_err(|_| InstallError::State);
188    }
189    let selected = get("--manifest-sha256")?;
190    let encoded = get("--trust-key-hex")?;
191    let key = super::trust_key(encoded)?;
192    let result = if operation == "activate" {
193        if flags.len() != 4 || get("--expected-active")? != selected {
194            return Err(InstallError::Usage);
195        }
196        super::configuration::activate(Path::new(get("--root")?), selected, encoded)?
197    } else if operation == "invoke" {
198        if flags.len() != 5 || get("--expected-active")? != selected {
199            return Err(InstallError::Usage);
200        }
201        #[cfg(not(unix))]
202        return Err(InstallError::Exchange);
203        #[cfg(unix)]
204        {
205            // Explicit staging operator input, NOT production policy admission.
206            let bytes = read_regular(
207                Path::new(get("--request")?),
208                lean_ctx_protocol::runtime_exchange::MAX_RUNTIME_EXCHANGE_BYTES,
209            )?;
210            let request = serde_json::from_slice(&bytes).map_err(|_| InstallError::Exchange)?;
211            let answer =
212                super::session::invoke(Path::new(get("--root")?), selected, &key, &request)?;
213            // G08: the runtime's answer is foreign content entering the host.
214            // Where protection is active its narrative is admitted by the
215            // Context Gateway — bound: unchanged, or the answer is not
216            // written — before it becomes this command's document.
217            crate::core::execution_ledger::host::admit_foreign_document_narrative(
218                "runtime-invoke:",
219                &answer,
220                "engine-runtime-invoke",
221            )
222            .map_err(|_| InstallError::Exchange)?;
223            answer
224        }
225    } else if operation == "health" {
226        if flags.len() != 4 || get("--expected-active")? != selected {
227            return Err(InstallError::Usage);
228        }
229        super::health::check(Path::new(get("--root")?), selected, &key)?
230    } else if operation == "rollback" {
231        if flags.len() != 4 {
232            return Err(InstallError::Usage);
233        }
234        install::rollback(
235            Path::new(get("--root")?),
236            get("--expected-active")?,
237            selected,
238            &key,
239        )?
240    } else {
241        let expected = if operation == "verify" { 5 } else { 7 };
242        if flags.len() != expected + usize::from(companions.is_some()) {
243            return Err(InstallError::Usage);
244        }
245        let (archive, manifest, signature) = if operation == "download" {
246            get("--root")?;
247            get("--expected-active")?;
248            let fetched = super::channel::download(
249                [
250                    get("--archive-url")?,
251                    get("--manifest-url")?,
252                    get("--signature-url")?,
253                ],
254                selected,
255                &key,
256            )?;
257            (fetched.archive, fetched.manifest, fetched.signature)
258        } else {
259            (
260                read_regular(Path::new(get("--archive")?), MAX_ARCHIVE)?,
261                read_regular(Path::new(get("--manifest")?), 65_536)?,
262                read_regular(Path::new(get("--signature")?), 64)?,
263            )
264        };
265        let package = verify_kind(kind, &archive, &manifest, &signature, selected, &key)?;
266        if operation == "verify" {
267            serde_json::json!({"status": "verified", "receipt": package.receipt,
268                "runtime_started": false, "release_approved": false})
269        } else {
270            let root = Path::new(get("--root")?);
271            let expected_active = get("--expected-active")?;
272            if let Some(companions) = companions {
273                install::install_with_companions(
274                    root,
275                    expected_active,
276                    &package,
277                    &archive,
278                    &manifest,
279                    &signature,
280                    companions,
281                )?
282            } else {
283                install::install(
284                    root,
285                    expected_active,
286                    &package,
287                    &archive,
288                    &manifest,
289                    &signature,
290                )?
291            }
292        }
293    };
294    serde_json::to_string_pretty(&result).map_err(|_| InstallError::State)
295}