1extern crate alloc;
118
119use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, rc::Rc, string::String, vec::Vec};
120use core::{borrow::Borrow, cell::RefCell, fmt};
121
122use aranya_policy_vm::{
123 ActionContext, CommandContext, ExitReason, KVPair, Machine, MachineIO, MachineStack,
124 OpenContext, PolicyContext, RunState, Stack, Struct, Value,
125 ast::{Identifier, Persistence},
126};
127use buggy::{BugExt, bug};
128use spin::Mutex;
129use tracing::{error, info, instrument};
130
131use crate::{
132 ActionPlacement, CommandPlacement, FactPerspective, MergeIds, Perspective, Prior, Priority,
133 command::{CmdId, Command},
134 engine::{EngineError, NullSink, Policy, Sink},
135};
136
137mod error;
138mod io;
139mod protocol;
140pub mod testing;
141
142pub use error::*;
143pub use io::*;
144pub use protocol::*;
145
146#[macro_export]
158macro_rules! vm_action {
159 ($name:ident($($arg:expr),* $(,)?)) => {
160 $crate::VmAction {
161 name: ::aranya_policy_vm::ident!(stringify!($name)),
162 args: [$(::aranya_policy_vm::Value::from($arg)),*].as_slice().into(),
163 }
164 };
165}
166
167#[macro_export]
181macro_rules! vm_effect {
182 ($name:ident { $($field:ident : $val:expr),* $(,)? }) => {
183 $crate::VmEffectData {
184 name: ::aranya_policy_vm::ident!(stringify!($name)),
185 fields: vec![$(
186 ::aranya_policy_vm::KVPair::new(::aranya_policy_vm::ident!(stringify!($field)), $val.into())
187 ),*],
188 }
189 };
190}
191
192pub struct VmPolicy<E> {
194 machine: Machine,
195 engine: Mutex<E>,
196 ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
197 priority_map: BTreeMap<Identifier, VmPriority>,
198}
199
200impl<E> VmPolicy<E> {
201 pub fn new(
203 machine: Machine,
204 engine: E,
205 ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
206 ) -> Result<Self, VmPolicyError> {
207 let priority_map = get_command_priorities(&machine)?;
208 Ok(Self {
209 machine,
210 engine: Mutex::new(engine),
211 ffis,
212 priority_map,
213 })
214 }
215
216 fn source_location<M>(&self, rs: &RunState<'_, M>) -> String
217 where
218 M: MachineIO<MachineStack>,
219 {
220 rs.source_location()
221 .unwrap_or_else(|| String::from("(unknown location)"))
222 }
223}
224
225fn get_command_priorities(
227 machine: &Machine,
228) -> Result<BTreeMap<Identifier, VmPriority>, AttributeError> {
229 let mut priority_map = BTreeMap::new();
230 for def in machine.command_defs.iter() {
231 let name = &def.name;
232 let attrs = &def.attributes;
233 let finalize = attrs
234 .get("finalize")
235 .map(|attr| match attr.value {
236 Value::Bool(b) => Ok(b),
237 _ => Err(AttributeError::type_mismatch(
238 name.as_str(),
239 "finalize",
240 "Bool",
241 &attr.value.type_name(),
242 )),
243 })
244 .transpose()?
245 == Some(true);
246 let priority: Option<u32> = attrs
247 .get("priority")
248 .map(|attr| match attr.value {
249 Value::Int(b) => b.try_into().map_err(|_| {
250 AttributeError::int_range(
251 name.as_str(),
252 "priority",
253 u32::MIN.into(),
254 u32::MAX.into(),
255 )
256 }),
257 _ => Err(AttributeError::type_mismatch(
258 name.as_str(),
259 "priority",
260 "Int",
261 &attr.value.type_name(),
262 )),
263 })
264 .transpose()?;
265 match (finalize, priority) {
266 (false, None) => {}
267 (false, Some(p)) => {
268 priority_map.insert(name.name.clone(), VmPriority::Basic(p));
269 }
270 (true, None) => {
271 priority_map.insert(name.name.clone(), VmPriority::Finalize);
272 }
273 (true, Some(_)) => {
274 return Err(AttributeError::exclusive(
275 name.as_str(),
276 "finalize",
277 "priority",
278 ));
279 }
280 }
281 }
282 Ok(priority_map)
283}
284
285impl<E: aranya_crypto::Engine> VmPolicy<E> {
286 #[allow(clippy::too_many_arguments)]
287 #[instrument(skip_all, fields(name = name.as_str()))]
288 fn evaluate_rule<'a, P>(
289 &self,
290 name: Identifier,
291 fields: &[KVPair],
292 envelope: Envelope<'_>,
293 facts: &'a mut P,
294 sink: &'a mut impl Sink<VmEffect>,
295 ctx: CommandContext,
296 placement: CommandPlacement,
297 ) -> Result<(), EngineError>
298 where
299 P: FactPerspective,
300 {
301 let facts = RefCell::new(facts);
302 let sink = RefCell::new(sink);
303 let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
304 let mut rs = self.machine.create_run_state(&io, ctx);
305 let this_data = Struct::new(name, fields);
306 match rs.call_command_policy(this_data.clone(), envelope.clone().into()) {
307 Ok(reason) => match reason {
308 ExitReason::Normal => Ok(()),
309 ExitReason::Yield => bug!("unexpected yield"),
310 ExitReason::Check => {
311 info!("Check {}", self.source_location(&rs));
312
313 match placement {
314 CommandPlacement::OnGraphAtOrigin | CommandPlacement::OffGraph => {
315 return Err(EngineError::Check);
317 }
318 CommandPlacement::OnGraphInBraid => {
319 }
321 }
322
323 let CommandContext::Policy(policy_ctx) = rs.get_context() else {
325 error!(
326 "Non-policy context while evaluating rule: {:?}",
327 rs.get_context()
328 );
329 return Err(EngineError::InternalError);
330 };
331 let recall_ctx = CommandContext::Recall(policy_ctx.clone());
332 rs.set_context(recall_ctx);
333 self.recall_internal(&mut rs, this_data, envelope)
334 }
335 ExitReason::Panic => {
336 info!("Panicked {}", self.source_location(&rs));
337 Err(EngineError::Panic)
338 }
339 },
340 Err(e) => {
341 error!("\n{e}");
342 Err(EngineError::InternalError)
343 }
344 }
345 }
346
347 fn recall_internal<M>(
348 &self,
349 rs: &mut RunState<'_, M>,
350 this_data: Struct,
351 envelope: Envelope<'_>,
352 ) -> Result<(), EngineError>
353 where
354 M: MachineIO<MachineStack>,
355 {
356 match rs.call_command_recall(this_data, envelope.into()) {
357 Ok(ExitReason::Normal) => Err(EngineError::Check),
358 Ok(ExitReason::Yield) => bug!("unexpected yield"),
359 Ok(ExitReason::Check) => {
360 info!("Recall failed: {}", self.source_location(rs));
361 Err(EngineError::Check)
362 }
363 Ok(ExitReason::Panic) | Err(_) => {
364 info!("Recall panicked: {}", self.source_location(rs));
365 Err(EngineError::Panic)
366 }
367 }
368 }
369
370 #[instrument(skip_all, fields(name = name.as_str()))]
371 fn open_command<P>(
372 &self,
373 name: Identifier,
374 envelope: Envelope<'_>,
375 facts: &mut P,
376 ) -> Result<Struct, EngineError>
377 where
378 P: FactPerspective,
379 {
380 let facts = RefCell::new(facts);
381 let mut sink = NullSink;
382 let sink2 = RefCell::new(&mut sink);
383 let io = RefCell::new(VmPolicyIO::new(&facts, &sink2, &self.engine, &self.ffis));
384 let ctx = CommandContext::Open(OpenContext { name: name.clone() });
385 let mut rs = self.machine.create_run_state(&io, ctx);
386 let status = rs.call_open(name, envelope.into());
387 match status {
388 Ok(reason) => match reason {
389 ExitReason::Normal => {
390 let v = rs.consume_return().map_err(|e| {
391 error!("Could not pull envelope from stack: {e}");
392 EngineError::InternalError
393 })?;
394 Ok(v.try_into().map_err(|e| {
395 error!("Envelope is not a struct: {e}");
396 EngineError::InternalError
397 })?)
398 }
399 ExitReason::Yield => bug!("unexpected yield"),
400 ExitReason::Check => {
401 info!("Check {}", self.source_location(&rs));
402 Err(EngineError::Check)
403 }
404 ExitReason::Panic => {
405 info!("Panicked {}", self.source_location(&rs));
406 Err(EngineError::Check)
407 }
408 },
409 Err(e) => {
410 error!("\n{e}");
411 Err(EngineError::InternalError)
412 }
413 }
414 }
415}
416
417#[derive(Clone, Debug, PartialEq, Eq)]
419pub struct VmAction<'a> {
420 pub name: Identifier,
422 pub args: Cow<'a, [Value]>,
424}
425
426#[derive(Debug)]
430pub struct VmEffectData {
431 pub name: Identifier,
433 pub fields: Vec<KVPair>,
435}
436
437impl PartialEq<VmEffect> for VmEffectData {
438 fn eq(&self, other: &VmEffect) -> bool {
439 self.name == other.name && self.fields == other.fields
440 }
441}
442
443impl PartialEq<VmEffectData> for VmEffect {
444 fn eq(&self, other: &VmEffectData) -> bool {
445 self.name == other.name && self.fields == other.fields
446 }
447}
448
449#[derive(Clone, Debug, PartialEq, Eq)]
451pub struct VmEffect {
452 pub name: Identifier,
454 pub fields: Vec<KVPair>,
456 pub command: CmdId,
458 pub recalled: bool,
460}
461
462#[derive(Copy, Clone, Debug, PartialEq)]
463enum VmPriority {
464 Basic(u32),
465 Finalize,
466}
467
468impl Default for VmPriority {
469 fn default() -> Self {
470 Self::Basic(0)
471 }
472}
473
474impl From<VmPriority> for Priority {
475 fn from(value: VmPriority) -> Self {
476 match value {
477 VmPriority::Basic(p) => Self::Basic(p),
478 VmPriority::Finalize => Self::Finalize,
479 }
480 }
481}
482
483impl<E> VmPolicy<E> {
484 fn get_command_priority(&self, name: &Identifier) -> VmPriority {
485 debug_assert!(self.machine.command_defs.contains(name));
486 self.priority_map.get(name).copied().unwrap_or_default()
487 }
488}
489
490impl<E: aranya_crypto::Engine> Policy for VmPolicy<E> {
491 type Action<'a> = VmAction<'a>;
492 type Effect = VmEffect;
493 type Command<'a> = VmProtocol<'a>;
494
495 fn serial(&self) -> u32 {
496 0u32
498 }
499
500 #[instrument(skip_all)]
501 fn call_rule(
502 &self,
503 command: &impl Command,
504 facts: &mut impl FactPerspective,
505 sink: &mut impl Sink<Self::Effect>,
506 placement: CommandPlacement,
507 ) -> Result<(), EngineError> {
508 let unpacked: VmProtocolData<'_> = postcard::from_bytes(command.bytes()).map_err(|e| {
509 error!("Could not deserialize: {e:?}");
510 EngineError::Read
511 })?;
512 let (command_info, expected_priority) = {
513 match unpacked {
514 VmProtocolData::Init {
515 author_id,
516 kind,
517 serialized_fields,
518 signature,
519 ..
520 } => (
521 Some((
522 Envelope {
523 parent_id: CmdId::default(),
524 author_id,
525 command_id: command.id(),
526 payload: Cow::Borrowed(serialized_fields),
527 signature: Cow::Borrowed(signature),
528 },
529 kind,
530 author_id,
531 )),
532 Priority::Init,
533 ),
534 VmProtocolData::Basic {
535 parent,
536 kind,
537 author_id,
538 serialized_fields,
539 signature,
540 } => {
541 let priority = self.get_command_priority(&kind).into();
542 (
543 Some((
544 Envelope {
545 parent_id: parent.id,
546 author_id,
547 command_id: command.id(),
548 payload: Cow::Borrowed(serialized_fields),
549 signature: Cow::Borrowed(signature),
550 },
551 kind,
552 author_id,
553 )),
554 priority,
555 )
556 }
557 _ => (None, Priority::Merge),
559 }
560 };
561
562 if command.priority() != expected_priority {
563 error!(
564 "Expected priority {:?}, got {:?}",
565 expected_priority,
566 command.priority()
567 );
568 bug!("Command has invalid priority");
569 }
570
571 if let Some((envelope, kind, author_id)) = command_info {
572 let def = self.machine.command_defs.get(&kind).ok_or_else(|| {
573 error!("unknown command {kind}");
574 EngineError::InternalError
575 })?;
576
577 match (placement, &def.persistence) {
578 (CommandPlacement::OnGraphAtOrigin, Persistence::Persistent) => {}
579 (CommandPlacement::OnGraphInBraid, Persistence::Persistent) => {}
580 (CommandPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
581 (CommandPlacement::OnGraphAtOrigin, Persistence::Ephemeral(_)) => {
582 error!("cannot evaluate ephemeral command on-graph");
583 return Err(EngineError::InternalError);
584 }
585 (CommandPlacement::OnGraphInBraid, Persistence::Ephemeral(_)) => {
586 error!("cannot evaluate ephemeral command in braid");
587 return Err(EngineError::InternalError);
588 }
589 (CommandPlacement::OffGraph, Persistence::Persistent) => {
590 error!("cannot evaluate persistent command off-graph");
591 return Err(EngineError::InternalError);
592 }
593 }
594
595 let command_struct = self.open_command(kind.clone(), envelope.clone(), facts)?;
596 let fields: Vec<KVPair> = command_struct
597 .fields
598 .into_iter()
599 .map(|(k, v)| KVPair::new(k, v))
600 .collect();
601 let ctx = CommandContext::Policy(PolicyContext {
602 name: kind.clone(),
603 id: command.id(),
604 author: author_id,
605 version: CmdId::default().into(),
606 });
607 self.evaluate_rule(
608 kind,
609 fields.as_slice(),
610 envelope,
611 facts,
612 sink,
613 ctx,
614 placement,
615 )?
616 }
617 Ok(())
618 }
619
620 #[instrument(skip_all, fields(name = action.name.as_str()))]
621 fn call_action(
622 &self,
623 action: Self::Action<'_>,
624 facts: &mut impl Perspective,
625 sink: &mut impl Sink<Self::Effect>,
626 action_placement: ActionPlacement,
627 ) -> Result<(), EngineError> {
628 let VmAction { name, args } = action;
629
630 let def = self
631 .machine
632 .action_defs
633 .get(&name)
634 .ok_or(EngineError::InternalError)?;
635
636 match (action_placement, &def.persistence) {
637 (ActionPlacement::OnGraph, Persistence::Persistent) => {}
638 (ActionPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
639 (ActionPlacement::OnGraph, Persistence::Ephemeral(_)) => {
640 error!("cannot call ephemeral action on-graph");
641 return Err(EngineError::InternalError);
642 }
643 (ActionPlacement::OffGraph, Persistence::Persistent) => {
644 error!("cannot call persistent action off-graph");
645 return Err(EngineError::InternalError);
646 }
647 }
648
649 let parent = match facts.head_address()? {
650 Prior::None => None,
651 Prior::Single(id) => Some(id),
652 Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
653 };
654 let ctx_parent = parent.unwrap_or_default();
657 let facts = Rc::new(RefCell::new(facts));
658 let sink = Rc::new(RefCell::new(sink));
659 let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
660 let ctx = CommandContext::Action(ActionContext {
661 name: name.clone(),
662 head_id: ctx_parent.id,
663 });
664 let command_placement = match action_placement {
665 ActionPlacement::OnGraph => CommandPlacement::OnGraphAtOrigin,
666 ActionPlacement::OffGraph => CommandPlacement::OffGraph,
667 };
668 {
669 let mut rs = self.machine.create_run_state(&io, ctx);
670 let mut exit_reason = match args {
671 Cow::Borrowed(args) => rs.call_action(name, args.iter().cloned()),
672 Cow::Owned(args) => rs.call_action(name, args),
673 }
674 .map_err(|e| {
675 error!("\n{e}");
676 EngineError::InternalError
677 })?;
678 loop {
679 match exit_reason {
680 ExitReason::Normal => {
681 break;
683 }
684 ExitReason::Yield => {
685 let command_struct: Struct = rs.stack.pop().map_err(|e| {
687 error!("should have command struct: {e}");
688 EngineError::InternalError
689 })?;
690 let command_name = command_struct.name.clone();
691
692 let seal_ctx = rs.get_context().seal_from_action(command_name.clone())?;
693 let mut rs_seal = self.machine.create_run_state(&io, seal_ctx);
694 match rs_seal.call_seal(command_struct).map_err(|e| {
695 error!("Cannot seal command: {}", e);
696 EngineError::Panic
697 })? {
698 ExitReason::Normal => (),
699 r @ ExitReason::Yield
700 | r @ ExitReason::Check
701 | r @ ExitReason::Panic => {
702 error!("Could not seal command: {}", r);
703 return Err(EngineError::Panic);
704 }
705 }
706
707 let envelope_struct: Struct = rs_seal.stack.pop().map_err(|e| {
709 error!("Expected a sealed envelope {e}");
710 EngineError::InternalError
711 })?;
712 let envelope = Envelope::try_from(envelope_struct).map_err(|e| {
713 error!("Malformed envelope: {e}");
714 EngineError::InternalError
715 })?;
716
717 let parent = match RefCell::borrow_mut(Rc::borrow(&facts)).head_address()? {
720 Prior::None => None,
721 Prior::Single(id) => Some(id),
722 Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
723 };
724
725 let priority = if parent.is_some() {
726 self.get_command_priority(&command_name).into()
727 } else {
728 Priority::Init
729 };
730
731 let data = match parent {
732 None => VmProtocolData::Init {
733 policy: 0u64.to_le_bytes(),
735 author_id: envelope.author_id,
736 kind: command_name.clone(),
737 serialized_fields: &envelope.payload,
738 signature: &envelope.signature,
739 },
740 Some(parent) => VmProtocolData::Basic {
741 author_id: envelope.author_id,
742 parent,
743 kind: command_name.clone(),
744 serialized_fields: &envelope.payload,
745 signature: &envelope.signature,
746 },
747 };
748 let wrapped = postcard::to_allocvec(&data)
749 .assume("can serialize vm protocol data")?;
750 let new_command =
751 VmProtocol::new(&wrapped, envelope.command_id, data, priority);
752
753 self.call_rule(
754 &new_command,
755 *RefCell::borrow_mut(Rc::borrow(&facts)),
756 *RefCell::borrow_mut(Rc::borrow(&sink)),
757 command_placement,
758 )?;
759 RefCell::borrow_mut(Rc::borrow(&facts))
760 .add_command(&new_command)
761 .map_err(|e| {
762 error!("{e}");
763 EngineError::Write
764 })?;
765
766 rs.update_context_with_new_head(new_command.id())?;
768
769 exit_reason = rs.run().map_err(|e| {
771 error!("{e}");
772 EngineError::InternalError
773 })?;
774 }
775 ExitReason::Check => {
776 info!("Check {}", self.source_location(&rs));
777 return Err(EngineError::Check);
778 }
779 ExitReason::Panic => {
780 info!("Panicked {}", self.source_location(&rs));
781 return Err(EngineError::Panic);
782 }
783 };
784 }
785 }
786
787 Ok(())
788 }
789
790 fn merge<'a>(
791 &self,
792 target: &'a mut [u8],
793 ids: MergeIds,
794 ) -> Result<Self::Command<'a>, EngineError> {
795 let (left, right) = ids.into();
796 let c = VmProtocolData::Merge { left, right };
797 let id = aranya_crypto::merge_cmd_id::<E::CS>(left.id, right.id);
798 let data = postcard::to_slice(&c, target).map_err(|e| {
799 error!("{e}");
800 EngineError::Write
801 })?;
802 Ok(VmProtocol::new(data, id, c, Priority::Merge))
803 }
804}
805
806impl fmt::Display for VmAction<'_> {
807 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
808 let mut d = f.debug_tuple(self.name.as_str());
809 for arg in self.args.as_ref() {
810 d.field(&DebugViaDisplay(arg));
811 }
812 d.finish()
813 }
814}
815
816impl fmt::Display for VmEffect {
817 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
818 let mut d = f.debug_struct(self.name.as_str());
819 for field in &self.fields {
820 d.field(field.key().as_str(), &DebugViaDisplay(field.value()));
821 }
822 d.finish()
823 }
824}
825
826struct DebugViaDisplay<T>(T);
828
829impl<T: fmt::Display> fmt::Debug for DebugViaDisplay<T> {
830 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
831 write!(f, "{}", self.0)
832 }
833}
834
835#[cfg(test)]
836mod test {
837 use alloc::format;
838
839 use aranya_policy_compiler::Compiler;
840 use aranya_policy_lang::lang::parse_policy_str;
841 use aranya_policy_vm::ast::Version;
842
843 use super::*;
844
845 #[test]
846 fn test_get_command_priorities() {
847 fn process(attrs: &str) -> Result<Option<VmPriority>, AttributeError> {
848 let policy = format!(
849 r#"
850 command Test {{
851 attributes {{
852 {attrs}
853 }}
854 fields {{ }}
855 seal {{ return todo() }}
856 open {{ return todo() }}
857 policy {{ }}
858 }}
859 "#
860 );
861 let ast = parse_policy_str(&policy, Version::V2).unwrap_or_else(|e| panic!("{e}"));
862 let module = Compiler::new(&ast)
863 .compile()
864 .unwrap_or_else(|e| panic!("{e}"));
865 let machine = Machine::from_module(module).expect("can create machine");
866 let priorities = get_command_priorities(&machine)?;
867 Ok(priorities.get("Test").copied())
868 }
869
870 assert_eq!(process(""), Ok(None));
871 assert_eq!(process("finalize: false"), Ok(None));
872
873 assert_eq!(process("priority: 42"), Ok(Some(VmPriority::Basic(42))));
874 assert_eq!(
875 process("finalize: false, priority: 42"),
876 Ok(Some(VmPriority::Basic(42)))
877 );
878 assert_eq!(
879 process("priority: 42, finalize: false"),
880 Ok(Some(VmPriority::Basic(42)))
881 );
882
883 assert_eq!(process("finalize: true"), Ok(Some(VmPriority::Finalize)));
884
885 assert_eq!(
886 process("finalize: 42"),
887 Err(AttributeError::type_mismatch(
888 "Test", "finalize", "Bool", "Int"
889 ))
890 );
891 assert_eq!(
892 process("priority: false"),
893 Err(AttributeError::type_mismatch(
894 "Test", "priority", "Int", "Bool"
895 ))
896 );
897 assert_eq!(
898 process("priority: -1"),
899 Err(AttributeError::int_range(
900 "Test",
901 "priority",
902 u32::MIN.into(),
903 u32::MAX.into(),
904 ))
905 );
906 assert_eq!(
907 process(&format!("priority: {}", i64::MAX)),
908 Err(AttributeError::int_range(
909 "Test",
910 "priority",
911 u32::MIN.into(),
912 u32::MAX.into(),
913 ))
914 );
915 assert_eq!(
916 process("finalize: true, priority: 42"),
917 Err(AttributeError::exclusive("Test", "finalize", "priority"))
918 )
919 }
920}