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 [`Engine`](super::Engine), 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//!     user: UserId::random(&mut 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 engine = MyEngine::new();
82//! let provider = MyStorageProvider::new();
83//! let mut cs = ClientState::new(engine, provider);
84//! let mut sink = MySink::new();
85//!
86//! let storage_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 storage 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, sync::Arc, vec::Vec};
120use core::fmt;
121
122use aranya_buggy::bug;
123use aranya_policy_vm::{
124    ActionContext, CommandContext, ExitReason, KVPair, Machine, MachineIO, MachineStack,
125    OpenContext, PolicyContext, RunState, SealContext, Struct, Value,
126};
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,
134};
135
136mod error;
137mod io;
138mod protocol;
139pub mod testing;
140
141pub use error::*;
142pub use io::*;
143pub use protocol::*;
144
145/// Creates a [`VmAction`].
146///
147/// This must be used directly to avoid lifetime issues, not assigned to a variable.
148///
149/// # Example
150///
151/// ```ignore
152/// let x = 42;
153/// let y = String::from("asdf");
154/// client.action(storage_id, sink, vm_action!(foobar(x, y)))
155/// ```
156#[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/// Creates a [`VmEffectData`].
167///
168/// This is mostly useful for testing expected effects, and is expected to be compared
169/// against a [`VmEffect`].
170///
171/// # Example
172///
173/// ```ignore
174/// let val = 3;
175/// sink.add_expectation(vm_effect!(StuffHappened { x: 1, y: val }));
176///
177/// client.action(storage_id, sink, vm_action!(create(val)))
178/// ```
179#[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
191/// A [Policy] implementation that uses the Policy VM.
192pub struct VmPolicy<E> {
193    machine: Machine,
194    engine: Mutex<E>,
195    ffis: Mutex<Vec<Box<dyn FfiCallable<E> + Send + 'static>>>,
196    // TODO(chip): replace or fill this with priorities from attributes
197    priority_map: Arc<BTreeMap<String, u32>>,
198}
199
200impl<E> VmPolicy<E> {
201    /// Create a new `VmPolicy` from a [Machine]
202    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 = VmPolicy::<E>::get_command_priorities(&machine)?;
208        Ok(Self {
209            machine,
210            engine: Mutex::from(engine),
211            ffis: Mutex::from(ffis),
212            priority_map: Arc::new(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(String::from("(unknown location)"))
222    }
223
224    /// Scans command attributes for priorities and creates the priority map from them.
225    fn get_command_priorities(machine: &Machine) -> Result<BTreeMap<String, u32>, VmPolicyError> {
226        let mut priority_map = BTreeMap::new();
227        for (name, attrs) in &machine.command_attributes {
228            if let Some(Value::Int(p)) = attrs.get("priority") {
229                let pv = (*p).try_into().map_err(|e| {
230                    error!(
231                        ?e,
232                        "Priority out of range in {name}: {p} does not fit in u32"
233                    );
234                    VmPolicyError::Unknown
235                })?;
236                priority_map.insert(name.clone(), pv);
237            }
238        }
239        Ok(priority_map)
240    }
241}
242
243impl<E: aranya_crypto::Engine> VmPolicy<E> {
244    #[allow(clippy::too_many_arguments)]
245    #[instrument(skip_all, fields(name = name))]
246    fn evaluate_rule<'a, P>(
247        &self,
248        name: &str,
249        fields: &[KVPair],
250        envelope: Envelope<'_>,
251        facts: &'a mut P,
252        sink: &'a mut impl Sink<VmEffect>,
253        ctx: &CommandContext<'_>,
254        recall: CommandRecall,
255    ) -> Result<(), EngineError>
256    where
257        P: FactPerspective,
258    {
259        let mut ffis = self.ffis.lock();
260        let mut eng = self.engine.lock();
261        let mut io = VmPolicyIO::new(facts, sink, &mut *eng, &mut ffis);
262        let mut rs = self.machine.create_run_state(&mut io, ctx);
263        let self_data = Struct::new(name, fields);
264        match rs.call_command_policy(&self_data.name, &self_data, envelope.clone().into()) {
265            Ok(reason) => match reason {
266                ExitReason::Normal => Ok(()),
267                ExitReason::Check => {
268                    info!("Check {}", self.source_location(&rs));
269                    // Construct a new recall context from the policy context
270                    let CommandContext::Policy(policy_ctx) = ctx else {
271                        error!("Non-policy context while evaluating rule: {ctx:?}");
272                        return Err(EngineError::InternalError);
273                    };
274                    let recall_ctx = CommandContext::Recall(policy_ctx.clone());
275                    rs.set_context(&recall_ctx);
276                    self.recall_internal(recall, &mut rs, name, &self_data, envelope)
277                }
278                ExitReason::Panic => {
279                    info!("Panicked {}", self.source_location(&rs));
280                    Err(EngineError::Panic)
281                }
282            },
283            Err(e) => {
284                error!("\n{e}");
285                Err(EngineError::InternalError)
286            }
287        }
288    }
289
290    fn recall_internal<M>(
291        &self,
292        recall: CommandRecall,
293        rs: &mut RunState<'_, M>,
294        name: &str,
295        self_data: &Struct,
296        envelope: Envelope<'_>,
297    ) -> Result<(), EngineError>
298    where
299        M: MachineIO<MachineStack>,
300    {
301        match recall {
302            CommandRecall::None => Err(EngineError::Check),
303            CommandRecall::OnCheck => {
304                match rs.call_command_recall(name, self_data, envelope.into()) {
305                    Ok(ExitReason::Normal) => Err(EngineError::Check),
306                    Ok(ExitReason::Check) => {
307                        info!("Recall failed: {}", self.source_location(rs));
308                        Err(EngineError::Check)
309                    }
310                    Ok(ExitReason::Panic) | Err(_) => {
311                        info!("Recall panicked: {}", self.source_location(rs));
312                        Err(EngineError::Panic)
313                    }
314                }
315            }
316        }
317    }
318
319    #[instrument(skip_all, fields(name = name))]
320    fn open_command<P>(
321        &self,
322        name: &str,
323        envelope: Envelope<'_>,
324        facts: &mut P,
325    ) -> Result<Struct, EngineError>
326    where
327        P: FactPerspective,
328    {
329        let mut sink = NullSink;
330        let mut ffis = self.ffis.lock();
331        let mut eng = self.engine.lock();
332        let mut io = VmPolicyIO::new(facts, &mut sink, &mut *eng, &mut ffis);
333        let ctx = CommandContext::Open(OpenContext { name });
334        let mut rs = self.machine.create_run_state(&mut io, &ctx);
335        let status = rs.call_open(name, envelope.into());
336        match status {
337            Ok(reason) => match reason {
338                ExitReason::Normal => {
339                    let v = rs.consume_return().map_err(|e| {
340                        error!("Could not pull envelope from stack: {e}");
341                        EngineError::InternalError
342                    })?;
343                    Ok(v.try_into().map_err(|e| {
344                        error!("Envelope is not a struct: {e}");
345                        EngineError::InternalError
346                    })?)
347                }
348                ExitReason::Check => {
349                    info!("Check {}", self.source_location(&rs));
350                    Err(EngineError::Check)
351                }
352                ExitReason::Panic => {
353                    info!("Panicked {}", self.source_location(&rs));
354                    Err(EngineError::Check)
355                }
356            },
357            Err(e) => {
358                error!("\n{e}");
359                Err(EngineError::InternalError)
360            }
361        }
362    }
363
364    #[instrument(skip_all, fields(name = name))]
365    fn seal_command(
366        &self,
367        name: &str,
368        fields: impl IntoIterator<Item = impl Into<(String, Value)>>,
369        ctx_parent: CommandId,
370        facts: &mut impl FactPerspective,
371    ) -> Result<Envelope<'static>, EngineError> {
372        let mut sink = NullSink;
373        let mut ffis = self.ffis.lock();
374        let mut eng = self.engine.lock();
375        let mut io = VmPolicyIO::new(facts, &mut sink, &mut *eng, &mut ffis);
376        let ctx = CommandContext::Seal(SealContext {
377            name,
378            head_id: ctx_parent.into(),
379        });
380        let mut rs = self.machine.create_run_state(&mut io, &ctx);
381        let command_struct = Struct::new(name, fields);
382        let status = rs.call_seal(name, &command_struct);
383        match status {
384            Ok(reason) => match reason {
385                ExitReason::Normal => {
386                    let v = rs.consume_return().map_err(|e| {
387                        error!("Could not pull envelope from stack: {e}");
388                        EngineError::InternalError
389                    })?;
390                    let strukt = Struct::try_from(v).map_err(|e| {
391                        error!("Envelope is not a struct: {e}");
392                        EngineError::InternalError
393                    })?;
394                    let envelope = Envelope::try_from(strukt).map_err(|e| {
395                        error!("Malformed Envelope: {e}");
396                        EngineError::InternalError
397                    })?;
398                    Ok(envelope)
399                }
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::Panic)
407                }
408            },
409            Err(e) => {
410                error!("\n{e}");
411                Err(EngineError::InternalError)
412            }
413        }
414    }
415}
416
417/// [`VmPolicy`]'s actions.
418#[derive(Clone, Debug, PartialEq, Eq)]
419pub struct VmAction<'a> {
420    /// The name of the action.
421    pub name: &'a str,
422    /// The arguments of the action.
423    pub args: Cow<'a, [Value]>,
424}
425
426/// A partial version of [`VmEffect`] containing only the data. Created by
427/// [`vm_effect!`] and used to compare only the name and fields against the full
428/// `VmEffect`.
429#[derive(Debug)]
430pub struct VmEffectData {
431    /// The name of the effect.
432    pub name: String,
433    /// The fields of the effect.
434    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/// [`VmPolicy`]'s effects.
450#[derive(Clone, Debug, PartialEq, Eq)]
451pub struct VmEffect {
452    /// The name of the effect.
453    pub name: String,
454    /// The fields of the effect.
455    pub fields: Vec<KVPair>,
456    /// The command ID that produced this effect
457    pub command: CommandId,
458    /// Was this produced from a recall block?
459    pub recalled: bool,
460}
461
462impl<E: aranya_crypto::Engine> Policy for VmPolicy<E> {
463    type Action<'a> = VmAction<'a>;
464    type Effect = VmEffect;
465    type Command<'a> = VmProtocol<'a>;
466
467    fn serial(&self) -> u32 {
468        // TODO(chip): Implement an actual serial number
469        0u32
470    }
471
472    #[instrument(skip_all)]
473    fn call_rule(
474        &self,
475        command: &impl Command,
476        facts: &mut impl FactPerspective,
477        sink: &mut impl Sink<Self::Effect>,
478        recall: CommandRecall,
479    ) -> Result<(), EngineError> {
480        let unpacked: VmProtocolData<'_> = postcard::from_bytes(command.bytes()).map_err(|e| {
481            error!("Could not deserialize: {e:?}");
482            EngineError::Read
483        })?;
484        match unpacked {
485            VmProtocolData::Init {
486                author_id,
487                kind,
488                serialized_fields,
489                signature,
490                ..
491            } => {
492                let envelope = Envelope {
493                    parent_id: CommandId::default(),
494                    author_id,
495                    command_id: command.id(),
496                    payload: Cow::Borrowed(serialized_fields),
497                    signature: Cow::Borrowed(signature),
498                };
499                let command_struct = self.open_command(kind, envelope.clone(), facts)?;
500                let fields: Vec<KVPair> = command_struct
501                    .fields
502                    .into_iter()
503                    .map(|(k, v)| KVPair::new(&k, v))
504                    .collect();
505                let ctx = CommandContext::Policy(PolicyContext {
506                    name: kind,
507                    id: command.id().into(),
508                    author: author_id,
509                    version: CommandId::default().into(),
510                });
511                self.evaluate_rule(kind, fields.as_slice(), envelope, facts, sink, &ctx, recall)?
512            }
513            VmProtocolData::Basic {
514                parent,
515                kind,
516                author_id,
517                serialized_fields,
518                signature,
519            } => {
520                let envelope = Envelope {
521                    parent_id: parent.id,
522                    author_id,
523                    command_id: command.id(),
524                    payload: Cow::Borrowed(serialized_fields),
525                    signature: Cow::Borrowed(signature),
526                };
527                let command_struct = self.open_command(kind, envelope.clone(), facts)?;
528                let fields: Vec<KVPair> = command_struct
529                    .fields
530                    .into_iter()
531                    .map(|(k, v)| KVPair::new(&k, v))
532                    .collect();
533                let ctx = CommandContext::Policy(PolicyContext {
534                    name: kind,
535                    id: command.id().into(),
536                    author: author_id,
537                    version: CommandId::default().into(),
538                });
539                self.evaluate_rule(kind, fields.as_slice(), envelope, facts, sink, &ctx, recall)?
540            }
541            // Merges always pass because they're an artifact of the graph
542            _ => (),
543        }
544
545        Ok(())
546    }
547
548    #[instrument(skip_all, fields(name = action.name))]
549    fn call_action(
550        &self,
551        action: Self::Action<'_>,
552        facts: &mut impl Perspective,
553        sink: &mut impl Sink<Self::Effect>,
554    ) -> Result<(), EngineError> {
555        let VmAction { name, args } = action;
556
557        let parent = match facts.head_address()? {
558            Prior::None => None,
559            Prior::Single(id) => Some(id),
560            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
561        };
562        // FIXME(chip): This is kind of wrong, but it avoids having to
563        // plumb Option<Id> into the VM and FFI
564        let ctx_parent = parent.unwrap_or_default();
565
566        let publish_stack = {
567            let mut ffis = self.ffis.lock();
568            let mut eng = self.engine.lock();
569            let mut io = VmPolicyIO::new(facts, sink, &mut *eng, &mut ffis);
570            let ctx = CommandContext::Action(ActionContext {
571                name,
572                head_id: ctx_parent.id.into(),
573            });
574            {
575                let mut rs = self.machine.create_run_state(&mut io, &ctx);
576                let exit_reason = match args {
577                    Cow::Borrowed(args) => rs.call_action(name, args.iter().cloned()),
578                    Cow::Owned(args) => rs.call_action(name, args),
579                }
580                .map_err(|e| {
581                    error!("\n{e}");
582                    EngineError::InternalError
583                })?;
584                match exit_reason {
585                    ExitReason::Normal => {}
586                    ExitReason::Check => {
587                        info!("Check {}", self.source_location(&rs));
588                        return Err(EngineError::Check);
589                    }
590                    ExitReason::Panic => {
591                        info!("Panicked {}", self.source_location(&rs));
592                        return Err(EngineError::Panic);
593                    }
594                };
595            }
596            io.into_publish_stack()
597        };
598
599        for (name, fields) in publish_stack {
600            let envelope = self.seal_command(&name, fields, ctx_parent.id, facts)?;
601            let data = match parent {
602                None => VmProtocolData::Init {
603                    // TODO(chip): where does the policy value come from?
604                    policy: 0u64.to_le_bytes(),
605                    author_id: envelope.author_id,
606                    kind: &name,
607                    serialized_fields: &envelope.payload,
608                    signature: &envelope.signature,
609                },
610                Some(parent) => VmProtocolData::Basic {
611                    author_id: envelope.author_id,
612                    parent,
613                    kind: &name,
614                    serialized_fields: &envelope.payload,
615                    signature: &envelope.signature,
616                },
617            };
618            let wrapped = postcard::to_allocvec(&data)?;
619            let new_command = VmProtocol::new(
620                &wrapped,
621                envelope.command_id,
622                data,
623                Arc::clone(&self.priority_map),
624            );
625
626            self.call_rule(&new_command, facts, sink, CommandRecall::None)?;
627            facts.add_command(&new_command).map_err(|e| {
628                error!("{e}");
629                EngineError::Write
630            })?;
631        }
632
633        Ok(())
634    }
635
636    fn merge<'a>(
637        &self,
638        target: &'a mut [u8],
639        ids: MergeIds,
640    ) -> Result<Self::Command<'a>, EngineError> {
641        let (left, right) = ids.into();
642        let c = VmProtocolData::Merge { left, right };
643        let data = postcard::to_slice(&c, target).map_err(|e| {
644            error!("{e}");
645            EngineError::Write
646        })?;
647        let id = CommandId::hash_for_testing_only(data);
648        Ok(VmProtocol::new(data, id, c, Arc::clone(&self.priority_map)))
649    }
650}
651
652impl fmt::Display for VmAction<'_> {
653    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
654        let mut d = f.debug_tuple(self.name);
655        for arg in self.args.as_ref() {
656            d.field(&DebugViaDisplay(arg));
657        }
658        d.finish()
659    }
660}
661
662impl fmt::Display for VmEffect {
663    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
664        let mut d = f.debug_struct(&self.name);
665        for field in &self.fields {
666            d.field(field.key(), &DebugViaDisplay(field.value()));
667        }
668        d.finish()
669    }
670}
671
672/// Implements `Debug` via `T`'s `Display` impl.
673struct DebugViaDisplay<T>(T);
674
675impl<T: fmt::Display> fmt::Debug for DebugViaDisplay<T> {
676    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
677        write!(f, "{}", self.0)
678    }
679}