Skip to main content

semantic_memory/
origin_authority.rs

1//! Immutable origin labels and laundering-resistant governed-access decisions.
2
3use crate::{Fact, SearchReplayReportV1, SearchResult, StoredGraphEdge};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7pub const ORIGIN_AUTHORITY_LABEL_V1: &str = "origin_authority_label_v1";
8pub const ORIGIN_AUTHORITY_DECISION_V1: &str = "origin_authority_decision_v1";
9pub const GOVERNED_ACCESS_POLICY_V1: &str = "governed_access_policy_v1";
10
11/// A principal whose data, consent, or delegated authority is being used.
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct SubjectPrincipalV1(pub String);
15
16impl SubjectPrincipalV1 {
17    pub fn new(value: impl Into<String>) -> Result<Self, String> {
18        let value = value.into();
19        if value.trim().is_empty() {
20            Err("subject principal must be non-empty".into())
21        } else {
22            Ok(Self(value))
23        }
24    }
25}
26
27/// The authenticated principal making an access request now.
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
29#[serde(transparent)]
30pub struct CallerPrincipalV1(pub String);
31
32impl CallerPrincipalV1 {
33    pub fn new(value: impl Into<String>) -> Result<Self, String> {
34        let value = value.into();
35        if value.trim().is_empty() {
36            Err("caller principal must be non-empty".into())
37        } else {
38            Ok(Self(value))
39        }
40    }
41}
42
43/// The complete audience asserted at access time. It is a set, never a single ambiguous string.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
45pub struct AudienceV1(pub Vec<String>);
46
47impl AudienceV1 {
48    pub fn new(mut values: Vec<String>) -> Self {
49        values.retain(|value| !value.trim().is_empty());
50        values.sort();
51        values.dedup();
52        Self(values)
53    }
54
55    fn intersects(&self, other: &Self) -> bool {
56        self.0.iter().any(|value| other.0.contains(value))
57    }
58}
59
60/// Exact resource scope for v1. Optional fields must agree whenever both sides name them.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
62pub struct NamespaceScopeV1 {
63    pub namespace: String,
64    pub domain: Option<String>,
65    pub workspace_id: Option<String>,
66    pub repo_id: Option<String>,
67}
68
69impl NamespaceScopeV1 {
70    pub fn exact(namespace: impl Into<String>) -> Self {
71        Self {
72            namespace: namespace.into(),
73            ..Self::default()
74        }
75    }
76
77    pub fn is_bound(&self) -> bool {
78        !self.namespace.trim().is_empty()
79    }
80
81    fn permits_namespace(&self, namespace: &str) -> bool {
82        self.is_bound() && self.namespace == namespace
83    }
84
85    fn permits_scope(&self, requested: &Self) -> bool {
86        self.is_bound()
87            && requested.is_bound()
88            && self.namespace == requested.namespace
89            && optional_scope_matches(&self.domain, &requested.domain)
90            && optional_scope_matches(&self.workspace_id, &requested.workspace_id)
91            && optional_scope_matches(&self.repo_id, &requested.repo_id)
92    }
93}
94
95fn optional_scope_matches(resource: &Option<String>, requested: &Option<String>) -> bool {
96    match (resource, requested) {
97        (Some(resource), Some(requested)) => resource == requested,
98        // A request may not widen an explicitly constrained resource; an unconstrained resource
99        // likewise cannot be selected through a more-specific invented scope.
100        (None, None) => true,
101        _ => false,
102    }
103}
104
105/// Compatibility shape for a caller-carried lease request.
106///
107/// This value is never authority: the local crate has no issuer-controlled lease resolver, so
108/// governed evaluation fail-closes every delegation/elevation request that carries one.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct DelegationElevationLeaseV1 {
111    pub lease_id: String,
112    pub delegator: SubjectPrincipalV1,
113    pub delegatee: CallerPrincipalV1,
114    pub purposes: Vec<GovernedAccessPurposeV1>,
115    pub scope: NamespaceScopeV1,
116    pub audience: AudienceV1,
117    pub expires_at: String,
118    #[serde(default)]
119    pub revoked: bool,
120    pub elevation: bool,
121}
122
123impl DelegationElevationLeaseV1 {
124    pub fn delegation(
125        lease_id: impl Into<String>,
126        delegator: impl Into<String>,
127        delegatee: impl Into<String>,
128        purposes: Vec<GovernedAccessPurposeV1>,
129        scope: NamespaceScopeV1,
130        audience: Vec<String>,
131        expires_at: impl Into<String>,
132    ) -> Self {
133        Self {
134            lease_id: lease_id.into(),
135            delegator: SubjectPrincipalV1(delegator.into()),
136            delegatee: CallerPrincipalV1(delegatee.into()),
137            purposes,
138            scope,
139            audience: AudienceV1::new(audience),
140            expires_at: expires_at.into(),
141            revoked: false,
142            elevation: false,
143        }
144    }
145
146    pub fn elevation(
147        lease_id: impl Into<String>,
148        subject: impl Into<String>,
149        caller: impl Into<String>,
150        scope: NamespaceScopeV1,
151        expires_at: impl Into<String>,
152    ) -> Self {
153        Self {
154            lease_id: lease_id.into(),
155            delegator: SubjectPrincipalV1(subject.into()),
156            delegatee: CallerPrincipalV1(caller.into()),
157            purposes: vec![GovernedAccessPurposeV1::Admin],
158            scope,
159            audience: AudienceV1::default(),
160            expires_at: expires_at.into(),
161            revoked: false,
162            elevation: true,
163        }
164    }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum OriginRiskV1 {
170    Low,
171    Medium,
172    High,
173    Critical,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub enum AuthorityScopeV1 {
179    Denied,
180    PrincipalOnly,
181    Audience,
182    Universal,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct AuthorityScopesV1 {
187    pub recall: AuthorityScopeV1,
188    pub assertion: AuthorityScopeV1,
189    pub action: AuthorityScopeV1,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum OriginClassV1 {
195    UserStatement,
196    ExternalEvidence,
197    ToolOutput,
198    OperatorSystem,
199    Derived,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204pub enum ElevationRequirementV1 {
205    Never,
206    ExplicitOperatorApproval,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum RevocationStatusV1 {
212    Active,
213    PendingReview,
214    Revoked,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "snake_case")]
219pub enum OriginDerivationKindV1 {
220    Summary,
221    Rephrase,
222    TrustedToolEcho,
223    Corroboration,
224    Other,
225}
226
227/// Immutable label fixed at the canonical write boundary.
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229pub struct OriginAuthorityLabelV1 {
230    pub schema_version: String,
231    pub origin_class: OriginClassV1,
232    pub origin_principal: String,
233    pub origin_channel: String,
234    pub origin_digest: String,
235    pub risk: OriginRiskV1,
236    pub scopes: AuthorityScopesV1,
237    pub elevation: ElevationRequirementV1,
238    pub revocation_reference: Option<String>,
239    pub revocation_status: RevocationStatusV1,
240    pub audience: Vec<String>,
241    pub ancestor_digests: Vec<String>,
242    pub derivation_kind: Option<OriginDerivationKindV1>,
243    /// Principal the assertion is about. Kept separate from the write principal so a caller
244    /// cannot substitute a subject merely by presenting an otherwise valid audience.
245    #[serde(default)]
246    pub subject_principal: Option<SubjectPrincipalV1>,
247    /// Audience captured at write time. `audience` above remains for decoding V1 records.
248    #[serde(default)]
249    pub audience_at_write: AudienceV1,
250    /// Immutable resource scope captured at the canonical write boundary.
251    #[serde(default)]
252    pub resource_scope: NamespaceScopeV1,
253    #[serde(default = "default_policy_version")]
254    pub policy_version: String,
255    #[serde(default)]
256    pub policy_digest: String,
257}
258
259fn default_policy_version() -> String {
260    GOVERNED_ACCESS_POLICY_V1.into()
261}
262
263impl OriginAuthorityLabelV1 {
264    #[allow(clippy::too_many_arguments)]
265    pub fn new(
266        origin_class: OriginClassV1,
267        origin_principal: impl Into<String>,
268        origin_channel: impl Into<String>,
269        origin_digest: impl Into<String>,
270        risk: OriginRiskV1,
271        scopes: AuthorityScopesV1,
272        elevation: ElevationRequirementV1,
273        revocation_reference: Option<String>,
274        revocation_status: RevocationStatusV1,
275        mut audience: Vec<String>,
276    ) -> Result<Self, String> {
277        let origin_principal = origin_principal.into();
278        let origin_channel = origin_channel.into();
279        let origin_digest = origin_digest.into();
280        if origin_principal.trim().is_empty()
281            || origin_channel.trim().is_empty()
282            || origin_digest.trim().is_empty()
283        {
284            return Err("origin principal, channel, and digest must be non-empty".into());
285        }
286        if revocation_status != RevocationStatusV1::Active
287            && revocation_reference.as_deref().map_or(true, str::is_empty)
288        {
289            return Err("non-active origin labels require a revocation reference".into());
290        }
291        audience.retain(|value| !value.trim().is_empty());
292        audience.sort();
293        audience.dedup();
294        let mut label = Self {
295            schema_version: ORIGIN_AUTHORITY_LABEL_V1.into(),
296            origin_class,
297            origin_principal,
298            origin_channel,
299            origin_digest,
300            risk,
301            scopes,
302            elevation,
303            revocation_reference,
304            revocation_status,
305            audience: audience.clone(),
306            ancestor_digests: Vec::new(),
307            derivation_kind: None,
308            subject_principal: None,
309            audience_at_write: AudienceV1::new(audience.clone()),
310            resource_scope: NamespaceScopeV1::default(),
311            policy_version: default_policy_version(),
312            policy_digest: String::new(),
313        };
314        label.refresh_policy_digest()?;
315        Ok(label)
316    }
317
318    pub fn with_subject_principal(mut self, subject: SubjectPrincipalV1) -> Self {
319        self.subject_principal = Some(subject);
320        self.refresh_policy_digest()
321            .expect("origin policy label serialization is infallible");
322        self
323    }
324
325    pub fn with_resource_scope(mut self, scope: NamespaceScopeV1) -> Self {
326        self.resource_scope = scope;
327        self.refresh_policy_digest()
328            .expect("origin policy label serialization is infallible");
329        self
330    }
331
332    pub(crate) fn bind_resource_scope(mut self, scope: NamespaceScopeV1) -> Result<Self, String> {
333        if !self.resource_scope.is_bound() {
334            self.resource_scope = scope;
335        }
336        self.refresh_policy_digest()?;
337        Ok(self)
338    }
339
340    fn effective_audience(&self) -> AudienceV1 {
341        if self.audience_at_write.0.is_empty() {
342            AudienceV1::new(self.audience.clone())
343        } else {
344            self.audience_at_write.clone()
345        }
346    }
347
348    fn effective_subject(&self) -> SubjectPrincipalV1 {
349        self.subject_principal
350            .clone()
351            .unwrap_or_else(|| SubjectPrincipalV1(self.origin_principal.clone()))
352    }
353
354    fn refresh_policy_digest(&mut self) -> Result<(), String> {
355        self.policy_digest = self.computed_policy_digest()?;
356        Ok(())
357    }
358
359    fn computed_policy_digest(&self) -> Result<String, String> {
360        let bytes = serde_json::to_vec(&(
361            &self.policy_version,
362            &self.origin_principal,
363            &self.subject_principal,
364            self.effective_audience(),
365            &self.resource_scope,
366            &self.scopes,
367            self.elevation,
368        ))
369        .map_err(|error| format!("serialize access policy label: {error}"))?;
370        Ok(format!("blake3:{}", blake3::hash(&bytes).to_hex()))
371    }
372
373    /// Deterministically derive a label using maximum ancestor risk and minimum authority.
374    pub fn derive(
375        ancestors: &[Self],
376        kind: OriginDerivationKindV1,
377        derived_content_digest: impl Into<String>,
378    ) -> Result<Self, String> {
379        if ancestors.is_empty() {
380            return Err("derived origin requires at least one ancestor".into());
381        }
382        let derived_content_digest = derived_content_digest.into();
383        if derived_content_digest.trim().is_empty() {
384            return Err("derived content digest must be non-empty".into());
385        }
386        let risk = ancestors.iter().map(|label| label.risk).max().unwrap();
387        let scopes = AuthorityScopesV1 {
388            recall: ancestors
389                .iter()
390                .map(|label| label.scopes.recall)
391                .min()
392                .unwrap(),
393            assertion: ancestors
394                .iter()
395                .map(|label| label.scopes.assertion)
396                .min()
397                .unwrap(),
398            action: ancestors
399                .iter()
400                .map(|label| label.scopes.action)
401                .min()
402                .unwrap(),
403        };
404        let principal = if ancestors
405            .iter()
406            .all(|label| label.origin_principal == ancestors[0].origin_principal)
407        {
408            ancestors[0].origin_principal.clone()
409        } else {
410            "multiple-origins".into()
411        };
412        let mut audience = ancestors[0].audience.clone();
413        for ancestor in &ancestors[1..] {
414            audience.retain(|entry| ancestor.audience.contains(entry));
415        }
416        let mut ancestor_digests = ancestors
417            .iter()
418            .map(label_digest)
419            .collect::<Result<Vec<_>, _>>()?;
420        ancestor_digests.sort();
421        let digest_input = serde_json::to_vec(&(
422            "origin-derivation-v1",
423            kind,
424            &derived_content_digest,
425            &ancestor_digests,
426            risk,
427            &scopes,
428        ))
429        .map_err(|error| format!("serialize origin derivation: {error}"))?;
430        let subject_principal = if ancestors
431            .iter()
432            .all(|label| label.effective_subject() == ancestors[0].effective_subject())
433        {
434            Some(ancestors[0].effective_subject())
435        } else {
436            None
437        };
438        let resource_scope = if ancestors
439            .iter()
440            .all(|label| label.resource_scope == ancestors[0].resource_scope)
441        {
442            ancestors[0].resource_scope.clone()
443        } else {
444            NamespaceScopeV1::default()
445        };
446        let mut label = Self {
447            schema_version: ORIGIN_AUTHORITY_LABEL_V1.into(),
448            origin_class: OriginClassV1::Derived,
449            origin_principal: principal,
450            origin_channel: format!("derived:{kind:?}").to_lowercase(),
451            origin_digest: format!("blake3:{}", blake3::hash(&digest_input).to_hex()),
452            risk,
453            scopes,
454            // Transformations never confer authority; a separate future promotion record would
455            // be required even when every ancestor was operator-originated.
456            elevation: ElevationRequirementV1::Never,
457            revocation_reference: ancestors
458                .iter()
459                .find_map(|label| label.revocation_reference.clone()),
460            revocation_status: ancestors
461                .iter()
462                .map(|label| label.revocation_status)
463                .max_by_key(|status| match status {
464                    RevocationStatusV1::Active => 0,
465                    RevocationStatusV1::PendingReview => 1,
466                    RevocationStatusV1::Revoked => 2,
467                })
468                .unwrap(),
469            audience: audience.clone(),
470            ancestor_digests,
471            derivation_kind: Some(kind),
472            subject_principal,
473            audience_at_write: AudienceV1::new(audience.clone()),
474            resource_scope,
475            policy_version: default_policy_version(),
476            policy_digest: String::new(),
477        };
478        label.refresh_policy_digest()?;
479        Ok(label)
480    }
481
482    pub fn operator_system(principal: &str, channel: &str) -> Self {
483        Self::new(
484            OriginClassV1::OperatorSystem,
485            principal,
486            channel,
487            format!(
488                "blake3:{}",
489                blake3::hash(format!("operator:{principal}:{channel}").as_bytes()).to_hex()
490            ),
491            OriginRiskV1::Low,
492            AuthorityScopesV1 {
493                recall: AuthorityScopeV1::Universal,
494                assertion: AuthorityScopeV1::Universal,
495                action: AuthorityScopeV1::Universal,
496            },
497            ElevationRequirementV1::ExplicitOperatorApproval,
498            None,
499            RevocationStatusV1::Active,
500            vec![principal.to_string()],
501        )
502        .expect("operator origin constants are valid")
503    }
504}
505
506pub fn label_digest(label: &OriginAuthorityLabelV1) -> Result<String, String> {
507    let bytes =
508        serde_json::to_vec(label).map_err(|error| format!("serialize origin label: {error}"))?;
509    Ok(format!("blake3:{}", blake3::hash(&bytes).to_hex()))
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
513pub struct OriginAuthorityRecordV1 {
514    pub fact_id: String,
515    pub label: OriginAuthorityLabelV1,
516    pub label_digest: String,
517    pub recorded_at: String,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
521#[serde(rename_all = "snake_case")]
522pub enum GovernedAccessPurposeV1 {
523    Recall,
524    Assertion,
525    Action,
526    Export,
527    Replay,
528    Admin,
529}
530
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532pub struct GovernedAccessRequestV1 {
533    /// Compatibility mirror of `caller.0`; governed evaluation rejects disagreement.
534    pub principal: String,
535    /// Compatibility mirror of the first typed audience member; it is never used as a fallback.
536    pub audience: String,
537    pub purpose: GovernedAccessPurposeV1,
538    /// Compatibility mirror of `scope.namespace`; governed evaluation rejects disagreement.
539    pub namespace: String,
540    pub caller: CallerPrincipalV1,
541    pub subject: SubjectPrincipalV1,
542    pub audiences: AudienceV1,
543    pub scope: NamespaceScopeV1,
544    pub delegation_or_elevation: Option<DelegationElevationLeaseV1>,
545    pub policy_version: String,
546    pub policy_digest: String,
547}
548
549impl GovernedAccessRequestV1 {
550    pub fn new(
551        principal: impl Into<String>,
552        audience: impl Into<String>,
553        purpose: GovernedAccessPurposeV1,
554        namespace: impl Into<String>,
555    ) -> Self {
556        let principal = principal.into();
557        let audience = audience.into();
558        let namespace = namespace.into();
559        Self::for_principals(
560            CallerPrincipalV1(principal.clone()),
561            SubjectPrincipalV1(principal),
562            vec![audience],
563            purpose,
564            NamespaceScopeV1::exact(namespace),
565        )
566    }
567
568    pub fn for_principals(
569        caller: CallerPrincipalV1,
570        subject: SubjectPrincipalV1,
571        audience: Vec<String>,
572        purpose: GovernedAccessPurposeV1,
573        scope: NamespaceScopeV1,
574    ) -> Self {
575        let audiences = AudienceV1::new(audience);
576        let policy_version = GOVERNED_ACCESS_POLICY_V1.to_string();
577        let policy_digest = access_request_digest(
578            &caller,
579            &subject,
580            &audiences,
581            purpose,
582            &scope,
583            None,
584            &policy_version,
585        );
586        Self {
587            principal: caller.0.clone(),
588            audience: audiences.0.first().cloned().unwrap_or_default(),
589            purpose,
590            namespace: scope.namespace.clone(),
591            caller,
592            subject,
593            audiences,
594            scope,
595            delegation_or_elevation: None,
596            policy_version,
597            policy_digest,
598        }
599    }
600
601    pub fn with_delegation_or_elevation(mut self, lease: DelegationElevationLeaseV1) -> Self {
602        self.delegation_or_elevation = Some(lease);
603        self.policy_digest = access_request_digest(
604            &self.caller,
605            &self.subject,
606            &self.audiences,
607            self.purpose,
608            &self.scope,
609            self.delegation_or_elevation.as_ref(),
610            &self.policy_version,
611        );
612        self
613    }
614
615    pub fn with_purpose(mut self, purpose: GovernedAccessPurposeV1) -> Self {
616        self.purpose = purpose;
617        self.policy_digest = access_request_digest(
618            &self.caller,
619            &self.subject,
620            &self.audiences,
621            self.purpose,
622            &self.scope,
623            self.delegation_or_elevation.as_ref(),
624            &self.policy_version,
625        );
626        self
627    }
628
629    pub fn with_audiences(mut self, audience: Vec<String>) -> Self {
630        self.audiences = AudienceV1::new(audience);
631        self.audience = self.audiences.0.first().cloned().unwrap_or_default();
632        self.policy_digest = access_request_digest(
633            &self.caller,
634            &self.subject,
635            &self.audiences,
636            self.purpose,
637            &self.scope,
638            self.delegation_or_elevation.as_ref(),
639            &self.policy_version,
640        );
641        self
642    }
643
644    pub fn with_lease_revoked(mut self) -> Self {
645        if let Some(lease) = &mut self.delegation_or_elevation {
646            lease.revoked = true;
647        }
648        self.policy_digest = access_request_digest(
649            &self.caller,
650            &self.subject,
651            &self.audiences,
652            self.purpose,
653            &self.scope,
654            self.delegation_or_elevation.as_ref(),
655            &self.policy_version,
656        );
657        self
658    }
659}
660
661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
662pub struct OriginAuthorityDecisionV1 {
663    pub schema_version: String,
664    pub fact_id: String,
665    pub principal: String,
666    pub audience_compat: String,
667    pub purpose: GovernedAccessPurposeV1,
668    pub allowed: bool,
669    pub reasons: Vec<String>,
670    pub origin_label_digest: Option<String>,
671    pub revocation_reference: Option<String>,
672    pub decision_digest: String,
673    pub caller: CallerPrincipalV1,
674    pub subject: SubjectPrincipalV1,
675    pub audience: AudienceV1,
676    pub scope: NamespaceScopeV1,
677    pub policy_version: String,
678    pub policy_digest: String,
679    pub outcome: PolicyDecisionV1,
680    pub lease_id: Option<String>,
681}
682
683/// Typed terminal outcome carried by every governed access decision receipt.
684#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
685#[serde(rename_all = "snake_case")]
686pub enum PolicyDecisionV1 {
687    Allow,
688    Deny,
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct GovernedFactAccessV1 {
693    pub fact: Option<Fact>,
694    pub decision: OriginAuthorityDecisionV1,
695    pub origin: Option<OriginAuthorityRecordV1>,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct GovernedSearchResponseV1 {
700    pub results: Vec<SearchResult>,
701    pub decisions: Vec<OriginAuthorityDecisionV1>,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize)]
705pub struct GovernedFactListResponseV1 {
706    pub facts: Vec<Fact>,
707    pub decisions: Vec<OriginAuthorityDecisionV1>,
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct GovernedGraphResponseV1 {
712    pub edges: Vec<StoredGraphEdge>,
713    pub decisions: Vec<OriginAuthorityDecisionV1>,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct GovernedReplayResponseV1 {
718    pub replay: SearchReplayReportV1,
719    pub allowed_result_ids: Vec<String>,
720    pub decisions: Vec<OriginAuthorityDecisionV1>,
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize)]
724pub struct GovernedStateResolutionResponseV1 {
725    pub response: crate::ResolvedMemoryAnswerV1,
726    pub decisions: Vec<OriginAuthorityDecisionV1>,
727}
728
729/// Projection rows have no implicit authority. Until an imported projection carries a durable
730/// origin label, governed reads return an empty collection plus denial receipts.
731#[derive(Debug, Clone, Serialize, Deserialize)]
732pub struct GovernedProjectionResponseV1<T> {
733    pub items: Vec<T>,
734    pub decisions: Vec<OriginAuthorityDecisionV1>,
735}
736
737pub(crate) fn decide(
738    fact_id: &str,
739    fact_namespace: Option<&str>,
740    origin: Option<&OriginAuthorityRecordV1>,
741    dynamic_revocation: Option<&str>,
742    request: &GovernedAccessRequestV1,
743) -> OriginAuthorityDecisionV1 {
744    let Some(record) = origin else {
745        return evaluate_governed_access_v1(
746            fact_id,
747            fact_namespace,
748            None,
749            dynamic_revocation,
750            request,
751        );
752    };
753    if label_digest(&record.label).ok().as_deref() != Some(record.label_digest.as_str()) {
754        // Make an inconsistent stored record fail in the canonical evaluator rather than
755        // treating the database row's stale digest as advisory metadata.
756        let mut inconsistent = record.label.clone();
757        inconsistent.policy_digest.clear();
758        return evaluate_governed_access_v1(
759            fact_id,
760            fact_namespace,
761            Some(&inconsistent),
762            dynamic_revocation,
763            request,
764        );
765    }
766    evaluate_governed_access_v1(
767        fact_id,
768        fact_namespace,
769        Some(&record.label),
770        dynamic_revocation,
771        request,
772    )
773}
774
775/// The sole policy evaluator for every governed read and mutation path. Callers may acquire
776/// candidates through caches, replay, graph traversal, or a direct ID, but only this function may
777/// authorize returning content. It deliberately has no permissive compatibility branch.
778pub fn evaluate_governed_access_v1(
779    resource_id: &str,
780    resource_namespace: Option<&str>,
781    label: Option<&OriginAuthorityLabelV1>,
782    dynamic_revocation: Option<&str>,
783    request: &GovernedAccessRequestV1,
784) -> OriginAuthorityDecisionV1 {
785    let mut reasons = Vec::new();
786    let mut allowed = true;
787    if request.policy_version != GOVERNED_ACCESS_POLICY_V1
788        || request.policy_digest
789            != access_request_digest(
790                &request.caller,
791                &request.subject,
792                &request.audiences,
793                request.purpose,
794                &request.scope,
795                request.delegation_or_elevation.as_ref(),
796                &request.policy_version,
797            )
798        || request.principal != request.caller.0
799        || request.namespace != request.scope.namespace
800        || request.audience != request.audiences.0.first().cloned().unwrap_or_default()
801        || request.caller.0.trim().is_empty()
802        || request.subject.0.trim().is_empty()
803        || request.audiences.0.is_empty()
804        || !request.scope.is_bound()
805    {
806        allowed = false;
807        reasons.push("invalid_access_request".into());
808    }
809    let Some(label) = label else {
810        allowed = false;
811        reasons.push("origin_absent".into());
812        return finish_decision(
813            resource_id,
814            request,
815            allowed,
816            reasons,
817            None,
818            dynamic_revocation,
819        );
820    };
821    if label.policy_version != GOVERNED_ACCESS_POLICY_V1
822        || label.computed_policy_digest().ok().as_deref() != Some(label.policy_digest.as_str())
823    {
824        allowed = false;
825        reasons.push("policy_label_inconsistent".into());
826    }
827    if resource_namespace != Some(request.scope.namespace.as_str())
828        || !label
829            .resource_scope
830            .permits_namespace(request.scope.namespace.as_str())
831        || !label.resource_scope.permits_scope(&request.scope)
832    {
833        allowed = false;
834        reasons.push("namespace_scope_mismatch".into());
835    }
836    let revocation_reference = dynamic_revocation
837        .map(str::to_string)
838        .or_else(|| label.revocation_reference.clone());
839    if dynamic_revocation.is_some() || label.revocation_status != RevocationStatusV1::Active {
840        allowed = false;
841        reasons.push("origin_revoked_or_pending".into());
842    }
843    let lease = request.delegation_or_elevation.as_ref();
844    let valid_delegation = validate_lease(lease, request, false, &mut reasons);
845    let valid_elevation = validate_lease(lease, request, true, &mut reasons);
846    if request.caller.0 != request.subject.0 && !valid_delegation && !valid_elevation {
847        allowed = false;
848        reasons.push("caller_subject_delegation_required".into());
849    }
850    let label_audience = label.effective_audience();
851    if !request.audiences.intersects(&label_audience) {
852        allowed = false;
853        reasons.push("audience_intersection_empty".into());
854    }
855    if label.origin_class == OriginClassV1::Derived
856        && label.subject_principal.is_none()
857        && label.origin_principal == "multiple-origins"
858    {
859        allowed = false;
860        reasons.push("cross_principal_derived_subject_ambiguous".into());
861    }
862    if request.purpose == GovernedAccessPurposeV1::Admin {
863        if !valid_elevation {
864            allowed = false;
865            reasons.push("admin_elevation_required".into());
866        }
867    } else {
868        let scope = match request.purpose {
869            GovernedAccessPurposeV1::Recall => label.scopes.recall,
870            GovernedAccessPurposeV1::Assertion => label.scopes.assertion,
871            GovernedAccessPurposeV1::Action => label.scopes.action,
872            GovernedAccessPurposeV1::Export | GovernedAccessPurposeV1::Replay => {
873                label.scopes.recall
874            }
875            GovernedAccessPurposeV1::Admin => unreachable!("admin is handled above"),
876        };
877        let in_scope = match scope {
878            AuthorityScopeV1::Denied => false,
879            AuthorityScopeV1::PrincipalOnly => request.caller.0 == label.origin_principal,
880            AuthorityScopeV1::Audience => request.audiences.intersects(&label_audience),
881            AuthorityScopeV1::Universal => true,
882        };
883        if !in_scope {
884            allowed = false;
885            reasons.push("scope_or_principal_denied".into());
886        }
887    }
888    if reasons.is_empty() {
889        reasons.push("origin_authority_satisfied".into());
890    }
891    finish_decision(
892        resource_id,
893        request,
894        allowed,
895        reasons,
896        label_digest(label).ok(),
897        revocation_reference.as_deref(),
898    )
899}
900
901fn validate_lease(
902    lease: Option<&DelegationElevationLeaseV1>,
903    request: &GovernedAccessRequestV1,
904    require_elevation: bool,
905    reasons: &mut Vec<String>,
906) -> bool {
907    let Some(lease) = lease else {
908        return false;
909    };
910    if lease.elevation != require_elevation {
911        return false;
912    }
913    if lease.revoked {
914        reasons.push("delegation_revoked".into());
915        return false;
916    }
917    let expiry = DateTime::parse_from_rfc3339(&lease.expires_at)
918        .ok()
919        .map(|value| value.with_timezone(&Utc));
920    if expiry.map_or(true, |expiry| expiry <= Utc::now()) {
921        reasons.push("delegation_expired".into());
922        return false;
923    }
924    if lease.lease_id.trim().is_empty()
925        || lease.delegator != request.subject
926        || lease.delegatee != request.caller
927        || !lease.scope.permits_scope(&request.scope)
928        || (!require_elevation && !lease.purposes.contains(&request.purpose))
929        || (!lease.audience.0.is_empty() && !request.audiences.intersects(&lease.audience))
930    {
931        reasons.push("delegation_scope_denied".into());
932        return false;
933    }
934    reasons.push("untrusted_caller_carried_lease".into());
935    false
936}
937
938fn access_request_digest(
939    caller: &CallerPrincipalV1,
940    subject: &SubjectPrincipalV1,
941    audiences: &AudienceV1,
942    purpose: GovernedAccessPurposeV1,
943    scope: &NamespaceScopeV1,
944    lease: Option<&DelegationElevationLeaseV1>,
945    policy_version: &str,
946) -> String {
947    let bytes = serde_json::to_vec(&(
948        policy_version,
949        caller,
950        subject,
951        audiences,
952        purpose,
953        scope,
954        lease,
955    ))
956    .expect("access policy request serialization cannot fail");
957    format!("blake3:{}", blake3::hash(&bytes).to_hex())
958}
959
960fn finish_decision(
961    fact_id: &str,
962    request: &GovernedAccessRequestV1,
963    allowed: bool,
964    reasons: Vec<String>,
965    origin_label_digest: Option<String>,
966    revocation_reference: Option<&str>,
967) -> OriginAuthorityDecisionV1 {
968    let digest_bytes = serde_json::to_vec(&(
969        ORIGIN_AUTHORITY_DECISION_V1,
970        fact_id,
971        request,
972        allowed,
973        &reasons,
974        &origin_label_digest,
975        revocation_reference,
976    ))
977    .expect("decision contract serialization cannot fail");
978    OriginAuthorityDecisionV1 {
979        schema_version: ORIGIN_AUTHORITY_DECISION_V1.into(),
980        fact_id: fact_id.into(),
981        principal: request.principal.clone(),
982        audience_compat: request.audience.clone(),
983        purpose: request.purpose,
984        allowed,
985        reasons,
986        origin_label_digest,
987        revocation_reference: revocation_reference.map(str::to_string),
988        decision_digest: format!("blake3:{}", blake3::hash(&digest_bytes).to_hex()),
989        caller: request.caller.clone(),
990        subject: request.subject.clone(),
991        audience: request.audiences.clone(),
992        scope: request.scope.clone(),
993        policy_version: request.policy_version.clone(),
994        policy_digest: request.policy_digest.clone(),
995        outcome: if allowed {
996            PolicyDecisionV1::Allow
997        } else {
998            PolicyDecisionV1::Deny
999        },
1000        lease_id: request
1001            .delegation_or_elevation
1002            .as_ref()
1003            .map(|lease| lease.lease_id.clone()),
1004    }
1005}