Skip to main content

cookie_agent_protocol/
approval.rs

1use std::{borrow::Cow, fmt};
2
3use jiff::Timestamp;
4use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    ApprovalId, PermissionAction, PermissionEffect, SafeCode, SafeErrorMessage, SessionId,
9    Sha256Digest, TreeApprovalGrantId, WildcardPattern,
10};
11
12#[derive(Clone, Debug, Eq, Hash, JsonSchema, PartialEq, Serialize)]
13#[serde(deny_unknown_fields)]
14pub struct OperationFingerprint {
15    digest: Sha256Digest,
16}
17impl OperationFingerprint {
18    const DOMAIN: &'static [u8] = b"cookie-agent.operation-fingerprint.v7\0";
19    #[must_use]
20    pub fn from_prepared_operation(operation: &PreparedOperationIdentity) -> Self {
21        Self {
22            digest: Sha256Digest::hash_domain_separated(
23                Self::DOMAIN,
24                &operation.canonical_fingerprint_bytes(),
25            ),
26        }
27    }
28    #[must_use]
29    pub const fn digest(&self) -> &Sha256Digest {
30        &self.digest
31    }
32}
33impl<'de> Deserialize<'de> for OperationFingerprint {
34    fn deserialize<D>(d: D) -> Result<Self, D::Error>
35    where
36        D: serde::Deserializer<'de>,
37    {
38        #[derive(Deserialize)]
39        #[serde(deny_unknown_fields)]
40        struct Wire {
41            digest: Sha256Digest,
42        }
43        Ok(Self {
44            digest: Wire::deserialize(d)?.digest,
45        })
46    }
47}
48
49#[derive(Clone, Debug, Eq, Hash, JsonSchema, PartialEq, Serialize)]
50#[serde(deny_unknown_fields)]
51pub struct PreparedResourceDigest {
52    digest: Sha256Digest,
53}
54impl PreparedResourceDigest {
55    const DOMAIN: &'static [u8] = b"cookie-agent.prepared-resource-digest.v7\0";
56    #[must_use]
57    pub fn from_canonical_binding_bytes(bytes: &[u8]) -> Self {
58        Self {
59            digest: Sha256Digest::hash_domain_separated(Self::DOMAIN, bytes),
60        }
61    }
62    #[must_use]
63    pub const fn digest(&self) -> &Sha256Digest {
64        &self.digest
65    }
66}
67impl<'de> Deserialize<'de> for PreparedResourceDigest {
68    fn deserialize<D>(d: D) -> Result<Self, D::Error>
69    where
70        D: serde::Deserializer<'de>,
71    {
72        #[derive(Deserialize)]
73        #[serde(deny_unknown_fields)]
74        struct Wire {
75            digest: Sha256Digest,
76        }
77        Ok(Self {
78            digest: Wire::deserialize(d)?.digest,
79        })
80    }
81}
82
83#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub struct PreparedResourceIdentity(String);
85impl PreparedResourceIdentity {
86    pub const MAX_BYTES: usize = 512;
87    pub fn new(value: impl Into<String>) -> Result<Self, ApprovalSchemaError> {
88        let value = value.into();
89        if value.is_empty()
90            || value.len() > Self::MAX_BYTES
91            || !value.bytes().all(|byte| {
92                byte.is_ascii_lowercase()
93                    || byte.is_ascii_digit()
94                    || matches!(byte, b'-' | b'_' | b'.' | b':')
95            })
96            || value.starts_with("fd:")
97            || value.starts_with("tmp:")
98            || value.starts_with("temp:")
99        {
100            return Err(ApprovalSchemaError::InvalidPreparedIdentity);
101        }
102        Ok(Self(value))
103    }
104    #[must_use]
105    pub fn as_str(&self) -> &str {
106        &self.0
107    }
108}
109impl fmt::Display for PreparedResourceIdentity {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        self.0.fmt(f)
112    }
113}
114impl Serialize for PreparedResourceIdentity {
115    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
116    where
117        S: serde::Serializer,
118    {
119        s.serialize_str(&self.0)
120    }
121}
122impl<'de> Deserialize<'de> for PreparedResourceIdentity {
123    fn deserialize<D>(d: D) -> Result<Self, D::Error>
124    where
125        D: serde::Deserializer<'de>,
126    {
127        Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
128    }
129}
130impl JsonSchema for PreparedResourceIdentity {
131    fn inline_schema() -> bool {
132        true
133    }
134    fn schema_name() -> Cow<'static, str> {
135        Cow::Borrowed("PreparedResourceIdentity")
136    }
137    fn json_schema(_: &mut SchemaGenerator) -> Schema {
138        json_schema!({"type":"string","minLength":1,"maxLength":512,"pattern":"^[a-z0-9][a-z0-9._:-]*$"})
139    }
140}
141
142pub type PreparedCapabilityOperation = PreparedResourceIdentity;
143
144#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
145#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
146pub enum ApprovalBoundary {
147    Exact,
148    CommandPrefix { prefix: String },
149    DelegationTree { root_session_id: SessionId },
150}
151
152#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
153#[serde(rename_all = "snake_case")]
154pub enum ApprovalResourceSource {
155    PrimaryOperation,
156    SecondaryOperation,
157    ModelRequest,
158    DoomLoopGuard,
159}
160#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
161#[serde(rename_all = "snake_case")]
162pub enum PreparedBindingLifetime {
163    ProcessLocal,
164    RestartStable,
165}
166
167#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
168#[serde(deny_unknown_fields)]
169pub struct PreparedApprovalResource {
170    pub capability: PermissionAction,
171    pub canonical: PreparedResourceIdentity,
172    pub binding_digest: PreparedResourceDigest,
173    pub binding_lifetime: PreparedBindingLifetime,
174    pub boundary: ApprovalBoundary,
175    pub source: ApprovalResourceSource,
176}
177
178#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
179#[serde(deny_unknown_fields)]
180pub struct MatchedPermissionRule {
181    pub source_layer: SafeCode,
182    pub action: PermissionAction,
183    pub resource: WildcardPattern,
184    pub effect: PermissionEffect,
185}
186#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
187#[serde(deny_unknown_fields)]
188pub struct DecisionTrace {
189    pub action: PermissionAction,
190    pub normalized_resource: String,
191    pub candidates: Vec<MatchedPermissionRule>,
192    pub effect: PermissionEffect,
193    pub precedence_reason: String,
194}
195#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
196#[serde(deny_unknown_fields)]
197pub struct ApprovalEvaluation {
198    pub resource_digest: PreparedResourceDigest,
199    pub effect: PermissionEffect,
200    pub trace: DecisionTrace,
201}
202
203#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
204#[serde(rename_all = "snake_case")]
205pub enum ApprovalTrigger {
206    PermissionPolicy,
207    ModelToolApproval,
208    InternalAgent,
209    DoomLoop,
210}
211
212#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, JsonSchema, PartialEq, Serialize)]
213#[serde(rename_all = "snake_case")]
214pub enum PermissionMode {
215    #[default]
216    AutoApprove,
217    AutoApproveN,
218    AutoApproveY,
219    Ask,
220    Yolo,
221}
222#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
223#[serde(deny_unknown_fields)]
224pub struct ApprovalCapability {
225    pub action: PermissionAction,
226    pub operation: PreparedCapabilityOperation,
227}
228#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
229#[serde(rename_all = "snake_case")]
230pub enum PreparedCapabilityLifetime {
231    ProcessLocal,
232}
233
234#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
235#[serde(deny_unknown_fields)]
236pub struct PreparedOperationIdentity {
237    normalized_arguments_digest: Sha256Digest,
238    #[schemars(length(min = 1, max = 256))]
239    capabilities: Vec<ApprovalCapability>,
240    #[schemars(length(min = 1, max = 256))]
241    resources: Vec<PreparedApprovalResource>,
242    execution_context_digest: Sha256Digest,
243    capability_lifetime: PreparedCapabilityLifetime,
244}
245impl PreparedOperationIdentity {
246    pub fn new(
247        normalized_arguments_digest: Sha256Digest,
248        capabilities: Vec<ApprovalCapability>,
249        resources: Vec<PreparedApprovalResource>,
250        execution_context_digest: Sha256Digest,
251    ) -> Result<Self, ApprovalSchemaError> {
252        if capabilities.is_empty() || capabilities.len() > 256 {
253            return Err(ApprovalSchemaError::MissingCapability);
254        }
255        if resources.is_empty() {
256            return Err(ApprovalSchemaError::MissingResource);
257        }
258        if resources.len() > 256 {
259            return Err(ApprovalSchemaError::TooManyResources);
260        }
261        if resources.iter().any(|resource| {
262            !capabilities
263                .iter()
264                .any(|capability| capability.action == resource.capability)
265        }) {
266            return Err(ApprovalSchemaError::UnboundResourceCapability);
267        }
268        let mut digests = resources
269            .iter()
270            .map(|resource| resource.binding_digest.digest().as_str())
271            .collect::<Vec<_>>();
272        digests.sort_unstable();
273        if digests.windows(2).any(|pair| pair[0] == pair[1]) {
274            return Err(ApprovalSchemaError::DuplicateResourceBinding);
275        }
276        Ok(Self {
277            normalized_arguments_digest,
278            capabilities,
279            resources,
280            execution_context_digest,
281            capability_lifetime: PreparedCapabilityLifetime::ProcessLocal,
282        })
283    }
284    #[must_use]
285    pub fn capabilities(&self) -> &[ApprovalCapability] {
286        &self.capabilities
287    }
288    #[must_use]
289    pub fn resources(&self) -> &[PreparedApprovalResource] {
290        &self.resources
291    }
292    fn canonical_fingerprint_bytes(&self) -> Vec<u8> {
293        let mut value =
294            serde_json::to_value(self).expect("protocol identity serialization cannot fail");
295        if let Some(resources) = value
296            .get_mut("resources")
297            .and_then(serde_json::Value::as_array_mut)
298        {
299            for resource in &mut *resources {
300                if let Some(object) = resource.as_object_mut() {
301                    object.remove("boundary");
302                    object.remove("source");
303                }
304            }
305            resources.sort_by_key(serde_json::Value::to_string);
306        }
307        if let Some(capabilities) = value
308            .get_mut("capabilities")
309            .and_then(serde_json::Value::as_array_mut)
310        {
311            capabilities.sort_by_key(serde_json::Value::to_string);
312        }
313        serde_json::to_vec(&value).expect("protocol identity serialization cannot fail")
314    }
315}
316impl<'de> Deserialize<'de> for PreparedOperationIdentity {
317    fn deserialize<D>(d: D) -> Result<Self, D::Error>
318    where
319        D: serde::Deserializer<'de>,
320    {
321        #[derive(Deserialize)]
322        #[serde(deny_unknown_fields)]
323        struct Wire {
324            normalized_arguments_digest: Sha256Digest,
325            capabilities: Vec<ApprovalCapability>,
326            resources: Vec<PreparedApprovalResource>,
327            execution_context_digest: Sha256Digest,
328            capability_lifetime: PreparedCapabilityLifetime,
329        }
330        let w = Wire::deserialize(d)?;
331        let value = Self::new(
332            w.normalized_arguments_digest,
333            w.capabilities,
334            w.resources,
335            w.execution_context_digest,
336        )
337        .map_err(serde::de::Error::custom)?;
338        if w.capability_lifetime != PreparedCapabilityLifetime::ProcessLocal {
339            return Err(serde::de::Error::custom(
340                "prepared capability lifetime must be process_local",
341            ));
342        }
343        Ok(value)
344    }
345}
346
347#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
348#[serde(deny_unknown_fields)]
349pub struct ApprovalConstraints {
350    pub allow_once: bool,
351    pub allow_tree_grant: bool,
352    pub cancellable: bool,
353    #[serde(deserialize_with = "crate::deserialize_required_option")]
354    #[schemars(with = "crate::NullableSchema<Timestamp>", required)]
355    pub expires_at: Option<Timestamp>,
356}
357
358#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
359#[serde(rename_all = "snake_case")]
360pub enum ApprovalReasonCode {
361    PolicyAllowed,
362    PolicyDenied,
363    PolicyRequiresApproval,
364    ModelRequested,
365    InternalAgentAllowed,
366    InternalAgentDenied,
367    AutoApproveNRejected,
368    AutoApproveYApproved,
369    YoloApproved,
370    TreeGrantMatched,
371    UserApprovedOnce,
372    UserApprovedTree,
373    UserRejected,
374    UserCancelled,
375    RequestCancelled,
376    DoomLoopDetected,
377    ConstraintViolation,
378    OperationChanged,
379    PreparedCapabilityLost,
380    UnsupportedPlatform,
381    ApprovalExpired,
382    Escalated,
383    Unattended,
384    SystemError,
385}
386#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
387#[serde(rename_all = "snake_case")]
388pub enum ApprovalDecisionSource {
389    Policy,
390    Model,
391    InternalAgent,
392    PermissionMode,
393    TreeGrant,
394    User,
395    DoomLoopGuard,
396    System,
397}
398#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
399#[serde(rename_all = "snake_case")]
400pub enum ApprovalStatus {
401    Pending,
402    Escalated,
403    Approved,
404    Rejected,
405    Cancelled,
406    Expired,
407}
408
409#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
410#[serde(deny_unknown_fields)]
411pub struct ApprovalFeedback {
412    pub message: SafeErrorMessage,
413}
414#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
415#[serde(rename_all = "snake_case")]
416pub enum ApprovalInternalDecisionKind {
417    Allow,
418    Deny,
419    Ask,
420    Escalate,
421}
422#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
423#[serde(deny_unknown_fields)]
424pub struct ApprovalInternalDecision {
425    pub decision: ApprovalInternalDecisionKind,
426    pub source: ApprovalDecisionSource,
427    pub reason_code: ApprovalReasonCode,
428    #[schemars(length(min = 1, max = 256))]
429    pub evaluations: Vec<ApprovalEvaluation>,
430}
431impl ApprovalInternalDecision {
432    pub fn validate(&self) -> Result<(), ApprovalSchemaError> {
433        if self.evaluations.is_empty() {
434            return Err(ApprovalSchemaError::MissingEvaluation);
435        }
436        if self.evaluations.len() > 256 {
437            return Err(ApprovalSchemaError::TooManyEvaluations);
438        }
439        let coherent = match self.decision {
440            ApprovalInternalDecisionKind::Allow => matches!(
441                self.reason_code,
442                ApprovalReasonCode::PolicyAllowed
443                    | ApprovalReasonCode::InternalAgentAllowed
444                    | ApprovalReasonCode::YoloApproved
445                    | ApprovalReasonCode::TreeGrantMatched
446            ),
447            ApprovalInternalDecisionKind::Deny => matches!(
448                self.reason_code,
449                ApprovalReasonCode::PolicyDenied
450                    | ApprovalReasonCode::InternalAgentDenied
451                    | ApprovalReasonCode::DoomLoopDetected
452                    | ApprovalReasonCode::ConstraintViolation
453                    | ApprovalReasonCode::OperationChanged
454                    | ApprovalReasonCode::PreparedCapabilityLost
455                    | ApprovalReasonCode::UnsupportedPlatform
456                    | ApprovalReasonCode::Unattended
457                    | ApprovalReasonCode::SystemError
458            ),
459            ApprovalInternalDecisionKind::Ask => matches!(
460                self.reason_code,
461                ApprovalReasonCode::PolicyRequiresApproval | ApprovalReasonCode::ModelRequested
462            ),
463            ApprovalInternalDecisionKind::Escalate => {
464                self.reason_code == ApprovalReasonCode::Escalated
465            }
466        };
467        let source_coherent = match self.reason_code {
468            ApprovalReasonCode::PolicyAllowed
469            | ApprovalReasonCode::PolicyDenied
470            | ApprovalReasonCode::PolicyRequiresApproval
471            | ApprovalReasonCode::YoloApproved => self.source == ApprovalDecisionSource::Policy,
472            ApprovalReasonCode::ModelRequested => self.source == ApprovalDecisionSource::Model,
473            ApprovalReasonCode::InternalAgentAllowed | ApprovalReasonCode::InternalAgentDenied => {
474                self.source == ApprovalDecisionSource::InternalAgent
475            }
476            ApprovalReasonCode::TreeGrantMatched => {
477                self.source == ApprovalDecisionSource::TreeGrant
478            }
479            ApprovalReasonCode::DoomLoopDetected => {
480                self.source == ApprovalDecisionSource::DoomLoopGuard
481            }
482            ApprovalReasonCode::Escalated => !matches!(
483                self.source,
484                ApprovalDecisionSource::TreeGrant
485                    | ApprovalDecisionSource::User
486                    | ApprovalDecisionSource::PermissionMode
487                    | ApprovalDecisionSource::DoomLoopGuard
488            ),
489            _ => self.source == ApprovalDecisionSource::System,
490        };
491        if coherent && source_coherent {
492            Ok(())
493        } else {
494            Err(ApprovalSchemaError::ContradictoryDecision)
495        }
496    }
497}
498impl<'de> Deserialize<'de> for ApprovalInternalDecision {
499    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
500    where
501        D: serde::Deserializer<'de>,
502    {
503        #[derive(Deserialize)]
504        #[serde(deny_unknown_fields)]
505        struct Wire {
506            decision: ApprovalInternalDecisionKind,
507            source: ApprovalDecisionSource,
508            reason_code: ApprovalReasonCode,
509            evaluations: Vec<ApprovalEvaluation>,
510        }
511        let wire = Wire::deserialize(deserializer)?;
512        let value = Self {
513            decision: wire.decision,
514            source: wire.source,
515            reason_code: wire.reason_code,
516            evaluations: wire.evaluations,
517        };
518        value.validate().map_err(serde::de::Error::custom)?;
519        Ok(value)
520    }
521}
522#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
523#[serde(rename_all = "snake_case")]
524pub enum ApprovalUserDecision {
525    ApproveOnce,
526    ApproveTree,
527    Reject,
528    Cancel,
529}
530#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
531#[serde(rename_all = "snake_case")]
532pub enum ApprovalFinalOutcome {
533    Approved,
534    Rejected,
535    Cancelled,
536    Expired,
537}
538#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
539#[serde(deny_unknown_fields)]
540pub struct ApprovalFinalDecision {
541    pub outcome: ApprovalFinalOutcome,
542    pub source: ApprovalDecisionSource,
543    pub reason_code: ApprovalReasonCode,
544    #[serde(deserialize_with = "crate::deserialize_required_option")]
545    #[schemars(with = "crate::NullableSchema<ApprovalFeedback>", required)]
546    pub feedback: Option<ApprovalFeedback>,
547    #[serde(deserialize_with = "crate::deserialize_required_option")]
548    #[schemars(with = "crate::NullableSchema<TreeApprovalGrantId>", required)]
549    pub tree_grant_id: Option<TreeApprovalGrantId>,
550}
551impl ApprovalFinalDecision {
552    pub fn validate(&self) -> Result<(), ApprovalSchemaError> {
553        let outcome_coherent = match self.outcome {
554            ApprovalFinalOutcome::Approved => matches!(
555                self.reason_code,
556                ApprovalReasonCode::PolicyAllowed
557                    | ApprovalReasonCode::InternalAgentAllowed
558                    | ApprovalReasonCode::AutoApproveYApproved
559                    | ApprovalReasonCode::YoloApproved
560                    | ApprovalReasonCode::TreeGrantMatched
561                    | ApprovalReasonCode::UserApprovedOnce
562                    | ApprovalReasonCode::UserApprovedTree
563            ),
564            ApprovalFinalOutcome::Rejected => matches!(
565                self.reason_code,
566                ApprovalReasonCode::PolicyDenied
567                    | ApprovalReasonCode::InternalAgentDenied
568                    | ApprovalReasonCode::AutoApproveNRejected
569                    | ApprovalReasonCode::UserRejected
570                    | ApprovalReasonCode::DoomLoopDetected
571                    | ApprovalReasonCode::ConstraintViolation
572                    | ApprovalReasonCode::UnsupportedPlatform
573                    | ApprovalReasonCode::Unattended
574                    | ApprovalReasonCode::SystemError
575            ),
576            ApprovalFinalOutcome::Cancelled => matches!(
577                self.reason_code,
578                ApprovalReasonCode::UserCancelled
579                    | ApprovalReasonCode::RequestCancelled
580                    | ApprovalReasonCode::OperationChanged
581                    | ApprovalReasonCode::PreparedCapabilityLost
582            ),
583            ApprovalFinalOutcome::Expired => {
584                self.reason_code == ApprovalReasonCode::ApprovalExpired
585            }
586        };
587        let source_coherent = match self.reason_code {
588            ApprovalReasonCode::UserApprovedOnce
589            | ApprovalReasonCode::UserApprovedTree
590            | ApprovalReasonCode::UserRejected
591            | ApprovalReasonCode::UserCancelled => self.source == ApprovalDecisionSource::User,
592            ApprovalReasonCode::PolicyAllowed
593            | ApprovalReasonCode::PolicyDenied
594            | ApprovalReasonCode::YoloApproved => self.source == ApprovalDecisionSource::Policy,
595            ApprovalReasonCode::InternalAgentAllowed | ApprovalReasonCode::InternalAgentDenied => {
596                self.source == ApprovalDecisionSource::InternalAgent
597            }
598            ApprovalReasonCode::AutoApproveNRejected | ApprovalReasonCode::AutoApproveYApproved => {
599                self.source == ApprovalDecisionSource::PermissionMode
600            }
601            ApprovalReasonCode::TreeGrantMatched => {
602                self.source == ApprovalDecisionSource::TreeGrant
603            }
604            ApprovalReasonCode::DoomLoopDetected => {
605                self.source == ApprovalDecisionSource::DoomLoopGuard
606            }
607            _ => self.source == ApprovalDecisionSource::System,
608        };
609        let grant_coherent = self.tree_grant_id.is_some()
610            == matches!(
611                self.reason_code,
612                ApprovalReasonCode::UserApprovedTree | ApprovalReasonCode::TreeGrantMatched
613            );
614        if outcome_coherent && source_coherent && grant_coherent {
615            Ok(())
616        } else {
617            Err(ApprovalSchemaError::ContradictoryDecision)
618        }
619    }
620}
621impl<'de> Deserialize<'de> for ApprovalFinalDecision {
622    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
623    where
624        D: serde::Deserializer<'de>,
625    {
626        #[derive(Deserialize)]
627        #[serde(deny_unknown_fields)]
628        struct Wire {
629            outcome: ApprovalFinalOutcome,
630            source: ApprovalDecisionSource,
631            reason_code: ApprovalReasonCode,
632            #[serde(deserialize_with = "crate::deserialize_required_option")]
633            feedback: Option<ApprovalFeedback>,
634            #[serde(deserialize_with = "crate::deserialize_required_option")]
635            tree_grant_id: Option<TreeApprovalGrantId>,
636        }
637        let wire = Wire::deserialize(deserializer)?;
638        let value = Self {
639            outcome: wire.outcome,
640            source: wire.source,
641            reason_code: wire.reason_code,
642            feedback: wire.feedback,
643            tree_grant_id: wire.tree_grant_id,
644        };
645        value.validate().map_err(serde::de::Error::custom)?;
646        Ok(value)
647    }
648}
649
650#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
651#[serde(deny_unknown_fields)]
652pub struct ApprovalRequest {
653    approval_id: ApprovalId,
654    #[schemars(range(min = 1))]
655    revision: u64,
656    trigger: ApprovalTrigger,
657    operation_fingerprint: OperationFingerprint,
658    operation: PreparedOperationIdentity,
659    #[schemars(length(min = 1, max = 256))]
660    evaluations: Vec<ApprovalEvaluation>,
661    constraints: ApprovalConstraints,
662}
663impl ApprovalRequest {
664    pub fn new(
665        approval_id: ApprovalId,
666        revision: u64,
667        trigger: ApprovalTrigger,
668        operation: PreparedOperationIdentity,
669        evaluations: Vec<ApprovalEvaluation>,
670        constraints: ApprovalConstraints,
671    ) -> Result<Self, ApprovalSchemaError> {
672        if revision == 0 {
673            return Err(ApprovalSchemaError::ZeroRevision);
674        }
675        if evaluations.is_empty() {
676            return Err(ApprovalSchemaError::MissingEvaluation);
677        }
678        if evaluations.len() > 256 {
679            return Err(ApprovalSchemaError::TooManyEvaluations);
680        }
681        let mut resources = operation
682            .resources()
683            .iter()
684            .map(|resource| resource.binding_digest.digest().as_str())
685            .collect::<Vec<_>>();
686        resources.sort_unstable();
687        let mut evaluated = evaluations
688            .iter()
689            .map(|evaluation| evaluation.resource_digest.digest().as_str())
690            .collect::<Vec<_>>();
691        evaluated.sort_unstable();
692        if resources != evaluated {
693            return Err(ApprovalSchemaError::IncompleteEvaluations);
694        }
695        if constraints.allow_tree_grant
696            && operation.resources().iter().any(|resource| {
697                resource.binding_lifetime == PreparedBindingLifetime::ProcessLocal
698                    || matches!(
699                        resource.capability,
700                        PermissionAction::Read | PermissionAction::Write
701                    )
702            })
703        {
704            return Err(ApprovalSchemaError::InvalidTreeGrantConstraint);
705        }
706        let operation_fingerprint = OperationFingerprint::from_prepared_operation(&operation);
707        Ok(Self {
708            approval_id,
709            revision,
710            trigger,
711            operation_fingerprint,
712            operation,
713            evaluations,
714            constraints,
715        })
716    }
717    #[must_use]
718    pub const fn approval_id(&self) -> ApprovalId {
719        self.approval_id
720    }
721    #[must_use]
722    pub const fn operation_fingerprint(&self) -> &OperationFingerprint {
723        &self.operation_fingerprint
724    }
725    #[must_use]
726    pub const fn operation(&self) -> &PreparedOperationIdentity {
727        &self.operation
728    }
729}
730impl<'de> Deserialize<'de> for ApprovalRequest {
731    fn deserialize<D>(d: D) -> Result<Self, D::Error>
732    where
733        D: serde::Deserializer<'de>,
734    {
735        #[derive(Deserialize)]
736        #[serde(deny_unknown_fields)]
737        struct Wire {
738            approval_id: ApprovalId,
739            revision: u64,
740            trigger: ApprovalTrigger,
741            operation_fingerprint: OperationFingerprint,
742            operation: PreparedOperationIdentity,
743            evaluations: Vec<ApprovalEvaluation>,
744            constraints: ApprovalConstraints,
745        }
746        let w = Wire::deserialize(d)?;
747        let fingerprint = w.operation_fingerprint.clone();
748        let value = Self::new(
749            w.approval_id,
750            w.revision,
751            w.trigger,
752            w.operation,
753            w.evaluations,
754            w.constraints,
755        )
756        .map_err(serde::de::Error::custom)?;
757        if value.operation_fingerprint != fingerprint {
758            return Err(serde::de::Error::custom(
759                "approval operation fingerprint mismatch",
760            ));
761        }
762        Ok(value)
763    }
764}
765
766#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
767#[serde(deny_unknown_fields)]
768pub struct TreeApprovalGrant {
769    pub grant_id: TreeApprovalGrantId,
770    pub root_session_id: SessionId,
771    pub approval_id: ApprovalId,
772    pub operation_fingerprint: OperationFingerprint,
773    #[schemars(length(min = 1, max = 256))]
774    pub capabilities: Vec<ApprovalCapability>,
775    #[schemars(length(min = 1, max = 256))]
776    pub resources: Vec<PreparedApprovalResource>,
777    pub created_at: Timestamp,
778}
779impl TreeApprovalGrant {
780    pub fn validate(&self) -> Result<(), ApprovalSchemaError> {
781        if self.capabilities.is_empty() || self.capabilities.len() > 256 {
782            return Err(ApprovalSchemaError::MissingCapability);
783        }
784        if self.resources.is_empty() {
785            return Err(ApprovalSchemaError::MissingResource);
786        }
787        if self.resources.len() > 256 {
788            return Err(ApprovalSchemaError::TooManyResources);
789        }
790        if self
791            .resources
792            .iter()
793            .any(|resource| resource.binding_lifetime != PreparedBindingLifetime::RestartStable)
794        {
795            return Err(ApprovalSchemaError::ProcessLocalTreeGrant);
796        }
797        if self.resources.iter().any(|resource| {
798            matches!(
799                resource.capability,
800                PermissionAction::Read | PermissionAction::Write
801            )
802        }) {
803            return Err(ApprovalSchemaError::FilesystemTreeGrant);
804        }
805        if self.resources.iter().any(|resource| {
806            !self
807                .capabilities
808                .iter()
809                .any(|capability| capability.action == resource.capability)
810        }) {
811            return Err(ApprovalSchemaError::UnboundResourceCapability);
812        }
813        Ok(())
814    }
815}
816impl<'de> Deserialize<'de> for TreeApprovalGrant {
817    fn deserialize<D>(d: D) -> Result<Self, D::Error>
818    where
819        D: serde::Deserializer<'de>,
820    {
821        #[derive(Deserialize)]
822        #[serde(deny_unknown_fields)]
823        struct Wire {
824            grant_id: TreeApprovalGrantId,
825            root_session_id: SessionId,
826            approval_id: ApprovalId,
827            operation_fingerprint: OperationFingerprint,
828            capabilities: Vec<ApprovalCapability>,
829            resources: Vec<PreparedApprovalResource>,
830            created_at: Timestamp,
831        }
832        let w = Wire::deserialize(d)?;
833        let value = Self {
834            grant_id: w.grant_id,
835            root_session_id: w.root_session_id,
836            approval_id: w.approval_id,
837            operation_fingerprint: w.operation_fingerprint,
838            capabilities: w.capabilities,
839            resources: w.resources,
840            created_at: w.created_at,
841        };
842        value.validate().map_err(serde::de::Error::custom)?;
843        Ok(value)
844    }
845}
846
847#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
848#[serde(deny_unknown_fields)]
849pub struct ApprovalRecord {
850    pub session_id: SessionId,
851    pub request: ApprovalRequest,
852    pub status: ApprovalStatus,
853    #[serde(deserialize_with = "crate::deserialize_required_option")]
854    #[schemars(with = "crate::NullableSchema<ApprovalInternalDecision>", required)]
855    pub internal_decision: Option<ApprovalInternalDecision>,
856    #[serde(deserialize_with = "crate::deserialize_required_option")]
857    #[schemars(with = "crate::NullableSchema<ApprovalUserDecision>", required)]
858    pub user_decision: Option<ApprovalUserDecision>,
859    #[serde(deserialize_with = "crate::deserialize_required_option")]
860    #[schemars(with = "crate::NullableSchema<ApprovalFinalDecision>", required)]
861    pub final_decision: Option<ApprovalFinalDecision>,
862}
863impl ApprovalRecord {
864    pub fn validate(&self) -> Result<(), ApprovalSchemaError> {
865        if let Some(internal) = &self.internal_decision {
866            internal.validate()?;
867        }
868        if let Some(final_decision) = &self.final_decision {
869            final_decision.validate()?;
870        }
871        let coherent = match self.status {
872            ApprovalStatus::Pending => {
873                self.user_decision.is_none()
874                    && self.final_decision.is_none()
875                    && self.internal_decision.as_ref().is_none_or(|decision| {
876                        decision.decision == ApprovalInternalDecisionKind::Ask
877                    })
878            }
879            ApprovalStatus::Escalated => {
880                self.internal_decision.as_ref().is_some_and(|decision| {
881                    decision.decision == ApprovalInternalDecisionKind::Escalate
882                }) && self.user_decision.is_none()
883                    && self.final_decision.is_none()
884            }
885            ApprovalStatus::Approved => self
886                .final_decision
887                .as_ref()
888                .is_some_and(|decision| decision.outcome == ApprovalFinalOutcome::Approved),
889            ApprovalStatus::Rejected => self
890                .final_decision
891                .as_ref()
892                .is_some_and(|decision| decision.outcome == ApprovalFinalOutcome::Rejected),
893            ApprovalStatus::Cancelled => self
894                .final_decision
895                .as_ref()
896                .is_some_and(|decision| decision.outcome == ApprovalFinalOutcome::Cancelled),
897            ApprovalStatus::Expired => self
898                .final_decision
899                .as_ref()
900                .is_some_and(|decision| decision.outcome == ApprovalFinalOutcome::Expired),
901        };
902        let decisions_coherent = match &self.final_decision {
903            None => self.user_decision.is_none(),
904            Some(final_decision) if final_decision.source == ApprovalDecisionSource::User => {
905                self.internal_decision.as_ref().is_some_and(|decision| {
906                    decision.decision == ApprovalInternalDecisionKind::Escalate
907                }) && self.user_decision.is_some_and(|user| {
908                    matches!(
909                        (user, final_decision.outcome, final_decision.reason_code),
910                        (
911                            ApprovalUserDecision::ApproveOnce,
912                            ApprovalFinalOutcome::Approved,
913                            ApprovalReasonCode::UserApprovedOnce
914                        ) | (
915                            ApprovalUserDecision::ApproveTree,
916                            ApprovalFinalOutcome::Approved,
917                            ApprovalReasonCode::UserApprovedTree
918                        ) | (
919                            ApprovalUserDecision::Reject,
920                            ApprovalFinalOutcome::Rejected,
921                            ApprovalReasonCode::UserRejected
922                        ) | (
923                            ApprovalUserDecision::Cancel,
924                            ApprovalFinalOutcome::Cancelled,
925                            ApprovalReasonCode::UserCancelled
926                        )
927                    )
928                })
929            }
930            Some(final_decision)
931                if final_decision.source == ApprovalDecisionSource::PermissionMode =>
932            {
933                self.user_decision.is_none()
934                    && self.internal_decision.as_ref().is_some_and(|internal| {
935                        internal.decision == ApprovalInternalDecisionKind::Escalate
936                            && internal.source == ApprovalDecisionSource::InternalAgent
937                            && internal.reason_code == ApprovalReasonCode::Escalated
938                    })
939            }
940            Some(final_decision)
941                if matches!(
942                    final_decision.outcome,
943                    ApprovalFinalOutcome::Approved | ApprovalFinalOutcome::Rejected
944                ) =>
945            {
946                self.user_decision.is_none()
947                    && self.internal_decision.as_ref().is_some_and(|internal| {
948                        internal.source == final_decision.source
949                            && internal.reason_code == final_decision.reason_code
950                            && matches!(
951                                (internal.decision, final_decision.outcome),
952                                (
953                                    ApprovalInternalDecisionKind::Allow,
954                                    ApprovalFinalOutcome::Approved
955                                ) | (
956                                    ApprovalInternalDecisionKind::Deny,
957                                    ApprovalFinalOutcome::Rejected
958                                )
959                            )
960                    })
961            }
962            Some(_) => self.user_decision.is_none() && self.internal_decision.is_none(),
963        };
964        if coherent && decisions_coherent {
965            Ok(())
966        } else {
967            Err(ApprovalSchemaError::ContradictoryLifecycle)
968        }
969    }
970}
971impl<'de> Deserialize<'de> for ApprovalRecord {
972    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
973    where
974        D: serde::Deserializer<'de>,
975    {
976        #[derive(Deserialize)]
977        #[serde(deny_unknown_fields)]
978        struct Wire {
979            session_id: SessionId,
980            request: ApprovalRequest,
981            status: ApprovalStatus,
982            #[serde(deserialize_with = "crate::deserialize_required_option")]
983            internal_decision: Option<ApprovalInternalDecision>,
984            #[serde(deserialize_with = "crate::deserialize_required_option")]
985            user_decision: Option<ApprovalUserDecision>,
986            #[serde(deserialize_with = "crate::deserialize_required_option")]
987            final_decision: Option<ApprovalFinalDecision>,
988        }
989        let wire = Wire::deserialize(deserializer)?;
990        let value = Self {
991            session_id: wire.session_id,
992            request: wire.request,
993            status: wire.status,
994            internal_decision: wire.internal_decision,
995            user_decision: wire.user_decision,
996            final_decision: wire.final_decision,
997        };
998        value.validate().map_err(serde::de::Error::custom)?;
999        Ok(value)
1000    }
1001}
1002
1003#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1004pub enum ApprovalSchemaError {
1005    InvalidPreparedIdentity,
1006    MissingCapability,
1007    MissingResource,
1008    TooManyResources,
1009    UnboundResourceCapability,
1010    DuplicateResourceBinding,
1011    ZeroRevision,
1012    MissingEvaluation,
1013    IncompleteEvaluations,
1014    InvalidTreeGrantConstraint,
1015    ProcessLocalTreeGrant,
1016    FilesystemTreeGrant,
1017    TooManyEvaluations,
1018    ContradictoryDecision,
1019    ContradictoryLifecycle,
1020}
1021impl fmt::Display for ApprovalSchemaError {
1022    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1023        f.write_str(match self {
1024            Self::InvalidPreparedIdentity => "invalid stable prepared resource identity",
1025            Self::MissingCapability => "prepared operation requires at least one capability",
1026            Self::MissingResource => "prepared operation requires at least one resource",
1027            Self::TooManyResources => "prepared operation exceeds 256 resources",
1028            Self::UnboundResourceCapability => {
1029                "prepared resource capability is not present in operation capabilities"
1030            }
1031            Self::DuplicateResourceBinding => "prepared resource binding digests must be unique",
1032            Self::ZeroRevision => "approval revision must be positive",
1033            Self::MissingEvaluation => "approval requires at least one evaluation",
1034            Self::IncompleteEvaluations => {
1035                "approval evaluations must exactly cover prepared resources"
1036            }
1037            Self::InvalidTreeGrantConstraint => {
1038                "process-local or filesystem resources cannot allow tree grants"
1039            }
1040            Self::ProcessLocalTreeGrant => "tree grants require restart-stable resources",
1041            Self::FilesystemTreeGrant => "filesystem resources cannot be durable tree grants",
1042            Self::TooManyEvaluations => "approval evaluations exceed 256 entries",
1043            Self::ContradictoryDecision => {
1044                "approval decision source, reason, and outcome contradict"
1045            }
1046            Self::ContradictoryLifecycle => "approval lifecycle status and decisions contradict",
1047        })
1048    }
1049}
1050impl std::error::Error for ApprovalSchemaError {}