Skip to main content

agent_effects_store/
contract.rs

1//! Persisted assumptions that determine whether execution is safe.
2
3use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6
7use crate::EffectKind;
8
9/// The verification guarantee recorded with an effect.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case", tag = "mode")]
12pub enum VerificationContract {
13    /// No remote verification is available.
14    None,
15    /// A lookup reads its own writes.
16    Authoritative,
17    /// Absence is trusted only after the recorded settle interval.
18    EventuallyConsistent {
19        /// Minimum time since the attempt ended.
20        settle: Duration,
21    },
22}
23
24/// Immutable execution assumptions recorded before the first attempt.
25///
26/// Contracts are compared before replay, lease acquisition, or execution.
27/// Retry delays and timeouts are incidental configuration; the lifetime
28/// attempt ceiling is safety relevant and must remain unchanged on reattach.
29/// Changing code while retaining `handler_version` asserts that its external
30/// semantics are compatible with this contract.
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct ExecutionContract {
34    /// Contract representation version. Currently `1`.
35    pub format_version: u32,
36    /// What repeating the external operation does.
37    pub kind: EffectKind,
38    /// The application attests remote deduplication of the stable key.
39    pub remote_idempotency: bool,
40    /// How the runtime may interpret a remote lookup.
41    pub verification: VerificationContract,
42    /// Application-defined semantic version, not a crate release version.
43    pub handler_version: String,
44    /// Optional trusted provider or adapter identity; never a credential.
45    pub adapter: Option<String>,
46    /// A human decision is required before the first attempt.
47    pub require_approval: bool,
48    /// Risk policy permits automatic retries.
49    pub automatic_retry: bool,
50    /// Lifetime attempt ceiling, including the initial attempt.
51    pub max_attempts: u32,
52}
53
54impl ExecutionContract {
55    /// Conservative assumptions for an unverified irreversible or reversible
56    /// operation, with the specified lifetime retry budget.
57    pub fn new(kind: EffectKind, max_attempts: u32) -> Self {
58        Self {
59            format_version: 1,
60            kind,
61            remote_idempotency: false,
62            verification: VerificationContract::None,
63            handler_version: "1".into(),
64            adapter: None,
65            require_approval: false,
66            automatic_retry: true,
67            max_attempts,
68        }
69    }
70
71    /// Whether `requested` preserves the recorded execution assumptions.
72    /// All fields must match; unknown representation versions are refused.
73    pub fn permits(&self, requested: &Self) -> bool {
74        if self.format_version != 1 || requested.format_version != 1 {
75            return false;
76        }
77        self == requested
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use proptest::prelude::*;
85
86    proptest! {
87        #[test]
88        fn compatibility_never_grants_changed_execution_assumptions(
89            remote in any::<bool>(), approval in any::<bool>(),
90            automatic in any::<bool>(), attempts in any::<u32>(),
91            format in 2_u32..u32::MAX,
92        ) {
93            let mut stored = ExecutionContract::new(EffectKind::IrreversibleWrite, attempts);
94            stored.remote_idempotency = remote;
95            stored.require_approval = approval;
96            stored.automatic_retry = automatic;
97            prop_assert!(stored.permits(&stored));
98            let mut changed = stored.clone();
99            changed.remote_idempotency = !remote;
100            prop_assert!(!stored.permits(&changed));
101            changed = stored.clone();
102            changed.require_approval = !approval;
103            prop_assert!(!stored.permits(&changed));
104            changed = stored.clone();
105            changed.automatic_retry = !automatic;
106            prop_assert!(!stored.permits(&changed));
107            changed = stored.clone();
108            changed.format_version = format;
109            prop_assert!(!stored.permits(&changed));
110            prop_assert!(!changed.permits(&changed));
111        }
112    }
113}