Skip to main content

semantic_memory/
authority_contracts.rs

1//! Versioned wire contracts for authority, witnessed retrieval, and injection governance.
2
3use crate::StateView;
4use serde::{Deserialize, Deserializer, Serialize};
5
6macro_rules! bounded_f64 {
7    ($name:ident, $min:expr, $max:expr) => {
8        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
9        #[serde(transparent)]
10        pub struct $name(f64);
11
12        impl $name {
13            pub fn new(value: f64) -> Result<Self, String> {
14                if !value.is_finite() || value < $min || value > $max {
15                    return Err(format!(
16                        "{} must be finite and within [{}, {}]",
17                        stringify!($name),
18                        $min,
19                        $max
20                    ));
21                }
22                Ok(Self(value))
23            }
24
25            pub fn get(self) -> f64 {
26                self.0
27            }
28        }
29
30        impl<'de> Deserialize<'de> for $name {
31            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
32                let value = f64::deserialize(deserializer)?;
33                Self::new(value).map_err(serde::de::Error::custom)
34            }
35        }
36    };
37}
38
39bounded_f64!(Probability, 0.0, 1.0);
40bounded_f64!(Confidence, 0.0, 1.0);
41bounded_f64!(CosineSimilarity, -1.0, 1.0);
42bounded_f64!(NonNegativeWeight, 0.0, f64::MAX);
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(transparent)]
46pub struct AuthoritySnapshotId(pub String);
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(transparent)]
50pub struct RetrievalEpoch(pub u64);
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct MemoryEnvelopeV1 {
54    pub schema_version: String,
55    pub memory_id: String,
56    pub namespace: String,
57    pub content: String,
58    pub source: Option<String>,
59    pub valid_at: String,
60    pub recorded_at: String,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct CapabilityManifestV1 {
65    pub schema_version: String,
66    pub principal: String,
67    pub capabilities: Vec<String>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum StageOutcomeV1 {
73    NotPlanned,
74    Skipped,
75    AnalysisOnly,
76    Applied,
77    Degraded,
78    Failed,
79    BudgetExceeded,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct RetrievalWitnessV1 {
84    pub schema_version: String,
85    pub request_id: String,
86    pub evaluated_at: String,
87    pub authority_snapshot_id: AuthoritySnapshotId,
88    pub retrieval_epoch: RetrievalEpoch,
89    pub query_digest: String,
90    pub config_digest: String,
91    pub ordered_result_ids: Vec<String>,
92    pub ordered_result_digests: Vec<String>,
93    pub stage_outcomes: Vec<(String, StageOutcomeV1)>,
94    pub degradations: Vec<String>,
95    pub cached_witness_parent: Option<String>,
96}
97
98/// Snapshot metadata used by governing systems for cache validation and replay integrity.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct AuthorityStateV1 {
101    pub snapshot_id: AuthoritySnapshotId,
102    pub retrieval_epoch: RetrievalEpoch,
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct RetrievalResponseV1<T> {
107    pub schema_version: String,
108    pub state_view: StateView,
109    pub results: Vec<T>,
110    pub witness: RetrievalWitnessV1,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum InjectionDisposition {
116    Admitted,
117    PartiallyAdmitted,
118    Rejected,
119    FailedClosed,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct InjectionDecisionV1 {
124    pub schema_version: String,
125    pub retrieval_receipt_id: String,
126    pub principal: String,
127    pub host: String,
128    pub policy_digest: String,
129    pub admitted_ids: Vec<String>,
130    pub rejected_ids: Vec<String>,
131    pub disposition: InjectionDisposition,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct SupersessionReceiptV1 {
136    pub schema_version: String,
137    pub operation_id: String,
138    pub caller_idempotency_key: String,
139    pub superseded_id: String,
140    pub replacement_id: String,
141    pub authority_snapshot_id: AuthoritySnapshotId,
142    pub retrieval_epoch: RetrievalEpoch,
143    pub committed_at: String,
144}
145
146/// Capability-bearing caller permit for the governed authority mutation lane.
147///
148/// Ordinary callers cannot construct or deserialize this credential:
149/// ```compile_fail
150/// use semantic_memory::{AuthorityAdmission, AuthorityPermit};
151/// let _forged = AuthorityPermit {
152///     principal: "attacker".into(),
153///     caller_id: "attacker".into(),
154///     capability: AuthorityPermit::APPEND_CAPABILITY.into(),
155///     admission: AuthorityAdmission::OperatorSystem,
156///     origin_authority: None,
157/// };
158/// ```
159#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
160pub struct AuthorityPermit {
161    pub(crate) principal: String,
162    pub(crate) caller_id: String,
163    pub(crate) capability: String,
164    pub(crate) admission: AuthorityAdmission,
165    /// Immutable origin proposed for the governed write. Missing origin fails closed.
166    #[serde(default)]
167    pub(crate) origin_authority: Option<crate::OriginAuthorityLabelV1>,
168}
169
170/// Admission basis carried by a governed authority permit.
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "snake_case")]
173pub enum AuthorityAdmission {
174    Unspecified,
175    Evidence { evidence_refs: Vec<String> },
176    OperatorSystem,
177}
178
179/// Trusted in-process issuer. It has no public constructor, so ordinary callers cannot mint
180/// capability-bearing permits or supply caller identities as request data.
181///
182/// The operator-token constructor (`from_operator_token`) is the production path:
183/// it requires an explicit secret provided by the operator at process startup.
184/// Without that token, no issuer exists and governed mutations fail closed.
185pub struct AuthorityIssuer {
186    _private: (),
187}
188
189impl Clone for AuthorityIssuer {
190    fn clone(&self) -> Self {
191        Self { _private: () }
192    }
193}
194
195impl AuthorityIssuer {
196    #[cfg(feature = "testing")]
197    pub(crate) fn trusted() -> Self {
198        Self { _private: () }
199    }
200
201    /// Construct an issuer from an operator-provided token.
202    ///
203    /// The token is validated (non-empty, no internal whitespace) but not
204    /// stored — the issuer is a zero-sized capability that exists only in
205    /// the process that was started with the correct token. The token itself
206    /// is consumed and not retained, so it cannot leak through the issuer.
207    ///
208    /// Returns `None` if the token is empty or invalid, preserving fail-closed
209    /// semantics for misconfigured deployments.
210    pub fn from_operator_token(token: &str) -> Option<Self> {
211        let token = token.trim();
212        if token.is_empty() || token.chars().any(char::is_whitespace) {
213            return None;
214        }
215        Some(Self { _private: () })
216    }
217
218    pub fn mint_operator_system(
219        &self,
220        principal: impl Into<String>,
221        caller_id: impl Into<String>,
222        capability: impl Into<String>,
223    ) -> AuthorityPermit {
224        let principal = principal.into();
225        let caller_id = caller_id.into();
226        AuthorityPermit {
227            origin_authority: Some(crate::OriginAuthorityLabelV1::operator_system(
228                &principal, &caller_id,
229            )),
230            principal,
231            caller_id,
232            capability: capability.into(),
233            admission: AuthorityAdmission::OperatorSystem,
234        }
235    }
236
237    pub fn mint_with_resolved_evidence(
238        &self,
239        principal: impl Into<String>,
240        caller_id: impl Into<String>,
241        capability: impl Into<String>,
242        evidence: Vec<ResolvedEvidenceDigest>,
243        origin_authority: crate::OriginAuthorityLabelV1,
244    ) -> AuthorityPermit {
245        AuthorityPermit {
246            principal: principal.into(),
247            caller_id: caller_id.into(),
248            capability: capability.into(),
249            admission: AuthorityAdmission::Evidence {
250                evidence_refs: evidence.into_iter().map(|digest| digest.0).collect(),
251            },
252            origin_authority: Some(origin_authority),
253        }
254    }
255}
256
257/// Resolver-produced immutable evidence identity.
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct ResolvedEvidenceDigest(String);
260
261impl ResolvedEvidenceDigest {
262    pub fn from_resolver(digest: String) -> Option<Self> {
263        let hex = digest.strip_prefix("blake3:")?;
264        (hex.len() == 64 && hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
265            .then_some(Self(digest))
266    }
267}
268
269impl AuthorityPermit {
270    pub const APPEND_CAPABILITY: &'static str = "memory.authority.append";
271    pub const SUPERSEDE_CAPABILITY: &'static str = "memory.authority.supersede";
272    pub const REDACT_CAPABILITY: &'static str = "memory.authority.redact";
273    pub const REVOKE_ORIGIN_CAPABILITY: &'static str = "memory.authority.revoke_origin";
274    pub const FORGET_CAPABILITY: &'static str = "memory.authority.forget";
275
276    #[cfg(feature = "testing")]
277    pub fn new(
278        principal: impl Into<String>,
279        caller_id: impl Into<String>,
280        capability: impl Into<String>,
281    ) -> Self {
282        Self {
283            principal: principal.into(),
284            caller_id: caller_id.into(),
285            capability: capability.into(),
286            admission: AuthorityAdmission::Unspecified,
287            origin_authority: None,
288        }
289    }
290
291    /// Construct a permit for a fact proposal supported by explicit evidence.
292    #[cfg(feature = "testing")]
293    pub fn with_evidence(
294        principal: impl Into<String>,
295        caller_id: impl Into<String>,
296        capability: impl Into<String>,
297        evidence_refs: Vec<String>,
298    ) -> Self {
299        Self {
300            principal: principal.into(),
301            caller_id: caller_id.into(),
302            capability: capability.into(),
303            admission: AuthorityAdmission::Evidence { evidence_refs },
304            origin_authority: None,
305        }
306    }
307
308    /// Construct the explicit operator/system bypass used for trusted inserts.
309    #[cfg(feature = "testing")]
310    pub fn operator_system(
311        principal: impl Into<String>,
312        caller_id: impl Into<String>,
313        capability: impl Into<String>,
314    ) -> Self {
315        AuthorityIssuer::trusted().mint_operator_system(principal, caller_id, capability)
316    }
317
318    /// Bind an immutable origin label to this governed write permit.
319    #[cfg(feature = "testing")]
320    pub fn with_origin(mut self, origin: crate::OriginAuthorityLabelV1) -> Self {
321        self.origin_authority = Some(origin);
322        self
323    }
324}
325
326/// Authority mutation kind recorded in the operation journal and receipt.
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
328#[serde(rename_all = "snake_case")]
329pub enum AuthorityOperationKind {
330    Append,
331    Supersede,
332    Redact,
333}
334
335/// Typed fault-injection points for atomic authority tests.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338pub enum AuthorityFaultStage {
339    BeforeAppend,
340    AfterAppend,
341    BeforeLineage,
342    AfterLineage,
343    BeforeJournal,
344    AfterJournal,
345    BeforeEpoch,
346    AfterEpoch,
347    BeforeReceipt,
348    AfterReceipt,
349    BeforeForgettingMutation,
350    AfterForgettingMutation,
351    BeforeForgettingReceipt,
352    AfterForgettingReceipt,
353    BeforeShadowPromotion,
354    AfterShadowPromotion,
355}
356
357/// Durable receipt for one governed authority mutation.
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359pub struct AuthorityReceiptV1 {
360    pub schema_version: String,
361    pub receipt_id: String,
362    pub operation_id: String,
363    pub caller_idempotency_key: String,
364    pub principal: String,
365    pub caller_id: String,
366    pub operation_kind: AuthorityOperationKind,
367    pub before_snapshot_id: AuthoritySnapshotId,
368    pub after_snapshot_id: AuthoritySnapshotId,
369    pub before_epoch: RetrievalEpoch,
370    pub after_epoch: RetrievalEpoch,
371    pub affected_ids: Vec<String>,
372    pub content_digest: String,
373    /// Digest of the immutable origin label persisted for the written fact.
374    #[serde(default)]
375    pub origin_label_digest: Option<String>,
376    pub receipt_digest: String,
377    pub committed_at: String,
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn bounded_numbers_reject_non_finite_and_out_of_range() {
386        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.1, 1.1] {
387            assert!(Probability::new(value).is_err());
388            assert!(serde_json::from_str::<Probability>(&value.to_string()).is_err());
389        }
390        assert!(CosineSimilarity::new(-1.0).is_ok());
391        assert!(CosineSimilarity::new(1.0).is_ok());
392        assert!(NonNegativeWeight::new(-f64::EPSILON).is_err());
393    }
394
395    #[test]
396    fn bounded_number_serde_round_trips_boundaries() {
397        for value in [0.0, 0.5, 1.0] {
398            let bounded = Probability::new(value).unwrap();
399            let json = serde_json::to_string(&bounded).unwrap();
400            assert_eq!(serde_json::from_str::<Probability>(&json).unwrap(), bounded);
401        }
402    }
403}