lean_ctx/core/intelligence_runtime/
mod.rs1mod 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#[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#[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#[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;
71pub 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
89const EXECUTABLE_NAME: &str = if cfg!(windows) {
91 "leanctx-intelligence.exe"
92} else {
93 "leanctx-intelligence"
94};
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub(crate) enum RuntimeKind {
103 Intelligence,
105 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 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
181pub(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 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 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}