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    let binding = request.binding();
67    super::observed_context::admit_request_context(
68        &digest,
69        &observation.request,
70        binding,
71        &observation.context,
72        MembershipError::RequestMismatch,
73        MembershipError::ContextMismatch,
74    )?;
75    if observation.inventory != *request.inventory() {
76        return Err(MembershipError::InventoryMismatch);
77    }
78    super::observed_context::admit_limit(
79        request.max_remote_observations(),
80        observation.remote_observations,
81        |limit, reported| MembershipError::ObservationLimitExceeded { limit, reported },
82    )?;
83    Ok(MembershipView {
84        request: digest,
85        inventory: &observation.inventory,
86        selected_targets: request.selected_targets(),
87        revision: observation.revision.as_ref(),
88        evidence: &observation.evidence,
89        remote_observations: observation.remote_observations,
90    })
91}
92
93/// Typed mismatch; no rejected result admits an effect or changes original authority.
94#[derive(Debug, Eq, Error, PartialEq)]
95pub enum MembershipError {
96    /// Original intent, operation, challenge, boundary or descriptive ceiling changed.
97    #[error("membership request identity mismatch")]
98    RequestMismatch,
99    /// Actually observed canonical context differs from the original declaration.
100    #[error("membership observed {0} mismatch")]
101    ContextMismatch(&'static str),
102    /// Full current inventory differs, including unselected relationships/metadata.
103    #[error("membership inventory mismatch")]
104    InventoryMismatch,
105    /// Provider reports more remote observations than the descriptive request permits.
106    #[error("membership reports {reported} observations above ceiling {limit}")]
107    ObservationLimitExceeded {
108        /// Original descriptive ceiling; not a journal spending allowance.
109        limit: u32,
110        /// Actual provider-reported remote observations.
111        reported: u32,
112    },
113}
114
115#[cfg(test)]
116mod tests;