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