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 {
70 Self {
71 modules: BTreeMap::new(),
72 next_object: 0,
73 drops: Vec::new(),
74 calls: 0,
75 }
76 }
77
78 pub fn install(&mut self, module: InfinityModule) -> u64 {
80 let id = self.next_object;
81 self.next_object += 1;
82 self.modules.insert(id, module);
83 id
84 }
85
86 pub fn module(&self, object: u64) -> Option<&InfinityModule> {
88 self.modules.get(&object)
89 }
90
91 pub fn module_count(&self) -> usize {
93 self.modules.len()
94 }
95
96 fn require(&self, object: u64) -> Result<&InfinityModule> {
97 self.modules
98 .get(&object)
99 .ok_or(Error::InvalidArgument("unknown module"))
100 }
101
102 fn path_arg(call: &Call) -> Option<String> {
103 call.args.iter().find_map(|arg| match arg {
104 Arg::Value(Value::Path(path)) => Some(path.clone()),
105 _ => None,
106 })
107 }
108
109 fn resolve(&self, module: &InfinityModule, call: &Call) -> Result<(String, NodeId)> {
110 let path =
111 Self::path_arg(call).ok_or(Error::InvalidArgument("a path argument is required"))?;
112 let node = active_infinity::interop::resolve_path(module, &path)
113 .ok_or(Error::InvalidArgument("path does not resolve"))?;
114 Ok((path, node))
115 }
116}
117
118impl Provider for ModuleProvider {
119 fn name(&self) -> &'static str {
120 "module"
121 }
122
123 fn methods(&self) -> Vec<String> {
124 METHODS.iter().map(|method| method.to_string()).collect()
125 }
126
127 fn mutating(&self, method: &str) -> bool {
128 method == "patch"
129 }
130
131 fn invoke(&mut self, object: u64, call: &Call) -> Result<Response> {
132 self.calls += 1;
133 let module = self.require(object)?.clone();
134 let response = match call.method.as_str() {
135 "evaluate" => {
136 let module = match Self::path_arg(call) {
137 Some(path) => {
138 let node = active_infinity::interop::resolve_path(&module, &path)
139 .ok_or(Error::InvalidArgument("path does not resolve"))?;
140 restrict(&module, node)?
141 }
142 None => module,
143 };
144 let budget = call.budget.to_active_budget();
145 let (value, _diagnostics) = active_infinity::interop::evaluate(&module, budget)
146 .map_err(|error| match &error {
147 active_infinity::Error::Evaluation(code) => Error::Domain(code),
148 other => Error::Payload(other.code().to_string()),
149 })?;
150 Response::Value(security::from_active_value(&value))
151 }
152 "why" => {
153 let (_path, node) = self.resolve(&module, call)?;
154 let labels = why(&module, node);
155 Response::Labels(apply_redaction(&module, labels, call.redact))
156 }
157 "impact" => {
158 let (_path, node) = self.resolve(&module, call)?;
159 let labels = impact(&module, node);
160 Response::Labels(apply_redaction(&module, labels, call.redact))
161 }
162 "subgraph" => {
163 let (_path, node) = self.resolve(&module, call)?;
164 let sub = restrict(&module, node)?;
165 Response::Module(redacted_module_bytes(&sub)?)
166 }
167 "patch" => {
168 let (_path, node) = self.resolve(&module, call)?;
169 if call.read_only {
170 return Err(Error::ReadOnly);
171 }
172 let replacement = call
173 .args
174 .iter()
175 .find_map(|arg| match arg {
176 Arg::Value(Value::Path(_)) => None,
179 Arg::Value(value) => literal_node(value),
180 _ => None,
181 })
182 .ok_or(Error::InvalidArgument("patch needs a literal value"))?;
183 let mut patched = module.clone();
184 *patched.node_mut(node)? = replacement;
185 patched.set_provenance(Some(Provenance {
186 producer: Some("active-rpc/patch".to_string()),
187 timestamp_ms: 0,
188 model: None,
189 tool: Some("patch".to_string()),
190 parent: None,
191 entries: Vec::new(),
192 }));
193 let bytes = redacted_module_bytes(&patched)?;
194 self.modules.insert(object, patched);
200 Response::Module(bytes)
201 }
202 "dependents" => {
203 let (_path, node) = self.resolve(&module, call)?;
204 let labels: Vec<String> = dependents(&module, node)
205 .into_iter()
206 .map(|id| active_infinity::interop::node_label(&module, id))
207 .collect();
208 Response::Labels(apply_redaction(&module, labels, call.redact))
209 }
210 "dependencies" => {
211 let (_path, node) = self.resolve(&module, call)?;
212 let labels: Vec<String> = dependencies(&module, node)
213 .into_iter()
214 .map(|id| active_infinity::interop::node_label(&module, id))
215 .collect();
216 Response::Labels(apply_redaction(&module, labels, call.redact))
217 }
218 "content_hash" => {
219 let bytes =
220 encode(&module).map_err(|error| Error::Payload(error.code().to_string()))?;
221 let hash = active_infinity::content_hash(&bytes)
222 .map_err(|error| Error::Payload(error.code().to_string()))?;
223 Response::Value(Value::Text(format!(
224 "blake3:{}",
225 active_infinity::ir::hash_hex(&hash)
226 )))
227 }
228 "stats" => {
229 let mut map = BTreeMap::new();
230 map.insert("nodes".to_string(), Value::Int(module.node_count() as i64));
231 map.insert("edges".to_string(), Value::Int(module.edges().len() as i64));
232 map.insert(
233 "secrets".to_string(),
234 Value::Int(
235 module
236 .nodes()
237 .iter()
238 .filter(|node| node.is_secret())
239 .count() as i64,
240 ),
241 );
242 map.insert(
243 "has_provenance".to_string(),
244 Value::Bool(module.provenance().is_some()),
245 );
246 Response::Value(Value::Map(map))
247 }
248 "annotations" => {
249 let tainted_ids = tainted_nodes(&module);
250 let mut out = Vec::new();
251 for annotation in module.annotations() {
252 let (kind, text) = match &annotation.kind {
253 AnnotationKind::Intent(text) => ("intent", text.clone()),
254 AnnotationKind::Constraint { source, .. } => ("constraint", source.clone()),
255 AnnotationKind::AiImmutable(flag) => ("ai_immutable", flag.to_string()),
256 };
257 let hidden = (call.redact && tainted_ids.contains(&annotation.node))
261 || text.contains(security::SECRET_MARKER);
262 let text = if hidden {
263 Value::Secret
264 } else {
265 Value::Text(text)
266 };
267 out.push(Value::Map(
268 [
269 ("node".to_string(), Value::Int(annotation.node.0 as i64)),
270 ("kind".to_string(), Value::Text(kind.to_string())),
271 ("text".to_string(), text),
272 ]
273 .into_iter()
274 .collect(),
275 ));
276 }
277 Response::Value(Value::List(out))
278 }
279 "provenance" => match module.provenance() {
280 None => Response::Value(Value::Null),
281 Some(prov) => {
282 let mut map = BTreeMap::new();
283 if let Some(producer) = &prov.producer {
284 map.insert("producer".to_string(), Value::Text(producer.clone()));
285 }
286 if let Some(model) = &prov.model {
287 map.insert("model".to_string(), Value::Text(model.clone()));
288 }
289 if let Some(tool) = &prov.tool {
290 map.insert("tool".to_string(), Value::Text(tool.clone()));
291 }
292 map.insert(
293 "timestamp_ms".to_string(),
294 Value::Int(prov.timestamp_ms as i64),
295 );
296 Response::Value(Value::Map(map))
297 }
298 },
299 other => {
300 return Err(Error::MethodNotGranted {
301 method: other.to_string(),
302 })
303 }
304 };
305 Ok(response)
306 }
307
308 fn on_drop(&mut self, object: u64, cap: CapId) {
309 self.drops.push((object, cap));
310 }
311
312 fn take_drop_notifications(&mut self) -> Vec<(u64, CapId)> {
313 std::mem::take(&mut self.drops)
314 }
315}
316
317fn apply_redaction(module: &InfinityModule, labels: Vec<String>, redact: bool) -> Vec<String> {
318 if !redact {
319 return labels;
320 }
321 let tainted = tainted_labels(module);
322 security::redact_cone_labels(labels, &tainted)
323}
324
325fn tainted_labels(module: &InfinityModule) -> BTreeSet<String> {
331 let count = module.node_count();
332 if count == 0 {
333 return BTreeSet::new();
334 }
335 let mut tainted = vec![false; count];
336 let mut stack: Vec<usize> = Vec::new();
339 for (index, node) in module.nodes().iter().enumerate() {
340 if node.is_secret() {
341 tainted[index] = true;
342 stack.push(index);
343 }
344 }
345 while let Some(index) = stack.pop() {
346 if let Some(node) = module.nodes().get(index) {
347 for child in node.children() {
348 if !tainted[child.index()] {
349 tainted[child.index()] = true;
350 stack.push(child.index());
351 }
352 }
353 }
354 }
355 let mut changed = true;
356 while changed {
357 changed = false;
358 for (index, node) in module.nodes().iter().enumerate() {
359 if tainted[index] {
360 continue;
361 }
362 if node
363 .children()
364 .iter()
365 .any(|child| tainted.get(child.index()).copied().unwrap_or(false))
366 {
367 tainted[index] = true;
368 changed = true;
369 }
370 }
371 }
372 let mut labels = BTreeSet::new();
373 for (index, is_tainted) in tainted.iter().enumerate() {
374 if *is_tainted {
375 labels.insert(active_infinity::interop::node_label(
376 module,
377 NodeId(index as u32),
378 ));
379 }
380 }
381 labels.extend(secret_tainted_paths(module));
384 labels
385}
386
387fn redacted_module_bytes(module: &InfinityModule) -> Result<Vec<u8>> {
394 let mut redacted = module.clone();
395 for index in 0..redacted.node_count() {
396 let id = NodeId(index as u32);
397 let is_secret = redacted
398 .get(id)
399 .map(|node| node.is_secret())
400 .unwrap_or(false);
401 if is_secret {
402 *redacted.node_mut(id)? = Inode::Str(security::SECRET_MARKER.to_string());
403 }
404 }
405 let bytes = encode(&redacted).map_err(|error| Error::Payload(error.code().to_string()))?;
406 security::check_module_payload(&bytes)?;
407 Ok(bytes)
408}
409
410fn tainted_nodes(module: &InfinityModule) -> BTreeSet<NodeId> {
412 let mut tainted = BTreeSet::new();
413 let mut stack: Vec<NodeId> = Vec::new();
414 for (index, node) in module.nodes().iter().enumerate() {
415 if node.is_secret() {
416 let id = NodeId(index as u32);
417 tainted.insert(id);
418 stack.push(id);
419 }
420 }
421 while let Some(id) = stack.pop() {
422 if let Some(node) = module.get(id) {
423 for child in node.children() {
424 if tainted.insert(child) {
425 stack.push(child);
426 }
427 }
428 }
429 }
430 let mut changed = true;
432 while changed {
433 changed = false;
434 for (index, node) in module.nodes().iter().enumerate() {
435 let id = NodeId(index as u32);
436 if tainted.contains(&id) {
437 continue;
438 }
439 if node.children().iter().any(|child| tainted.contains(child)) {
440 tainted.insert(id);
441 changed = true;
442 }
443 }
444 }
445 tainted
446}
447
448fn restrict(module: &InfinityModule, node: NodeId) -> Result<InfinityModule> {
453 let cone = subgraph(module, &[node]);
454 let mut sub = InfinityModule::new();
455 let mut map = BTreeMap::new();
456 for id in &cone {
457 let cloned = module
458 .get(*id)
459 .cloned()
460 .ok_or(Error::InvalidArgument("subgraph node disappeared"))?;
461 map.insert(*id, sub.push(cloned));
462 }
463 for (old, new) in &map {
464 if let Some(node) = module.get(*old).cloned() {
465 let rewritten = remap_children(&node, &map)?;
466 *sub.node_mut(*new)? = rewritten;
467 }
468 }
469 sub.set_root(map.get(&node).copied());
470 Ok(sub)
471}
472
473fn remap_children(node: &Inode, map: &BTreeMap<NodeId, NodeId>) -> Result<Inode> {
475 let resolve = |id: NodeId| -> Result<NodeId> {
476 map.get(&id)
477 .copied()
478 .ok_or(Error::InvalidArgument("subgraph is not closed"))
479 };
480 let rewritten = match node {
481 Inode::Array { elements } => Inode::Array {
482 elements: elements
483 .iter()
484 .map(|id| resolve(*id))
485 .collect::<Result<_>>()?,
486 },
487 Inode::Table { fields } => Inode::Table {
488 fields: fields
489 .iter()
490 .map(|(name, id)| Ok((name.clone(), resolve(*id)?)))
491 .collect::<Result<_>>()?,
492 },
493 Inode::Quantity { value, unit } => Inode::Quantity {
494 value: resolve(*value)?,
495 unit: unit.clone(),
496 },
497 Inode::Ref(id) => Inode::Ref(resolve(*id)?),
498 Inode::SecretRef { locator } => Inode::SecretRef {
499 locator: resolve(*locator)?,
500 },
501 other => other.clone(),
502 };
503 Ok(rewritten)
504}
505
506fn literal_node(value: &Value) -> Option<Inode> {
507 match value {
508 Value::Null => Some(Inode::Null),
509 Value::Bool(flag) => Some(Inode::Bool(*flag)),
510 Value::Int(number) => Some(Inode::Int(*number)),
511 Value::Float(number) => Some(Inode::Float(*number)),
512 Value::Text(text) => Some(Inode::Str(text.clone())),
513 Value::Path(path) => Some(Inode::Str(path.clone())),
514 Value::List(_) | Value::Map(_) | Value::Secret | Value::Module(_) => None,
517 }
518}
519
520pub fn decode_module(bytes: &[u8]) -> Result<InfinityModule> {
522 decode(bytes).map_err(|error| Error::Payload(error.code().to_string()))
523}
524
525pub fn module_from_source(source: &str, name: &str) -> Result<InfinityModule> {
527 active_infinity::interop::from_active_source(source, name)
528 .map_err(|error| Error::Payload(error.code().to_string()))
529}
530
531pub fn resolve_path(module: &InfinityModule, path: &str) -> Option<NodeId> {
533 active_infinity::interop::resolve_path(module, path)
534}
535
536pub fn cone_ids(module: &InfinityModule, path: &str) -> Result<Vec<NodeId>> {
538 let node = active_infinity::interop::resolve_path(module, path)
539 .ok_or(Error::InvalidArgument("path does not resolve"))?;
540 Ok(subgraph(module, &[node]))
541}