lean_ctx/core/intelligence_runtime/
code_security.rs1use super::{InstallError, Result};
6
7pub(crate) const MAX_SOURCE_BYTES: usize = 65_536;
8const MAX_LINES: usize = 2048;
9const MAX_FINDINGS: usize = 8;
10
11#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize)]
12#[serde(rename_all = "snake_case")]
13enum Rule {
14 ShellInterpretationEnabled,
15 TlsVerificationDisabled,
16}
17
18#[derive(serde::Deserialize)]
19#[serde(deny_unknown_fields)]
20struct Finding {
21 rule_id: Rule,
22 line: usize,
23}
24
25#[derive(serde::Deserialize)]
26#[serde(deny_unknown_fields)]
27struct Hints {
28 schema_version: u32,
29 strategy: String,
30 source_sha256: String,
31 supported: bool,
32 truncated: bool,
33 findings: Vec<Finding>,
34}
35
36fn language(path: &str) -> Option<&'static str> {
37 match std::path::Path::new(path).extension()?.to_str()? {
38 "py" | "pyi" => Some("python"),
39 "js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
40 "ts" | "tsx" => Some("typescript"),
41 "go" => Some("go"),
42 "rs" => Some("rust"),
43 "cs" => Some("csharp"),
44 "java" => Some("java"),
45 _ => None,
46 }
47}
48
49pub fn enabled_for(path: &str) -> bool {
50 let config = crate::core::config::Config::load_global().intelligence_runtime;
51 language(path).is_some()
52 && config.enabled
53 && config.accept_proprietary
54 && super::bootstrap::permits_config(&config)
55}
56
57pub fn note(path: &str, source: &str) -> Option<String> {
61 let language = language(path)?;
62 let config = crate::core::config::Config::load_global().intelligence_runtime;
63 if !config.enabled || !config.accept_proprietary || !super::bootstrap::permits_config(&config) {
64 return None;
65 }
66 if source.len() > MAX_SOURCE_BYTES
67 || source.lines().count() > MAX_LINES
68 || source.contains('\0')
69 {
70 return Some(
71 "LeanCTX | Hive code hints: not analyzed (source exceeds the supported text bounds)."
72 .into(),
73 );
74 }
75 Some(match invoke(&config, language, source) {
76 Ok(hints) => render(&hints),
77 Err(_) => {
78 "LeanCTX | Hive code hints unavailable; existing read protections are unchanged.".into()
79 }
80 })
81}
82
83fn render(hints: &Hints) -> String {
84 if !hints.supported {
85 return "LeanCTX | Hive code hints: no checks available for this language.".into();
86 }
87 if hints.findings.is_empty() {
88 return "LeanCTX | Hive code hints: no matching patterns in inspected source (limited static checks)."
89 .into();
90 }
91 let mut text =
92 String::from("LeanCTX | Hive code hints (limited static checks; review required):");
93 for finding in &hints.findings {
94 let message = match finding.rule_id {
95 Rule::TlsVerificationDisabled => {
96 "TLS certificate verification is explicitly disabled; review connection trust."
97 }
98 Rule::ShellInterpretationEnabled => {
99 "Shell interpretation is enabled; review command construction and untrusted input."
100 }
101 };
102 text.push_str(&format!(
105 "\n- Inspected source line {}: {message}",
106 finding.line
107 ));
108 }
109 if hints.truncated {
110 text.push_str("\nAdditional matching patterns were omitted at the finding limit.");
111 }
112 text
113}
114
115#[cfg(any(unix, windows, test))]
116fn validate(raw: &str, source: &str) -> Result<Hints> {
117 if raw.len() > 4096 {
118 return Err(InstallError::Exchange);
119 }
120 let hints: Hints = serde_json::from_str(raw).map_err(|_| InstallError::Exchange)?;
121 let lines = source.lines().count();
122 if hints.schema_version != 1
123 || hints.strategy != "bounded_static_v1"
124 || hints.source_sha256 != super::sha256(source.as_bytes())
125 || hints.findings.len() > MAX_FINDINGS
126 || (!hints.supported && (!hints.findings.is_empty() || hints.truncated))
127 || (hints.truncated && hints.findings.len() != MAX_FINDINGS)
128 || hints.findings.iter().any(|f| f.line == 0 || f.line > lines)
129 || hints
130 .findings
131 .windows(2)
132 .any(|pair| (pair[0].line, &pair[0].rule_id) >= (pair[1].line, &pair[1].rule_id))
133 {
134 return Err(InstallError::Exchange);
135 }
136 Ok(hints)
137}
138
139#[cfg(any(unix, windows))]
140fn invoke(
141 config: &crate::core::config::IntelligenceRuntimeConfig,
142 language: &str,
143 source: &str,
144) -> Result<Hints> {
145 use lean_ctx_protocol::runtime_exchange::{RuntimeRequestV1, RuntimeResponseV1};
146 use serde_json::json;
147 use std::{
148 path::Path,
149 time::{SystemTime, UNIX_EPOCH},
150 };
151 let root = Path::new(&config.root);
152 let trust = super::trust_key(&config.trust_key_hex)?;
153 let input =
154 json!({"schema_version":1,"language":language,"source":source,"max_findings":MAX_FINDINGS})
155 .to_string();
156 let binding = super::sha256(input.as_bytes());
157 let input_ref = format!("projection:sha256:{binding}");
158 let (request, executed) = super::session::invoke_built(
159 root,
160 &config.manifest_sha256,
161 &trust,
162 |package| {
163 if !super::health::supports_code_security(package) {
164 return Err(InstallError::Manifest);
165 }
166 let deadline = SystemTime::now()
167 .duration_since(UNIX_EPOCH)
168 .ok()
169 .and_then(|now| u64::try_from(now.as_millis()).ok())
170 .and_then(|now| now.checked_add(2000))
171 .ok_or(InstallError::Exchange)?;
172 let request: RuntimeRequestV1 = serde_json::from_value(json!({
173 "protocol_version":lean_ctx_protocol::runtime_exchange::RUNTIME_EXCHANGE_VERSION,
174 "session_id":format!("code-{binding}"),"request_id":format!("code-{binding}"),
175 "sequence":1,"deadline_unix_ms":deadline,"input":input,
176 "invocation":{"schema_version":1,"invocation_id":format!("code-{binding}"),
177 "engine":{"engine_id":"leanctx-intelligence","engine_version":package.receipt.version},
178 "operation":{"capability_id":"pro.runtime.code_security","capability_version":"1.0.0"},
179 "input_ref":input_ref,"input_digest":format!("sha256:{binding}"),"source_refs":[input_ref],
180 "policy_admission":{"decision":"admitted","policy_ref":format!("policy:code-{binding}")}}
181 })).map_err(|_| InstallError::Exchange)?;
182 Ok(request)
183 },
184 )?;
185 let response: RuntimeResponseV1 =
186 serde_json::from_value(executed["response"].clone()).map_err(|_| InstallError::Exchange)?;
187 response
188 .validate_for(&request)
189 .map_err(|_| InstallError::Exchange)?;
190 if response.observation.status != lean_ctx_protocol::EngineObservationStatusV1::Succeeded
191 || crate::core::config::Config::load_global().intelligence_runtime != *config
192 || super::install::active_manifest(root)? != config.manifest_sha256
193 {
194 return Err(InstallError::Exchange);
195 }
196 validate(
197 response.output.as_deref().ok_or(InstallError::Exchange)?,
198 source,
199 )
200}
201
202#[cfg(not(any(unix, windows)))]
203fn invoke(_: &crate::core::config::IntelligenceRuntimeConfig, _: &str, _: &str) -> Result<Hints> {
204 Err(InstallError::Exchange)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 #[test]
211 fn private_findings_cannot_change_source_or_inject_text() {
212 let source = "first\nsecond\n";
213 let valid = serde_json::json!({"schema_version":1,"strategy":"bounded_static_v1",
214 "source_sha256":super::super::sha256(source.as_bytes()),"supported":true,"truncated":false,
215 "findings":[{"rule_id":"tls_verification_disabled","line":2}]});
216 let rendered = render(&validate(&valid.to_string(), source).unwrap());
217 assert!(rendered.contains("Inspected source line 2"));
218 for (field, replacement) in [
219 ("source_sha256", serde_json::json!("other")),
220 ("supported", serde_json::json!(false)),
221 ("truncated", serde_json::json!(true)),
222 (
223 "findings",
224 serde_json::json!([{"rule_id":"tls_verification_disabled","line":0}]),
225 ),
226 (
227 "findings",
228 serde_json::json!([{"rule_id":"tls_verification_disabled","line":3}]),
229 ),
230 (
231 "findings",
232 serde_json::json!([{"rule_id":"secret text","line":1}]),
233 ),
234 (
235 "findings",
236 serde_json::json!([{"rule_id":"tls_verification_disabled","line":1,"source":"secret"}]),
237 ),
238 (
239 "findings",
240 serde_json::json!([{"rule_id":"tls_verification_disabled","line":2},{"rule_id":"tls_verification_disabled","line":2}]),
241 ),
242 ] {
243 let mut invalid = valid.clone();
244 invalid[field] = replacement;
245 assert!(validate(&invalid.to_string(), source).is_err(), "{field}");
246 }
247 }
248}