Skip to main content

aranya_runtime/
vm_policy.rs

1//! VmPolicy implements a [Policy] that evaluates actions and commands via the [Policy
2//! VM](../../policy_vm/index.html).
3//!
4//! ## Creating a `VmPolicy` instance
5//!
6//! To use `VmPolicy` in your [`PolicyStore`](super::PolicyStore), you need to provide a Policy VM
7//! [`Machine`], a [`aranya_crypto::Engine`], and a Vec of Boxed FFI implementations. The Machine
8//! will be created by either compiling a policy document (see
9//! [`parse_policy_document()`](../../policy_lang/lang/fn.parse_policy_document.html) and
10//! [`Compiler`](../../policy_compiler/struct.Compiler.html)), or loading a compiled policy
11//! module (see [`Machine::from_module()`]). The crypto engine comes from your favorite
12//! implementation
13//! ([`DefaultEngine::from_entropy()`](aranya_crypto::default::DefaultEngine::from_entropy) is a
14//! good choice for testing). The list of FFIs is a list of things that implement
15//! [`FfiModule`](aranya_policy_vm::ffi::FfiModule), most likely via the [ffi attribute
16//! macro](../../policy_vm/ffi/attr.ffi.html). The list of FFI modules _must_ be in the same
17//! order as the FFI schemas given during VM construction.
18//!
19//! ```ignore
20//! // Create a `Machine` by compiling policy from source.
21//! let ast = parse_policy_document(policy_doc).unwrap();
22//! let machine = Compiler::new(&ast)
23//!     .ffi_modules(&[TestFfiEnvelope::SCHEMA])
24//!     .compile()
25//!     .unwrap();
26//! // Create a `aranya_crypto::Engine` implementation
27//! let (eng, _) = DefaultEngine::from_entropy(Rng);
28//! // Create a list of FFI module implementations
29//! let ffi_modules = vec![Box::from(TestFfiEnvelope {
30//!     device: DeviceId::random(Rng),
31//! })];
32//! // And finally, create the VmPolicy
33//! let policy = VmPolicy::new(machine, eng, ffi_modules).unwrap();
34//! ```
35//!
36//! ## Actions and Effects
37//!
38//! The VM represents actions as a kind of function, which has a name and a list of
39//! parameters. [`VmPolicy`] represents those actions as [`VmAction`]. Calling an action
40//! via [`call_action()`](VmPolicy::call_action) requires you to give it an action of
41//! that type. You can use the [`vm_action!()`](crate::vm_action) macro to create this
42//! more comfortably.
43//!
44//! The VM represents effects as a named struct containing a set of fields. `VmPolicy`
45//! represents this as [`VmEffect`]. Effects captured via [`Sink`]s will have this type.
46//! You can use the [`vm_effect!()`](crate::vm_effect) macro to create effects.
47//!
48//! ## The "init" command and action
49//!
50//! To create a graph, there must be a command that is the ancestor of all commands in that
51//! graph - the "init" command. In `VmPolicy`, that command is created via a special action
52//! given as the second argument to
53//! [`ClientState::new_graph()`](crate::ClientState::new_graph). The first command produced
54//! by that action becomes the "init" command. It has basically all the same properties as
55//! any other command, except it has no parent.
56//!
57//! So for this example policy:
58//!
59//! ```policy
60//! command Init {
61//!     fields {
62//!         nonce int,
63//!     }
64//!     seal { ... }
65//!     open { ... }
66//!     policy {
67//!         finish {}
68//!     }
69//! }
70//!
71//! action init(nonce int) {
72//!     publish Init {
73//!         nonce: nonce,
74//!     }
75//! }
76//! ```
77//!
78//! This is an example of initializing a graph with `new_graph()`:
79//!
80//! ```ignore
81//! let policy_store = MyPolicyStore::new();
82//! let provider = MyStorageProvider::new();
83//! let mut cs = ClientState::new(policy_store, provider);
84//! let mut sink = MySink::new();
85//!
86//! let graph_id = cs
87//!     .new_graph(&[0u8], vm_action!(init(0)), &mut sink)
88//!     .expect("could not create graph");
89//! ```
90//!
91//! Because the ID of this initial command is also the ID of the resulting graph,
92//! some data within the command must be present to ensure that multiple initial commands
93//! create distinct IDs for each graph. If no other suitable data exists, it is good
94//! practice to add a nonce field that is distinct for each graph.
95//!
96//! ## Priorities
97//!
98//! `VmPolicy` uses the policy language's attributes system to report command priorities to
99//! the runtime. You can specify the priority of a command by adding the `priority`
100//! attribute. It should be an `int` literal.
101//!
102//! ```policy
103//! command Foo {
104//!     attributes {
105//!         priority: 3
106//!     }
107//!     // ... fields, policy, etc.
108//! }
109//! ```
110//!
111//! ## Policy Interface Generator
112//!
113//! A more comfortable way to use `VmPolicy` is via the [Policy Interface
114//! Generator](../../policy_ifgen/index.html). It creates a Rust interface for actions and
115//! effects from a policy document.
116
117extern crate alloc;
118
119use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, string::String, vec::Vec};
120use core::fmt;
121
122use aranya_crypto::BaseId;
123use aranya_policy_vm::{
124    ActionContext, CommandContext, CommandDef, ExitReason, KVPair, Machine, MachineIO,
125    MachineStack, OpenContext, PolicyContext, RunState, Stack as _, Struct, Value,
126    ast::{Identifier, Persistence},
127};
128use buggy::{BugExt as _, bug};
129use tracing::{error, info, instrument};
130
131use crate::{
132    ActionPlacement, Address, CommandPlacement, FactPerspective, MergeIds, Perspective, Prior,
133    Priority,
134    command::{CmdId, Command},
135    policy::{NullSink, Policy, PolicyError, Sink},
136};
137
138mod error;
139mod io;
140mod protocol;
141pub mod testing;
142
143pub use error::*;
144pub use io::*;
145pub use protocol::*;
146
147/// Creates a [`VmAction`].
148///
149/// This must be used directly to avoid lifetime issues, not assigned to a variable.
150///
151/// # Example
152///
153/// ```ignore
154/// let x = 42;
155/// let y = text!("asdf");
156/// client.action(graph_id, sink, vm_action!(foobar(x, y)))
157/// ```
158#[macro_export]
159macro_rules! vm_action {
160    ($name:ident($($arg:expr),* $(,)?)) => {
161        $crate::VmAction {
162            name: ::aranya_policy_vm::ident!(stringify!($name)),
163            args: [$(::aranya_policy_vm::Value::from($arg)),*].as_slice().into(),
164        }
165    };
166}
167
168/// Creates a [`VmEffectData`].
169///
170/// This is mostly useful for testing expected effects, and is expected to be compared
171/// against a [`VmEffect`].
172///
173/// # Example
174///
175/// ```ignore
176/// let val = 3;
177/// sink.add_expectation(vm_effect!(StuffHappened { x: 1, y: val }));
178///
179/// client.action(graph_id, sink, vm_action!(create(val)))
180/// ```
181#[macro_export]
182macro_rules! vm_effect {
183    ($name:ident { $($field:ident : $val:expr),* $(,)? }) => {
184        $crate::VmEffectData {
185            name: ::aranya_policy_vm::ident!(stringify!($name)),
186            fields: vec![$(
187                ::aranya_policy_vm::KVPair::new(::aranya_policy_vm::ident!(stringify!($field)), $val.into())
188            ),*],
189        }
190    };
191}
192
193/// A [Policy] implementation that uses the Policy VM.
194pub struct VmPolicy<CE> {
195    machine: Machine,
196    engine: CE,
197    ffis: Vec<Box<dyn FfiCallable<CE> + Send + 'static>>,
198    priority_map: BTreeMap<Identifier, VmPriority>,
199}
200
201impl<CE> VmPolicy<CE> {
202    /// Create a new `VmPolicy` from a [Machine]
203    pub fn new(
204        machine: Machine,
205        engine: CE,
206        ffis: Vec<Box<dyn FfiCallable<CE> + Send + 'static>>,
207    ) -> Result<Self, VmPolicyError> {
208        let priority_map = get_command_priorities(&machine)?;
209        Ok(Self {
210            machine,
211            engine,
212            ffis,
213            priority_map,
214        })
215    }
216
217    fn source_location<M>(&self, rs: &RunState<'_, M>) -> String
218    where
219        M: MachineIO<MachineStack>,
220    {
221        rs.source_location()
222            .unwrap_or_else(|| String::from("(unknown location)"))
223    }
224}
225
226/// Scans command attributes for priorities and creates the priority map from them.
227fn get_command_priorities(
228    machine: &Machine,
229) -> Result<BTreeMap<Identifier, VmPriority>, AttributeError> {
230    let mut priority_map = BTreeMap::new();
231    for def in machine.command_defs.iter() {
232        let name = &def.name.name;
233        let attrs = PriorityAttrs::load(name.as_str(), def)?;
234        match def.persistence {
235            Persistence::Persistent => {
236                priority_map.insert(name.clone(), get_command_priority(name, &attrs)?);
237            }
238            Persistence::Ephemeral { .. } => {
239                if attrs != PriorityAttrs::default() {
240                    return Err(AttributeError(
241                        "ephemeral command must not have priority".into(),
242                    ));
243                }
244            }
245        }
246    }
247    Ok(priority_map)
248}
249
250#[derive(Default, PartialEq)]
251struct PriorityAttrs {
252    init: bool,
253    finalize: bool,
254    priority: Option<u32>,
255}
256
257impl PriorityAttrs {
258    fn load(name: &str, def: &CommandDef) -> Result<Self, AttributeError> {
259        let attrs = &def.attributes;
260        let init = attrs
261            .get("init")
262            .map(|attr| match attr.value {
263                Value::Bool(b) => Ok(b),
264                _ => Err(AttributeError::type_mismatch(
265                    name,
266                    "finalize",
267                    "Bool",
268                    &attr.value.type_name(),
269                )),
270            })
271            .transpose()?
272            == Some(true);
273        let finalize = attrs
274            .get("finalize")
275            .map(|attr| match attr.value {
276                Value::Bool(b) => Ok(b),
277                _ => Err(AttributeError::type_mismatch(
278                    name,
279                    "finalize",
280                    "Bool",
281                    &attr.value.type_name(),
282                )),
283            })
284            .transpose()?
285            == Some(true);
286        let priority: Option<u32> = attrs
287            .get("priority")
288            .map(|attr| match attr.value {
289                Value::Int(b) => b.try_into().map_err(|_| {
290                    AttributeError::int_range(name, "priority", u32::MIN.into(), u32::MAX.into())
291                }),
292                _ => Err(AttributeError::type_mismatch(
293                    name,
294                    "priority",
295                    "Int",
296                    &attr.value.type_name(),
297                )),
298            })
299            .transpose()?;
300        Ok(Self {
301            init,
302            finalize,
303            priority,
304        })
305    }
306}
307
308fn get_command_priority(
309    name: &Identifier,
310    attrs: &PriorityAttrs,
311) -> Result<VmPriority, AttributeError> {
312    match (attrs.init, attrs.finalize, attrs.priority) {
313        (true, true, _) => Err(AttributeError::exclusive(name.as_str(), "init", "finalize")),
314        (true, false, Some(_)) => Err(AttributeError::exclusive(name.as_str(), "init", "priority")),
315        (true, false, None) => Ok(VmPriority::Init),
316
317        (false, true, Some(_)) => Err(AttributeError::exclusive(
318            name.as_str(),
319            "finalize",
320            "priority",
321        )),
322        (false, true, None) => Ok(VmPriority::Finalize),
323
324        (false, false, Some(n)) => Ok(VmPriority::Basic(n)),
325
326        (false, false, None) => Err(AttributeError::missing(
327            name.as_str(),
328            "init | finalize | priority",
329        )),
330    }
331}
332
333impl<CE: aranya_crypto::Engine> VmPolicy<CE> {
334    #[allow(clippy::too_many_arguments)]
335    #[instrument(skip_all, fields(name = name.as_str()))]
336    fn evaluate_rule<'a, P>(
337        &self,
338        name: Identifier,
339        fields: &[KVPair],
340        envelope: Envelope<'_>,
341        facts: &'a mut P,
342        sink: &'a mut impl Sink<VmEffect>,
343        ctx: CommandContext,
344        placement: CommandPlacement,
345    ) -> Result<(), PolicyError>
346    where
347        P: FactPerspective,
348    {
349        let mut io = VmPolicyIO::new(facts, sink, &self.engine, &self.ffis);
350        let mut rs = self.machine.create_run_state(&mut io, ctx);
351        let this_data = Struct::new(name, fields);
352        match rs.call_command_policy(this_data.clone(), envelope.clone().into()) {
353            Ok(reason) => match reason {
354                ExitReason::Normal => Ok(()),
355                ExitReason::Yield => bug!("unexpected yield"),
356                ExitReason::Check => {
357                    info!("Check {}", self.source_location(&rs));
358
359                    match placement {
360                        CommandPlacement::OnGraphAtOrigin | CommandPlacement::OffGraph => {
361                            // Immediate check failure.
362                            return Err(PolicyError::Check);
363                        }
364                        CommandPlacement::OnGraphInBraid => {
365                            // Perform recall.
366                        }
367                    }
368
369                    // Construct a new recall context from the policy context
370                    let CommandContext::Policy(policy_ctx) = rs.get_context() else {
371                        error!(
372                            "Non-policy context while evaluating rule: {:?}",
373                            rs.get_context()
374                        );
375                        return Err(PolicyError::InternalError);
376                    };
377                    let recall_ctx = CommandContext::Recall(policy_ctx.clone());
378                    rs.set_context(recall_ctx);
379                    self.recall_internal(&mut rs, this_data, envelope)
380                }
381                ExitReason::Panic => {
382                    info!("Panicked {}", self.source_location(&rs));
383                    Err(PolicyError::Panic)
384                }
385            },
386            Err(e) => {
387                error!("\n{e}");
388                Err(PolicyError::InternalError)
389            }
390        }
391    }
392
393    fn recall_internal<M>(
394        &self,
395        rs: &mut RunState<'_, M>,
396        this_data: Struct,
397        envelope: Envelope<'_>,
398    ) -> Result<(), PolicyError>
399    where
400        M: MachineIO<MachineStack>,
401    {
402        match rs.call_command_recall(this_data, envelope.into()) {
403            Ok(ExitReason::Normal) => Err(PolicyError::Check),
404            Ok(ExitReason::Yield) => bug!("unexpected yield"),
405            Ok(ExitReason::Check) => {
406                info!("Recall failed: {}", self.source_location(rs));
407                Err(PolicyError::Check)
408            }
409            Ok(ExitReason::Panic) | Err(_) => {
410                info!("Recall panicked: {}", self.source_location(rs));
411                Err(PolicyError::Panic)
412            }
413        }
414    }
415
416    #[instrument(skip_all, fields(name = name.as_str()))]
417    fn open_command<P>(
418        &self,
419        name: Identifier,
420        envelope: Envelope<'_>,
421        facts: &mut P,
422    ) -> Result<Struct, PolicyError>
423    where
424        P: FactPerspective,
425    {
426        let mut sink = NullSink;
427        let mut io = VmPolicyIO::new(facts, &mut sink, &self.engine, &self.ffis);
428        let ctx = CommandContext::Open(OpenContext { name: name.clone() });
429        let mut rs = self.machine.create_run_state(&mut io, ctx);
430        let status = rs.call_open(name, envelope.into());
431        match status {
432            Ok(reason) => match reason {
433                ExitReason::Normal => {
434                    let v = rs.consume_return().map_err(|e| {
435                        error!("Could not pull envelope from stack: {e}");
436                        PolicyError::InternalError
437                    })?;
438                    Ok(v.try_into().map_err(|e| {
439                        error!("Envelope is not a struct: {e}");
440                        PolicyError::InternalError
441                    })?)
442                }
443                ExitReason::Yield => bug!("unexpected yield"),
444                ExitReason::Check => {
445                    info!("Check {}", self.source_location(&rs));
446                    Err(PolicyError::Check)
447                }
448                ExitReason::Panic => {
449                    info!("Panicked {}", self.source_location(&rs));
450                    Err(PolicyError::Check)
451                }
452            },
453            Err(e) => {
454                error!("\n{e}");
455                Err(PolicyError::InternalError)
456            }
457        }
458    }
459}
460
461/// [`VmPolicy`]'s actions.
462#[derive(Clone, Debug, PartialEq, Eq)]
463pub struct VmAction<'a> {
464    /// The name of the action.
465    pub name: Identifier,
466    /// The arguments of the action.
467    pub args: Cow<'a, [Value]>,
468}
469
470/// A partial version of [`VmEffect`] containing only the data. Created by
471/// [`vm_effect!`] and used to compare only the name and fields against the full
472/// `VmEffect`.
473#[derive(Debug)]
474pub struct VmEffectData {
475    /// The name of the effect.
476    pub name: Identifier,
477    /// The fields of the effect.
478    pub fields: Vec<KVPair>,
479}
480
481impl PartialEq<VmEffect> for VmEffectData {
482    fn eq(&self, other: &VmEffect) -> bool {
483        self.name == other.name && self.fields == other.fields
484    }
485}
486
487impl PartialEq<VmEffectData> for VmEffect {
488    fn eq(&self, other: &VmEffectData) -> bool {
489        self.name == other.name && self.fields == other.fields
490    }
491}
492
493/// [`VmPolicy`]'s effects.
494#[derive(Clone, Debug, PartialEq, Eq)]
495pub struct VmEffect {
496    /// The name of the effect.
497    pub name: Identifier,
498    /// The fields of the effect.
499    pub fields: Vec<KVPair>,
500    /// The command ID that produced this effect
501    pub command: CmdId,
502    /// Was this produced from a recall block?
503    pub recalled: bool,
504}
505
506#[derive(Copy, Clone, Debug, PartialEq)]
507enum VmPriority {
508    Init,
509    Basic(u32),
510    Finalize,
511}
512
513impl Default for VmPriority {
514    fn default() -> Self {
515        Self::Basic(0)
516    }
517}
518
519impl From<VmPriority> for Priority {
520    fn from(value: VmPriority) -> Self {
521        match value {
522            VmPriority::Init => Self::Init,
523            VmPriority::Basic(p) => Self::Basic(p),
524            VmPriority::Finalize => Self::Finalize,
525        }
526    }
527}
528
529impl<CE> VmPolicy<CE> {
530    fn get_command_priority(&self, name: &Identifier) -> VmPriority {
531        debug_assert!(self.machine.command_defs.contains(name));
532        self.priority_map.get(name).copied().unwrap_or_default()
533    }
534}
535
536impl<CE: aranya_crypto::Engine> Policy for VmPolicy<CE> {
537    type Action<'a> = VmAction<'a>;
538    type Effect = VmEffect;
539    type Command<'a> = VmProtocol<'a>;
540
541    fn serial(&self) -> u32 {
542        // TODO(chip): Implement an actual serial number
543        0u32
544    }
545
546    #[instrument(skip_all)]
547    fn call_rule(
548        &self,
549        command: &impl Command,
550        facts: &mut impl FactPerspective,
551        sink: &mut impl Sink<Self::Effect>,
552        placement: CommandPlacement,
553    ) -> Result<(), PolicyError> {
554        let parent_id = match command.parent() {
555            Prior::None => CmdId::default(),
556            Prior::Single(parent) => parent.id,
557            Prior::Merge(_, _) => bug!("merge commands are not evaluated"),
558        };
559
560        let VmProtocolData {
561            author_id,
562            kind,
563            serialized_fields,
564            signature,
565        } = postcard::from_bytes(command.bytes()).map_err(|e| {
566            error!("Could not deserialize: {e:?}");
567            PolicyError::Read
568        })?;
569
570        let expected_priority = self.get_command_priority(&kind).into();
571        if command.priority() != expected_priority {
572            error!(
573                "Expected priority {:?}, got {:?}",
574                expected_priority,
575                command.priority()
576            );
577            bug!("Command has invalid priority");
578        }
579
580        let def = self.machine.command_defs.get(&kind).ok_or_else(|| {
581            error!("unknown command {kind}");
582            PolicyError::InternalError
583        })?;
584
585        let envelope = Envelope {
586            parent_id,
587            author_id,
588            command_id: command.id(),
589            payload: Cow::Borrowed(serialized_fields),
590            signature: Cow::Borrowed(signature),
591        };
592
593        match (placement, &def.persistence) {
594            (CommandPlacement::OnGraphAtOrigin, Persistence::Persistent) => {}
595            (CommandPlacement::OnGraphInBraid, Persistence::Persistent) => {}
596            (CommandPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
597            (CommandPlacement::OnGraphAtOrigin, Persistence::Ephemeral(_)) => {
598                error!("cannot evaluate ephemeral command on-graph");
599                return Err(PolicyError::InternalError);
600            }
601            (CommandPlacement::OnGraphInBraid, Persistence::Ephemeral(_)) => {
602                error!("cannot evaluate ephemeral command in braid");
603                return Err(PolicyError::InternalError);
604            }
605            (CommandPlacement::OffGraph, Persistence::Persistent) => {
606                error!("cannot evaluate persistent command off-graph");
607                return Err(PolicyError::InternalError);
608            }
609        }
610
611        let command_struct = self.open_command(kind.clone(), envelope.clone(), facts)?;
612        let fields: Vec<KVPair> = command_struct
613            .fields
614            .into_iter()
615            .map(|(k, v)| KVPair::new(k, v))
616            .collect();
617        let ctx = CommandContext::Policy(PolicyContext {
618            name: kind.clone(),
619            id: command.id(),
620            author: author_id,
621            version: BaseId::default(),
622        });
623        self.evaluate_rule(
624            kind,
625            fields.as_slice(),
626            envelope,
627            facts,
628            sink,
629            ctx,
630            placement,
631        )?;
632
633        Ok(())
634    }
635
636    #[instrument(skip_all, fields(name = action.name.as_str()))]
637    fn call_action(
638        &self,
639        action: Self::Action<'_>,
640        facts: &mut impl Perspective,
641        sink: &mut impl Sink<Self::Effect>,
642        action_placement: ActionPlacement,
643    ) -> Result<(), PolicyError> {
644        let VmAction { name, args } = action;
645
646        let def = self.machine.action_defs.get(&name).ok_or_else(|| {
647            error!("action not found");
648            PolicyError::InternalError
649        })?;
650
651        match (action_placement, &def.persistence) {
652            (ActionPlacement::OnGraph, Persistence::Persistent) => {}
653            (ActionPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
654            (ActionPlacement::OnGraph, Persistence::Ephemeral(_)) => {
655                error!("cannot call ephemeral action on-graph");
656                return Err(PolicyError::InternalError);
657            }
658            (ActionPlacement::OffGraph, Persistence::Persistent) => {
659                error!("cannot call persistent action off-graph");
660                return Err(PolicyError::InternalError);
661            }
662        }
663
664        let parent = match facts.head_address()? {
665            Prior::None => None,
666            Prior::Single(id) => Some(id),
667            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
668        };
669        // FIXME(chip): This is kind of wrong, but it avoids having to
670        // plumb `Option<CmdId>` into the VM and FFI
671        let ctx_parent = parent.map(|a| a.id).unwrap_or_default();
672        let mut io = VmPolicyIO::new(facts, sink, &self.engine, &self.ffis);
673        let ctx = CommandContext::Action(ActionContext {
674            name: name.clone(),
675            head_id: ctx_parent,
676        });
677        let command_placement = match action_placement {
678            ActionPlacement::OnGraph => CommandPlacement::OnGraphAtOrigin,
679            ActionPlacement::OffGraph => CommandPlacement::OffGraph,
680        };
681        {
682            let mut rs = self.machine.create_run_state(&mut io, ctx);
683            let mut exit_reason = match args {
684                Cow::Borrowed(args) => rs.call_action(name, args.iter().cloned()),
685                Cow::Owned(args) => rs.call_action(name, args),
686            }
687            .map_err(|e| {
688                error!("\n{e}");
689                PolicyError::InternalError
690            })?;
691            loop {
692                match exit_reason {
693                    ExitReason::Normal => {
694                        // Action completed
695                        break;
696                    }
697                    ExitReason::Yield => {
698                        // Command was published.
699                        let command_struct: Struct = rs.stack.pop().map_err(|e| {
700                            error!("should have command struct: {e}");
701                            PolicyError::InternalError
702                        })?;
703                        let command_name = command_struct.name.clone();
704
705                        let seal_ctx = rs.get_context().seal_from_action(command_name.clone())?;
706                        let mut rs_seal = self.machine.create_run_state(rs.io, seal_ctx);
707                        match rs_seal.call_seal(command_struct).map_err(|e| {
708                            error!("Cannot seal command: {}", e);
709                            PolicyError::Panic
710                        })? {
711                            ExitReason::Normal => (),
712                            r @ (ExitReason::Yield | ExitReason::Check | ExitReason::Panic) => {
713                                error!("Could not seal command: {}", r);
714                                return Err(PolicyError::Panic);
715                            }
716                        }
717
718                        // Grab sealed envelope from stack
719                        let envelope_struct: Struct = rs_seal.stack.pop().map_err(|e| {
720                            error!("Expected a sealed envelope {e}");
721                            PolicyError::InternalError
722                        })?;
723                        let envelope = Envelope::try_from(envelope_struct).map_err(|e| {
724                            error!("Malformed envelope: {e}");
725                            PolicyError::InternalError
726                        })?;
727
728                        // The parent of a basic command should be the command that was added to the perspective on the previous
729                        // iteration of the loop
730                        let parent = rs.io.facts.head_address()?;
731                        let priority = self.get_command_priority(&command_name).into();
732
733                        let policy;
734                        match parent {
735                            Prior::None => {
736                                // TODO(chip): where does the policy value come from?
737                                policy = Some(0u64.to_le_bytes());
738                                if !matches!(priority, Priority::Init) {
739                                    error!(
740                                        "Command {command_name} has invalid priority {priority:?}"
741                                    );
742                                    return Err(PolicyError::InternalError);
743                                }
744                            }
745                            Prior::Single(_) => {
746                                policy = None;
747                                if !matches!(priority, Priority::Basic(_) | Priority::Finalize) {
748                                    error!(
749                                        "Command {command_name} has invalid priority {priority:?}"
750                                    );
751                                    return Err(PolicyError::InternalError);
752                                }
753                            }
754                            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
755                        }
756
757                        let data = VmProtocolData {
758                            author_id: envelope.author_id,
759                            kind: command_name.clone(),
760                            serialized_fields: &envelope.payload,
761                            signature: &envelope.signature,
762                        };
763
764                        let wrapped = postcard::to_allocvec(&data)
765                            .assume("can serialize vm protocol data")?;
766
767                        let new_command = VmProtocol {
768                            id: envelope.command_id,
769                            priority,
770                            parent,
771                            policy,
772                            data: &wrapped,
773                        };
774
775                        self.call_rule(&new_command, rs.io.facts, rs.io.sink, command_placement)?;
776                        rs.io.facts.add_command(&new_command).map_err(|e| {
777                            error!("{e}");
778                            PolicyError::Write
779                        })?;
780
781                        // After publishing a new command, the RunState's context must be updated to reflect the new head
782                        rs.update_context_with_new_head(new_command.id())?;
783
784                        // Resume action after last Publish
785                        exit_reason = rs.run().map_err(|e| {
786                            error!("{e}");
787                            PolicyError::InternalError
788                        })?;
789                    }
790                    ExitReason::Check => {
791                        info!("Check {}", self.source_location(&rs));
792                        return Err(PolicyError::Check);
793                    }
794                    ExitReason::Panic => {
795                        info!("Panicked {}", self.source_location(&rs));
796                        return Err(PolicyError::Panic);
797                    }
798                }
799            }
800        }
801
802        Ok(())
803    }
804
805    fn merge<'a>(
806        &self,
807        _target: &'a mut [u8],
808        ids: MergeIds,
809    ) -> Result<Self::Command<'a>, PolicyError> {
810        let (left, right): (Address, Address) = ids.into();
811        let id = aranya_crypto::merge_cmd_id::<CE::CS>(left.id, right.id);
812        Ok(VmProtocol {
813            id,
814            priority: Priority::Merge,
815            parent: Prior::Merge(left, right),
816            policy: None,
817            data: &[],
818        })
819    }
820}
821
822impl fmt::Display for VmAction<'_> {
823    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
824        let mut d = f.debug_tuple(self.name.as_str());
825        for arg in self.args.as_ref() {
826            d.field(&DebugViaDisplay(arg));
827        }
828        d.finish()
829    }
830}
831
832impl fmt::Display for VmEffect {
833    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
834        let mut d = f.debug_struct(self.name.as_str());
835        for field in &self.fields {
836            d.field(field.key().as_str(), &DebugViaDisplay(field.value()));
837        }
838        d.finish()
839    }
840}
841
842/// Implements `Debug` via `T`'s `Display` impl.
843struct DebugViaDisplay<T>(T);
844
845impl<T: fmt::Display> fmt::Debug for DebugViaDisplay<T> {
846    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
847        write!(f, "{}", self.0)
848    }
849}
850
851#[cfg(test)]
852mod test {
853    use alloc::format;
854
855    use aranya_policy_compiler::Compiler;
856    use aranya_policy_lang::lang::parse_policy_str;
857    use aranya_policy_vm::ast::Version;
858
859    use super::*;
860
861    #[test]
862    fn test_require_command_priority() {
863        let cases = [
864            r#"command Test {
865                fields {}
866                seal { return todo() }
867                open { return todo() }
868                policy {}
869            }"#,
870            r#"command Test {
871                attributes {}
872                fields {}
873                seal { return todo() }
874                open { return todo() }
875                policy {}
876            }"#,
877            r#"command Test {
878                attributes {
879                    init: false,
880                    finalize: false,
881                }
882                fields {}
883                seal { return todo() }
884                open { return todo() }
885                policy {}
886            }"#,
887        ];
888
889        for case in cases {
890            let ast = parse_policy_str(case, Version::V2).unwrap_or_else(|e| panic!("{e}"));
891            let module = Compiler::new(&ast)
892                .compile()
893                .unwrap_or_else(|e| panic!("{e}"));
894            let machine = Machine::from_module(module).expect("can create machine");
895            let err = get_command_priorities(&machine).expect_err("should fail");
896            assert_eq!(
897                err,
898                AttributeError::missing("Test", "init | finalize | priority")
899            );
900        }
901    }
902
903    #[test]
904    fn test_get_command_priorities() {
905        fn process(attrs: &str) -> Result<VmPriority, AttributeError> {
906            let policy = format!(
907                r#"
908                command Test {{
909                    attributes {{
910                        {attrs}
911                    }}
912                    fields {{ }}
913                    seal {{ return todo() }}
914                    open {{ return todo() }}
915                    policy {{ }}
916                }}
917                "#
918            );
919            let ast = parse_policy_str(&policy, Version::V2).unwrap_or_else(|e| panic!("{e}"));
920            let module = Compiler::new(&ast)
921                .compile()
922                .unwrap_or_else(|e| panic!("{e}"));
923            let machine = Machine::from_module(module).expect("can create machine");
924            let priorities = get_command_priorities(&machine)?;
925            Ok(*priorities.get("Test").expect("priorities are mandatory"))
926        }
927
928        assert_eq!(process("priority: 42"), Ok(VmPriority::Basic(42)));
929        assert_eq!(
930            process("finalize: false, priority: 42"),
931            Ok(VmPriority::Basic(42))
932        );
933        assert_eq!(
934            process("init: false, priority: 42, finalize: false"),
935            Ok(VmPriority::Basic(42))
936        );
937
938        assert_eq!(process("init: true"), Ok(VmPriority::Init));
939        assert_eq!(process("finalize: true"), Ok(VmPriority::Finalize));
940
941        assert_eq!(
942            process("finalize: 42"),
943            Err(AttributeError::type_mismatch(
944                "Test", "finalize", "Bool", "Int"
945            ))
946        );
947        assert_eq!(
948            process("priority: false"),
949            Err(AttributeError::type_mismatch(
950                "Test", "priority", "Int", "Bool"
951            ))
952        );
953        assert_eq!(
954            process("priority: -1"),
955            Err(AttributeError::int_range(
956                "Test",
957                "priority",
958                u32::MIN.into(),
959                u32::MAX.into(),
960            ))
961        );
962        assert_eq!(
963            process(&format!("priority: {}", i64::MAX)),
964            Err(AttributeError::int_range(
965                "Test",
966                "priority",
967                u32::MIN.into(),
968                u32::MAX.into(),
969            ))
970        );
971
972        assert_eq!(
973            process("finalize: true, priority: 42"),
974            Err(AttributeError::exclusive("Test", "finalize", "priority"))
975        );
976        assert_eq!(
977            process("init: true, priority: 42"),
978            Err(AttributeError::exclusive("Test", "init", "priority"))
979        );
980        assert_eq!(
981            process("init: true, finalize: true"),
982            Err(AttributeError::exclusive("Test", "init", "finalize"))
983        );
984    }
985}