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