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 encode(&self) -> Result<Vec<u8>> {
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(rmp_serde::to_vec_named(self)?)
280    }
281    pub fn decode(bytes: &[u8]) -> Result<Self> {
282        if bytes.is_empty() || bytes.len() > MAX_ACCEPTANCE_BYTES {
283            return Err(invalid("publication intent byte bound exceeded"));
284        }
285        let value: Self =
286            preflight::decode(bytes, false, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
287        if value.encode()? != bytes {
288            return Err(invalid("noncanonical publication intent"));
289        }
290        Ok(value)
291    }
292    pub fn id(&self) -> Result<ContentHash> {
293        Ok(ContentHash::compute_typed(INTENT_FORMAT, &self.encode()?))
294    }
295}
296#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
297#[serde(deny_unknown_fields)]
298pub struct OriginalBoundaryAcceptance {
299    pub version: u16,
300    pub publication_intent: ContentHash,
301    pub originals_manifest: ContentHash,
302    pub original_account: Uuid,
303    pub kinds: BTreeSet<BoundaryOriginalKind>,
304    pub accepting_publisher: [u8; 32],
305    /// Current explicit acceptance, independent of each immutable original author.
306    pub accepting_author: SourceAuthor,
307}
308impl OriginalBoundaryAcceptance {
309    pub fn encode(&self) -> Result<Vec<u8>> {
310        self.accepting_author.validate()?;
311        let SourceAuthor::Account { actor, .. } = &self.accepting_author else {
312            return Err(invalid(
313                "boundary acceptance requires explicit account authority",
314            ));
315        };
316        if self.version != 1
317            || self.kinds.is_empty()
318            || self.original_account.is_nil()
319            || actor.principal_id != self.original_account
320            || self.accepting_publisher == [0; 32]
321        {
322            return Err(invalid("invalid boundary acceptance identity"));
323        }
324        let bytes = rmp_serde::to_vec_named(self)?;
325        if bytes.len() > MAX_ACCEPTANCE_BYTES {
326            return Err(invalid("boundary acceptance byte bound exceeded"));
327        }
328        Ok(bytes)
329    }
330    pub fn decode(bytes: &[u8]) -> Result<Self> {
331        if bytes.is_empty() || bytes.len() > MAX_ACCEPTANCE_BYTES {
332            return Err(invalid("boundary acceptance byte bound exceeded"));
333        }
334        let value: Self =
335            preflight::decode(bytes, false, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
336        if value.encode()? != bytes {
337            return Err(invalid("noncanonical boundary acceptance"));
338        }
339        Ok(value)
340    }
341    pub fn id(&self) -> Result<ContentHash> {
342        Ok(ContentHash::compute_typed(FORMAT, &self.encode()?))
343    }
344    /// Only verifies exact signed intent/selection. This is NOT current authority
345    /// or proof that the original credentials ever authorized an action.
346    pub fn selected<'a>(
347        &self,
348        intent: &PublicationIntent,
349        manifest: &'a OriginalPublicationManifest,
350    ) -> Result<Vec<&'a OriginalManifestEntry>> {
351        self.encode()?;
352        let SourceAuthor::Account { spool, .. } = &self.accepting_author else {
353            return Err(invalid("account acceptance required"));
354        };
355        if *spool != intent.spool
356            || self.publication_intent != intent.id()?
357            || self.originals_manifest != manifest.id()?
358        {
359            return Err(invalid(
360                "boundary acceptance differs from exact publication",
361            ));
362        }
363        let selected: Vec<_> = manifest
364            .entries
365            .iter()
366            .filter(|entry| {
367                let Some(authority) = &entry.authority else {
368                    return false;
369                };
370                if authority.spool != intent.spool
371                    || authority.actor.principal_id != self.original_account
372                {
373                    return false;
374                }
375                let kind = match entry.subject {
376                    ManifestSubject::Genesis(_) => BoundaryOriginalKind::AccountGenesis,
377                    ManifestSubject::Source(_) => BoundaryOriginalKind::Source,
378                    ManifestSubject::OwnershipClaim(_) => BoundaryOriginalKind::OwnershipClaim,
379                    ManifestSubject::OwnershipResolution(_) => {
380                        BoundaryOriginalKind::OwnershipResolution
381                    }
382                    ManifestSubject::OtherOperation(_) => return false,
383                };
384                self.kinds.contains(&kind)
385            })
386            .collect();
387        if selected.is_empty() {
388            return Err(invalid("boundary acceptance selects no original"));
389        }
390        Ok(selected)
391    }
392}
393/// Explicit canonical receipt basis; historical original-author testimony is
394/// never reinterpreted as a fresh boundary acceptance.
395#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
396pub enum AdmissionBasis {
397    OriginalAuthority,
398    BoundaryAcceptance { acceptance: ContentHash },
399}
400impl AdmissionBasis {
401    /// Structural evidence binding only. The crypto layer verifies its signature;
402    /// an independently pinned per-original executor receipt attests membership.
403    pub fn authorize_evidence(
404        &self,
405        evidence: Option<&OriginalBoundaryAcceptance>,
406        spool: Uuid,
407        account: Uuid,
408        kind: Option<BoundaryOriginalKind>,
409    ) -> Result<()> {
410        match (self, evidence) {
411            (Self::OriginalAuthority, None) => Ok(()),
412            (Self::BoundaryAcceptance { acceptance }, Some(value)) => {
413                let SourceAuthor::Account {
414                    spool: accepting_spool,
415                    ..
416                } = &value.accepting_author
417                else {
418                    return Err(invalid("boundary receipt requires account acceptance"));
419                };
420                if value.id()? != *acceptance
421                    || *accepting_spool != spool
422                    || value.original_account != account
423                    || !kind.is_some_and(|kind| value.kinds.contains(&kind))
424                {
425                    return Err(invalid(
426                        "boundary receipt evidence differs from original authority scope",
427                    ));
428                }
429                Ok(())
430            }
431            _ => Err(invalid(
432                "receipt requires exactly its matched admission basis evidence",
433            )),
434        }
435    }
436}
437fn invalid(message: &str) -> HeddleError {
438    HeddleError::InvalidObject(message.into())
439}
440
441#[cfg(test)]
442#[path = "original_boundary_acceptance_tests.rs"]
443mod tests;