Skip to main content

ic_backup/policy/membership/
mod.rs

1//! Pure admission of a current provider result; no IO, scheduling or authority mutation.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    inventory::InventoryRecord,
6    membership::{MembershipObservation, MembershipObservationRequest},
7};
8use thiserror::Error;
9
10/// Read-only matching observation; not a dispatch permit, continuity proof or fresh permission.
11#[derive(Clone, Debug)]
12pub struct MembershipView<'a> {
13    request: ArtifactChecksumRecord,
14    inventory: &'a InventoryRecord,
15    selected_targets: &'a [String],
16    revision: Option<&'a ArtifactChecksumRecord>,
17    evidence: &'a ArtifactChecksumRecord,
18    remote_observations: u32,
19}
20
21impl MembershipView<'_> {
22    /// Read the exact original challenge/boundary-bound request digest.
23    #[must_use]
24    pub const fn request(&self) -> &ArtifactChecksumRecord {
25        &self.request
26    }
27    /// Read the complete matching current inventory supplied by the provider.
28    #[must_use]
29    pub const fn inventory(&self) -> &InventoryRecord {
30        self.inventory
31    }
32    /// Read original exact selection in canonical order, without dispatch order.
33    #[must_use]
34    pub const fn selected_targets(&self) -> &[String] {
35        self.selected_targets
36    }
37    /// Read optional current revision; matching revisions alone prove no continuity.
38    #[must_use]
39    pub const fn revision(&self) -> Option<&ArtifactChecksumRecord> {
40        self.revision
41    }
42    /// Read the provider-owned opaque evidence identifier, not a signature or permission.
43    #[must_use]
44    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
45        self.evidence
46    }
47    /// Read reported calls; this does not reserve, refund or replenish any allowance.
48    #[must_use]
49    pub const fn remote_observations(&self) -> u32 {
50        self.remote_observations
51    }
52}
53
54/// Validate exact current request/context/full inventory and descriptive call bound.
55///
56/// The caller qualifies provider authenticity, freshness and coherent custody.
57/// This policy calls no provider, serializes no records, changes no plan/journal
58/// and makes no application consistency, lifecycle or permission claim.
59/// # Errors
60/// Rejects another request, context, inventory or excessive reported observations.
61pub fn validate<'a>(
62    request: &'a MembershipObservationRequest<'_>,
63    observation: &'a MembershipObservation,
64) -> Result<MembershipView<'a>, MembershipError> {
65    let digest = request.digest();
66    if observation.request != digest {
67        return Err(MembershipError::RequestMismatch);
68    }
69    let binding = request.binding();
70    if observation.context.network() != binding.network() {
71        return Err(MembershipError::ContextMismatch("network"));
72    }
73    if observation.context.caller() != binding.caller() {
74        return Err(MembershipError::ContextMismatch("caller"));
75    }
76    if observation.context.release() != binding.release() {
77        return Err(MembershipError::ContextMismatch("release"));
78    }
79    if observation.inventory != *request.inventory() {
80        return Err(MembershipError::InventoryMismatch);
81    }
82    if observation.remote_observations > request.max_remote_observations() {
83        return Err(MembershipError::ObservationLimitExceeded {
84            limit: request.max_remote_observations(),
85            reported: observation.remote_observations,
86        });
87    }
88    Ok(MembershipView {
89        request: digest,
90        inventory: &observation.inventory,
91        selected_targets: request.selected_targets(),
92        revision: observation.revision.as_ref(),
93        evidence: &observation.evidence,
94        remote_observations: observation.remote_observations,
95    })
96}
97
98/// Typed mismatch; no rejected result admits an effect or changes original authority.
99#[derive(Debug, Eq, Error, PartialEq)]
100pub enum MembershipError {
101    /// Original intent, operation, challenge, boundary or descriptive ceiling changed.
102    #[error("membership request identity mismatch")]
103    RequestMismatch,
104    /// Actually observed canonical context differs from the original declaration.
105    #[error("membership observed {0} mismatch")]
106    ContextMismatch(&'static str),
107    /// Full current inventory differs, including unselected relationships/metadata.
108    #[error("membership inventory mismatch")]
109    InventoryMismatch,
110    /// Provider reports more remote observations than the descriptive request permits.
111    #[error("membership reports {reported} observations above ceiling {limit}")]
112    ObservationLimitExceeded {
113        /// Original descriptive ceiling; not a journal spending allowance.
114        limit: u32,
115        /// Actual provider-reported remote observations.
116        reported: u32,
117    },
118}
119
120#[cfg(test)]
121mod tests;