Skip to main content

heddle_object_model/object/
original_boundary_acceptance.rs

1//! Explicit present-tense acceptance of immutable originals. This never claims
2//! that an expired or revoked original credential authorized a past action.
3#[path = "original_boundary_preflight.rs"]
4mod preflight;
5
6use std::collections::BTreeSet;
7
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use super::{
12    ContentHash, StateId,
13    thread_authority_admission::OriginalAuthorityBinding,
14    thread_replication::{
15        GenesisOwner, SourceAuthor, ThreadGenesis, ThreadOperation, metadata::AUTHORITY_FORMAT,
16        ownership_claim::ThreadOwnershipClaim, ownership_resolution::ThreadOwnershipResolution,
17    },
18};
19use crate::error::{HeddleError, Result};
20
21pub const FORMAT: &str = "heddle-original-boundary-acceptance-v1";
22pub const MANIFEST_FORMAT: &str = "heddle-original-publication-manifest-v1";
23pub const INTENT_FORMAT: &str = "heddle-original-publication-intent-v1";
24pub const MAX_RECORDS: usize = 10_384;
25pub const MAX_MANIFEST_BYTES: usize = 16 * 1024 * 1024;
26pub const MAX_ACCEPTANCE_BYTES: usize = 96 * 1024;
27
28/// The manifest classifies all original records, including ineligible metadata
29/// and unclaimed local sources; their inclusion grants no acceptance authority.
30#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
31pub enum ManifestSubject {
32    Genesis(ContentHash),
33    Source(ContentHash),
34    OtherOperation(ContentHash),
35    OwnershipClaim(ContentHash),
36    OwnershipResolution(ContentHash),
37}
38impl ManifestSubject {
39    pub fn id(&self) -> ContentHash {
40        match self {
41            Self::Genesis(id)
42            | Self::Source(id)
43            | Self::OtherOperation(id)
44            | Self::OwnershipClaim(id)
45            | Self::OwnershipResolution(id) => *id,
46        }
47    }
48}
49#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct OriginalManifestEntry {
52    pub subject: ManifestSubject,
53    pub thread: ContentHash,
54    pub publisher: [u8; 32],
55    pub authority: Option<OriginalAuthorityBinding>,
56}
57impl OriginalManifestEntry {
58    /// Caller verifies the original signature before building this descriptor.
59    pub fn from_operation(operation: &ThreadOperation) -> Result<Self> {
60        let id = operation.id()?;
61        Ok(Self {
62            subject: if operation.source_result()?.is_some() {
63                ManifestSubject::Source(id)
64            } else {
65                ManifestSubject::OtherOperation(id)
66            },
67            thread: operation.thread,
68            publisher: operation.publisher,
69            authority: OriginalAuthorityBinding::from_operation(operation)?,
70        })
71    }
72    /// Caller verifies the creator signature. Genesis does not directly sign an
73    /// agent field: None here means unspecified, not human attribution. The exact
74    /// envelope digest binds the identity the host subsequently inspects.
75    pub fn from_genesis(genesis: &ThreadGenesis, creator_authority: &[u8]) -> Result<Self> {
76        let authority = match genesis.owner {
77            GenesisOwner::Account(account) => {
78                if creator_authority.is_empty() || creator_authority.len() > 64 * 1024 {
79                    return Err(invalid(
80                        "account genesis requires bounded creator authority",
81                    ));
82                }
83                Some(OriginalAuthorityBinding {
84                    spool: Uuid::parse_str(&genesis.spool)
85                        .map_err(|_| invalid("invalid genesis Spool"))?,
86                    actor: super::CollaborationActor {
87                        principal_id: account,
88                        agent_id: None,
89                    },
90                    authority_digest: ContentHash::compute_typed(
91                        AUTHORITY_FORMAT,
92                        creator_authority,
93                    ),
94                })
95            }
96            GenesisOwner::LocalKey(_) => {
97                if !creator_authority.is_empty() {
98                    return Err(invalid("local genesis has no account authority"));
99                }
100                None
101            }
102        };
103        let id = genesis.id()?;
104        Ok(Self {
105            subject: ManifestSubject::Genesis(id),
106            thread: id,
107            publisher: genesis.creator,
108            authority,
109        })
110    }
111    /// Caller verifies both original claim signatures and its immutable genesis.
112    pub fn from_claim(claim: &ThreadOwnershipClaim) -> Result<Self> {
113        claim.encode()?;
114        let SourceAuthor::Account {
115            spool,
116            actor,
117            authority_digest,
118            ..
119        } = &claim.acceptance
120        else {
121            return Err(invalid("claim requires explicit account authority"));
122        };
123        Ok(Self {
124            subject: ManifestSubject::OwnershipClaim(claim.id()?),
125            thread: claim.thread,
126            publisher: claim.accepting_publisher,
127            authority: Some(OriginalAuthorityBinding {
128                spool: *spool,
129                actor: actor.clone(),
130                authority_digest: *authority_digest,
131            }),
132        })
133    }
134    pub fn from_resolution(resolution: &ThreadOwnershipResolution) -> Result<Self> {
135        resolution.encode()?;
136        let SourceAuthor::Account {
137            spool,
138            actor,
139            authority_digest,
140            ..
141        } = &resolution.acceptance
142        else {
143            return Err(invalid("resolution requires explicit account acceptance"));
144        };
145        Ok(Self {
146            subject: ManifestSubject::OwnershipResolution(resolution.id()?),
147            thread: resolution.thread,
148            publisher: resolution.accepting_publisher,
149            authority: Some(OriginalAuthorityBinding {
150                spool: *spool,
151                actor: actor.clone(),
152                authority_digest: *authority_digest,
153            }),
154        })
155    }
156    fn validate(&self) -> Result<()> {
157        if self.publisher == [0; 32]
158            || self.subject.id().as_bytes() == &[0; 32]
159            || self.thread.as_bytes() == &[0; 32]
160        {
161            return Err(invalid("invalid original manifest identity"));
162        }
163        if matches!(self.subject, ManifestSubject::Genesis(_))
164            && (self.subject.id() != self.thread
165                || self
166                    .authority
167                    .as_ref()
168                    .is_some_and(|binding| binding.actor.agent_id.is_some()))
169        {
170            return Err(invalid("genesis manifest cannot assert an unsigned agent"));
171        }
172        if let Some(binding) = &self.authority
173            && (binding.spool.is_nil()
174                || binding.actor.principal_id.is_nil()
175                || binding.actor.agent_id.as_ref().is_some_and(|id| {
176                    id.is_empty() || id.len() > 256 || id.chars().any(char::is_control)
177                }))
178        {
179            return Err(invalid("invalid original manifest authority"));
180        }
181        Ok(())
182    }
183}
184#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
185#[serde(deny_unknown_fields)]
186pub struct OriginalPublicationManifest {
187    pub version: u16,
188    pub entries: Vec<OriginalManifestEntry>,
189}
190impl OriginalPublicationManifest {
191    pub fn new(mut entries: Vec<OriginalManifestEntry>) -> Result<Self> {
192        if entries.len() > MAX_RECORDS {
193            return Err(invalid("original manifest record bound exceeded"));
194        }
195        entries.sort_by(|a, b| a.subject.cmp(&b.subject));
196        let value = Self {
197            version: 1,
198            entries,
199        };
200        value.encode()?;
201        Ok(value)
202    }
203    pub fn encode(&self) -> Result<Vec<u8>> {
204        if self.version != 1 || self.entries.is_empty() || self.entries.len() > MAX_RECORDS {
205            return Err(invalid("original manifest record bound exceeded"));
206        }
207        let mut ids = BTreeSet::new();
208        for entry in &self.entries {
209            entry.validate()?;
210            // Source and OtherOperation share the original operation ID domain.
211            let domain = match entry.subject {
212                ManifestSubject::Genesis(_) => 0,
213                ManifestSubject::Source(_) | ManifestSubject::OtherOperation(_) => 1,
214                ManifestSubject::OwnershipClaim(_) => 2,
215                ManifestSubject::OwnershipResolution(_) => 3,
216            };
217            if !ids.insert((domain, entry.subject.id())) {
218                return Err(invalid("duplicate original manifest identity"));
219            }
220        }
221        if self
222            .entries
223            .windows(2)
224            .any(|pair| pair[0].subject >= pair[1].subject)
225        {
226            return Err(invalid("original manifest is not sorted"));
227        }
228        let bytes = rmp_serde::to_vec_named(self)?;
229        if bytes.len() > MAX_MANIFEST_BYTES {
230            return Err(invalid("original manifest byte bound exceeded"));
231        }
232        Ok(bytes)
233    }
234    pub fn decode(bytes: &[u8]) -> Result<Self> {
235        if bytes.is_empty() || bytes.len() > MAX_MANIFEST_BYTES {
236            return Err(invalid("original manifest byte bound exceeded"));
237        }
238        let value: Self =
239            preflight::decode(bytes, true, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
240        if value.encode()? != bytes {
241            return Err(invalid("noncanonical original manifest"));
242        }
243        Ok(value)
244    }
245    pub fn id(&self) -> Result<ContentHash> {
246        Ok(ContentHash::compute_typed(MANIFEST_FORMAT, &self.encode()?))
247    }
248}
249#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
250pub enum BoundaryOriginalKind {
251    Source,
252    AccountGenesis,
253    OwnershipClaim,
254    OwnershipResolution,
255}
256#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
257#[serde(deny_unknown_fields)]
258pub struct PublicationIntent {
259    pub spool: Uuid,
260    pub spool_genesis: ContentHash,
261    pub thread: ContentHash,
262    pub revision: StateId,
263    /// Digest of the complete ordered pack/index inventory, verified by intake.
264    pub inventory: ContentHash,
265    pub sharing_policy: Option<ContentHash>,
266    pub source: [u8; 32],
267    pub destination: [u8; 32],
268    pub client_operation_id: Uuid,
269}
270impl PublicationIntent {
271    pub fn id(&self) -> Result<ContentHash> {
272        if self.spool.is_nil()
273            || self.client_operation_id.is_nil()
274            || self.source == [0; 32]
275            || self.destination == [0; 32]
276        {
277            return Err(invalid("invalid boundary publication intent"));
278        }
279        Ok(ContentHash::compute_typed(
280            INTENT_FORMAT,
281            &rmp_serde::to_vec_named(self)?,
282        ))
283    }
284}
285#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
286#[serde(deny_unknown_fields)]
287pub struct OriginalBoundaryAcceptance {
288    pub version: u16,
289    pub publication_intent: ContentHash,
290    pub originals_manifest: ContentHash,
291    pub original_account: Uuid,
292    pub kinds: BTreeSet<BoundaryOriginalKind>,
293    pub accepting_publisher: [u8; 32],
294    /// Current explicit acceptance, independent of each immutable original author.
295    pub accepting_author: SourceAuthor,
296}
297impl OriginalBoundaryAcceptance {
298    pub fn encode(&self) -> Result<Vec<u8>> {
299        self.accepting_author.validate()?;
300        let SourceAuthor::Account { actor, .. } = &self.accepting_author else {
301            return Err(invalid(
302                "boundary acceptance requires explicit account authority",
303            ));
304        };
305        if self.version != 1
306            || self.kinds.is_empty()
307            || self.original_account.is_nil()
308            || actor.principal_id != self.original_account
309            || self.accepting_publisher == [0; 32]
310        {
311            return Err(invalid("invalid boundary acceptance identity"));
312        }
313        let bytes = rmp_serde::to_vec_named(self)?;
314        if bytes.len() > MAX_ACCEPTANCE_BYTES {
315            return Err(invalid("boundary acceptance byte bound exceeded"));
316        }
317        Ok(bytes)
318    }
319    pub fn decode(bytes: &[u8]) -> Result<Self> {
320        if bytes.is_empty() || bytes.len() > MAX_ACCEPTANCE_BYTES {
321            return Err(invalid("boundary acceptance byte bound exceeded"));
322        }
323        let value: Self =
324            preflight::decode(bytes, false, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
325        if value.encode()? != bytes {
326            return Err(invalid("noncanonical boundary acceptance"));
327        }
328        Ok(value)
329    }
330    pub fn id(&self) -> Result<ContentHash> {
331        Ok(ContentHash::compute_typed(FORMAT, &self.encode()?))
332    }
333    /// Only verifies exact signed intent/selection. This is NOT current authority
334    /// or proof that the original credentials ever authorized an action.
335    pub fn selected<'a>(
336        &self,
337        intent: &PublicationIntent,
338        manifest: &'a OriginalPublicationManifest,
339    ) -> Result<Vec<&'a OriginalManifestEntry>> {
340        self.encode()?;
341        let SourceAuthor::Account { spool, .. } = &self.accepting_author else {
342            return Err(invalid("account acceptance required"));
343        };
344        if *spool != intent.spool
345            || self.publication_intent != intent.id()?
346            || self.originals_manifest != manifest.id()?
347        {
348            return Err(invalid(
349                "boundary acceptance differs from exact publication",
350            ));
351        }
352        let selected: Vec<_> = manifest
353            .entries
354            .iter()
355            .filter(|entry| {
356                let Some(authority) = &entry.authority else {
357                    return false;
358                };
359                if authority.spool != intent.spool
360                    || authority.actor.principal_id != self.original_account
361                {
362                    return false;
363                }
364                let kind = match entry.subject {
365                    ManifestSubject::Genesis(_) => BoundaryOriginalKind::AccountGenesis,
366                    ManifestSubject::Source(_) => BoundaryOriginalKind::Source,
367                    ManifestSubject::OwnershipClaim(_) => BoundaryOriginalKind::OwnershipClaim,
368                    ManifestSubject::OwnershipResolution(_) => {
369                        BoundaryOriginalKind::OwnershipResolution
370                    }
371                    ManifestSubject::OtherOperation(_) => return false,
372                };
373                self.kinds.contains(&kind)
374            })
375            .collect();
376        if selected.is_empty() {
377            return Err(invalid("boundary acceptance selects no original"));
378        }
379        Ok(selected)
380    }
381}
382/// Explicit canonical receipt basis; historical original-author testimony is
383/// never reinterpreted as a fresh boundary acceptance.
384#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
385pub enum AdmissionBasis {
386    OriginalAuthority,
387    BoundaryAcceptance { acceptance: ContentHash },
388}
389impl AdmissionBasis {
390    /// Structural evidence binding only. The crypto layer verifies its signature;
391    /// an independently pinned per-original executor receipt attests membership.
392    pub fn authorize_evidence(
393        &self,
394        evidence: Option<&OriginalBoundaryAcceptance>,
395        spool: Uuid,
396        account: Uuid,
397        kind: Option<BoundaryOriginalKind>,
398    ) -> Result<()> {
399        match (self, evidence) {
400            (Self::OriginalAuthority, None) => Ok(()),
401            (Self::BoundaryAcceptance { acceptance }, Some(value)) => {
402                let SourceAuthor::Account {
403                    spool: accepting_spool,
404                    ..
405                } = &value.accepting_author
406                else {
407                    return Err(invalid("boundary receipt requires account acceptance"));
408                };
409                if value.id()? != *acceptance
410                    || *accepting_spool != spool
411                    || value.original_account != account
412                    || !kind.is_some_and(|kind| value.kinds.contains(&kind))
413                {
414                    return Err(invalid(
415                        "boundary receipt evidence differs from original authority scope",
416                    ));
417                }
418                Ok(())
419            }
420            _ => Err(invalid(
421                "receipt requires exactly its matched admission basis evidence",
422            )),
423        }
424    }
425}
426fn invalid(message: &str) -> HeddleError {
427    HeddleError::InvalidObject(message.into())
428}
429
430#[cfg(test)]
431#[path = "original_boundary_acceptance_tests.rs"]
432mod tests;