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 Response::Module(redacted_module_bytes(&sub)?)
165 }
166 "patch" => {
167 let (_path, node) = self.resolve(&module, call)?;
168 if call.read_only {
169 return Err(Error::ReadOnly);
170 }
171 let replacement = call
172 .args
173 .iter()
174 .find_map(|arg| match arg {
175 Arg::Value(Value::Path(_)) => None,
178 Arg::Value(value) => literal_node(value),
179 _ => None,
180 })
181 .ok_or(Error::InvalidArgument("patch needs a literal value"))?;
182 let mut patched = module.clone();
183 *patched.node_mut(node)? = replacement;
184 patched.set_provenance(Some(Provenance {
185 producer: Some("active-rpc/patch".to_string()),
186 timestamp_ms: 0,
187 model: None,
188 tool: Some("patch".to_string()),
189 parent: None,
190 entries: Vec::new(),
191 }));
192 let bytes = redacted_module_bytes(&patched)?;
193 self.modules.insert(object, patched);
199 Response::Module(bytes)
200 }
201 "dependents" => {
202 let (_path, node) = self.resolve(&module, call)?;
203 let labels: Vec<String> = dependents(&module, node)
204 .into_iter()
205 .map(|id| active_infinity::interop::node_label(&module, id))
206 .collect();
207 Response::Labels(apply_redaction(&module, labels, call.redact))
208 }
209 "dependencies" => {
210 let (_path, node) = self.resolve(&module, call)?;
211 let labels: Vec<String> = dependencies(&module, node)
212 .into_iter()
213 .map(|id| active_infinity::interop::node_label(&module, id))
214 .collect();
215 Response::Labels(apply_redaction(&module, labels, call.redact))
216 }
217 "content_hash" => {
218 let bytes =
219 encode(&module).map_err(|error| Error::Payload(error.code().to_string()))?;
220 let hash = active_infinity::content_hash(&bytes)
221 .map_err(|error| Error::Payload(error.code().to_string()))?;
222 Response::Value(Value::Text(format!(
223 "blake3:{}",
224 active_infinity::ir::hash_hex(&hash)
225 )))
226 }
227 "stats" => {
228 let mut map = BTreeMap::new();
229 map.insert("nodes".to_string(), Value::Int(module.node_count() as i64));
230 map.insert("edges".to_string(), Value::Int(module.edges().len() as i64));
231 map.insert(
232 "secrets".to_string(),
233 Value::Int(
234 module
235 .nodes()
236 .iter()
237 .filter(|node| node.is_secret())
238 .count() as i64,
239 ),
240 );
241 map.insert(
242 "has_provenance".to_string(),
243 Value::Bool(module.provenance().is_some()),
244 );
245 Response::Value(Value::Map(map))
246 }
247 "annotations" => {
248 let tainted_ids = tainted_nodes(&module);
249 let mut out = Vec::new();
250 for annotation in module.annotations() {
251 let (kind, text) = match &annotation.kind {
252 AnnotationKind::Intent(text) => ("intent", text.clone()),
253 AnnotationKind::Constraint { source, .. } => ("constraint", source.clone()),
254 AnnotationKind::AiImmutable(flag) => ("ai_immutable", flag.to_string()),
255 };
256 let hidden = (call.redact && tainted_ids.contains(&annotation.node))
260 || text.contains(security::SECRET_MARKER);
261 let text = if hidden {
262 Value::Secret
263 } else {
264 Value::Text(text)
265 };
266 out.push(Value::Map(
267 [
268 ("node".to_string(), Value::Int(annotation.node.0 as i64)),
269 ("kind".to_string(), Value::Text(kind.to_string())),
270 ("text".to_string(), text),
271 ]
272 .into_iter()
273 .collect(),
274 ));
275 }
276 Response::Value(Value::List(out))
277 }
278 "provenance" => match module.provenance() {
279 None => Response::Value(Value::Null),
280 Some(prov) => {
281 let mut map = BTreeMap::new();
282 if let Some(producer) = &prov.producer {
283 map.insert("producer".to_string(), Value::Text(producer.clone()));
284 }
285 if let Some(model) = &prov.model {
286 map.insert("model".to_string(), Value::Text(model.clone()));
287 }
288 if let Some(tool) = &prov.tool {
289 map.insert("tool".to_string(), Value::Text(tool.clone()));
290 }
291 map.insert(
292 "timestamp_ms".to_string(),
293 Value::Int(prov.timestamp_ms as i64),
294 );
295 Response::Value(Value::Map(map))
296 }
297 },
298 other => {
299 return Err(Error::MethodNotGranted {
300 method: other.to_string(),
301 })
302 }
303 };
304 Ok(response)
305 }
306
307 fn on_drop(&mut self, object: u64, cap: CapId) {
308 self.drops.push((object, cap));
309 }
310
311 fn take_drop_notifications(&mut self) -> Vec<(u64, CapId)> {
312 std::mem::take(&mut self.drops)
313 }
314}
315
316fn apply_redaction(module: &InfinityModule, labels: Vec<String>, redact: bool) -> Vec<String> {
317 if !redact {
318 return labels;
319 }
320 let tainted = tainted_labels(module);
321 security::redact_cone_labels(labels, &tainted)
322}
323
324fn tainted_labels(module: &InfinityModule) -> BTreeSet<String> {
330 let count = module.node_count();
331 if count == 0 {
332 return BTreeSet::new();
333 }
334 let mut tainted = vec![false; count];
335 let mut stack: Vec<usize> = Vec::new();
338 for (index, node) in module.nodes().iter().enumerate() {
339 if node.is_secret() {
340 tainted[index] = true;
341 stack.push(index);
342 }
343 }
344 while let Some(index) = stack.pop() {
345 if let Some(node) = module.nodes().get(index) {
346 for child in node.children() {
347 if !tainted[child.index()] {
348 tainted[child.index()] = true;
349 stack.push(child.index());
350 }
351 }
352 }
353 }
354 let mut changed = true;
355 while changed {
356 changed = false;
357 for (index, node) in module.nodes().iter().enumerate() {
358 if tainted[index] {
359 continue;
360 }
361 if node
362 .children()
363 .iter()
364 .any(|child| tainted.get(child.index()).copied().unwrap_or(false))
365 {
366 tainted[index] = true;
367 changed = true;
368 }
369 }
370 }
371 let mut labels = BTreeSet::new();
372 for (index, is_tainted) in tainted.iter().enumerate() {
373 if *is_tainted {
374 labels.insert(active_infinity::interop::node_label(
375 module,
376 NodeId(index as u32),
377 ));
378 }
379 }
380 labels.extend(secret_tainted_paths(module));
383 labels
384}
385
386fn redacted_module_bytes(module: &InfinityModule) -> Result<Vec<u8>> {
393 let mut redacted = module.clone();
394 for index in 0..redacted.node_count() {
395 let id = NodeId(index as u32);
396 let is_secret = redacted
397 .get(id)
398 .map(|node| node.is_secret())
399 .unwrap_or(false);
400 if is_secret {
401 *redacted.node_mut(id)? = Inode::Str(security::SECRET_MARKER.to_string());
402 }
403 }
404 let bytes = encode(&redacted).map_err(|error| Error::Payload(error.code().to_string()))?;
405 security::check_module_payload(&bytes)?;
406 Ok(bytes)
407}
408
409fn tainted_nodes(module: &InfinityModule) -> BTreeSet<NodeId> {
411 let mut tainted = BTreeSet::new();
412 let mut stack: Vec<NodeId> = Vec::new();
413 for (index, node) in module.nodes().iter().enumerate() {
414 if node.is_secret() {
415 let id = NodeId(index as u32);
416 tainted.insert(id);
417 stack.push(id);
418 }
419 }
420 while let Some(id) = stack.pop() {
421 if let Some(node) = module.get(id) {
422 for child in node.children() {
423 if tainted.insert(child) {
424 stack.push(child);
425 }
426 }
427 }
428 }
429 let mut changed = true;
431 while changed {
432 changed = false;
433 for (index, node) in module.nodes().iter().enumerate() {
434 let id = NodeId(index as u32);
435 if tainted.contains(&id) {
436 continue;
437 }
438 if node.children().iter().any(|child| tainted.contains(child)) {
439 tainted.insert(id);
440 changed = true;
441 }
442 }
443 }
444 tainted
445}
446
447fn restrict(module: &InfinityModule, node: NodeId) -> Result<InfinityModule> {
452 let cone = subgraph(module, &[node]);
453 let mut sub = InfinityModule::new();
454 let mut map = BTreeMap::new();
455 for id in &cone {
456 let cloned = module
457 .get(*id)
458 .cloned()
459 .ok_or(Error::InvalidArgument("subgraph node disappeared"))?;
460 map.insert(*id, sub.push(cloned));
461 }
462 for (old, new) in &map {
463 if let Some(node) = module.get(*old).cloned() {
464 let rewritten = remap_children(&node, &map)?;
465 *sub.node_mut(*new)? = rewritten;
466 }
467 }
468 sub.set_root(map.get(&node).copied());
469 Ok(sub)
470}
471
472fn remap_children(node: &Inode, map: &BTreeMap<NodeId, NodeId>) -> Result<Inode> {
474 let resolve = |id: NodeId| -> Result<NodeId> {
475 map.get(&id)
476 .copied()
477 .ok_or(Error::InvalidArgument("subgraph is not closed"))
478 };
479 let rewritten = match node {
480 Inode::Array { elements } => Inode::Array {
481 elements: elements
482 .iter()
483 .map(|id| resolve(*id))
484 .collect::<Result<_>>()?,
485 },
486 Inode::Table { fields } => Inode::Table {
487 fields: fields
488 .iter()
489 .map(|(name, id)| Ok((name.clone(), resolve(*id)?)))
490 .collect::<Result<_>>()?,
491 },
492 Inode::Quantity { value, unit } => Inode::Quantity {
493 value: resolve(*value)?,
494 unit: unit.clone(),
495 },
496 Inode::Ref(id) => Inode::Ref(resolve(*id)?),
497 Inode::SecretRef { locator } => Inode::SecretRef {
498 locator: resolve(*locator)?,
499 },
500 other => other.clone(),
501 };
502 Ok(rewritten)
503}
504
505fn literal_node(value: &Value) -> Option<Inode> {
506 match value {
507 Value::Null => Some(Inode::Null),
508 Value::Bool(flag) => Some(Inode::Bool(*flag)),
509 Value::Int(number) => Some(Inode::Int(*number)),
510 Value::Float(number) => Some(Inode::Float(*number)),
511 Value::Text(text) => Some(Inode::Str(text.clone())),
512 Value::Path(path) => Some(Inode::Str(path.clone())),
513 Value::List(_) | Value::Map(_) | Value::Secret | Value::Module(_) => None,
516 }
517}
518
519pub fn decode_module(bytes: &[u8]) -> Result<InfinityModule> {
521 decode(bytes).map_err(|error| Error::Payload(error.code().to_string()))
522}
523
524pub fn module_from_source(source: &str, name: &str) -> Result<InfinityModule> {
526 active_infinity::interop::from_active_source(source, name)
527 .map_err(|error| Error::Payload(error.code().to_string()))
528}
529
530pub fn resolve_path(module: &InfinityModule, path: &str) -> Option<NodeId> {
532 active_infinity::interop::resolve_path(module, path)
533}
534
535pub fn cone_ids(module: &InfinityModule, path: &str) -> Result<Vec<NodeId>> {
537 let node = active_infinity::interop::resolve_path(module, path)
538 .ok_or(Error::InvalidArgument("path does not resolve"))?;
539 Ok(subgraph(module, &[node]))
540}