1use std::collections::{BTreeMap, BTreeSet};
23
24use active_infinity::interop::{
25 dependencies, dependents, impact, secret_tainted_paths, subgraph, why,
26};
27use active_infinity::{decode, encode, AnnotationKind, InfinityModule, Inode, NodeId, Provenance};
28
29use crate::cap::CapId;
30use crate::error::{Error, Result};
31use crate::kernel::Provider;
32use crate::security;
33use crate::value::Response;
34use crate::value::{Arg, Call, Value};
35
36pub const METHODS: &[&str] = &[
38 "evaluate",
39 "why",
40 "impact",
41 "subgraph",
42 "patch",
43 "dependents",
44 "dependencies",
45 "content_hash",
46 "stats",
47 "annotations",
48 "provenance",
49];
50
51pub struct ModuleProvider {
53 modules: BTreeMap<u64, InfinityModule>,
54 next_object: u64,
55 pub drops: Vec<(u64, CapId)>,
57 pub calls: u64,
59}
60
61impl Default for ModuleProvider {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl ModuleProvider {
68 pub fn new() -> Self {
69 Self {
70 modules: BTreeMap::new(),
71 next_object: 0,
72 drops: Vec::new(),
73 calls: 0,
74 }
75 }
76
77 pub fn install(&mut self, module: InfinityModule) -> u64 {
79 let id = self.next_object;
80 self.next_object += 1;
81 self.modules.insert(id, module);
82 id
83 }
84
85 pub fn module(&self, object: u64) -> Option<&InfinityModule> {
87 self.modules.get(&object)
88 }
89
90 pub fn module_count(&self) -> usize {
92 self.modules.len()
93 }
94
95 fn require(&self, object: u64) -> Result<&InfinityModule> {
96 self.modules
97 .get(&object)
98 .ok_or(Error::InvalidArgument("unknown module"))
99 }
100
101 fn path_arg(call: &Call) -> Option<String> {
102 call.args.iter().find_map(|arg| match arg {
103 Arg::Value(Value::Path(path)) => Some(path.clone()),
104 _ => None,
105 })
106 }
107
108 fn resolve(&self, module: &InfinityModule, call: &Call) -> Result<(String, NodeId)> {
109 let path =
110 Self::path_arg(call).ok_or(Error::InvalidArgument("a path argument is required"))?;
111 let node = active_infinity::interop::resolve_path(module, &path)
112 .ok_or(Error::InvalidArgument("path does not resolve"))?;
113 Ok((path, node))
114 }
115}
116
117impl Provider for ModuleProvider {
118 fn name(&self) -> &'static str {
119 "module"
120 }
121
122 fn methods(&self) -> Vec<String> {
123 METHODS.iter().map(|method| method.to_string()).collect()
124 }
125
126 fn mutating(&self, method: &str) -> bool {
127 method == "patch"
128 }
129
130 fn invoke(&mut self, object: u64, call: &Call) -> Result<Response> {
131 self.calls += 1;
132 let module = self.require(object)?.clone();
133 let response = match call.method.as_str() {
134 "evaluate" => {
135 let module = match Self::path_arg(call) {
136 Some(path) => {
137 let node = active_infinity::interop::resolve_path(&module, &path)
138 .ok_or(Error::InvalidArgument("path does not resolve"))?;
139 restrict(&module, node)?
140 }
141 None => module,
142 };
143 let budget = call.budget.to_active_budget();
144 let (value, _diagnostics) = active_infinity::interop::evaluate(&module, budget)
145 .map_err(|error| match &error {
146 active_infinity::Error::Evaluation(code) => Error::Domain(code),
147 other => Error::Payload(other.code().to_string()),
148 })?;
149 Response::Value(security::from_active_value(&value))
150 }
151 "why" => {
152 let (_path, node) = self.resolve(&module, call)?;
153 let labels = why(&module, node);
154 Response::Labels(apply_redaction(&module, labels, call.redact))
155 }
156 "impact" => {
157 let (_path, node) = self.resolve(&module, call)?;
158 let labels = impact(&module, node);
159 Response::Labels(apply_redaction(&module, labels, call.redact))
160 }
161 "subgraph" => {
162 let (_path, node) = self.resolve(&module, call)?;
163 let sub = restrict(&module, node)?;
164 let bytes =
165 encode(&sub).map_err(|error| Error::Payload(error.code().to_string()))?;
166 Response::Module(bytes)
167 }
168 "patch" => {
169 let (_path, node) = self.resolve(&module, call)?;
170 if call.read_only {
171 return Err(Error::ReadOnly);
172 }
173 let replacement = call
174 .args
175 .iter()
176 .find_map(|arg| match arg {
177 Arg::Value(Value::Path(_)) => None,
180 Arg::Value(value) => literal_node(value),
181 _ => None,
182 })
183 .ok_or(Error::InvalidArgument("patch needs a literal value"))?;
184 let mut patched = module.clone();
185 *patched.node_mut(node)? = replacement;
186 patched.set_provenance(Some(Provenance {
187 producer: Some("active-rpc/patch".to_string()),
188 timestamp_ms: 0,
189 model: None,
190 tool: Some("patch".to_string()),
191 parent: None,
192 entries: Vec::new(),
193 }));
194 let bytes =
195 encode(&patched).map_err(|error| Error::Payload(error.code().to_string()))?;
196 self.modules.insert(object, patched);
202 Response::Module(bytes)
203 }
204 "dependents" => {
205 let (_path, node) = self.resolve(&module, call)?;
206 let labels: Vec<String> = dependents(&module, node)
207 .into_iter()
208 .map(|id| active_infinity::interop::node_label(&module, id))
209 .collect();
210 Response::Labels(apply_redaction(&module, labels, call.redact))
211 }
212 "dependencies" => {
213 let (_path, node) = self.resolve(&module, call)?;
214 let labels: Vec<String> = dependencies(&module, node)
215 .into_iter()
216 .map(|id| active_infinity::interop::node_label(&module, id))
217 .collect();
218 Response::Labels(apply_redaction(&module, labels, call.redact))
219 }
220 "content_hash" => {
221 let bytes =
222 encode(&module).map_err(|error| Error::Payload(error.code().to_string()))?;
223 let hash = active_infinity::content_hash(&bytes)
224 .map_err(|error| Error::Payload(error.code().to_string()))?;
225 Response::Value(Value::Text(format!(
226 "blake3:{}",
227 active_infinity::ir::hash_hex(&hash)
228 )))
229 }
230 "stats" => {
231 let mut map = BTreeMap::new();
232 map.insert("nodes".to_string(), Value::Int(module.node_count() as i64));
233 map.insert("edges".to_string(), Value::Int(module.edges().len() as i64));
234 map.insert(
235 "secrets".to_string(),
236 Value::Int(
237 module
238 .nodes()
239 .iter()
240 .filter(|node| node.is_secret())
241 .count() as i64,
242 ),
243 );
244 map.insert(
245 "has_provenance".to_string(),
246 Value::Bool(module.provenance().is_some()),
247 );
248 Response::Value(Value::Map(map))
249 }
250 "annotations" => {
251 let mut out = Vec::new();
252 for annotation in module.annotations() {
253 let (kind, text) = match &annotation.kind {
254 AnnotationKind::Intent(text) => ("intent", text.clone()),
255 AnnotationKind::Constraint { source, .. } => ("constraint", source.clone()),
256 AnnotationKind::AiImmutable(flag) => ("ai_immutable", flag.to_string()),
257 };
258 let text = if call.redact && text.contains("***") {
259 Value::Secret
260 } else {
261 Value::Text(text)
262 };
263 out.push(Value::Map(
264 [
265 ("node".to_string(), Value::Int(annotation.node.0 as i64)),
266 ("kind".to_string(), Value::Text(kind.to_string())),
267 ("text".to_string(), text),
268 ]
269 .into_iter()
270 .collect(),
271 ));
272 }
273 Response::Value(Value::List(out))
274 }
275 "provenance" => match module.provenance() {
276 None => Response::Value(Value::Null),
277 Some(prov) => {
278 let mut map = BTreeMap::new();
279 if let Some(producer) = &prov.producer {
280 map.insert("producer".to_string(), Value::Text(producer.clone()));
281 }
282 if let Some(model) = &prov.model {
283 map.insert("model".to_string(), Value::Text(model.clone()));
284 }
285 if let Some(tool) = &prov.tool {
286 map.insert("tool".to_string(), Value::Text(tool.clone()));
287 }
288 map.insert(
289 "timestamp_ms".to_string(),
290 Value::Int(prov.timestamp_ms as i64),
291 );
292 Response::Value(Value::Map(map))
293 }
294 },
295 other => {
296 return Err(Error::MethodNotGranted {
297 method: other.to_string(),
298 })
299 }
300 };
301 Ok(response)
302 }
303
304 fn on_drop(&mut self, object: u64, cap: CapId) {
305 self.drops.push((object, cap));
306 }
307
308 fn take_drop_notifications(&mut self) -> Vec<(u64, CapId)> {
309 std::mem::take(&mut self.drops)
310 }
311}
312
313fn apply_redaction(module: &InfinityModule, labels: Vec<String>, redact: bool) -> Vec<String> {
314 if !redact {
315 return labels;
316 }
317 let tainted = tainted_labels(module);
318 security::redact_cone_labels(labels, &tainted)
319}
320
321fn tainted_labels(module: &InfinityModule) -> BTreeSet<String> {
327 let count = module.node_count();
328 if count == 0 {
329 return BTreeSet::new();
330 }
331 let mut tainted = vec![false; count];
332 let mut stack: Vec<usize> = Vec::new();
335 for (index, node) in module.nodes().iter().enumerate() {
336 if node.is_secret() {
337 tainted[index] = true;
338 stack.push(index);
339 }
340 }
341 while let Some(index) = stack.pop() {
342 if let Some(node) = module.nodes().get(index) {
343 for child in node.children() {
344 if !tainted[child.index()] {
345 tainted[child.index()] = true;
346 stack.push(child.index());
347 }
348 }
349 }
350 }
351 let mut changed = true;
352 while changed {
353 changed = false;
354 for (index, node) in module.nodes().iter().enumerate() {
355 if tainted[index] {
356 continue;
357 }
358 if node
359 .children()
360 .iter()
361 .any(|child| tainted.get(child.index()).copied().unwrap_or(false))
362 {
363 tainted[index] = true;
364 changed = true;
365 }
366 }
367 }
368 let mut labels = BTreeSet::new();
369 for (index, is_tainted) in tainted.iter().enumerate() {
370 if *is_tainted {
371 labels.insert(active_infinity::interop::node_label(
372 module,
373 NodeId(index as u32),
374 ));
375 }
376 }
377 labels.extend(secret_tainted_paths(module));
380 labels
381}
382
383fn restrict(module: &InfinityModule, node: NodeId) -> Result<InfinityModule> {
388 let cone = subgraph(module, &[node]);
389 let mut sub = InfinityModule::new();
390 let mut map = BTreeMap::new();
391 for id in &cone {
392 let cloned = module
393 .get(*id)
394 .cloned()
395 .ok_or(Error::InvalidArgument("subgraph node disappeared"))?;
396 map.insert(*id, sub.push(cloned));
397 }
398 for (old, new) in &map {
399 if let Some(node) = module.get(*old).cloned() {
400 let rewritten = remap_children(&node, &map)?;
401 *sub.node_mut(*new)? = rewritten;
402 }
403 }
404 sub.set_root(map.get(&node).copied());
405 Ok(sub)
406}
407
408fn remap_children(node: &Inode, map: &BTreeMap<NodeId, NodeId>) -> Result<Inode> {
410 let resolve = |id: NodeId| -> Result<NodeId> {
411 map.get(&id)
412 .copied()
413 .ok_or(Error::InvalidArgument("subgraph is not closed"))
414 };
415 let rewritten = match node {
416 Inode::Array { elements } => Inode::Array {
417 elements: elements
418 .iter()
419 .map(|id| resolve(*id))
420 .collect::<Result<_>>()?,
421 },
422 Inode::Table { fields } => Inode::Table {
423 fields: fields
424 .iter()
425 .map(|(name, id)| Ok((name.clone(), resolve(*id)?)))
426 .collect::<Result<_>>()?,
427 },
428 Inode::Quantity { value, unit } => Inode::Quantity {
429 value: resolve(*value)?,
430 unit: unit.clone(),
431 },
432 Inode::Ref(id) => Inode::Ref(resolve(*id)?),
433 Inode::SecretRef { locator } => Inode::SecretRef {
434 locator: resolve(*locator)?,
435 },
436 other => other.clone(),
437 };
438 Ok(rewritten)
439}
440
441fn literal_node(value: &Value) -> Option<Inode> {
442 match value {
443 Value::Null => Some(Inode::Null),
444 Value::Bool(flag) => Some(Inode::Bool(*flag)),
445 Value::Int(number) => Some(Inode::Int(*number)),
446 Value::Float(number) => Some(Inode::Float(*number)),
447 Value::Text(text) => Some(Inode::Str(text.clone())),
448 Value::Path(path) => Some(Inode::Str(path.clone())),
449 Value::List(_) | Value::Map(_) | Value::Secret | Value::Module(_) => None,
452 }
453}
454
455pub fn decode_module(bytes: &[u8]) -> Result<InfinityModule> {
457 decode(bytes).map_err(|error| Error::Payload(error.code().to_string()))
458}
459
460pub fn module_from_source(source: &str, name: &str) -> Result<InfinityModule> {
462 active_infinity::interop::from_active_source(source, name)
463 .map_err(|error| Error::Payload(error.code().to_string()))
464}
465
466pub fn resolve_path(module: &InfinityModule, path: &str) -> Option<NodeId> {
468 active_infinity::interop::resolve_path(module, path)
469}
470
471pub fn cone_ids(module: &InfinityModule, path: &str) -> Result<Vec<NodeId>> {
473 let node = active_infinity::interop::resolve_path(module, path)
474 .ok_or(Error::InvalidArgument("path does not resolve"))?;
475 Ok(subgraph(module, &[node]))
476}