Skip to main content

agent_approval_interchange/
lib.rs

1//! Agent Approval Interchange Specification 1.0 support library.
2
3use chrono::{DateTime, Utc};
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7use sha2::{Digest, Sha256};
8use std::collections::HashMap;
9use thiserror::Error;
10use uuid::Uuid;
11
12/// Error returned for invalid AAIS documents or state transitions.
13#[derive(Debug, Error)]
14pub enum ApprovalError {
15    #[error("validation error: {0}")]
16    Validation(String),
17    #[error("conflict: {0}")]
18    Conflict(String),
19    #[error(transparent)]
20    Json(#[from] serde_json::Error),
21}
22
23pub type Result<T> = std::result::Result<T, ApprovalError>;
24
25#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct Origin {
28    pub harness: String,
29    pub session_id: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub run_id: Option<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub task_id: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub graph_id: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub node_id: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub agent_id: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub profile: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub subagent_run_id: Option<String>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub parent_run_id: Option<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub project: Option<String>,
48}
49
50#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct Presentation {
53    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
54    pub redacted: bool,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub binding_hint: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub details: Option<String>,
59}
60
61#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct Action {
64    pub kind: String,
65    pub name: String,
66    pub summary: String,
67    pub arguments: Map<String, Value>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub resource: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub working_directory: Option<String>,
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub effects: Vec<String>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub presentation: Option<Presentation>,
76}
77
78#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct Risk {
81    pub level: String,
82    pub reasons: Vec<String>,
83}
84
85#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
86#[serde(deny_unknown_fields)]
87pub struct Choice {
88    pub decision: String,
89    pub scope: String,
90    pub label: String,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub scope_constraints: Option<Map<String, Value>>,
93}
94
95#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct Request {
98    pub id: String,
99    pub created_at: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub expires_at: Option<String>,
102    pub status: String,
103    pub origin: Origin,
104    pub action: Action,
105    pub action_digest: String,
106    pub risk: Risk,
107    pub choices: Vec<Choice>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub extensions: Option<Map<String, Value>>,
110}
111
112#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113#[serde(deny_unknown_fields)]
114pub struct Actor {
115    pub id: String,
116    #[serde(rename = "type")]
117    pub actor_type: String,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub display_name: Option<String>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub authenticated_by: Option<String>,
122}
123
124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct Decision {
127    pub id: String,
128    pub request_id: String,
129    pub action_digest: String,
130    pub decided_at: String,
131    pub decision: String,
132    pub scope: String,
133    pub actor: Actor,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub extensions: Option<Map<String, Value>>,
136}
137
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
139#[serde(deny_unknown_fields)]
140pub struct Resolution {
141    pub id: String,
142    pub request_id: String,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub decision_id: Option<String>,
145    pub action_digest: String,
146    pub resolved_at: String,
147    pub outcome: String,
148    pub message: String,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub effective_scope: Option<String>,
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub extensions: Option<Map<String, Value>>,
153}
154
155#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
156#[serde(deny_unknown_fields)]
157pub struct Snapshot {
158    pub as_of_sequence: u64,
159    pub pending: Vec<Request>,
160}
161
162#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct Activity {
165    pub request_id: String,
166    pub message: String,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub stage: Option<String>,
169}
170
171#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
172#[serde(deny_unknown_fields)]
173pub struct Envelope {
174    pub aais: String,
175    #[serde(rename = "type")]
176    pub event_type: String,
177    pub id: String,
178    pub occurred_at: String,
179    pub sequence: u64,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub stream: Option<String>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub extensions: Option<Map<String, Value>>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub request: Option<Request>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub decision: Option<Decision>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub resolution: Option<Resolution>,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub snapshot: Option<Snapshot>,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub activity: Option<Activity>,
194}
195
196fn valid_id(value: &str) -> bool {
197    !value.is_empty()
198        && value.len() <= 200
199        && Regex::new(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$")
200            .unwrap()
201            .is_match(value)
202}
203fn parse_time(value: &str) -> Result<DateTime<Utc>> {
204    if !(value.ends_with('Z') || Regex::new(r"[+-]\d\d:\d\d$").unwrap().is_match(value)) {
205        return Err(ApprovalError::Validation(
206            "timestamp requires an explicit offset".into(),
207        ));
208    }
209    DateTime::parse_from_rfc3339(value)
210        .map(|v| v.with_timezone(&Utc))
211        .map_err(|_| ApprovalError::Validation("invalid RFC 3339 timestamp".into()))
212}
213
214/// Compute the RFC 8785 / SHA-256 binding for an action.
215pub fn action_digest(action: &Action) -> Result<String> {
216    let canonical =
217        serde_jcs::to_vec(action).map_err(|e| ApprovalError::Validation(e.to_string()))?;
218    Ok(format!("sha256:{:x}", Sha256::digest(canonical)))
219}
220
221/// Options for constructing a requested envelope.
222pub struct CreateRequestOptions {
223    pub action: Action,
224    pub origin: Origin,
225    pub risk: Risk,
226    pub choices: Vec<Choice>,
227    pub sequence: u64,
228    pub stream: Option<String>,
229    pub request_id: Option<String>,
230    pub event_id: Option<String>,
231    pub created_at: Option<DateTime<Utc>>,
232    pub expires_at: Option<DateTime<Utc>>,
233}
234
235/// Build and validate an `approval.requested` envelope.
236pub fn create_request(options: CreateRequestOptions) -> Result<Envelope> {
237    let created = options.created_at.unwrap_or_else(Utc::now);
238    let request = Request {
239        id: options
240            .request_id
241            .unwrap_or_else(|| format!("apr_{}", Uuid::new_v4().simple())),
242        created_at: created.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true),
243        expires_at: options
244            .expires_at
245            .map(|value| value.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)),
246        status: "pending".into(),
247        origin: options.origin,
248        action_digest: action_digest(&options.action)?,
249        action: options.action,
250        risk: options.risk,
251        choices: options.choices,
252        extensions: None,
253    };
254    let envelope = Envelope {
255        aais: "1.0".into(),
256        event_type: "approval.requested".into(),
257        id: options
258            .event_id
259            .unwrap_or_else(|| format!("evt_{}", Uuid::new_v4().simple())),
260        occurred_at: request.created_at.clone(),
261        sequence: options.sequence,
262        stream: options.stream,
263        extensions: None,
264        request: Some(request),
265        decision: None,
266        resolution: None,
267        snapshot: None,
268        activity: None,
269    };
270    validate(&envelope)?;
271    Ok(envelope)
272}
273
274/// Options for constructing a decision envelope.
275pub struct CreateDecisionOptions {
276    pub decision: String,
277    pub scope: String,
278    pub actor: Actor,
279    pub sequence: u64,
280    pub stream: Option<String>,
281    pub decision_id: Option<String>,
282    pub event_id: Option<String>,
283    pub decided_at: Option<DateTime<Utc>>,
284}
285
286/// Build a decision bound to an `approval.requested` envelope.
287pub fn create_decision(requested: &Envelope, options: CreateDecisionOptions) -> Result<Envelope> {
288    validate(requested)?;
289    if requested.event_type != "approval.requested" {
290        return Err(ApprovalError::Validation(
291            "create_decision requires approval.requested".into(),
292        ));
293    }
294    let at = options.decided_at.unwrap_or_else(Utc::now);
295    let request = requested.request.as_ref().unwrap();
296    let decision = Decision {
297        id: options
298            .decision_id
299            .unwrap_or_else(|| format!("dec_{}", Uuid::new_v4().simple())),
300        request_id: request.id.clone(),
301        action_digest: request.action_digest.clone(),
302        decided_at: at.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true),
303        decision: options.decision,
304        scope: options.scope,
305        actor: options.actor,
306        extensions: None,
307    };
308    let envelope = Envelope {
309        aais: "1.0".into(),
310        event_type: "approval.decided".into(),
311        id: options
312            .event_id
313            .unwrap_or_else(|| format!("evt_{}", Uuid::new_v4().simple())),
314        occurred_at: decision.decided_at.clone(),
315        sequence: options.sequence,
316        stream: options.stream,
317        extensions: None,
318        request: None,
319        decision: Some(decision),
320        resolution: None,
321        snapshot: None,
322        activity: None,
323    };
324    validate(&envelope)?;
325    Ok(envelope)
326}
327
328/// Parse and perform Core validation on one AAIS JSON envelope.
329pub fn parse(data: &str) -> Result<Envelope> {
330    let envelope: Envelope = serde_json::from_str(data)?;
331    validate(&envelope)?;
332    Ok(envelope)
333}
334
335/// Validate an AAIS envelope.
336pub fn validate(e: &Envelope) -> Result<()> {
337    if e.aais != "1.0" || !valid_id(&e.id) {
338        return Err(ApprovalError::Validation(
339            "invalid envelope version or id".into(),
340        ));
341    }
342    parse_time(&e.occurred_at)?;
343    let payloads = [
344        e.request.is_some(),
345        e.decision.is_some(),
346        e.resolution.is_some(),
347        e.snapshot.is_some(),
348        e.activity.is_some(),
349    ]
350    .iter()
351    .filter(|x| **x)
352    .count();
353    if payloads != 1 {
354        return Err(ApprovalError::Validation(
355            "exactly one payload is required".into(),
356        ));
357    }
358    match e.event_type.as_str() {
359        "approval.requested" => validate_request(
360            e.request
361                .as_ref()
362                .ok_or_else(|| ApprovalError::Validation("request payload required".into()))?,
363        ),
364        "approval.decided" => validate_decision(
365            e.decision
366                .as_ref()
367                .ok_or_else(|| ApprovalError::Validation("decision payload required".into()))?,
368        ),
369        "approval.resolved" => validate_resolution(
370            e.resolution
371                .as_ref()
372                .ok_or_else(|| ApprovalError::Validation("resolution payload required".into()))?,
373        ),
374        "approval.snapshot" => {
375            let snapshot = e
376                .snapshot
377                .as_ref()
378                .ok_or_else(|| ApprovalError::Validation("snapshot payload required".into()))?;
379            for r in &snapshot.pending {
380                validate_request(r)?;
381            }
382            Ok(())
383        }
384        "approval.activity" => {
385            let a = e
386                .activity
387                .as_ref()
388                .ok_or_else(|| ApprovalError::Validation("activity payload required".into()))?;
389            if !valid_id(&a.request_id) || a.message.is_empty() {
390                Err(ApprovalError::Validation("invalid activity".into()))
391            } else {
392                Ok(())
393            }
394        }
395        _ => Err(ApprovalError::Validation("unknown event type".into())),
396    }
397}
398
399fn validate_request(r: &Request) -> Result<()> {
400    if !valid_id(&r.id)
401        || r.status != "pending"
402        || !valid_id(&r.origin.harness)
403        || !valid_id(&r.origin.session_id)
404    {
405        return Err(ApprovalError::Validation(
406            "invalid request identity or origin".into(),
407        ));
408    }
409    let created = parse_time(&r.created_at)?;
410    if let Some(expires) = &r.expires_at {
411        if parse_time(expires)? <= created {
412            return Err(ApprovalError::Validation(
413                "expires_at must be later than created_at".into(),
414            ));
415        }
416    }
417    if !valid_id(&r.action.kind) || !valid_id(&r.action.name) || r.action.summary.is_empty() {
418        return Err(ApprovalError::Validation("invalid action".into()));
419    }
420    if r.action
421        .presentation
422        .as_ref()
423        .is_some_and(|p| p.redacted && p.binding_hint.as_deref().unwrap_or("").is_empty())
424    {
425        return Err(ApprovalError::Validation(
426            "redacted action requires binding_hint".into(),
427        ));
428    }
429    if action_digest(&r.action)? != r.action_digest {
430        return Err(ApprovalError::Validation(
431            "action_digest does not match action".into(),
432        ));
433    }
434    if !Regex::new(r"^sha256:[0-9a-f]{64}$")
435        .unwrap()
436        .is_match(&r.action_digest)
437        || !["low", "medium", "high", "critical"].contains(&r.risk.level.as_str())
438        || r.risk.reasons.is_empty()
439        || r.choices.is_empty()
440    {
441        return Err(ApprovalError::Validation(
442            "invalid digest, risk, or choices".into(),
443        ));
444    }
445    let mut exit = false;
446    let mut seen = std::collections::HashSet::new();
447    for c in &r.choices {
448        if !["approve", "deny", "cancel"].contains(&c.decision.as_str())
449            || !["once", "session", "persistent"].contains(&c.scope.as_str())
450            || c.label.is_empty()
451        {
452            return Err(ApprovalError::Validation("invalid choice".into()));
453        }
454        if c.decision != "approve" {
455            exit = true;
456            if c.scope != "once" {
457                return Err(ApprovalError::Validation(
458                    "deny/cancel choice invalid".into(),
459                ));
460            }
461        }
462        if !seen.insert((c.decision.as_str(), c.scope.as_str())) {
463            return Err(ApprovalError::Validation(
464                "decision and scope tuples must be unique".into(),
465            ));
466        }
467        if c.scope != "once" && c.scope_constraints.as_ref().is_none_or(Map::is_empty) {
468            return Err(ApprovalError::Validation(
469                "broader scope requires constraints".into(),
470            ));
471        }
472    }
473    if !exit {
474        return Err(ApprovalError::Validation(
475            "at least one deny or cancel choice is required".into(),
476        ));
477    }
478    Ok(())
479}
480fn validate_decision(d: &Decision) -> Result<()> {
481    if !valid_id(&d.id)
482        || !valid_id(&d.request_id)
483        || !Regex::new(r"^sha256:[0-9a-f]{64}$")
484            .unwrap()
485            .is_match(&d.action_digest)
486        || !["approve", "deny", "cancel"].contains(&d.decision.as_str())
487        || !["once", "session", "persistent"].contains(&d.scope.as_str())
488        || !valid_id(&d.actor.id)
489        || !["human", "policy"].contains(&d.actor.actor_type.as_str())
490    {
491        return Err(ApprovalError::Validation("invalid decision".into()));
492    }
493    parse_time(&d.decided_at)?;
494    if d.decision != "approve" && d.scope != "once" {
495        return Err(ApprovalError::Validation(
496            "deny/cancel decision invalid".into(),
497        ));
498    }
499    Ok(())
500}
501fn validate_resolution(r: &Resolution) -> Result<()> {
502    if !valid_id(&r.id)
503        || !valid_id(&r.request_id)
504        || !Regex::new(r"^sha256:[0-9a-f]{64}$")
505            .unwrap()
506            .is_match(&r.action_digest)
507        || ![
508            "approved",
509            "denied",
510            "cancelled",
511            "expired",
512            "stale",
513            "conflict",
514            "invalid",
515        ]
516        .contains(&r.outcome.as_str())
517        || r.message.is_empty()
518    {
519        return Err(ApprovalError::Validation("invalid resolution".into()));
520    }
521    parse_time(&r.resolved_at)?;
522    Ok(())
523}
524
525/// Reference in-memory, replay-safe approval state machine.
526#[derive(Default)]
527pub struct ApprovalStore {
528    pending: HashMap<String, Envelope>,
529    resolutions: HashMap<String, Envelope>,
530    fingerprints: HashMap<String, String>,
531    pub last_sequence: u64,
532}
533impl ApprovalStore {
534    pub fn new() -> Self {
535        Self::default()
536    }
537    pub fn add(&mut self, e: Envelope) -> Result<()> {
538        validate(&e)?;
539        if e.event_type != "approval.requested" {
540            return Err(ApprovalError::Validation(
541                "add requires approval.requested".into(),
542            ));
543        }
544        let id = e.request.as_ref().unwrap().id.clone();
545        if let Some(previous) = self.pending.get(&id) {
546            if previous != &e {
547                return Err(ApprovalError::Conflict(format!(
548                    "request {id} already exists"
549                )));
550            }
551            return Ok(());
552        }
553        self.last_sequence = self.last_sequence.max(e.sequence);
554        self.pending.insert(id, e);
555        Ok(())
556    }
557    pub fn decide(
558        &mut self,
559        e: Envelope,
560        now: DateTime<Utc>,
561        current_action: Option<&Action>,
562    ) -> Result<Envelope> {
563        validate(&e)?;
564        if e.event_type != "approval.decided" {
565            return Err(ApprovalError::Validation(
566                "decide requires approval.decided".into(),
567            ));
568        }
569        let d = e.decision.as_ref().unwrap();
570        let fingerprint = format!(
571            "sha256:{:x}",
572            Sha256::digest(
573                serde_jcs::to_vec(d).map_err(|x| ApprovalError::Validation(x.to_string()))?
574            )
575        );
576        if let Some(previous) = self.resolutions.get(&d.request_id) {
577            if self.fingerprints.get(&d.request_id) == Some(&fingerprint) {
578                return Ok(previous.clone());
579            }
580            return Err(ApprovalError::Conflict("request already resolved".into()));
581        }
582        let requested = self
583            .pending
584            .get(&d.request_id)
585            .ok_or_else(|| ApprovalError::Validation("unknown pending request".into()))?;
586        let r = requested.request.as_ref().unwrap();
587        let (mut outcome, mut message) = match d.decision.as_str() {
588            "approve" => ("approved", "Approval accepted."),
589            "deny" => ("denied", "Action denied."),
590            _ => ("cancelled", "Approval cancelled."),
591        };
592        if r.expires_at
593            .as_ref()
594            .is_some_and(|x| parse_time(x).is_ok_and(|t| now >= t))
595        {
596            outcome = "expired";
597            message = "Approval request expired.";
598        } else if d.action_digest != r.action_digest
599            || current_action.is_some_and(|a| action_digest(a).is_ok_and(|x| x != r.action_digest))
600        {
601            outcome = "stale";
602            message = "The decision does not match the current action.";
603        } else if r
604            .choices
605            .iter()
606            .all(|c| c.decision != d.decision || c.scope != d.scope)
607        {
608            outcome = "invalid";
609            message = "The selected decision and scope were not offered.";
610        }
611        self.last_sequence = (self.last_sequence + 1).max(e.sequence);
612        let at = now.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true);
613        let resolution = Resolution {
614            id: format!("res_{}", d.id),
615            request_id: d.request_id.clone(),
616            decision_id: Some(d.id.clone()),
617            action_digest: r.action_digest.clone(),
618            resolved_at: at.clone(),
619            outcome: outcome.into(),
620            message: message.into(),
621            effective_scope: matches!(outcome, "approved" | "denied").then(|| d.scope.clone()),
622            extensions: None,
623        };
624        let result = Envelope {
625            aais: "1.0".into(),
626            event_type: "approval.resolved".into(),
627            id: format!("evt_res_{}", d.id),
628            occurred_at: at,
629            sequence: self.last_sequence,
630            stream: requested.stream.clone(),
631            extensions: None,
632            request: None,
633            decision: None,
634            resolution: Some(resolution),
635            snapshot: None,
636            activity: None,
637        };
638        validate(&result)?;
639        self.pending.remove(&d.request_id);
640        self.resolutions
641            .insert(d.request_id.clone(), result.clone());
642        self.fingerprints.insert(d.request_id.clone(), fingerprint);
643        Ok(result)
644    }
645    pub fn snapshot(&self, stream: Option<String>, now: DateTime<Utc>) -> Envelope {
646        Envelope {
647            aais: "1.0".into(),
648            event_type: "approval.snapshot".into(),
649            id: format!("evt_{}", Uuid::new_v4().simple()),
650            occurred_at: now.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true),
651            sequence: self.last_sequence,
652            stream,
653            extensions: None,
654            request: None,
655            decision: None,
656            resolution: None,
657            snapshot: Some(Snapshot {
658                as_of_sequence: self.last_sequence,
659                pending: self
660                    .pending
661                    .values()
662                    .filter(|e| {
663                        e.request
664                            .as_ref()
665                            .unwrap()
666                            .expires_at
667                            .as_ref()
668                            .is_none_or(|value| {
669                                parse_time(value).is_ok_and(|expires| now < expires)
670                            })
671                    })
672                    .map(|e| e.request.clone().unwrap())
673                    .collect(),
674            }),
675            activity: None,
676        }
677    }
678    pub fn from_snapshot(e: &Envelope) -> Result<Self> {
679        validate(e)?;
680        if e.event_type != "approval.snapshot" {
681            return Err(ApprovalError::Validation(
682                "from_snapshot requires approval.snapshot".into(),
683            ));
684        }
685        let mut store = Self::new();
686        store.last_sequence = e.snapshot.as_ref().unwrap().as_of_sequence;
687        for request in &e.snapshot.as_ref().unwrap().pending {
688            let wrapper = Envelope {
689                aais: "1.0".into(),
690                event_type: "approval.requested".into(),
691                id: format!("restore_{}", request.id),
692                occurred_at: request.created_at.clone(),
693                sequence: store.last_sequence,
694                stream: e.stream.clone(),
695                extensions: None,
696                request: Some(request.clone()),
697                decision: None,
698                resolution: None,
699                snapshot: None,
700                activity: None,
701            };
702            store.add(wrapper)?;
703        }
704        Ok(store)
705    }
706}