Skip to main content

lean_ctx/core/intelligence_runtime/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Public installation authority for the optional, separately licensed runtime.
3//! No private implementation, account, entitlement, or implicit network access.
4
5mod bootstrap;
6mod catalog;
7mod channel;
8mod cli;
9pub mod cockpit;
10pub mod code_security;
11pub mod compliance_frameworks;
12mod configuration;
13pub(crate) mod dashboard_pro;
14mod exchange;
15pub(crate) mod org_policy_verifier;
16pub mod personal_protection;
17pub(crate) mod semantic_coverage;
18pub mod semantic_detectors;
19
20// Exercise the same explicit private-directory prerequisite as the CLI.
21#[cfg(any(test, feature = "test-support"))]
22#[cfg_attr(
23    not(any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))),
24    allow(dead_code)
25)]
26pub(crate) fn private_root_for_test() -> tempfile::TempDir {
27    let mut builder = tempfile::Builder::new();
28    builder.prefix("runtime-install-test-");
29    #[cfg(unix)]
30    {
31        use std::os::unix::fs::PermissionsExt;
32        builder.permissions(std::fs::Permissions::from_mode(0o700));
33    }
34    builder.tempdir().expect("private test root")
35}
36
37/// Signs `document` under `domain` as a release manifest would be signed.
38#[cfg(all(
39    any(test, feature = "test-support"),
40    any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))
41))]
42pub(crate) fn sign_document(
43    document: &serde_json::Value,
44    key: &ed25519_dalek::SigningKey,
45    domain: &[u8],
46) -> (Vec<u8>, Vec<u8>, String) {
47    use ed25519_dalek::Signer;
48    let manifest = serde_json::to_vec(document).expect("manifest");
49    let mut payload = domain.to_vec();
50    payload.extend_from_slice(&manifest);
51    let signature = key.sign(&payload).to_bytes().to_vec();
52    let selected = sha256(&manifest);
53    (manifest, signature, selected)
54}
55
56/// The company-journey fixture endpoint for tests outside the Engine (the
57/// proxy's held-floor journey); inside the Engine it is the test module.
58#[cfg(all(
59    feature = "test-support",
60    not(test),
61    any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))
62))]
63#[allow(dead_code, unused_imports, clippy::unwrap_used)]
64#[path = "company_journey_tests.rs"]
65pub mod company_journey_support;
66mod semantic_worker;
67pub use configuration::{DISCLOSURE, inspect_setup};
68mod health;
69mod install;
70mod license;
71// The Hive connector's runtime lease registers the source.
72pub mod lease;
73mod manifest;
74mod rotation;
75#[cfg(any(unix, windows))]
76mod session;
77#[cfg(windows)]
78mod session_windows;
79
80#[cfg(test)]
81pub(crate) mod tests;
82
83use std::io::Read;
84use std::path::Path;
85
86pub use cli::run;
87use manifest::{VerifiedPackage, verify, verify_kind};
88
89// The signed archive member stays platform-neutral; only materialized files vary.
90const EXECUTABLE_NAME: &str = if cfg!(windows) {
91    "leanctx-intelligence.exe"
92} else {
93    "leanctx-intelligence"
94};
95
96/// The signed package kinds the runtime channel delivers. Both share the
97/// archive shape, the signature domains and the trust chain; the signed
98/// `runtime-description.json` names the kind (`engine_id`), and every use
99/// re-authenticates the package as the kind it expects, so one kind never
100/// runs as the other.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub(crate) enum RuntimeKind {
103    /// The Pro Intelligence Runtime: licence-bound outside staging.
104    Intelligence,
105    /// The Cockpit web UI: free for everyone, never licence-bound.
106    Cockpit,
107}
108
109impl RuntimeKind {
110    pub(crate) const fn engine_id(self) -> &'static str {
111        match self {
112            Self::Intelligence => "leanctx-intelligence",
113            Self::Cockpit => "leanctx-cockpit",
114        }
115    }
116
117    /// The store directory beside the context data: `<name>` for staging,
118    /// `<name>-production` for production. Kinds never share a store.
119    pub(crate) const fn store_name(self) -> &'static str {
120        match self {
121            Self::Intelligence => "intelligence-runtime",
122            Self::Cockpit => "cockpit-runtime",
123        }
124    }
125}
126
127#[derive(Debug, thiserror::Error)]
128pub enum InstallError {
129    #[error("invalid runtime installer arguments")]
130    Usage,
131    #[error("runtime download URL, transport or response rejected")]
132    Download,
133    #[error("runtime package I/O failed")]
134    Io(#[from] std::io::Error),
135    #[error("runtime metadata or package exceeds its size bound")]
136    Size,
137    #[error("runtime input is not a regular, non-symlink file")]
138    Input,
139    #[error("runtime manifest is not independently selected or compatible")]
140    Manifest,
141    #[error("runtime signature does not match the independent trust key")]
142    Signature,
143    #[error("runtime archive is invalid or incompatible with this host")]
144    Archive,
145    #[error("runtime installation directory is not private and canonical")]
146    Directory,
147    #[error("another runtime installation is active")]
148    Busy,
149    #[error("runtime selection changed or retained package is inconsistent")]
150    State,
151    #[error("runtime channel is older than the active one; a downgrade must be requested")]
152    Downgrade,
153    #[error("atomic runtime selection write failed")]
154    Commit,
155    #[error("runtime health check failed or exceeded its bound")]
156    Health,
157    #[error("runtime invocation is not supported by the authenticated package and this host")]
158    InvocationAdmission,
159    #[error("runtime invocation failed or exceeded its bound")]
160    Exchange,
161    #[error("runtime request or policy input was rejected")]
162    Policy,
163    #[error("runtime global configuration could not be updated")]
164    Configuration,
165}
166
167pub(crate) type Result<T> = std::result::Result<T, InstallError>;
168
169fn trust_key(encoded: &str) -> Result<[u8; 32]> {
170    if !is_digest(encoded, 64) {
171        return Err(InstallError::Usage);
172    }
173    let mut key = [0; 32];
174    for (index, byte) in key.iter_mut().enumerate() {
175        *byte = u8::from_str_radix(&encoded[index * 2..index * 2 + 2], 16)
176            .map_err(|_| InstallError::Usage)?;
177    }
178    Ok(key)
179}
180
181/// Open once and bound the actual read, including files that grow after stat.
182pub(crate) fn read_regular(path: &Path, limit: usize) -> Result<Vec<u8>> {
183    let metadata = std::fs::symlink_metadata(path)?;
184    if !regular_metadata(&metadata) {
185        return Err(InstallError::Input);
186    }
187    let mut options = std::fs::OpenOptions::new();
188    options.read(true);
189    #[cfg(unix)]
190    {
191        use std::os::unix::fs::OpenOptionsExt;
192        options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
193    }
194    #[cfg(windows)]
195    {
196        use std::os::windows::fs::OpenOptionsExt;
197        use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
198        // Bind the leaf itself even if it becomes a reparse point after stat.
199        options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
200    }
201    let file = options.open(path)?;
202    let metadata = file.metadata()?;
203    if !regular_metadata(&metadata) {
204        return Err(InstallError::Input);
205    }
206    if metadata.len() > limit as u64 {
207        return Err(InstallError::Size);
208    }
209    let mut bytes = Vec::new();
210    file.take(limit as u64 + 1).read_to_end(&mut bytes)?;
211    if bytes.len() > limit {
212        return Err(InstallError::Size);
213    }
214    Ok(bytes)
215}
216
217fn regular_metadata(metadata: &std::fs::Metadata) -> bool {
218    #[cfg(windows)]
219    {
220        use std::os::windows::fs::MetadataExt;
221        use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
222        // Symlinks are only one reparse tag; reject every reparse-backed input.
223        if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
224            return false;
225        }
226    }
227    metadata.is_file() && !metadata.file_type().is_symlink()
228}
229
230use super::updater::sha256_hex as sha256;
231
232pub(crate) fn is_digest(value: &str, length: usize) -> bool {
233    value.len() == length
234        && value
235            .bytes()
236            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
237}