Skip to main content

heddle_api/
import_authority.rs

1//! HYBRID canonical import authority. The existing heddle capability verifier
2//! supplies independently selected owner history/current permission. This
3//! module verifies the NEW typed parent grant and its bounded job child;
4//! it neither enrolls incoming roots nor substitutes for owner verification.
5use crate::heddle::api::v1alpha2::*;
6pub use crate::hybrid_codec::{Reject, strict_decode};
7use crate::hybrid_codec::{canonical, field, hash, key_id, record, signing_digest, verify, width};
8
9pub const PERMISSION_DOMAIN: &str = "heddle-import-member-permission-v1";
10pub const GENESIS_DOMAIN: &str = "heddle-import-genesis-authority-v1";
11pub const DELEGATION_DOMAIN: &str = "heddle-import-job-delegation-v1";
12pub const OPERATION_DOMAIN: &str = "heddle-delegated-import-operation-v1";
13pub const MANIFEST_DOMAIN: &str = "heddle-import-result-manifest-v1";
14pub const PUBLICATION_DOMAIN: &str = "heddle-import-publication-payload-v1";
15pub const MAX_BRANCHES: usize = 256;
16pub const MAX_REF_BYTES: usize = 1024;
17pub const MAX_RECORD_BYTES: usize = 64 * 1024;
18pub const MAX_BUNDLE_BYTES: usize = 1024 * 1024;
19pub const MAX_DELEGATION_WINDOW_SECONDS: u64 = 7 * 24 * 60 * 60;
20pub const MAX_COMMIT_REQUEST_BYTES: usize = 2 * MAX_BUNDLE_BYTES;
21/// Refs in one `ResolveImportSource`/`ProviderRefsRequest` page (proto: max 512).
22pub const MAX_IMPORT_SOURCE_REF_PAGE: usize = 512;
23/// Safety ceiling on complete discovery work, independent of retained refs.
24/// Validation retains only one previous name and at most `MAX_BRANCHES` selections.
25pub const MAX_IMPORT_SOURCE_REFS: usize = 1_048_576;
26pub const DISCOVERY_DOMAIN: &str = "heddle-import-discovery-v1";
27pub const CANCELLATION_NAMESPACE: &str = "heddle-import-cancel-v1";
28
29record!(AuthorizationSignature, signer_key_id:b, signature:b);
30record!(RecordSignature, public_key:b, signature:b);
31record!(SignedRecord, format:s, canonical_record:b, signatures:l);
32record!(ImportFrontierV1, format_version:u, thread_id:b, operation_ids:h);
33record!(ImportContentV1, format_version:u, canonical_capture:b);
34record!(ImportBoundaryAcceptanceV1, binding:m, signed_acceptance:m, originals_manifest:b, publication_intent:b, original_receipts:l);
35record!(ImportGenesisWitnessV1, format_version:u, binding:m, original_genesis:m, creator_authority_envelope:b, boundary_acceptance:o);
36record!(ImportAuthorityWitnessV1, format_version:u, kind:e, original:m, dependencies:l, authority_envelope:b, boundary_acceptances:l);
37record!(HostedLandingRequestProofV1, format_version:u, signing_identity:s, method_path:s, timestamp_millis:u, nonce:b, request_body:b, signature:m);
38record!(HostedLandingWitnessV1, format_version:u, execution:m, request:m, source_operation:m, review_evidence:l, authority_envelope:b);
39record!(ImportIdentityV1, spool_uuid:b, spool_genesis_digest:b, owner_id:b,
40    owner_account_uuid:b, owner_state_hash:b, ownership_transfer_sequence:u);
41record!(ImportOwnerChainV1, spool_genesis_digest:b, owner_state_hashes:q, transfer_audit_hashes:q);
42record!(ImportBranchLimitV1, ref_name:s, hash_algorithm:e, ref_mode:e, pinned_commit_oid:b,
43    genesis_digest:b, target_thread_id:b, expected_frontier_digest:b, slot_id:u, ref_disclosure:e);
44record!(ImportPermissionScopeV1, provider:s, source_url:s, branches:l, destination_version:b,
45    options_digest:b, converter_version:s, max_operations:u, max_result_bytes:u);
46record!(ImportMemberPermissionV1, format_version:u, identity:m, logical_job_id:b,
47    retry_lineage_id:b, subject_public_key:b, purpose:e, scope:m, not_before_unix_seconds:u,
48    expires_at_unix_seconds:u, cancellation_id:b, owner_chain_digest:b, nonce:b);
49record!(SignedImportMemberPermissionV1, body:m, owner_signature:m);
50record!(ImportGenesisAuthorityV1, format_version:u, identity:m, genesis_digest:b,
51    original_creator_signature:b, creator_public_key:b, creator_authority_envelope_digest:b,
52    parent_permission_digest:b, owner_chain_digest:b);
53record!(SignedImportGenesisAuthorityV1, body:m, creator_signature:m);
54record!(ImportBranchManifestV1, limit:m, genesis_authority_digest:b);
55record!(ImportJobDelegationV1, format_version:u, identity:m, delegation_id:b, logical_job_id:b,
56    retry_lineage_id:b, job_public_key:b, job_key_id:b, delegating_public_key:b,
57    parent_permission_digest:b, owner_chain_digest:b, purpose:e, scope:m, branch_manifest:l,
58    not_before_unix_seconds:u, expires_at_unix_seconds:u, cancellation_id:b);
59record!(ImportJobPreparationV1, format_version:u, identity:m, delegation_id:b, logical_job_id:b,
60    retry_lineage_id:b, job_public_key:b, job_key_id:b, owner_chain_digest:b, purpose:e,
61    scope:m, cancellation_id:b);
62record!(SignedImportJobDelegationV1, body:m, delegating_signature:m);
63record!(ImportCommittedSlotV1, ref_name:s, slot_id:u, signed_operation_digest:b,
64    resulting_frontier_digest:b, result_bytes:u);
65record!(ImportResultManifestV1, format_version:u, logical_job_id:b, retry_lineage_id:b, slots:l);
66record!(DelegatedImportOperationV1, format_version:u, spool_uuid:b, spool_genesis_digest:b,
67    logical_job_id:b, retry_lineage_id:b, physical_operation_id:b, delegation_digest:b,
68    ref_name:s, slot_id:u, hash_algorithm:e, observed_commit_oid:b, genesis_digest:b,
69    target_thread_id:b, expected_frontier_digest:b, resulting_frontier_digest:b,
70    resulting_content_digest:b, result_bytes:u, options_digest:b, converter_version:s);
71record!(SignedDelegatedImportOperationV1, body:m, job_signature:m);
72record!(ImportPublicationWitnessV1, format_version:u, signed_operation_digest:b, delegation_digest:b,
73    logical_job_id:b, retry_lineage_id:b, physical_operation_id:b, ref_name:s, slot_id:u,
74    hash_algorithm:e, observed_commit_oid:b, expected_frontier_digest:b, resulting_frontier_digest:b,
75    terminal_manifest_digest:b);
76
77pub fn signed_permission_digest(v: &SignedImportMemberPermissionV1) -> Result<Vec<u8>, Reject> {
78    signing_digest("heddle-signed-import-member-permission-v1", v)
79}
80pub fn owner_chain_digest(v: &ImportOwnerChainV1) -> Result<Vec<u8>, Reject> {
81    width(&v.spool_genesis_digest, 32)?;
82    if v.owner_state_hashes.is_empty()
83        || v.owner_state_hashes.len() > 64
84        || v.transfer_audit_hashes.len() > 64
85    {
86        return Err(Reject::Bounds);
87    }
88    for h in v.owner_state_hashes.iter().chain(&v.transfer_audit_hashes) {
89        width(h, 32)?;
90    }
91    if v.owner_state_hashes.windows(2).any(|w| w[0] >= w[1]) {
92        return Err(Reject::Canonical);
93    }
94    signing_digest("heddle-import-owner-chain-v1", v)
95}
96pub fn signed_genesis_digest(v: &SignedImportGenesisAuthorityV1) -> Result<Vec<u8>, Reject> {
97    signing_digest("heddle-signed-import-genesis-authority-v1", v)
98}
99pub fn signed_delegation_digest(v: &SignedImportJobDelegationV1) -> Result<Vec<u8>, Reject> {
100    signing_digest("heddle-signed-import-job-delegation-v1", v)
101}
102pub fn signed_operation_digest(v: &SignedDelegatedImportOperationV1) -> Result<Vec<u8>, Reject> {
103    signing_digest("heddle-signed-delegated-import-operation-v1", v)
104}
105pub fn manifest_digest(v: &ImportResultManifestV1) -> Result<Vec<u8>, Reject> {
106    signing_digest(MANIFEST_DOMAIN, v)
107}
108pub fn verify_authorization_signature(
109    key: &[u8],
110    domain: &str,
111    body: &impl crate::hybrid_codec::Canonical,
112    signature: &AuthorizationSignature,
113) -> Result<(), Reject> {
114    if signature.signer_key_id != key_id(key) {
115        return Err(Reject::Signature);
116    }
117    let bytes = canonical(body)?;
118    if bytes.len() > MAX_RECORD_BYTES {
119        return Err(Reject::Bounds);
120    }
121    verify(
122        key,
123        &hash(&[domain.as_bytes(), &bytes]),
124        &signature.signature,
125    )
126}
127
128/// Conservative canonical URL grammar for v1: lowercase DNS HTTPS host, no
129/// userinfo/query/fragment/port/escapes, ASCII unreserved path segments. No
130/// implicit URL normalization is performed by a signing implementation.
131pub fn canonical_https(value: &str, origin: bool) -> Result<(), Reject> {
132    if value.len() > 2048 {
133        return Err(Reject::Bounds);
134    }
135    let rest = value.strip_prefix("https://").ok_or(Reject::Canonical)?;
136    let (host, path) = rest.split_once('/').unwrap_or((rest, ""));
137    if host.is_empty()
138        || host.len() > 253
139        || host.split('.').any(|part| {
140            part.is_empty()
141                || part.len() > 63
142                || part.starts_with('-')
143                || part.ends_with('-')
144                || !part
145                    .bytes()
146                    .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
147        })
148        || (origin && rest != host)
149        || (!origin && path.is_empty())
150        || path.split('/').any(|p| {
151            p.is_empty() && !origin
152                || p == "."
153                || p == ".."
154                || !p
155                    .bytes()
156                    .all(|b| b.is_ascii_alphanumeric() || b"-._~".contains(&b))
157        })
158    {
159        return Err(Reject::Canonical);
160    }
161    Ok(())
162}
163fn identity(value: &ImportIdentityV1) -> Result<(), Reject> {
164    for v in [&value.spool_uuid, &value.owner_account_uuid] {
165        width(v, 16)?;
166        if v.iter().all(|b| *b == 0) {
167            return Err(Reject::Canonical);
168        }
169    }
170    for v in [
171        &value.spool_genesis_digest,
172        &value.owner_id,
173        &value.owner_state_hash,
174    ] {
175        width(v, 32)?;
176    }
177    Ok(())
178}
179fn interval(start: i64, end: i64, now: i64) -> Result<(), Reject> {
180    if start < 0 || end <= start {
181        return Err(Reject::Semantic);
182    }
183    if now < start || now >= end {
184        return Err(Reject::Expired);
185    }
186    Ok(())
187}
188// Structural validation never substitutes a synthetic historical clock.
189fn validity(start: i64, end: i64, now: i64, current: bool) -> Result<(), Reject> {
190    if start < 0 || end <= start {
191        return Err(Reject::Semantic);
192    }
193    if current {
194        interval(start, end, now)?;
195    }
196    Ok(())
197}
198/// Mirrors sley-refs 0.11.0 check_refname_format plus Git's branch-only
199/// leading-dash/HEAD checks. Sley remains a dev-only reference so the contract
200/// does not pull its filesystem/config/object stack into WASM consumers.
201/// See the shared corpus and differential property tests; never normalize.
202fn branch_ref(name: &str) -> Result<(), Reject> {
203    let short = name.strip_prefix("refs/heads/").ok_or(Reject::Canonical)?;
204    if short.is_empty() || short.starts_with('-') || short == "HEAD" {
205        return Err(Reject::Canonical);
206    }
207    git_ref_name(name)
208}
209
210fn git_ref_name(name: &str) -> Result<(), Reject> {
211    if name.ends_with('/')
212        || name.ends_with('.')
213        || name.contains("..")
214        || name.contains("//")
215        || name.contains("@{")
216        || name
217            .split('/')
218            .any(|p| p.starts_with('.') || p.ends_with(".lock"))
219        || name.bytes().any(|b| {
220            b <= b' ' || b == 0x7f || matches!(b, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\')
221        })
222    {
223        return Err(Reject::Canonical);
224    }
225    if name.len() > MAX_REF_BYTES {
226        return Err(Reject::Bounds);
227    }
228    Ok(())
229}
230
231fn branch(value: &ImportBranchLimitV1) -> Result<(), Reject> {
232    branch_ref(&value.ref_name)?;
233    let size = match value.hash_algorithm {
234        1 => 20,
235        2 => 32,
236        _ => return Err(Reject::Version),
237    };
238    match value.ref_mode {
239        1 => {
240            width(&value.pinned_commit_oid, size)?;
241            if value.ref_disclosure != 0 {
242                return Err(Reject::RefDisclosure);
243            }
244        }
245        2 if value.pinned_commit_oid.is_empty() => {
246            if value.ref_disclosure != 1 {
247                return Err(Reject::RefDisclosure);
248            }
249        }
250        _ => return Err(Reject::Semantic),
251    }
252    for v in [
253        &value.genesis_digest,
254        &value.target_thread_id,
255        &value.expected_frontier_digest,
256    ] {
257        width(v, 32)?;
258    }
259    Ok(())
260}
261
262/// Check independently observed OID knowledge before signing/preparation.
263/// Empty/unknown is None, never an implicit observe-mode selection or consent.
264pub fn validate_ref_selection(
265    value: &ImportBranchLimitV1,
266    known_commit_oid: Option<&[u8]>,
267) -> Result<(), Reject> {
268    branch(value)?;
269    if let Some(oid) = known_commit_oid {
270        width(oid, if value.hash_algorithm == 1 { 20 } else { 32 })?;
271        if value.ref_mode != 1 || value.pinned_commit_oid != oid {
272            return Err(Reject::RefPinning);
273        }
274    }
275    Ok(())
276}
277
278/// Converter option octets are selected verbatim from authenticated discovery.
279pub fn conversion_options_digest(version: &str, options: &[u8]) -> Result<Vec<u8>, Reject> {
280    if version.is_empty() || version.len() > 128 || !version.is_ascii() {
281        return Err(Reject::Canonical);
282    }
283    if options.len() > 4096 {
284        return Err(Reject::Bounds);
285    }
286    let mut bytes = Vec::new();
287    crate::hybrid_codec::counted(&mut bytes, version.as_bytes())?;
288    crate::hybrid_codec::counted(&mut bytes, options)?;
289    Ok(hash(&[b"heddle-import-conversion-options-v1", &bytes]))
290}
291
292/// Resolve explicit custody using a CURRENT authenticated connection provider.
293/// Public URLs never select an adapter by domain. Network/grant checks remain host gates.
294pub fn resolve_import_provider(
295    source: &ProviderRepository,
296    connection_provider: Option<&str>,
297) -> Result<&'static str, Reject> {
298    canonical_https(&source.clone_url, false)?;
299    if source.provider_repository_id.len() > 4096 || source.name.len() > 4096 {
300        return Err(Reject::Bounds);
301    }
302    if let Some(connection) = &source.connection {
303        let positive = |v: &str| {
304            !v.is_empty()
305                && v.bytes().all(|b| b.is_ascii_digit())
306                && v.parse::<u64>().is_ok_and(|id| id > 0)
307        };
308        if connection_provider != Some("github")
309            || connection.spool.is_some()
310            || connection.id.is_empty()
311            || !positive(&source.provider_repository_id)
312            || !positive(&source.installation_id)
313        {
314            return Err(Reject::SourceSelection);
315        }
316        let path = source
317            .clone_url
318            .strip_prefix("https://github.com/")
319            .ok_or(Reject::SourceSelection)?;
320        let parts: Vec<_> = path.split('/').collect();
321        if parts.len() != 2
322            || parts[0].is_empty()
323            || parts[1].strip_suffix(".git").is_none_or(str::is_empty)
324        {
325            return Err(Reject::SourceSelection);
326        }
327        Ok("github")
328    } else {
329        if connection_provider.is_some()
330            || source.private
331            || !source.installation_id.is_empty()
332            || (!source.provider_repository_id.is_empty()
333                && source.provider_repository_id != source.clone_url)
334        {
335            return Err(Reject::SourceSelection);
336        }
337        Ok("public-git")
338    }
339}
340
341/// Advisory Git size in KiB. UNKNOWN is explicit and must carry zero.
342pub fn validate_repository_size_estimate(source: &ProviderRepository) -> Result<(), Reject> {
343    match source.size_estimate_state {
344        0 if source.git_size_kib == 0 => Ok(()),
345        1 if source.connection.is_some() => Ok(()),
346        _ => Err(Reject::Canonical),
347    }
348}
349
350/// Preserve selected identity exactly. Redirect/SSRF and current grants are host gates.
351pub fn validate_resolve_import_source_response(
352    request: &ResolveImportSourceRequest,
353    response: &ResolveImportSourceResponse,
354    connection_provider: Option<&str>,
355) -> Result<(), Reject> {
356    use prost::Message;
357    if response.encoded_len() > MAX_BUNDLE_BYTES {
358        return Err(Reject::Bounds);
359    }
360    let selected = request.source.as_ref().ok_or(Reject::SourceSelection)?;
361    let resolved = response.source.as_ref().ok_or(Reject::SourceSelection)?;
362    let provider = resolve_import_provider(selected, connection_provider)?;
363    if resolve_import_provider(resolved, connection_provider)? != provider
364        || selected.clone_url != resolved.clone_url
365        || selected.connection != resolved.connection
366        || selected.installation_id != resolved.installation_id
367        || selected.private != resolved.private
368        || (selected.provider_repository_id != resolved.provider_repository_id
369            && (selected.connection.is_some() || !selected.provider_repository_id.is_empty()))
370        || (resolved.connection.is_none() && resolved.provider_repository_id != selected.clone_url)
371    {
372        return Err(Reject::SourceSelection);
373    }
374    validate_repository_size_estimate(resolved)?;
375    validate_repository_hash_algorithm_with_options(
376        resolved,
377        false,
378        DiscoveryOptions {
379            include_pull_request_refs: request.include_pull_request_refs,
380        },
381    )
382}
383
384/// Structural binding to signed provider/URL; durable custody association is a host invariant.
385fn validate_retained_import_source(
386    selector: &ImportSourceSelectionV1,
387    scope: &ImportPermissionScopeV1,
388) -> Result<(), Reject> {
389    let source = ProviderRepository {
390        connection: selector.connection.clone(),
391        provider_repository_id: selector.provider_repository_id.clone(),
392        installation_id: selector.installation_id.clone(),
393        private: selector.private,
394        clone_url: scope.source_url.clone(),
395        ..Default::default()
396    };
397    let provider =
398        resolve_import_provider(&source, selector.connection.as_ref().map(|_| "github"))?;
399    if scope.provider != provider
400        || (selector.connection.is_none() && selector.provider_repository_id != scope.source_url)
401    {
402        return Err(Reject::SourceSelection);
403    }
404    Ok(())
405}
406
407/// Opt-in discovery of pull-request heads. Merge refs are deliberately excluded.
408#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
409pub struct DiscoveryOptions {
410    pub include_pull_request_refs: bool,
411}
412
413/// Prepare checks current pins; Commit preserves frozen pins after movement/deletion.
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub enum DiscoverySelectionMode {
416    Prepare,
417    Commit,
418}
419
420/// Local validation evidence, never a signed record or a wire carrier.
421/// Private bindings prevent reuse for another source, selection or admission phase.
422#[derive(Debug)]
423pub struct DiscoveryDigest {
424    digest: [u8; 32],
425    ref_count: usize,
426    source_binding: Vec<u8>,
427    selection_binding: Vec<u8>,
428    mode: DiscoverySelectionMode,
429}
430impl DiscoveryDigest {
431    /// SHA-256 identifier of source custody/format, discovery options and the
432    /// complete ordered evidence (names, OIDs, classified kinds, algorithms/count).
433    /// Page boundaries, selections and admission phase do not affect this identifier.
434    /// It is local evidence, never signed content or authorization.
435    pub fn digest(&self) -> &[u8; 32] {
436        &self.digest
437    }
438    pub fn ref_count(&self) -> usize {
439        self.ref_count
440    }
441    fn check(
442        &self,
443        source: &ProviderRepository,
444        selected: &[ImportBranchLimitV1],
445        mode: DiscoverySelectionMode,
446    ) -> Result<(), Reject> {
447        if self.mode != mode
448            || self.source_binding != discovery_source_binding(source)?
449            || self.selection_binding != discovery_selection_binding(selected)?
450        {
451            return Err(Reject::SourceSelection);
452        }
453        Ok(())
454    }
455}
456
457// Evidence preserves alpha.45 name acceptance; only selected refs use branch_ref.
458// Classify supported discovery namespaces independently of untrusted kind hints.
459fn discovery_ref_kind(r: &ProviderRef, options: DiscoveryOptions) -> i32 {
460    if r.name.starts_with("refs/heads/") {
461        1
462    } else if r.name.starts_with("refs/tags/") {
463        2
464    } else if options.include_pull_request_refs
465        && r.name
466            .strip_prefix("refs/pull/")
467            .and_then(|n| n.strip_suffix("/head"))
468            .is_some_and(|n| {
469                !n.is_empty() && !n.starts_with('0') && n.bytes().all(|b| b.is_ascii_digit())
470            })
471    {
472        3
473    } else {
474        0
475    }
476}
477
478fn discovery_source_binding(source: &ProviderRepository) -> Result<Vec<u8>, Reject> {
479    use prost::Message;
480    // Bind custody/format only, excluding observations and all ref pages.
481    for text in [
482        &source.clone_url,
483        &source.provider_repository_id,
484        &source.installation_id,
485    ] {
486        if text.len() > 4096 {
487            return Err(Reject::Bounds);
488        }
489    }
490    if source
491        .connection
492        .as_ref()
493        .is_some_and(|c| c.encoded_len() > 4096)
494    {
495        return Err(Reject::Bounds);
496    }
497    let metadata = ProviderRepository {
498        connection: source.connection.clone(),
499        provider_repository_id: source.provider_repository_id.clone(),
500        clone_url: source.clone_url.clone(),
501        installation_id: source.installation_id.clone(),
502        private: source.private,
503        hash_algorithm: source.hash_algorithm,
504        ..Default::default()
505    };
506    Ok(hash(&[&metadata.encode_to_vec()]))
507}
508
509fn discovery_selection_binding(selected: &[ImportBranchLimitV1]) -> Result<Vec<u8>, Reject> {
510    use sha2::{Digest, Sha256};
511    if selected.len() > MAX_BRANCHES {
512        return Err(Reject::Bounds);
513    }
514    let mut digest = Sha256::new();
515    for (i, b) in selected.iter().enumerate() {
516        branch(b)?;
517        if i > 0 && selected[i - 1].ref_name >= b.ref_name {
518            return Err(Reject::Canonical);
519        }
520        digest.update(canonical(b)?);
521    }
522    Ok(digest.finalize().to_vec())
523}
524
525/// Bounded incremental validation of an authenticated, complete source snapshot.
526/// Pages MUST be globally ordered by UTF-8 ref-name bytes; strict order proves
527/// uniqueness with one previous name. Hosts must verify snapshot/cursor continuity
528/// and exhausted coverage before finish; this validator cannot detect truncation.
529/// State retains one bounded name, at most MAX_BRANCHES bounded selections and
530/// bounded source metadata, independent of total refs/pages.
531pub struct DiscoveryValidator {
532    digest: sha2::Sha256,
533    hash_algorithm: i32,
534    oid_size: Option<usize>,
535    options: DiscoveryOptions,
536    selected: Vec<ImportBranchLimitV1>,
537    previous: Option<String>,
538    ref_count: usize,
539    failed: bool,
540    source_binding: Vec<u8>,
541    selection_binding: Vec<u8>,
542    mode: DiscoverySelectionMode,
543}
544impl DiscoveryValidator {
545    pub fn new(
546        source: &ProviderRepository,
547        known: bool,
548        selected: &[ImportBranchLimitV1],
549        options: DiscoveryOptions,
550        mode: DiscoverySelectionMode,
551    ) -> Result<Self, Reject> {
552        use sha2::{Digest, Sha256};
553        let oid_size = match source.hash_algorithm {
554            1 => Some(40),
555            2 => Some(64),
556            0 if !known => None,
557            _ => return Err(Reject::Version),
558        };
559        let selection_binding = discovery_selection_binding(selected)?;
560        for b in selected {
561            if b.hash_algorithm != source.hash_algorithm {
562                return Err(Reject::SourceSelection);
563            }
564        }
565        let source_binding = discovery_source_binding(source)?;
566        let mut digest = Sha256::new();
567        digest.update(DISCOVERY_DOMAIN.as_bytes());
568        digest.update(&source_binding);
569        digest.update(u32::from(options.include_pull_request_refs).to_be_bytes());
570        digest.update((source.hash_algorithm as u32).to_be_bytes());
571        Ok(Self {
572            digest,
573            hash_algorithm: source.hash_algorithm,
574            oid_size,
575            options,
576            selected: selected.to_vec(),
577            previous: None,
578            ref_count: 0,
579            failed: false,
580            source_binding,
581            selection_binding,
582            mode,
583        })
584    }
585    pub fn push_page(&mut self, refs: &[ProviderRef]) -> Result<(), Reject> {
586        if self.failed {
587            return Err(Reject::Canonical);
588        }
589        let result = self.push_page_inner(refs);
590        if result.is_err() {
591            self.failed = true;
592        }
593        result
594    }
595    fn push_page_inner(&mut self, refs: &[ProviderRef]) -> Result<(), Reject> {
596        use sha2::Digest;
597        if refs.len() > MAX_IMPORT_SOURCE_REF_PAGE
598            || refs.len() > MAX_IMPORT_SOURCE_REFS - self.ref_count
599        {
600            return Err(Reject::Bounds);
601        }
602        for r in refs {
603            if r.name.len() > MAX_REF_BYTES {
604                return Err(Reject::Bounds);
605            }
606            if r.hash_algorithm != self.hash_algorithm {
607                return Err(Reject::SourceSelection);
608            }
609            if self.previous.as_ref().is_some_and(|p| p >= &r.name) {
610                return Err(Reject::Canonical);
611            }
612            if !r.head_oid.is_empty() {
613                let size = self.oid_size.ok_or(Reject::Version)?;
614                if r.head_oid.len() != size {
615                    return Err(Reject::SourceSelection);
616                }
617                if !r
618                    .head_oid
619                    .bytes()
620                    .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
621                {
622                    return Err(Reject::Canonical);
623                }
624            }
625            let kind = discovery_ref_kind(r, self.options);
626            if let Ok(i) = self.selected.binary_search_by(|b| b.ref_name.cmp(&r.name)) {
627                let b = &self.selected[i];
628                match self.mode {
629                    DiscoverySelectionMode::Prepare => {
630                        let oid = if r.head_oid.is_empty() {
631                            None
632                        } else {
633                            Some(hex::decode(&r.head_oid).map_err(|_| Reject::Canonical)?)
634                        };
635                        validate_ref_selection(b, oid.as_deref())?;
636                    }
637                    DiscoverySelectionMode::Commit => {
638                        if b.ref_mode == 2 && !r.head_oid.is_empty() {
639                            return Err(Reject::RefPinning);
640                        }
641                    }
642                }
643            }
644            // Canonical discovery encoding: sized UTF-8 name/OID, BE u32 kind and
645            // algorithm. A final BE u64 count makes boundaries unambiguous.
646            for text in [&r.name, &r.head_oid] {
647                self.digest.update((text.len() as u32).to_be_bytes());
648                self.digest.update(text.as_bytes());
649            }
650            self.digest.update((kind as u32).to_be_bytes());
651            self.digest.update((r.hash_algorithm as u32).to_be_bytes());
652            self.previous = Some(r.name.clone());
653            self.ref_count += 1;
654        }
655        Ok(())
656    }
657    pub fn finish(mut self) -> Result<DiscoveryDigest, Reject> {
658        use sha2::Digest;
659        if self.failed {
660            return Err(Reject::Canonical);
661        }
662        self.digest.update((self.ref_count as u64).to_be_bytes());
663        Ok(DiscoveryDigest {
664            digest: self.digest.finalize().into(),
665            ref_count: self.ref_count,
666            source_binding: self.source_binding,
667            selection_binding: self.selection_binding,
668            mode: self.mode,
669        })
670    }
671}
672
673/// Convenience adapter for callers that already have a complete list. Streaming
674/// hosts pass metadata to new(), discard each page after push_page(), then finish().
675pub fn discover_repository(
676    source: &ProviderRepository,
677    selected: &[ImportBranchLimitV1],
678    options: DiscoveryOptions,
679    mode: DiscoverySelectionMode,
680) -> Result<DiscoveryDigest, Reject> {
681    let mut validator = DiscoveryValidator::new(source, true, selected, options, mode)?;
682    for page in source.refs.chunks(MAX_IMPORT_SOURCE_REF_PAGE) {
683        validator.push_page(page)?;
684    }
685    validator.finish()
686}
687
688/// One discovery page, optionally unknown format; no SHA-1 fallback.
689pub fn validate_repository_hash_algorithm(
690    source: &ProviderRepository,
691    known: bool,
692) -> Result<(), Reject> {
693    validate_repository_hash_algorithm_with_options(source, known, DiscoveryOptions::default())
694}
695pub fn validate_repository_hash_algorithm_with_options(
696    source: &ProviderRepository,
697    known: bool,
698    options: DiscoveryOptions,
699) -> Result<(), Reject> {
700    let mut validator =
701        DiscoveryValidator::new(source, known, &[], options, DiscoverySelectionMode::Prepare)?;
702    validator.push_page(&source.refs)?;
703    validator.finish().map(|_| ())
704}
705/// Complete discovery uses the same incremental checks without retaining refs.
706pub fn validate_discovered_repository(source: &ProviderRepository) -> Result<(), Reject> {
707    discover_repository(
708        source,
709        &[],
710        DiscoveryOptions::default(),
711        DiscoverySelectionMode::Prepare,
712    )
713    .map(|_| ())
714}
715/// Compare independent complete discovery before signing, including known pins.
716pub fn validate_discovered_import_scope(
717    scope: &ImportPermissionScopeV1,
718    source: &ProviderRepository,
719) -> Result<(), Reject> {
720    if scope.source_url != source.clone_url {
721        return Err(Reject::SourceSelection);
722    }
723    discover_repository(
724        source,
725        &scope.branches,
726        DiscoveryOptions::default(),
727        DiscoverySelectionMode::Prepare,
728    )
729    .map(|_| ())
730}
731
732/// Host resolves the selector anew and checks current grants and selected-commit
733/// availability before issuing a reservation, for a new job. Source state
734/// MUST come from an authenticated job-state read or receiver-owned durable state;
735/// the current source resolver binds independently verified discovery to the proposal.
736pub fn prepare_import_source_scope(
737    request: &PrepareImportJobRequest,
738    current_source: &ProviderRepository,
739    connection_provider: Option<&str>,
740    configuration: &GetImportConfigurationResponse,
741    current_destination_version: &[u8],
742) -> Result<ImportPermissionScopeV1, Reject> {
743    prepare_import_source_scope_inner(
744        request,
745        current_source,
746        connection_provider,
747        configuration,
748        current_destination_version,
749        None,
750    )
751}
752/// Admit a completed local paged discovery without assembling all source refs.
753pub fn prepare_import_source_scope_with_discovery(
754    request: &PrepareImportJobRequest,
755    current_source: &ProviderRepository,
756    connection_provider: Option<&str>,
757    configuration: &GetImportConfigurationResponse,
758    current_destination_version: &[u8],
759    discovery: &DiscoveryDigest,
760) -> Result<ImportPermissionScopeV1, Reject> {
761    prepare_import_source_scope_inner(
762        request,
763        current_source,
764        connection_provider,
765        configuration,
766        current_destination_version,
767        Some(discovery),
768    )
769}
770fn prepare_import_source_scope_inner(
771    request: &PrepareImportJobRequest,
772    current_source: &ProviderRepository,
773    connection_provider: Option<&str>,
774    configuration: &GetImportConfigurationResponse,
775    current_destination_version: &[u8],
776    discovery: Option<&DiscoveryDigest>,
777) -> Result<ImportPermissionScopeV1, Reject> {
778    let selector = request.source.as_ref().ok_or(Reject::SourceSelection)?;
779    if selector.connection != current_source.connection
780        || (selector.provider_repository_id != current_source.provider_repository_id
781            && (selector.connection.is_some() || !selector.provider_repository_id.is_empty()))
782        || selector.installation_id != current_source.installation_id
783        || selector.private != current_source.private
784    {
785        return Err(Reject::SourceSelection);
786    }
787    let scope = request.proposed_scope.as_ref().ok_or(Reject::Canonical)?;
788    let provider = resolve_import_provider(current_source, connection_provider)?;
789    if scope.provider != provider {
790        return Err(Reject::SourceSelection);
791    }
792    if scope.source_url != current_source.clone_url {
793        return Err(Reject::SourceSelection);
794    }
795    if let Some(discovery) = discovery {
796        discovery.check(
797            current_source,
798            &scope.branches,
799            DiscoverySelectionMode::Prepare,
800        )?;
801    } else {
802        validate_discovered_import_scope(scope, current_source)?;
803    }
804    validate_import_configuration(configuration)?;
805    prepare_scope(scope, configuration, current_destination_version)
806}
807
808fn validate_provider_support(
809    provider: &str,
810    configuration: &GetImportConfigurationResponse,
811) -> Result<(), Reject> {
812    if !configuration
813        .providers
814        .iter()
815        .any(|p| p.provider == provider)
816    {
817        return Err(Reject::SourceSelection);
818    }
819    Ok(())
820}
821
822pub fn validate_import_configuration(v: &GetImportConfigurationResponse) -> Result<(), Reject> {
823    use prost::Message;
824    if v.encoded_len() > MAX_BUNDLE_BYTES || v.converters.is_empty() || v.converters.len() > 32 {
825        return Err(Reject::Bounds);
826    }
827    for (i, c) in v.converters.iter().enumerate() {
828        for text in [&c.converter_version, &c.options_encoding] {
829            if text.is_empty() || text.len() > 128 || !text.is_ascii() {
830                return Err(Reject::Canonical);
831            }
832        }
833        if i > 0 && v.converters[i - 1].converter_version >= c.converter_version {
834            return Err(Reject::Canonical);
835        }
836        if c.canonical_options.is_empty()
837            || c.canonical_options.len() > 64
838            || c.canonical_options.iter().any(|o| o.len() > 4096)
839        {
840            return Err(Reject::Bounds);
841        }
842        if c.canonical_options.windows(2).any(|w| w[0] >= w[1])
843            || !c.canonical_options.contains(&c.default_options)
844        {
845            return Err(Reject::Canonical);
846        }
847    }
848    if v.providers.is_empty() || v.providers.len() > 2 {
849        return Err(Reject::Bounds);
850    }
851    for (i, p) in v.providers.iter().enumerate() {
852        if i > 0 && v.providers[i - 1].provider >= p.provider {
853            return Err(Reject::Canonical);
854        }
855        let mode = match p.provider.as_str() {
856            "github" => 1,
857            "public-git" => 2,
858            _ => return Err(Reject::SourceSelection),
859        };
860        if p.source_modes != [mode] {
861            return Err(Reject::SourceSelection);
862        }
863    }
864    if let Some(default) = &v.default_converter_version
865        && !v.converters.iter().any(|c| &c.converter_version == default)
866    {
867        return Err(Reject::Canonical);
868    }
869    let l = v.limits.as_ref().ok_or(Reject::Canonical)?;
870    if l.max_branches == 0
871        || l.max_branches as usize > MAX_BRANCHES
872        || l.max_operations == 0
873        || l.max_operations as usize > MAX_BRANCHES
874        || l.max_result_bytes == 0
875    {
876        return Err(Reject::Bounds);
877    }
878    Ok(())
879}
880
881/// Host-side negotiation with a CURRENT authenticated configuration and CAS.
882/// Only an empty destination token is filled; all other choices survive exactly.
883pub fn prepare_scope(
884    proposed: &ImportPermissionScopeV1,
885    configuration: &GetImportConfigurationResponse,
886    current_destination_version: &[u8],
887) -> Result<ImportPermissionScopeV1, Reject> {
888    use ImportPreparationRefusalReason as Reason;
889    validate_import_configuration(configuration)?;
890    width(current_destination_version, 32)?;
891    if !proposed.destination_version.is_empty() && proposed.destination_version.len() != 32 {
892        return Err(Reject::PreparationRefused(Reason::InvalidScope));
893    }
894    if !proposed.destination_version.is_empty()
895        && proposed.destination_version != current_destination_version
896    {
897        return Err(Reject::PreparationRefused(Reason::DestinationConflict));
898    }
899    let mut selected = proposed.clone();
900    selected.destination_version = current_destination_version.to_vec();
901    validate_scope(&selected).map_err(|_| Reject::PreparationRefused(Reason::InvalidScope))?;
902    validate_provider_support(&selected.provider, configuration)
903        .map_err(|_| Reject::PreparationRefused(Reason::InvalidScope))?;
904    let converter = configuration
905        .converters
906        .iter()
907        .find(|c| c.converter_version == selected.converter_version)
908        .ok_or(Reject::PreparationRefused(Reason::UnsupportedConverter))?;
909    let supported = converter
910        .canonical_options
911        .iter()
912        .try_fold(false, |found, o| {
913            Ok::<_, Reject>(
914                found
915                    || conversion_options_digest(&converter.converter_version, o)?
916                        == selected.options_digest,
917            )
918        })?;
919    if !supported {
920        return Err(Reject::PreparationRefused(Reason::UnsupportedOptions));
921    }
922    let limits = configuration.limits.as_ref().ok_or(Reject::Canonical)?;
923    if selected.branches.len() > limits.max_branches as usize
924        || selected.max_operations > limits.max_operations
925        || selected.max_result_bytes > limits.max_result_bytes
926    {
927        return Err(Reject::PreparationRefused(Reason::BudgetExceeded));
928    }
929    Ok(selected)
930}
931
932/// Independently retained refs/slots for one non-terminal logical job. Include
933/// Prepared jobs and all original refs until logical termination, not just the
934/// current certificate's remaining scope. Hosts provide the complete inventory
935/// under the same transaction that reserves/activates; this helper stores nothing.
936pub struct ImportSpoolReservation<'a> {
937    pub spool_uuid: &'a [u8],
938    pub logical_job_id: &'a [u8],
939    pub branches: &'a [ImportBranchLimitV1],
940}
941
942/// Per-spool full refs and (full ref, slot_id) keys are exclusive across jobs,
943/// regardless of provider/source. Same-job checks retain exact branch identity.
944/// Run after scope negotiation and before atomically reserving every ref/slot.
945pub fn check_import_spool_reservations(
946    spool_uuid: &[u8],
947    logical_job_id: &[u8],
948    scope: &ImportPermissionScopeV1,
949    reservations: &[ImportSpoolReservation<'_>],
950) -> Result<(), Reject> {
951    initial_operation_id(spool_uuid, false)?;
952    initial_operation_id(logical_job_id, false)?;
953    validate_scope(scope)?;
954    for reservation in reservations {
955        initial_operation_id(reservation.spool_uuid, false)?;
956        initial_operation_id(reservation.logical_job_id, false)?;
957        if reservation.spool_uuid != spool_uuid {
958            continue;
959        }
960        let same_job = reservation.logical_job_id == logical_job_id;
961        for selected in &scope.branches {
962            let conflict = if same_job {
963                !reservation.branches.contains(selected)
964            } else {
965                reservation.branches.iter().any(|held| {
966                    held.ref_name == selected.ref_name
967                        || held.target_thread_id == selected.target_thread_id
968                        || held.genesis_digest == selected.genesis_digest
969                })
970            };
971            if conflict {
972                return Err(Reject::PreparationRefused(
973                    ImportPreparationRefusalReason::DestinationConflict,
974                ));
975            }
976        }
977    }
978    Ok(())
979}
980
981/// Commit-only wire mapping for reservation conflicts and stale destination CAS.
982/// These admission failures destroy prepared custody and do not freeze a receipt.
983pub fn import_commit_conflict_failure(
984    rejection: &Reject,
985) -> Option<crate::heddle::api::common::CallFailure> {
986    use crate::heddle::api::common::{CallFailure, CallFailureCode, ErrorDetail, ErrorReason};
987    let (code, reason) = match rejection {
988        Reject::PreparationRefused(ImportPreparationRefusalReason::DestinationConflict) => (
989            CallFailureCode::AlreadyExists,
990            ErrorReason::ImportDestinationConflict,
991        ),
992        Reject::StaleContext => (CallFailureCode::Aborted, ErrorReason::VersionConflict),
993        _ => return None,
994    };
995    Some(CallFailure {
996        code: code as i32,
997        message: String::new(),
998        error: Some(ErrorDetail {
999            reason: reason as i32,
1000            ..Default::default()
1001        }),
1002    })
1003}
1004
1005/// Current destination configuration/ownership/policy CAS at activation. Other
1006/// imports do not advance this token. Exact stored replay is resolved first.
1007pub fn check_import_destination_version(
1008    scope: &ImportPermissionScopeV1,
1009    current_destination_version: &[u8],
1010) -> Result<(), Reject> {
1011    width(&scope.destination_version, 32)?;
1012    width(current_destination_version, 32)?;
1013    if scope.destination_version != current_destination_version {
1014        return Err(Reject::StaleContext);
1015    }
1016    Ok(())
1017}
1018
1019/// Browser-side comparison before signing. A host cannot silently negotiate.
1020pub fn validate_preparation_response(
1021    request: &PrepareImportJobRequest,
1022    response: &PrepareImportJobResponse,
1023) -> Result<(), Reject> {
1024    if let Some(refusal) = &response.refusal {
1025        let reason = ImportPreparationRefusalReason::try_from(refusal.reason)
1026            .map_err(|_| Reject::Version)?;
1027        if reason == ImportPreparationRefusalReason::Unspecified
1028            || refusal.field.len() > 256
1029            || !refusal.field.is_ascii()
1030            || response.proposal.is_some()
1031            || response.reservation_expires_at_unix_seconds != 0
1032            || response.prepared_at_unix_seconds != 0
1033            || response.max_validity_duration_seconds != 0
1034            || response.clock_skew_allowance_seconds != 0
1035        {
1036            return Err(Reject::Canonical);
1037        }
1038        return Err(Reject::PreparationRefused(reason));
1039    }
1040    let p = response.proposal.as_ref().ok_or(Reject::Canonical)?;
1041    let returned = p.scope.as_ref().ok_or(Reject::Canonical)?;
1042    let mut requested = request.proposed_scope.clone().ok_or(Reject::Canonical)?;
1043    if requested.destination_version.is_empty() {
1044        requested.destination_version = returned.destination_version.clone();
1045    }
1046    if canonical(&requested)? != canonical(returned)?
1047        || p.identity != request.identity
1048        || p.retry_lineage_id != request.retry_lineage_id
1049    {
1050        return Err(Reject::PreparedFields);
1051    }
1052    validate_scope(returned)?;
1053    Ok(())
1054}
1055
1056/// Enforce inclusive decoded protobuf sizes before semantic admission.
1057/// Transport must also bound the whole uncompressed payload before decoding.
1058pub fn validate_commit_request_bounds(request: &CommitImportJobRequest) -> Result<(), Reject> {
1059    use prost::Message;
1060    if request.encoded_len() > MAX_COMMIT_REQUEST_BYTES
1061        || request
1062            .proof
1063            .as_ref()
1064            .ok_or(Reject::Canonical)?
1065            .encoded_len()
1066            > MAX_BUNDLE_BYTES
1067    {
1068        return Err(Reject::Bounds);
1069    }
1070    Ok(())
1071}
1072
1073/// Source association/provider comes from the host's authenticated resolver,
1074/// never a projection hint. Native base decoding/identity and current grants
1075/// remain host gates; this validates carrier, bounds and exact originals.
1076pub fn validate_commit_request(
1077    request: &CommitImportJobRequest,
1078    resolved_provider: &str,
1079    current_source: &ProviderRepository,
1080    configuration: &GetImportConfigurationResponse,
1081) -> Result<(), Reject> {
1082    validate_commit_request_inner(
1083        request,
1084        resolved_provider,
1085        current_source,
1086        configuration,
1087        None,
1088    )
1089}
1090/// Commit counterpart for completed local paged discovery; frozen pins survive movement.
1091pub fn validate_commit_request_with_discovery(
1092    request: &CommitImportJobRequest,
1093    resolved_provider: &str,
1094    current_source: &ProviderRepository,
1095    configuration: &GetImportConfigurationResponse,
1096    discovery: &DiscoveryDigest,
1097) -> Result<(), Reject> {
1098    validate_commit_request_inner(
1099        request,
1100        resolved_provider,
1101        current_source,
1102        configuration,
1103        Some(discovery),
1104    )
1105}
1106fn validate_commit_request_inner(
1107    request: &CommitImportJobRequest,
1108    resolved_provider: &str,
1109    current_source: &ProviderRepository,
1110    configuration: &GetImportConfigurationResponse,
1111    discovery: Option<&DiscoveryDigest>,
1112) -> Result<(), Reject> {
1113    use prost::Message;
1114    validate_commit_request_bounds(request)?;
1115    let source = request.source.as_ref().ok_or(Reject::SourceSelection)?;
1116    let proof = request.proof.as_ref().ok_or(Reject::Canonical)?;
1117    if request.client_operation_id.is_empty()
1118        || request.client_operation_id.len() > 128
1119        || request.destination.as_ref().is_none_or(|d| d.id.is_empty())
1120    {
1121        return Err(Reject::Canonical);
1122    }
1123    if request.initial_base_state.len() > 4096 || proof.encoded_len() > MAX_BUNDLE_BYTES {
1124        return Err(Reject::Bounds);
1125    }
1126    if proof.format_version != 1
1127        || proof.delegations.len() != 1
1128        || !proof.operations.is_empty()
1129        || proof.terminal_manifest.is_some()
1130        || !proof.manifests.is_empty()
1131    {
1132        return Err(Reject::Canonical);
1133    }
1134    let d = proof.delegations[0]
1135        .body
1136        .as_ref()
1137        .ok_or(Reject::Canonical)?;
1138    let scope = d.scope.as_ref().ok_or(Reject::Canonical)?;
1139    validate_scope(scope)?;
1140    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
1141    identity(id)?;
1142    let uuid = hex::encode(&id.spool_uuid);
1143    let destination_id = format!(
1144        "{}-{}-{}-{}-{}",
1145        &uuid[..8],
1146        &uuid[8..12],
1147        &uuid[12..16],
1148        &uuid[16..20],
1149        &uuid[20..]
1150    );
1151    if request
1152        .destination
1153        .as_ref()
1154        .is_none_or(|s| s.id != destination_id)
1155    {
1156        return Err(Reject::Scope);
1157    }
1158    if source.clone_url != scope.source_url
1159        || resolved_provider != scope.provider
1160        || source.provider_repository_id.len() > 4096
1161        || source.name.len() > 4096
1162    {
1163        return Err(Reject::SourceSelection);
1164    }
1165    if source.connection != current_source.connection
1166        || (source.provider_repository_id != current_source.provider_repository_id
1167            && (source.connection.is_some() || !source.provider_repository_id.is_empty()))
1168        || source.clone_url != current_source.clone_url
1169        || source.installation_id != current_source.installation_id
1170        || source.private != current_source.private
1171    {
1172        return Err(Reject::SourceSelection);
1173    }
1174    let provider = resolve_import_provider(
1175        current_source,
1176        current_source
1177            .connection
1178            .as_ref()
1179            .map(|_| resolved_provider),
1180    )?;
1181    if provider != resolved_provider {
1182        return Err(Reject::SourceSelection);
1183    }
1184    validate_import_configuration(configuration)?;
1185    validate_provider_support(provider, configuration)?;
1186    if let Some(discovery) = discovery {
1187        discovery.check(
1188            current_source,
1189            &scope.branches,
1190            DiscoverySelectionMode::Commit,
1191        )?;
1192    } else {
1193        discover_repository(
1194            current_source,
1195            &scope.branches,
1196            DiscoveryOptions::default(),
1197            DiscoverySelectionMode::Commit,
1198        )?;
1199    }
1200    if source.hash_algorithm != current_source.hash_algorithm {
1201        return Err(Reject::SourceSelection);
1202    }
1203    // Current converter/options/budget support is also rechecked at activation.
1204    prepare_scope(scope, configuration, &scope.destination_version)?;
1205    if proof.original_geneses.len() != scope.branches.len()
1206        || proof.creator_authority_envelopes.len() != scope.branches.len()
1207        || proof.genesis_authorities.len() != scope.branches.len()
1208        || d.branch_manifest.len() != scope.branches.len()
1209    {
1210        return Err(Reject::GenesisBinding);
1211    }
1212    // Arrays are ordered exactly like the signed branch manifest, no second
1213    // association-by-name payload and no incoming regenerated branch originals.
1214    for (i, b) in scope.branches.iter().enumerate() {
1215        let original = &proof.original_geneses[i];
1216        let binding = &proof.genesis_authorities[i];
1217        let g = binding.body.as_ref().ok_or(Reject::GenesisBinding)?;
1218        let m = &d.branch_manifest[i];
1219        if native_id(original) != b.genesis_digest
1220            || g.genesis_digest != b.genesis_digest
1221            || m.limit.as_ref() != Some(b)
1222            || m.genesis_authority_digest != signed_genesis_digest(binding)?
1223            || g.creator_authority_envelope_digest != hash(&[&proof.creator_authority_envelopes[i]])
1224            || proof.creator_authority_envelopes[i].is_empty()
1225            || proof.creator_authority_envelopes[i].len() > MAX_RECORD_BYTES
1226            || !original.signatures.iter().any(|s| {
1227                s.public_key == g.creator_public_key && s.signature == g.original_creator_signature
1228            })
1229        {
1230            return Err(Reject::GenesisBinding);
1231        }
1232        verify_native(original, "heddle-thread-genesis-v1")?;
1233    }
1234    Ok(())
1235}
1236
1237/// This closed route has no initial-submission or authority-attachment role.
1238pub fn validate_import_source(_: &ImportSourceRequest) -> Result<(), Reject> {
1239    Err(Reject::ImportSourceRequiresCommit)
1240}
1241
1242/// Complete initial validation, excluding native model/owner history, live
1243/// source access and transaction checks owned by the hosted implementation.
1244pub fn verify_commit_submission(
1245    request: &CommitImportJobRequest,
1246    prepared: &PrepareImportJobResponse,
1247    resolved_provider: &str,
1248    current_source: &ProviderRepository,
1249    configuration: &GetImportConfigurationResponse,
1250    expected: &ImportOwnerExpectation<'_>,
1251) -> Result<VerifiedImportDelegation, Reject> {
1252    verify_commit_submission_inner(
1253        request,
1254        prepared,
1255        resolved_provider,
1256        current_source,
1257        configuration,
1258        expected,
1259        None,
1260    )
1261}
1262
1263/// Complete signed submission using finished local discovery evidence.
1264pub fn verify_commit_submission_with_discovery(
1265    request: &CommitImportJobRequest,
1266    prepared: &PrepareImportJobResponse,
1267    resolved_provider: &str,
1268    current_source: &ProviderRepository,
1269    configuration: &GetImportConfigurationResponse,
1270    expected: &ImportOwnerExpectation<'_>,
1271    discovery: &DiscoveryDigest,
1272) -> Result<VerifiedImportDelegation, Reject> {
1273    verify_commit_submission_inner(
1274        request,
1275        prepared,
1276        resolved_provider,
1277        current_source,
1278        configuration,
1279        expected,
1280        Some(discovery),
1281    )
1282}
1283
1284fn verify_commit_submission_inner(
1285    request: &CommitImportJobRequest,
1286    prepared: &PrepareImportJobResponse,
1287    resolved_provider: &str,
1288    current_source: &ProviderRepository,
1289    configuration: &GetImportConfigurationResponse,
1290    expected: &ImportOwnerExpectation<'_>,
1291    discovery: Option<&DiscoveryDigest>,
1292) -> Result<VerifiedImportDelegation, Reject> {
1293    validate_commit_request_inner(
1294        request,
1295        resolved_provider,
1296        current_source,
1297        configuration,
1298        discovery,
1299    )?;
1300    let proof = request.proof.as_ref().ok_or(Reject::Canonical)?;
1301    let member = proof.member_permission.as_ref();
1302    verify_prepared_delegation(
1303        prepared,
1304        &proof.delegations[0],
1305        member,
1306        &proof.genesis_authorities,
1307        expected,
1308    )
1309}
1310
1311pub fn validate_commit_response(
1312    request: &CommitImportJobRequest,
1313    response: &MutationResponse,
1314) -> Result<(), Reject> {
1315    let receipt = response.receipt.as_ref().ok_or(Reject::PendingOperation)?;
1316    let Some(mutation_receipt::Outcome::PendingOperation(operation)) = &receipt.outcome else {
1317        return Err(Reject::PendingOperation);
1318    };
1319    if request.client_operation_id.is_empty()
1320        || receipt.client_operation_id != request.client_operation_id
1321        || request.destination.is_none()
1322        || operation.spool != request.destination
1323        || operation.id
1324            != initial_operation_id(
1325                &request
1326                    .proof
1327                    .as_ref()
1328                    .ok_or(Reject::PendingOperation)?
1329                    .delegations
1330                    .first()
1331                    .and_then(|d| d.body.as_ref())
1332                    .ok_or(Reject::PendingOperation)?
1333                    .retry_lineage_id,
1334                false,
1335            )?
1336    {
1337        return Err(Reject::PendingOperation);
1338    }
1339    Ok(())
1340}
1341
1342/// Use a durable caller-scoped idempotency row BEFORE rechecking expired job
1343/// authority. Host stores the original request/receipt atomically with activation.
1344/// Changed inputs refuse OperationIdReused only for an ID with an accepted receipt.
1345pub fn check_commit_replay(
1346    request: &CommitImportJobRequest,
1347    stored: &CommitImportJobRequest,
1348    response: &MutationResponse,
1349) -> Result<(), Reject> {
1350    if request != stored {
1351        return Err(Reject::OperationIdReused);
1352    }
1353    validate_commit_response(request, response)
1354}
1355pub fn validate_scope(value: &ImportPermissionScopeV1) -> Result<(), Reject> {
1356    canonical_https(&value.source_url, false)?;
1357    if value.provider.is_empty()
1358        || value.provider.len() > 64
1359        || !value
1360            .provider
1361            .bytes()
1362            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
1363        || value.converter_version.is_empty()
1364        || value.converter_version.len() > 128
1365        || !value.converter_version.is_ascii()
1366    {
1367        return Err(Reject::Canonical);
1368    }
1369    width(&value.destination_version, 32)?;
1370    width(&value.options_digest, 32)?;
1371    if value.branches.is_empty()
1372        || value.branches.len() > MAX_BRANCHES
1373        || value.max_operations == 0
1374        || value.max_operations as usize > MAX_BRANCHES
1375        || value.max_result_bytes == 0
1376    {
1377        return Err(Reject::Bounds);
1378    }
1379    if (value.max_operations as usize) < value.branches.len() {
1380        return Err(Reject::Scope);
1381    }
1382    for (i, b) in value.branches.iter().enumerate() {
1383        branch(b)?;
1384        if value.branches[..i].iter().any(|other| {
1385            other.target_thread_id == b.target_thread_id || other.genesis_digest == b.genesis_digest
1386        }) {
1387            return Err(Reject::Scope);
1388        }
1389        if i > 0 && value.branches[i - 1].ref_name.as_bytes() >= b.ref_name.as_bytes() {
1390            return Err(Reject::Canonical);
1391        }
1392    }
1393    Ok(())
1394}
1395fn scope_subset(child: &ImportPermissionScopeV1, parent: &ImportPermissionScopeV1) -> bool {
1396    child.provider == parent.provider
1397        && child.source_url == parent.source_url
1398        && child.destination_version == parent.destination_version
1399        && child.options_digest == parent.options_digest
1400        && child.converter_version == parent.converter_version
1401        && child.max_operations <= parent.max_operations
1402        && child.max_result_bytes <= parent.max_result_bytes
1403        && child.branches.iter().all(|c| parent.branches.contains(c))
1404}
1405
1406/// Public context from the existing owner/keyring verifier, not from fields in
1407/// the incoming bundle. now is receiver/host time for new work or independently
1408/// verified witness observation time for retained history, NEVER author time.
1409pub struct ImportOwnerExpectation<'a> {
1410    pub identity: &'a ImportIdentityV1,
1411    pub owner_public_key: &'a [u8],
1412    pub owner_chain_digest: &'a [u8],
1413    /// From the independently verified EFFECTIVE owner state at `now`, not
1414    /// the immutable root. Accepted claim/deferral clearing is unbounded.
1415    pub authority_expires_at_seconds: i64,
1416    pub now_unix_seconds: i64,
1417    pub forbidden_job_keys: &'a [Vec<u8>], // Every user/root/witness key, including tombstones.
1418    pub known_job_associations: &'a [(Vec<u8>, Vec<u8>)], // key -> logical job.
1419}
1420/// Independently verified owner facts for retained bundle verification. Historical
1421/// check times are selected internally from authenticated receipts, never callers.
1422#[derive(Clone, Copy)]
1423pub struct ImportBundleOwnerExpectation<'a> {
1424    pub identity: &'a ImportIdentityV1,
1425    pub owner_public_key: &'a [u8],
1426    pub owner_chain_digest: &'a [u8],
1427    pub authority_expires_at_seconds: i64,
1428    /// Effective interval of these independently authenticated owner facts.
1429    pub effective_from_unix_seconds: i64,
1430    pub effective_until_unix_seconds: Option<i64>,
1431    pub forbidden_job_keys: &'a [Vec<u8>],
1432    pub forbidden_landing_keys: &'a [Vec<u8>],
1433    pub known_job_associations: &'a [(Vec<u8>, Vec<u8>)],
1434}
1435impl<'a> ImportBundleOwnerExpectation<'a> {
1436    fn at(self, time: i64, associations: &'a [(Vec<u8>, Vec<u8>)]) -> ImportOwnerExpectation<'a> {
1437        ImportOwnerExpectation {
1438            identity: self.identity,
1439            owner_public_key: self.owner_public_key,
1440            owner_chain_digest: self.owner_chain_digest,
1441            authority_expires_at_seconds: self.authority_expires_at_seconds,
1442            now_unix_seconds: time,
1443            forbidden_job_keys: self.forbidden_job_keys,
1444            known_job_associations: associations,
1445        }
1446    }
1447}
1448/// Inputs MUST come from the selected, independently verified effective state.
1449/// Historical verification selects the state at its authenticated observation.
1450pub fn effective_owner_authority_expiry(
1451    deferred_human: bool,
1452    claimable_until: i64,
1453) -> Result<i64, Reject> {
1454    if deferred_human {
1455        if claimable_until <= 0 {
1456            return Err(Reject::Scope);
1457        }
1458        Ok(claimable_until)
1459    } else {
1460        Ok(i64::MAX)
1461    }
1462}
1463pub fn verify_member_permission(
1464    signed: &SignedImportMemberPermissionV1,
1465    expected: &ImportOwnerExpectation<'_>,
1466) -> Result<(), Reject> {
1467    verify_member_permission_inner(signed, expected, true)
1468}
1469fn verify_member_permission_inner(
1470    signed: &SignedImportMemberPermissionV1,
1471    expected: &ImportOwnerExpectation<'_>,
1472    current: bool,
1473) -> Result<(), Reject> {
1474    let p = signed.body.as_ref().ok_or(Reject::ImportPermission)?;
1475    if p.format_version != 1 || p.purpose != 1 {
1476        return Err(Reject::ImportPermission);
1477    }
1478    identity(p.identity.as_ref().ok_or(Reject::Canonical)?)?;
1479    if p.identity.as_ref() != Some(expected.identity)
1480        || p.owner_chain_digest != expected.owner_chain_digest
1481    {
1482        return Err(Reject::Root);
1483    }
1484    width(&p.logical_job_id, 16)?;
1485    width(&p.retry_lineage_id, 16)?;
1486    width(&p.subject_public_key, 32)?;
1487    width(&p.cancellation_id, 32)?;
1488    width(&p.nonce, 32)?;
1489    width(&p.owner_chain_digest, 32)?;
1490    validate_scope(p.scope.as_ref().ok_or(Reject::Canonical)?)?;
1491    validity(
1492        p.not_before_unix_seconds,
1493        p.expires_at_unix_seconds,
1494        expected.now_unix_seconds,
1495        current,
1496    )?;
1497    if p.expires_at_unix_seconds > expected.authority_expires_at_seconds {
1498        return Err(Reject::Scope);
1499    }
1500    verify_authorization_signature(
1501        expected.owner_public_key,
1502        PERMISSION_DOMAIN,
1503        p,
1504        signed.owner_signature.as_ref().ok_or(Reject::Signature)?,
1505    )
1506}
1507
1508#[derive(Debug, Clone, PartialEq)]
1509pub struct VerifiedImportDelegation {
1510    body: ImportJobDelegationV1,
1511    digest: Vec<u8>,
1512    member: Option<SignedImportMemberPermissionV1>,
1513}
1514impl VerifiedImportDelegation {
1515    pub fn body(&self) -> &ImportJobDelegationV1 {
1516        &self.body
1517    }
1518    pub fn digest(&self) -> &[u8] {
1519        &self.digest
1520    }
1521}
1522pub fn verify_delegation(
1523    signed: &SignedImportJobDelegationV1,
1524    member: Option<&SignedImportMemberPermissionV1>,
1525    expected: &ImportOwnerExpectation<'_>,
1526) -> Result<VerifiedImportDelegation, Reject> {
1527    verify_delegation_inner(signed, member, expected, true)
1528}
1529fn verify_delegation_inner(
1530    signed: &SignedImportJobDelegationV1,
1531    member: Option<&SignedImportMemberPermissionV1>,
1532    expected: &ImportOwnerExpectation<'_>,
1533    current: bool,
1534) -> Result<VerifiedImportDelegation, Reject> {
1535    let d = signed.body.as_ref().ok_or(Reject::Canonical)?;
1536    if d.format_version != 1 || d.purpose != 1 {
1537        return Err(Reject::Version);
1538    }
1539    identity(d.identity.as_ref().ok_or(Reject::Canonical)?)?;
1540    if d.identity.as_ref() != Some(expected.identity)
1541        || d.owner_chain_digest != expected.owner_chain_digest
1542    {
1543        return Err(Reject::Root);
1544    }
1545    for v in [&d.delegation_id, &d.logical_job_id, &d.retry_lineage_id] {
1546        width(v, 16)?;
1547        if v.iter().all(|b| *b == 0) {
1548            return Err(Reject::Canonical);
1549        }
1550    }
1551    for v in [
1552        &d.job_public_key,
1553        &d.job_key_id,
1554        &d.delegating_public_key,
1555        &d.parent_permission_digest,
1556        &d.owner_chain_digest,
1557        &d.cancellation_id,
1558    ] {
1559        width(v, 32)?;
1560    }
1561    if d.job_key_id != key_id(&d.job_public_key) {
1562        return Err(Reject::Canonical);
1563    }
1564    if d.job_public_key == d.delegating_public_key
1565        || d.job_public_key == expected.owner_public_key
1566        || expected.forbidden_job_keys.contains(&d.job_public_key)
1567    {
1568        return Err(Reject::KeyRole);
1569    }
1570    if expected
1571        .known_job_associations
1572        .iter()
1573        .any(|(k, j)| k == &d.job_public_key && j != &d.logical_job_id)
1574    {
1575        return Err(Reject::Scope);
1576    }
1577    let scope = d.scope.as_ref().ok_or(Reject::Canonical)?;
1578    validate_scope(scope)?;
1579    if d.branch_manifest.len() != scope.branches.len() {
1580        return Err(Reject::Scope);
1581    }
1582    for (m, b) in d.branch_manifest.iter().zip(&scope.branches) {
1583        width(&m.genesis_authority_digest, 32)?;
1584        if m.limit.as_ref() != Some(b) {
1585            return Err(Reject::Scope);
1586        }
1587    }
1588    if i128::from(d.expires_at_unix_seconds) - i128::from(d.not_before_unix_seconds)
1589        > i128::from(MAX_DELEGATION_WINDOW_SECONDS)
1590    {
1591        return Err(Reject::ValidityBounds);
1592    }
1593    validity(
1594        d.not_before_unix_seconds,
1595        d.expires_at_unix_seconds,
1596        expected.now_unix_seconds,
1597        current,
1598    )?;
1599    if d.expires_at_unix_seconds > expected.authority_expires_at_seconds {
1600        return Err(Reject::Scope);
1601    }
1602    if d.delegating_public_key == expected.owner_public_key {
1603        if member.is_some() || d.parent_permission_digest != vec![0; 32] {
1604            return Err(Reject::ImportPermission);
1605        }
1606    } else {
1607        let member = member.ok_or(Reject::ImportPermission)?;
1608        verify_member_permission_inner(member, expected, current)?;
1609        let p = member.body.as_ref().ok_or(Reject::ImportPermission)?;
1610        if d.parent_permission_digest != signed_permission_digest(member)?
1611            || d.delegating_public_key != p.subject_public_key
1612            || d.logical_job_id != p.logical_job_id
1613            || d.retry_lineage_id != p.retry_lineage_id
1614            || d.not_before_unix_seconds < p.not_before_unix_seconds
1615            || d.expires_at_unix_seconds > p.expires_at_unix_seconds
1616            || !scope_subset(scope, p.scope.as_ref().ok_or(Reject::Canonical)?)
1617        {
1618            return Err(Reject::Scope);
1619        }
1620    }
1621    verify_authorization_signature(
1622        &d.delegating_public_key,
1623        DELEGATION_DOMAIN,
1624        d,
1625        signed
1626            .delegating_signature
1627            .as_ref()
1628            .ok_or(Reject::Signature)?,
1629    )?;
1630    Ok(VerifiedImportDelegation {
1631        body: d.clone(),
1632        digest: signed_delegation_digest(signed)?,
1633        member: member.cloned(),
1634    })
1635}
1636/// Frozen canonical projection. The browser completes only the fields absent
1637/// here; it cannot normalize or reduce even an otherwise authorized scope.
1638pub fn delegation_preparation(d: &ImportJobDelegationV1) -> ImportJobPreparationV1 {
1639    ImportJobPreparationV1 {
1640        format_version: d.format_version,
1641        identity: d.identity.clone(),
1642        delegation_id: d.delegation_id.clone(),
1643        logical_job_id: d.logical_job_id.clone(),
1644        retry_lineage_id: d.retry_lineage_id.clone(),
1645        job_public_key: d.job_public_key.clone(),
1646        job_key_id: d.job_key_id.clone(),
1647        owner_chain_digest: d.owner_chain_digest.clone(),
1648        purpose: d.purpose,
1649        scope: d.scope.clone(),
1650        cancellation_id: d.cancellation_id.clone(),
1651    }
1652}
1653
1654/// Validate Commit against the HOST-STORED preparation and independently
1655/// selected current authority. Native genesis/envelope verification, online
1656/// revocation, custody uniqueness and transactional activation remain host gates.
1657pub fn verify_prepared_delegation(
1658    prepared: &PrepareImportJobResponse,
1659    signed: &SignedImportJobDelegationV1,
1660    member: Option<&SignedImportMemberPermissionV1>,
1661    geneses: &[SignedImportGenesisAuthorityV1],
1662    expected: &ImportOwnerExpectation<'_>,
1663) -> Result<VerifiedImportDelegation, Reject> {
1664    verify_prepared_inner(prepared, signed, member, geneses, expected, false)
1665}
1666/// Browser signing preflight only: no execution or admission token is returned.
1667pub fn preflight_prepared_delegation(
1668    prepared: &PrepareImportJobResponse,
1669    signed: &SignedImportJobDelegationV1,
1670    member: Option<&SignedImportMemberPermissionV1>,
1671    geneses: &[SignedImportGenesisAuthorityV1],
1672    expected: &ImportOwnerExpectation<'_>,
1673) -> Result<(), Reject> {
1674    verify_prepared_inner(prepared, signed, member, geneses, expected, true).map(|_| ())
1675}
1676fn verify_prepared_inner(
1677    prepared: &PrepareImportJobResponse,
1678    signed: &SignedImportJobDelegationV1,
1679    member: Option<&SignedImportMemberPermissionV1>,
1680    geneses: &[SignedImportGenesisAuthorityV1],
1681    expected: &ImportOwnerExpectation<'_>,
1682    browser: bool,
1683) -> Result<VerifiedImportDelegation, Reject> {
1684    let proposal = prepared.proposal.as_ref().ok_or(Reject::Canonical)?;
1685    let d = signed.body.as_ref().ok_or(Reject::Canonical)?;
1686    if canonical(proposal)? != canonical(&delegation_preparation(d))? {
1687        return Err(Reject::PreparedFields);
1688    }
1689    let scope = proposal.scope.as_ref().ok_or(Reject::Canonical)?;
1690    if d.branch_manifest.len() != scope.branches.len() {
1691        return Err(Reject::PreparedFields);
1692    }
1693    for (m, b) in d.branch_manifest.iter().zip(&scope.branches) {
1694        let limit = m.limit.as_ref().ok_or(Reject::PreparedFields)?;
1695        if canonical(limit)? != canonical(b)? {
1696            return Err(Reject::PreparedFields);
1697        }
1698    }
1699    // Use i128 for host arithmetic so extreme advertised uint64 bounds cannot
1700    // wrap. Skew permits a future not-before, never grace after expiry.
1701    let now = i128::from(expected.now_unix_seconds);
1702    let start = i128::from(d.not_before_unix_seconds);
1703    let end = i128::from(d.expires_at_unix_seconds);
1704    let at = i128::from(prepared.prepared_at_unix_seconds);
1705    let skew = i128::from(prepared.clock_skew_allowance_seconds);
1706    if at < 0
1707        || now < 0
1708        || if browser { now + skew < at } else { now < at }
1709        || i128::from(prepared.reservation_expires_at_unix_seconds) != at + 3600
1710        || now >= i128::from(prepared.reservation_expires_at_unix_seconds)
1711    {
1712        return Err(Reject::Expired);
1713    }
1714    if prepared.max_validity_duration_seconds == 0
1715        || prepared.max_validity_duration_seconds > MAX_DELEGATION_WINDOW_SECONDS
1716        || start < 0
1717        || start < at - skew
1718        || start > now + skew
1719        || end <= start
1720        || end <= now
1721        || end - start > i128::from(prepared.max_validity_duration_seconds)
1722    {
1723        return Err(Reject::ValidityBounds);
1724    }
1725    if let Some(parent) = member {
1726        if browser {
1727            let p = parent.body.as_ref().ok_or(Reject::ImportPermission)?;
1728            verify_member_permission_inner(parent, expected, false)?;
1729            if i128::from(p.not_before_unix_seconds) > now + skew
1730                || i128::from(p.expires_at_unix_seconds) <= now
1731            {
1732                return Err(Reject::Expired);
1733            }
1734        } else {
1735            verify_member_permission(parent, expected)?;
1736        }
1737    }
1738    // Future not-before within skew can be committed, but verify_new_operation
1739    // still refuses execution until that exact signed time. Parent/owner expiry
1740    // and containment are checked without extending them by skew.
1741    let at_start = ImportOwnerExpectation {
1742        now_unix_seconds: expected.now_unix_seconds.max(d.not_before_unix_seconds),
1743        ..*expected
1744    };
1745    let verified = verify_delegation_inner(
1746        signed,
1747        member,
1748        if browser { expected } else { &at_start },
1749        !browser,
1750    )?;
1751    if geneses.len() != d.branch_manifest.len() {
1752        return Err(Reject::GenesisBinding);
1753    }
1754    for m in &d.branch_manifest {
1755        let branch = m.limit.as_ref().ok_or(Reject::Canonical)?;
1756        let g = geneses
1757            .iter()
1758            .find(|g| signed_genesis_digest(g).is_ok_and(|h| h == m.genesis_authority_digest))
1759            .ok_or(Reject::GenesisBinding)?;
1760        let body = g.body.as_ref().ok_or(Reject::GenesisBinding)?;
1761        if body.genesis_digest != branch.genesis_digest {
1762            return Err(Reject::GenesisBinding);
1763        }
1764        {
1765            verify_genesis_authority(
1766                g,
1767                &verified,
1768                &branch.genesis_digest,
1769                &body.original_creator_signature,
1770                &body.creator_authority_envelope_digest,
1771            )?;
1772        }
1773    }
1774    Ok(verified)
1775}
1776
1777pub fn verify_genesis_authority(
1778    signed: &SignedImportGenesisAuthorityV1,
1779    delegation: &VerifiedImportDelegation,
1780    original_genesis_digest: &[u8],
1781    original_signature: &[u8],
1782    envelope_digest: &[u8],
1783) -> Result<(), Reject> {
1784    let g = signed.body.as_ref().ok_or(Reject::Canonical)?;
1785    let d = &delegation.body;
1786    if g.format_version != 1 {
1787        return Err(Reject::Version);
1788    }
1789    width(&g.original_creator_signature, 64)?;
1790    for v in [
1791        &g.genesis_digest,
1792        &g.creator_public_key,
1793        &g.creator_authority_envelope_digest,
1794        &g.parent_permission_digest,
1795        &g.owner_chain_digest,
1796    ] {
1797        width(v, 32)?;
1798    }
1799    if g.identity != d.identity
1800        || g.creator_public_key != d.delegating_public_key
1801        || g.parent_permission_digest != d.parent_permission_digest
1802        || g.owner_chain_digest != d.owner_chain_digest
1803        || g.genesis_digest != original_genesis_digest
1804        || g.original_creator_signature != original_signature
1805        || g.creator_authority_envelope_digest != envelope_digest
1806        || !d.branch_manifest.iter().any(|m| {
1807            m.limit
1808                .as_ref()
1809                .is_some_and(|b| b.genesis_digest == g.genesis_digest)
1810                && signed_genesis_digest(signed).is_ok_and(|h| h == m.genesis_authority_digest)
1811        })
1812    {
1813        return Err(Reject::Scope);
1814    }
1815    verify_authorization_signature(
1816        &g.creator_public_key,
1817        GENESIS_DOMAIN,
1818        g,
1819        signed.creator_signature.as_ref().ok_or(Reject::Signature)?,
1820    )
1821}
1822/// Structural signature + scoped operation only. This does not establish
1823/// publication, current policy, cancellation, leases or conversion correctness.
1824pub fn verify_operation(
1825    signed: &SignedDelegatedImportOperationV1,
1826    delegation: &VerifiedImportDelegation,
1827) -> Result<(), Reject> {
1828    let o = signed.body.as_ref().ok_or(Reject::Canonical)?;
1829    let d = &delegation.body;
1830    if o.format_version != 1 {
1831        return Err(Reject::Version);
1832    }
1833    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
1834    width(&o.physical_operation_id, 16)?;
1835    for v in [
1836        &o.spool_genesis_digest,
1837        &o.delegation_digest,
1838        &o.genesis_digest,
1839        &o.target_thread_id,
1840        &o.expected_frontier_digest,
1841        &o.resulting_frontier_digest,
1842        &o.resulting_content_digest,
1843        &o.options_digest,
1844    ] {
1845        width(v, 32)?;
1846    }
1847    width(&o.spool_uuid, 16)?;
1848    width(&o.logical_job_id, 16)?;
1849    width(&o.retry_lineage_id, 16)?;
1850    let scope = d.scope.as_ref().ok_or(Reject::Canonical)?;
1851    let b = scope
1852        .branches
1853        .iter()
1854        .find(|b| b.ref_name == o.ref_name && b.slot_id == o.slot_id)
1855        .ok_or(Reject::Scope)?;
1856    let oid_len = match o.hash_algorithm {
1857        1 => 20,
1858        2 => 32,
1859        _ => return Err(Reject::Version),
1860    };
1861    width(&o.observed_commit_oid, oid_len)?;
1862    if o.spool_uuid != id.spool_uuid
1863        || o.spool_genesis_digest != id.spool_genesis_digest
1864        || o.logical_job_id != d.logical_job_id
1865        || o.retry_lineage_id != d.retry_lineage_id
1866        || o.delegation_digest != delegation.digest
1867        || o.hash_algorithm != b.hash_algorithm
1868        || (b.ref_mode == 1 && o.observed_commit_oid != b.pinned_commit_oid)
1869        || o.genesis_digest != b.genesis_digest
1870        || o.target_thread_id != b.target_thread_id
1871        || o.expected_frontier_digest != b.expected_frontier_digest
1872        || o.result_bytes > scope.max_result_bytes
1873        || o.result_bytes == 0
1874        || o.options_digest != scope.options_digest
1875        || o.converter_version != scope.converter_version
1876    {
1877        return Err(Reject::Scope);
1878    }
1879    verify_authorization_signature(
1880        &d.job_public_key,
1881        OPERATION_DOMAIN,
1882        o,
1883        signed.job_signature.as_ref().ok_or(Reject::Signature)?,
1884    )
1885}
1886pub fn verify_new_operation(
1887    signed: &SignedDelegatedImportOperationV1,
1888    delegation: &VerifiedImportDelegation,
1889    now_seconds: i64,
1890    committed_before: &ImportResultManifestV1,
1891) -> Result<(), Reject> {
1892    interval(
1893        delegation.body.not_before_unix_seconds,
1894        delegation.body.expires_at_unix_seconds,
1895        now_seconds,
1896    )?;
1897    check_import_publication_budget(signed, delegation, committed_before)
1898}
1899pub fn validate_manifest(m: &ImportResultManifestV1) -> Result<(), Reject> {
1900    if m.format_version != 1 {
1901        return Err(Reject::Version);
1902    }
1903    width(&m.logical_job_id, 16)?;
1904    width(&m.retry_lineage_id, 16)?;
1905    if m.slots.len() > MAX_BRANCHES {
1906        return Err(Reject::Bounds);
1907    }
1908    for (i, s) in m.slots.iter().enumerate() {
1909        width(&s.signed_operation_digest, 32)?;
1910        width(&s.resulting_frontier_digest, 32)?;
1911        branch_ref(&s.ref_name)?;
1912        if s.result_bytes == 0 {
1913            return Err(Reject::Bounds);
1914        }
1915        if i > 0 && (&m.slots[i - 1].ref_name, m.slots[i - 1].slot_id) >= (&s.ref_name, s.slot_id) {
1916            return Err(Reject::Canonical);
1917        }
1918    }
1919    Ok(())
1920}
1921
1922/// Persistent unique slot identity: logical job/ref/slot. Exact replay returns
1923/// the old receipt/manifest, never another publication or fresh witness.
1924pub fn check_slot_replay(
1925    committed: &ImportResultManifestV1,
1926    signed: &SignedDelegatedImportOperationV1,
1927) -> Result<bool, Reject> {
1928    validate_manifest(committed)?;
1929    let o = signed.body.as_ref().ok_or(Reject::Canonical)?;
1930    if committed.logical_job_id != o.logical_job_id
1931        || committed.retry_lineage_id != o.retry_lineage_id
1932    {
1933        return Err(Reject::Scope);
1934    }
1935    match committed
1936        .slots
1937        .iter()
1938        .find(|s| s.ref_name == o.ref_name && s.slot_id == o.slot_id)
1939    {
1940        Some(s)
1941            if s.signed_operation_digest == signed_operation_digest(signed)?
1942                && s.resulting_frontier_digest == o.resulting_frontier_digest
1943                && s.result_bytes == o.result_bytes =>
1944        {
1945            Ok(true)
1946        }
1947        Some(_) => Err(Reject::SlotConflict),
1948        None => Ok(false),
1949    }
1950}
1951/// Verify exact committed publication in addition to job signature/scope. The
1952/// owner/keyring verifier must resolve the statement's accepted state/order;
1953/// witness signature alone cannot establish that user authority or disclosure.
1954pub fn verify_publication(
1955    operation: &SignedDelegatedImportOperationV1,
1956    delegation: &VerifiedImportDelegation,
1957    manifest: &ImportResultManifestV1,
1958    statement: &crate::heddle::api::common::SignedHostedWitnessStatementV1,
1959    set: &crate::witness_trust::VerifiedWitnessSet,
1960    proof: Option<&crate::heddle::api::common::HostedWitnessHistoryProofV1>,
1961    now_ms: i64,
1962) -> Result<crate::witness_trust::ResolvedWitnessStatement, Reject> {
1963    validate_statement_boundary(statement.body.as_ref().ok_or(Reject::Canonical)?)?;
1964    verify_operation(operation, delegation)?;
1965    if !check_slot_replay(manifest, operation)? {
1966        return Err(Reject::Scope);
1967    }
1968    let mut before = manifest.clone();
1969    let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
1970    before
1971        .slots
1972        .retain(|slot| slot.ref_name != o.ref_name || slot.slot_id != o.slot_id);
1973    check_import_publication_budget(operation, delegation, &before)?;
1974    let d = &delegation.body;
1975    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
1976    let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
1977    let payload = ImportPublicationWitnessV1 {
1978        format_version: 1,
1979        signed_operation_digest: signed_operation_digest(operation)?,
1980        delegation_digest: delegation.digest.clone(),
1981        logical_job_id: o.logical_job_id.clone(),
1982        retry_lineage_id: o.retry_lineage_id.clone(),
1983        physical_operation_id: o.physical_operation_id.clone(),
1984        ref_name: o.ref_name.clone(),
1985        slot_id: o.slot_id,
1986        hash_algorithm: o.hash_algorithm,
1987        observed_commit_oid: o.observed_commit_oid.clone(),
1988        expected_frontier_digest: o.expected_frontier_digest.clone(),
1989        resulting_frontier_digest: o.resulting_frontier_digest.clone(),
1990        terminal_manifest_digest: manifest_digest(manifest)?,
1991    };
1992    if s.purpose != 3
1993        || s.spool_uuid != id.spool_uuid
1994        || s.spool_genesis_digest != id.spool_genesis_digest
1995        || s.owner_id != id.owner_id
1996        || s.owner_state_hash != id.owner_state_hash
1997        || s.ownership_transfer_sequence != id.ownership_transfer_sequence
1998        || s.authority_digest != delegation.digest
1999        || s.original_signatures_digest
2000            != hash(&[&operation
2001                .job_signature
2002                .as_ref()
2003                .ok_or(Reject::Signature)?
2004                .signature])
2005        || s.canonical_payload != canonical(&payload)?
2006        || s.basis != 1
2007    {
2008        return Err(Reject::Scope);
2009    }
2010    interval(
2011        d.not_before_unix_seconds,
2012        d.expires_at_unix_seconds,
2013        s.observed_at_unix_millis / 1000,
2014    )?;
2015    crate::witness_trust::resolve_statement(set, statement, proof, false, now_ms)
2016}
2017pub fn require_hybrid_peer(
2018    protocol: Option<&crate::heddle::api::common::ProtocolCompatibility>,
2019) -> Result<(), Reject> {
2020    let protocol = protocol.ok_or(Reject::Protocol)?;
2021    if protocol.protocol_version != 2
2022        || !protocol.mandatory_features.contains(&1)
2023        || protocol
2024            .mandatory_features
2025            .windows(2)
2026            .any(|pair| pair[0] >= pair[1])
2027        || protocol.mandatory_features.iter().any(|&feature| {
2028            crate::heddle::api::common::MandatoryProtocolFeature::try_from(feature)
2029                .map_or(true, |f| {
2030                    f == crate::heddle::api::common::MandatoryProtocolFeature::Unspecified
2031                })
2032        })
2033    {
2034        return Err(Reject::Protocol);
2035    }
2036    Ok(())
2037}
2038
2039/// Producer-owned logical-job/lease fence for RetryImportSource and final
2040/// publication. The physical retry row never supplies a new logical identity.
2041pub fn check_job_fence(
2042    logical_job_id: &[u8],
2043    active_delegation_digest: &[u8],
2044    expected_epoch: u64,
2045    active: &VerifiedImportDelegation,
2046    durable_epoch: u64,
2047) -> Result<(), Reject> {
2048    if logical_job_id != active.body.logical_job_id {
2049        return Err(Reject::Scope);
2050    }
2051    if expected_epoch != durable_epoch || active_delegation_digest != active.digest {
2052        return Err(Reject::StaleContext);
2053    }
2054    Ok(())
2055}
2056pub fn validate_public_bundle(bundle: &ImportPublicProofBundleV1) -> Result<(), Reject> {
2057    validate_bundle_bounds(bundle)?;
2058    validate_bundle_history(bundle, true)
2059}
2060fn validate_bundle_bounds(bundle: &ImportPublicProofBundleV1) -> Result<(), Reject> {
2061    crate::writer_authority::validate_owner_histories(&bundle.owner_histories)?;
2062    use prost::Message;
2063    if bundle.format_version != 1 {
2064        return Err(Reject::Version);
2065    }
2066    if bundle.encoded_len() > MAX_BUNDLE_BYTES
2067        || bundle.owner_histories.len() > 64
2068        || bundle.ownership_transfers.len() > 64
2069        || bundle.genesis_authorities.len() > MAX_BRANCHES
2070        || bundle.delegations.len() != 1
2071        || bundle.operations.len() > MAX_BRANCHES
2072        || bundle.statements.len() > 1024
2073        || bundle.history_proofs.len() > 1024
2074        || bundle.policies.len() > 256
2075        || bundle.original_geneses.len() > MAX_BRANCHES
2076        || bundle.creator_authority_envelopes.len() > MAX_BRANCHES
2077        || bundle.manifests.len() > MAX_BRANCHES
2078        || bundle.genesis_witnesses.len() > 256
2079        || bundle.authority_witnesses.len() > 256
2080        || bundle.landing_witnesses.len() > 256
2081    {
2082        return Err(Reject::Bounds);
2083    }
2084    Ok(())
2085}
2086
2087/// Typed adapter boundary: an authentic unrelated capability/online role must
2088/// never be selected as the parent of an import certificate.
2089pub enum ImportPermissionEvidence<'a> {
2090    Import(&'a SignedImportMemberPermissionV1),
2091    OwnerCapability(&'a SignedOwnerCapability),
2092    OnlineRole(&'a str),
2093}
2094pub fn select_import_permission(
2095    evidence: ImportPermissionEvidence<'_>,
2096) -> Result<&SignedImportMemberPermissionV1, Reject> {
2097    match evidence {
2098        ImportPermissionEvidence::Import(p) => Ok(p),
2099        ImportPermissionEvidence::OwnerCapability(_) | ImportPermissionEvidence::OnlineRole(_) => {
2100            Err(Reject::ImportPermission)
2101        }
2102    }
2103}
2104/// Hybrid dispatch has no legacy execution arm, even for an authentic witness.
2105pub fn require_import_operation_format(format: &str) -> Result<(), Reject> {
2106    if format != OPERATION_DOMAIN {
2107        return Err(Reject::Protocol);
2108    }
2109    Ok(())
2110}
2111pub fn frontier_digest(frontier: &ImportFrontierV1) -> Result<Vec<u8>, Reject> {
2112    if frontier.format_version != 1 {
2113        return Err(Reject::Version);
2114    }
2115    width(&frontier.thread_id, 32)?;
2116    if frontier.operation_ids.len() > 128 {
2117        return Err(Reject::Bounds);
2118    }
2119    for id in &frontier.operation_ids {
2120        width(id, 32)?;
2121    }
2122    if frontier.operation_ids.windows(2).any(|w| w[0] >= w[1]) {
2123        return Err(Reject::Canonical);
2124    }
2125    signing_digest("heddle-import-frontier-v1", frontier)
2126}
2127pub fn content_digest(content: &ImportContentV1) -> Result<Vec<u8>, Reject> {
2128    if content.format_version != 1 {
2129        return Err(Reject::Version);
2130    }
2131    if content.canonical_capture.is_empty() {
2132        return Err(Reject::Bounds);
2133    }
2134    signing_digest("heddle-import-content-v1", content)
2135}
2136pub fn signed_native_digest(record: &SignedRecord) -> Result<Vec<u8>, Reject> {
2137    signing_digest("heddle-signed-native-record-v1", record)
2138}
2139pub(crate) fn verify_native(record: &SignedRecord, format: &str) -> Result<(), Reject> {
2140    if record.format != format {
2141        return Err(Reject::Version);
2142    }
2143    if record.canonical_record.is_empty()
2144        || record.canonical_record.len() > MAX_RECORD_BYTES
2145        || record.signatures.is_empty()
2146        || record.signatures.len() > 16
2147    {
2148        return Err(Reject::Bounds);
2149    }
2150    let mut previous: Option<&[u8]> = None;
2151    let input = [format.as_bytes(), b"\0", &record.canonical_record].concat();
2152    for s in &record.signatures {
2153        if previous.is_some_and(|p| p >= s.public_key.as_slice()) {
2154            return Err(Reject::Canonical);
2155        }
2156        verify(&s.public_key, &input, &s.signature)?;
2157        previous = Some(&s.public_key);
2158    }
2159    Ok(())
2160}
2161/// Recompute transport commitments from exact native evidence. The caller's
2162/// native verifier additionally authenticates manifest membership, receipt
2163/// subjects/bases, accepting authority and canonical native encoding.
2164pub fn verify_boundary_acceptance(e: &ImportBoundaryAcceptanceV1) -> Result<(), Reject> {
2165    let binding = e.binding.as_ref().ok_or(Reject::BoundaryAcceptance)?;
2166    validate_boundary_binding(binding)?;
2167    let acceptance = e
2168        .signed_acceptance
2169        .as_ref()
2170        .ok_or(Reject::BoundaryAcceptance)?;
2171    verify_native(acceptance, "heddle-original-boundary-acceptance-v1")?;
2172    if acceptance.signatures.len() != 1 {
2173        return Err(Reject::Signature);
2174    }
2175    if e.originals_manifest.is_empty()
2176        || e.publication_intent.is_empty()
2177        || e.originals_manifest.len() > MAX_RECORD_BYTES
2178        || e.publication_intent.len() > MAX_RECORD_BYTES
2179        || e.original_receipts.is_empty()
2180        || e.original_receipts.len() > 128
2181    {
2182        return Err(Reject::Bounds);
2183    }
2184    if binding.acceptance_id != native_id(acceptance)
2185        || binding.signed_acceptance_digest != signed_native_digest(acceptance)?
2186        || binding.originals_manifest_digest
2187            != boundary_octets_digest(
2188                "heddle-boundary-originals-manifest-v1",
2189                &e.originals_manifest,
2190            )
2191        || binding.publication_intent_digest
2192            != boundary_octets_digest(
2193                "heddle-boundary-publication-intent-v1",
2194                &e.publication_intent,
2195            )
2196    {
2197        return Err(Reject::BoundaryAcceptance);
2198    }
2199    let native: NativeBoundarySelection =
2200        rmp_serde::from_slice(&acceptance.canonical_record).map_err(|_| Reject::Canonical)?;
2201    if native.originals_manifest.as_slice()
2202        != native_octets_id(
2203            "heddle-original-publication-manifest-v1",
2204            &e.originals_manifest,
2205        )
2206        || native.publication_intent.as_slice()
2207            != native_octets_id(
2208                "heddle-original-publication-intent-v1",
2209                &e.publication_intent,
2210            )
2211    {
2212        return Err(Reject::BoundaryAcceptance);
2213    }
2214    let mut digests = Vec::new();
2215    for receipt in &e.original_receipts {
2216        if ![
2217            "heddle-thread-genesis-admission-v2",
2218            "heddle-thread-authority-admission-v3",
2219        ]
2220        .contains(&receipt.format.as_str())
2221        {
2222            return Err(Reject::Version);
2223        }
2224        verify_native(receipt, &receipt.format)?;
2225        if receipt.signatures.len() != 1 {
2226            return Err(Reject::Signature);
2227        }
2228        let native: NativeBoundaryReceipt =
2229            rmp_serde::from_slice(&receipt.canonical_record).map_err(|_| Reject::Canonical)?;
2230        if native.basis
2231            != (NativeBoundaryBasis::BoundaryAcceptance {
2232                acceptance: binding
2233                    .acceptance_id
2234                    .as_slice()
2235                    .try_into()
2236                    .map_err(|_| Reject::Canonical)?,
2237            })
2238        {
2239            return Err(Reject::BoundaryAcceptance);
2240        }
2241        digests.push(signed_native_digest(receipt)?);
2242    }
2243    if digests != binding.original_receipt_digests {
2244        return Err(Reject::BoundaryAcceptance);
2245    }
2246    Ok(())
2247}
2248// These readers extract only the native commitment selectors. Full native
2249// canonicality, model validity, membership and authority remain the native gate.
2250#[derive(serde::Deserialize)]
2251struct NativeBoundarySelection {
2252    originals_manifest: [u8; 32],
2253    publication_intent: [u8; 32],
2254}
2255#[derive(serde::Deserialize, PartialEq)]
2256enum NativeBoundaryBasis {
2257    OriginalAuthority,
2258    BoundaryAcceptance { acceptance: [u8; 32] },
2259}
2260#[derive(serde::Deserialize)]
2261struct NativeBoundaryReceipt {
2262    basis: NativeBoundaryBasis,
2263    thread: [u8; 32],
2264    subject: Option<NativeBoundarySubject>,
2265}
2266#[derive(serde::Deserialize)]
2267enum NativeBoundarySubject {
2268    Operation([u8; 32]),
2269    OwnershipClaim([u8; 32]),
2270    OwnershipResolution([u8; 32]),
2271}
2272pub(crate) fn native_octets_id(format: &str, bytes: &[u8]) -> Vec<u8> {
2273    let mut h = blake3::Hasher::new();
2274    h.update(format.as_bytes());
2275    h.update(&(bytes.len() as u64).to_le_bytes());
2276    h.update(b"\0");
2277    h.update(bytes);
2278    h.finalize().as_bytes().to_vec()
2279}
2280pub(crate) fn boundary_original(
2281    e: &ImportBoundaryAcceptanceV1,
2282    original: &SignedRecord,
2283) -> Result<(), Reject> {
2284    let id = native_id(original);
2285    for receipt in &e.original_receipts {
2286        let value: NativeBoundaryReceipt =
2287            rmp_serde::from_slice(&receipt.canonical_record).map_err(|_| Reject::Canonical)?;
2288        let (format, subject) = match value.subject {
2289            None if receipt.format == "heddle-thread-genesis-admission-v2" => {
2290                ("heddle-thread-genesis-v1", value.thread)
2291            }
2292            Some(NativeBoundarySubject::Operation(id)) => ("heddle-thread-operation-v1", id),
2293            Some(NativeBoundarySubject::OwnershipClaim(id)) => {
2294                ("heddle-thread-ownership-claim-v1", id)
2295            }
2296            Some(NativeBoundarySubject::OwnershipResolution(id)) => {
2297                ("heddle-thread-ownership-resolution-v1", id)
2298            }
2299            _ => return Err(Reject::BoundaryAcceptance),
2300        };
2301        if original.format == format && id == subject {
2302            return Ok(());
2303        }
2304    }
2305    Err(Reject::BoundaryAcceptance)
2306}
2307pub fn boundary_octets_digest(domain: &str, bytes: &[u8]) -> Vec<u8> {
2308    hash(&[
2309        domain.as_bytes(),
2310        &(bytes.len() as u32).to_be_bytes(),
2311        bytes,
2312    ])
2313}
2314pub fn validate_boundary_binding(
2315    b: &crate::heddle::api::common::HostedWitnessBoundaryAcceptanceV1,
2316) -> Result<(), Reject> {
2317    if b.format_version != 1 {
2318        return Err(Reject::Version);
2319    }
2320    for digest in [
2321        &b.acceptance_id,
2322        &b.signed_acceptance_digest,
2323        &b.originals_manifest_digest,
2324        &b.publication_intent_digest,
2325    ] {
2326        width(digest, 32)?;
2327    }
2328    if b.original_receipt_digests.is_empty() || b.original_receipt_digests.len() > 128 {
2329        return Err(Reject::Bounds);
2330    }
2331    for digest in &b.original_receipt_digests {
2332        width(digest, 32)?;
2333    }
2334    if b.original_receipt_digests.windows(2).any(|w| w[0] >= w[1]) {
2335        return Err(Reject::Canonical);
2336    }
2337    Ok(())
2338}
2339pub fn validate_statement_boundary(
2340    s: &crate::heddle::api::common::HostedWitnessStatementV1,
2341) -> Result<(), Reject> {
2342    match (s.basis, s.boundary_acceptance.as_ref()) {
2343        (1, None) => Ok(()),
2344        (2, Some(b)) if s.purpose == 1 || s.purpose == 2 => validate_boundary_binding(b),
2345        _ => Err(Reject::BoundaryAcceptance),
2346    }
2347}
2348pub(crate) fn match_boundary(
2349    s: &crate::heddle::api::common::HostedWitnessStatementV1,
2350    evidence: &[ImportBoundaryAcceptanceV1],
2351) -> Result<(), Reject> {
2352    validate_statement_boundary(s)?;
2353    let mut previous = None;
2354    for e in evidence {
2355        verify_boundary_acceptance(e)?;
2356        let b = e.binding.as_ref().ok_or(Reject::BoundaryAcceptance)?;
2357        if previous.is_some_and(|p: &[u8]| p >= b.acceptance_id.as_slice()) {
2358            return Err(Reject::Canonical);
2359        }
2360        previous = Some(b.acceptance_id.as_slice());
2361    }
2362    if let Some(binding) = &s.boundary_acceptance
2363        && !evidence.iter().any(|e| e.binding.as_ref() == Some(binding))
2364    {
2365        return Err(Reject::BoundaryAcceptance);
2366    }
2367    Ok(())
2368}
2369fn native_dependencies(
2370    records: &[SignedRecord],
2371    evidence: &[ImportBoundaryAcceptanceV1],
2372    reviews_only: bool,
2373) -> Result<(), Reject> {
2374    if records.len() > 128 {
2375        return Err(Reject::Bounds);
2376    }
2377    let mut previous = None;
2378    for record in records {
2379        if reviews_only {
2380            require_review_operation(record)?;
2381        }
2382        match record.format.as_str() {
2383            "heddle-thread-genesis-v1"
2384            | "heddle-thread-operation-v1"
2385            | "heddle-thread-ownership-claim-v1"
2386            | "heddle-thread-ownership-resolution-v1" => (),
2387            "heddle-original-boundary-acceptance-v1"
2388            | "heddle-thread-genesis-admission-v2"
2389            | "heddle-thread-authority-admission-v3" => {
2390                if !evidence.iter().any(|e| {
2391                    e.signed_acceptance.as_ref() == Some(record)
2392                        || e.original_receipts.contains(record)
2393                }) {
2394                    return Err(Reject::BoundaryAcceptance);
2395                }
2396            }
2397            _ => return Err(Reject::Version),
2398        }
2399        verify_native(record, &record.format)?;
2400        let digest = signed_native_digest(record)?;
2401        if previous.as_ref().is_some_and(|p| p >= &digest) {
2402            return Err(Reject::Canonical);
2403        }
2404        previous = Some(digest);
2405    }
2406    Ok(())
2407}
2408#[derive(serde::Deserialize)]
2409struct ReviewOperation {
2410    body: ReviewBody,
2411}
2412#[derive(serde::Deserialize)]
2413struct ReviewBody {
2414    kind: String,
2415    canonical: Vec<u8>,
2416}
2417#[derive(serde::Deserialize)]
2418struct ReviewControl {
2419    control: ReviewKind,
2420}
2421#[derive(serde::Deserialize)]
2422struct ReviewKind {
2423    kind: String,
2424}
2425fn require_review_operation(record: &SignedRecord) -> Result<(), Reject> {
2426    if record.format != "heddle-thread-operation-v1" {
2427        return Err(Reject::Semantic);
2428    }
2429    let operation: ReviewOperation =
2430        rmp_serde::from_slice(&record.canonical_record).map_err(|_| Reject::Semantic)?;
2431    if operation.body.kind != "metadata" {
2432        return Err(Reject::Semantic);
2433    }
2434    let control: ReviewControl =
2435        rmp_serde::from_slice(&operation.body.canonical).map_err(|_| Reject::Semantic)?;
2436    if control.control.kind != "review" {
2437        return Err(Reject::Semantic);
2438    }
2439    Ok(())
2440}
2441pub(crate) fn original_signatures(
2442    records: &[&SignedRecord],
2443    extra: &[RecordSignature],
2444) -> Result<Vec<u8>, Reject> {
2445    let signatures = records
2446        .iter()
2447        .flat_map(|r| r.signatures.iter())
2448        .chain(extra.iter())
2449        .collect::<Vec<_>>();
2450    let mut out = (signatures.len() as u32).to_be_bytes().to_vec();
2451    for s in signatures {
2452        s.write(&mut out)?;
2453    }
2454    Ok(hash(&[b"heddle-hosted-original-signatures-v1", &out]))
2455}
2456
2457#[cfg(test)]
2458#[test]
2459fn sha2_golden_original_signatures() {
2460    let first = SignedRecord {
2461        signatures: vec![RecordSignature {
2462            public_key: vec![1; 32],
2463            signature: vec![2; 64],
2464        }],
2465        ..Default::default()
2466    };
2467    let second = SignedRecord {
2468        signatures: vec![RecordSignature {
2469            public_key: vec![3; 32],
2470            signature: vec![4; 64],
2471        }],
2472        ..Default::default()
2473    };
2474    let extra = RecordSignature {
2475        public_key: vec![5; 32],
2476        signature: vec![6; 64],
2477    };
2478    assert_eq!(
2479        hex::encode(original_signatures(&[&first, &second], &[extra]).expect("fixed signatures")),
2480        "04bcf713a6a5fdec2bbfae89ee71894db72db54ca75597831fea7192ef9bf3e1"
2481    );
2482}
2483use crate::hybrid_codec::Canonical;
2484/// Portable request-role refusal. Receiver facts include known job and forbidden request keys.
2485pub fn verify_landing_key_roles(
2486    payload: &HostedLandingWitnessV1,
2487    known_job_keys: &[Vec<u8>],
2488    forbidden_keys: &[Vec<u8>],
2489) -> Result<(), Reject> {
2490    let key = &payload
2491        .request
2492        .as_ref()
2493        .and_then(|r| r.signature.as_ref())
2494        .ok_or(Reject::Signature)?
2495        .public_key;
2496    if known_job_keys.contains(key) || forbidden_keys.contains(key) {
2497        return Err(Reject::KeyRole);
2498    }
2499    Ok(())
2500}
2501
2502/// Exact matching and original signatures do not replace native causal/authority/landing checks.
2503#[derive(Clone, Copy)]
2504pub enum WitnessPayload<'a> {
2505    Genesis(&'a ImportGenesisWitnessV1),
2506    Authority(&'a ImportAuthorityWitnessV1),
2507    Landing(&'a HostedLandingWitnessV1),
2508}
2509pub fn verify_witness_payload(
2510    statement: &crate::heddle::api::common::HostedWitnessStatementV1,
2511    payload: WitnessPayload<'_>,
2512) -> Result<(), Reject> {
2513    let (purpose, bytes, authority, signatures, publisher) = match payload {
2514        WitnessPayload::Genesis(p) => {
2515            if p.format_version != 1 {
2516                return Err(Reject::Version);
2517            }
2518            let original = p.original_genesis.as_ref().ok_or(Reject::Canonical)?;
2519            let binding = p.binding.as_ref().ok_or(Reject::Canonical)?;
2520            let b = binding.body.as_ref().ok_or(Reject::Canonical)?;
2521            match_boundary(
2522                statement,
2523                &p.boundary_acceptance.iter().cloned().collect::<Vec<_>>(),
2524            )?;
2525            if let Some(e) = &p.boundary_acceptance {
2526                boundary_original(e, original)?;
2527            }
2528            if (statement.basis == 2) != p.boundary_acceptance.is_some() {
2529                return Err(Reject::BoundaryAcceptance);
2530            }
2531            verify_native(original, "heddle-thread-genesis-v1")?;
2532            if native_id(original) != b.genesis_digest {
2533                return Err(Reject::Scope);
2534            }
2535            if let Some(id) = &b.identity {
2536                if statement.spool_uuid != id.spool_uuid
2537                    || statement.spool_genesis_digest != id.spool_genesis_digest
2538                    || statement.owner_id != id.owner_id
2539                    || statement.owner_state_hash != id.owner_state_hash
2540                    || statement.ownership_transfer_sequence != id.ownership_transfer_sequence
2541                {
2542                    return Err(Reject::Scope);
2543                }
2544            } else {
2545                return Err(Reject::Canonical);
2546            }
2547            let creator = original
2548                .signatures
2549                .iter()
2550                .find(|s| s.public_key == b.creator_public_key)
2551                .ok_or(Reject::Signature)?;
2552            if b.original_creator_signature != creator.signature
2553                || b.creator_authority_envelope_digest != hash(&[&p.creator_authority_envelope])
2554            {
2555                return Err(Reject::Scope);
2556            }
2557            verify_authorization_signature(
2558                &b.creator_public_key,
2559                GENESIS_DOMAIN,
2560                b,
2561                binding
2562                    .creator_signature
2563                    .as_ref()
2564                    .ok_or(Reject::Signature)?,
2565            )?;
2566            (
2567                1,
2568                canonical(p)?,
2569                signed_genesis_digest(binding)?,
2570                original_signatures(&[original], &[])?,
2571                key_id(&b.creator_public_key),
2572            )
2573        }
2574        WitnessPayload::Authority(p) => {
2575            if p.format_version != 1 {
2576                return Err(Reject::Version);
2577            }
2578            let original = p.original.as_ref().ok_or(Reject::Canonical)?;
2579            let format = match p.kind {
2580                1 => "heddle-thread-operation-v1",
2581                2 => "heddle-thread-ownership-claim-v1",
2582                3 => "heddle-thread-ownership-resolution-v1",
2583                _ => return Err(Reject::Version),
2584            };
2585            verify_native(original, format)?;
2586            if (p.kind == 2 || p.kind == 3) && original.signatures.len() != 2 {
2587                return Err(Reject::Signature);
2588            }
2589            if !original
2590                .signatures
2591                .iter()
2592                .any(|s| key_id(&s.public_key) == statement.publisher_key_id)
2593            {
2594                return Err(Reject::Signature);
2595            }
2596            if p.authority_envelope.is_empty() || p.authority_envelope.len() > MAX_RECORD_BYTES {
2597                return Err(Reject::Bounds);
2598            }
2599            match_boundary(statement, &p.boundary_acceptances)?;
2600            if let Some(b) = &statement.boundary_acceptance {
2601                let e = p
2602                    .boundary_acceptances
2603                    .iter()
2604                    .find(|e| e.binding.as_ref() == Some(b))
2605                    .ok_or(Reject::BoundaryAcceptance)?;
2606                boundary_original(e, original)?;
2607            }
2608            native_dependencies(&p.dependencies, &p.boundary_acceptances, false)?;
2609            let records = std::iter::once(original)
2610                .chain(p.dependencies.iter())
2611                .collect::<Vec<_>>();
2612            (
2613                2,
2614                canonical(p)?,
2615                hash(&[
2616                    b"heddle-hosted-authority-envelope-v1",
2617                    &(p.authority_envelope.len() as u32).to_be_bytes(),
2618                    &p.authority_envelope,
2619                ]),
2620                original_signatures(&records, &[])?,
2621                statement.publisher_key_id.clone(),
2622            )
2623        }
2624        WitnessPayload::Landing(p) => {
2625            if p.format_version != 1 {
2626                return Err(Reject::Version);
2627            }
2628            let execution = p.execution.as_ref().ok_or(Reject::Canonical)?;
2629            let source = p.source_operation.as_ref().ok_or(Reject::Canonical)?;
2630            let request = p.request.as_ref().ok_or(Reject::Canonical)?;
2631            if request.format_version != 1
2632                || request.method_path != "/heddle.api.v1alpha2.ThreadService/LandThread"
2633            {
2634                return Err(Reject::Version);
2635            }
2636            verify_native(execution, "heddle-thread-operation-v1")?;
2637            verify_native(source, "heddle-thread-operation-v1")?;
2638            match_boundary(statement, &[])?;
2639            native_dependencies(&p.review_evidence, &[], true)?;
2640            let signature = request.signature.as_ref().ok_or(Reject::Signature)?;
2641            if request.signing_identity
2642                != format!(
2643                    "principal:device-key:{}",
2644                    hex::encode(&signature.public_key)
2645                )
2646            {
2647                return Err(Reject::Signature);
2648            }
2649            width(&request.nonce, 16)?;
2650            if request.timestamp_millis <= 0
2651                || request.request_body.is_empty()
2652                || request.request_body.len() > MAX_RECORD_BYTES
2653                || p.authority_envelope.is_empty()
2654                || p.authority_envelope.len() > MAX_RECORD_BYTES
2655            {
2656                return Err(Reject::Bounds);
2657            }
2658            let input = crate::signing::unary_bytes(
2659                &request.signing_identity,
2660                &request.method_path,
2661                request.timestamp_millis,
2662                &request.nonce,
2663                &request.request_body,
2664            );
2665            verify(&signature.public_key, &input, &signature.signature)?;
2666            let records = [execution, source]
2667                .into_iter()
2668                .chain(p.review_evidence.iter())
2669                .collect::<Vec<_>>();
2670            (
2671                4,
2672                canonical(p)?,
2673                hash(&[
2674                    b"heddle-hosted-authority-envelope-v1",
2675                    &(p.authority_envelope.len() as u32).to_be_bytes(),
2676                    &p.authority_envelope,
2677                ]),
2678                original_signatures(&records, std::slice::from_ref(signature))?,
2679                key_id(&signature.public_key),
2680            )
2681        }
2682    };
2683    if bytes.len() > MAX_RECORD_BYTES {
2684        return Err(Reject::Bounds);
2685    }
2686    if statement.purpose != purpose
2687        || statement.canonical_payload != bytes
2688        || statement.authority_digest != authority
2689        || statement.original_signatures_digest != signatures
2690        || statement.publisher_key_id != publisher
2691    {
2692        return Err(Reject::Scope);
2693    }
2694    Ok(())
2695}
2696
2697pub fn resolve_bundle_permission<'a>(
2698    bundle: &'a ImportPublicProofBundleV1,
2699    digest: &[u8],
2700) -> Result<Option<&'a SignedImportMemberPermissionV1>, Reject> {
2701    width(digest, 32)?;
2702    if digest == [0; 32] {
2703        return Ok(None);
2704    }
2705    bundle
2706        .member_permission
2707        .iter()
2708        .find(|p| signed_permission_digest(p).is_ok_and(|d| d == digest))
2709        .map(Some)
2710        .ok_or(Reject::ImportPermission)
2711}
2712pub fn resolve_bundle_manifest<'a>(
2713    bundle: &'a ImportPublicProofBundleV1,
2714    digest: &[u8],
2715) -> Result<&'a ImportResultManifestV1, Reject> {
2716    width(digest, 32)?;
2717    bundle
2718        .manifests
2719        .iter()
2720        .find(|m| manifest_digest(m).is_ok_and(|d| d == digest))
2721        .ok_or(Reject::StaleManifest)
2722}
2723pub fn publication_payload(
2724    operation: &SignedDelegatedImportOperationV1,
2725    manifest: &ImportResultManifestV1,
2726) -> Result<ImportPublicationWitnessV1, Reject> {
2727    let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
2728    Ok(ImportPublicationWitnessV1 {
2729        format_version: 1,
2730        signed_operation_digest: signed_operation_digest(operation)?,
2731        delegation_digest: o.delegation_digest.clone(),
2732        logical_job_id: o.logical_job_id.clone(),
2733        retry_lineage_id: o.retry_lineage_id.clone(),
2734        physical_operation_id: o.physical_operation_id.clone(),
2735        ref_name: o.ref_name.clone(),
2736        slot_id: o.slot_id,
2737        hash_algorithm: o.hash_algorithm,
2738        observed_commit_oid: o.observed_commit_oid.clone(),
2739        expected_frontier_digest: o.expected_frontier_digest.clone(),
2740        resulting_frontier_digest: o.resulting_frontier_digest.clone(),
2741        terminal_manifest_digest: manifest_digest(manifest)?,
2742    })
2743}
2744/// Completeness and digest addressing only. Trust/signature verification still
2745/// uses independently selected owner contexts at each witnessed historical time.
2746fn validate_bundle_history(
2747    bundle: &ImportPublicProofBundleV1,
2748    require_admissions: bool,
2749) -> Result<(), Reject> {
2750    fn sorted<T>(
2751        values: &[T],
2752        digest: impl Fn(&T) -> Result<Vec<u8>, Reject>,
2753    ) -> Result<(), Reject> {
2754        let mut previous = None;
2755        for value in values {
2756            let d = digest(value)?;
2757            if previous.as_ref().is_some_and(|p| p >= &d) {
2758                return Err(Reject::Canonical);
2759            }
2760            previous = Some(d);
2761        }
2762        Ok(())
2763    }
2764    let mut foreign = crate::foreign_dependencies::References::new(
2765        &bundle.foreign_dependencies,
2766        ForeignDependencyOrigin::Import,
2767    )?;
2768    for p in &bundle.authority_witnesses {
2769        let subject_thread =
2770            crate::foreign_dependencies::thread(p.original.as_ref().ok_or(Reject::Canonical)?)?;
2771        if !bundle.genesis_witnesses.iter().any(|g| {
2772            g.original_genesis
2773                .as_ref()
2774                .is_some_and(|g| native_id(g) == subject_thread)
2775        }) {
2776            return Err(Reject::Scope);
2777        }
2778    }
2779    let job_keys = bundle
2780        .delegations
2781        .iter()
2782        .filter_map(|d| d.body.as_ref().map(|d| d.job_public_key.clone()))
2783        .collect::<Vec<_>>();
2784    for p in &bundle.landing_witnesses {
2785        verify_landing_key_roles(p, &job_keys, &[])?;
2786        let execution = p.execution.as_ref().ok_or(Reject::Canonical)?;
2787        let thread = crate::foreign_dependencies::thread(execution)?;
2788        if !bundle.genesis_witnesses.iter().any(|p| {
2789            p.original_genesis
2790                .as_ref()
2791                .is_some_and(|g| native_id(g) == thread)
2792        }) {
2793            return Err(Reject::Scope);
2794        }
2795    }
2796    // In-carrier genesis witnesses establish origin membership, never permission.
2797    for original in bundle
2798        .authority_witnesses
2799        .iter()
2800        .flat_map(|p| p.original.iter().chain(p.dependencies.iter()))
2801        .chain(bundle.landing_witnesses.iter().flat_map(|p| {
2802            p.execution
2803                .iter()
2804                .chain(p.source_operation.iter())
2805                .chain(p.review_evidence.iter())
2806        }))
2807    {
2808        if [
2809            "heddle-original-boundary-acceptance-v1",
2810            "heddle-thread-genesis-admission-v2",
2811            "heddle-thread-authority-admission-v3",
2812        ]
2813        .contains(&original.format.as_str())
2814        {
2815            continue;
2816        }
2817        let thread = crate::foreign_dependencies::thread(original)?;
2818        if !bundle.genesis_witnesses.iter().any(|p| {
2819            p.original_genesis
2820                .as_ref()
2821                .is_some_and(|g| native_id(g) == thread)
2822        }) {
2823            foreign.require(original)?;
2824        } else if original.format == "heddle-thread-genesis-v1"
2825            && !bundle
2826                .genesis_witnesses
2827                .iter()
2828                .any(|p| p.original_genesis.as_ref() == Some(original))
2829        {
2830            return Err(Reject::Scope);
2831        }
2832    }
2833    foreign.finish()?;
2834    for operation in &bundle.operations {
2835        if operation.body.is_none() {
2836            return Err(Reject::Canonical);
2837        }
2838    }
2839    for statement in &bundle.statements {
2840        let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
2841        validate_statement_boundary(s)?;
2842        require_policy_history(
2843            &bundle.policies,
2844            &s.spool_uuid,
2845            s.policy_sequence,
2846            &s.policy_state_hash,
2847        )?;
2848        let envelope = match s.purpose {
2849            1 if s.basis == 2 => Some(
2850                bundle
2851                    .genesis_witnesses
2852                    .iter()
2853                    .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
2854                    .ok_or(Reject::Scope)?
2855                    .creator_authority_envelope
2856                    .as_slice(),
2857            ),
2858            2 => Some(
2859                bundle
2860                    .authority_witnesses
2861                    .iter()
2862                    .find(|p| canonical(*p).is_ok_and(|bytes| bytes == s.canonical_payload))
2863                    .ok_or(Reject::Scope)?
2864                    .authority_envelope
2865                    .as_slice(),
2866            ),
2867            4 => Some(
2868                bundle
2869                    .landing_witnesses
2870                    .iter()
2871                    .find(|p| canonical(*p).is_ok_and(|bytes| bytes == s.canonical_payload))
2872                    .ok_or(Reject::Scope)?
2873                    .authority_envelope
2874                    .as_slice(),
2875            ),
2876            _ => None,
2877        };
2878        if let Some(envelope) = envelope {
2879            crate::writer_authority::check_witness_writer(
2880                s,
2881                envelope,
2882                &bundle.owner_histories,
2883                &bundle.policies,
2884                crate::writer_authority::spool_account_for_statement(
2885                    s,
2886                    bundle
2887                        .delegations
2888                        .iter()
2889                        .filter_map(|d| d.body.as_ref()?.identity.as_ref())
2890                        .chain(
2891                            bundle
2892                                .genesis_authorities
2893                                .iter()
2894                                .filter_map(|g| g.body.as_ref()?.identity.as_ref()),
2895                        ),
2896                    &bundle.ownership_transfers,
2897                )?,
2898                bundle
2899                    .authority_witnesses
2900                    .iter()
2901                    .find(|p| {
2902                        (p.kind == 2 || p.kind == 3)
2903                            && canonical(*p).is_ok_and(|v| v == s.canonical_payload)
2904                    })
2905                    .and_then(|p| p.original.as_ref())
2906                    .map(|r| r.signatures.as_slice())
2907                    .unwrap_or(&[]),
2908                if s.purpose == 1 {
2909                    bundle
2910                        .genesis_witnesses
2911                        .iter()
2912                        .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
2913                        .and_then(|p| p.boundary_acceptance.as_ref())
2914                } else {
2915                    bundle
2916                        .authority_witnesses
2917                        .iter()
2918                        .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
2919                        .and_then(|p| {
2920                            p.boundary_acceptances
2921                                .iter()
2922                                .find(|e| e.binding == s.boundary_acceptance)
2923                        })
2924                },
2925            )?;
2926        }
2927    }
2928    sorted(&bundle.manifests, manifest_digest)?;
2929    if let Some(p) = &bundle.member_permission
2930        && resolve_bundle_permission(bundle, &signed_permission_digest(p)?)? != Some(p)
2931    {
2932        return Err(Reject::ImportPermission);
2933    }
2934    let terminal = bundle.terminal_manifest.as_ref().ok_or(Reject::Canonical)?;
2935    if resolve_bundle_manifest(bundle, &manifest_digest(terminal)?)? != terminal {
2936        return Err(Reject::Canonical);
2937    }
2938    if bundle.delegations.len() != 1 {
2939        return Err(Reject::Canonical);
2940    }
2941    for d in &bundle.delegations {
2942        let body = d.body.as_ref().ok_or(Reject::Canonical)?;
2943        resolve_bundle_permission(bundle, &body.parent_permission_digest)?;
2944        for branch in &body.branch_manifest {
2945            let g = bundle
2946                .genesis_authorities
2947                .iter()
2948                .find(|g| {
2949                    signed_genesis_digest(g).is_ok_and(|h| h == branch.genesis_authority_digest)
2950                })
2951                .ok_or(Reject::Scope)?;
2952            let b = g.body.as_ref().ok_or(Reject::Canonical)?;
2953            resolve_bundle_permission(bundle, &b.parent_permission_digest)?;
2954            if !bundle
2955                .original_geneses
2956                .iter()
2957                .any(|o| native_id(o) == b.genesis_digest)
2958                || !bundle
2959                    .creator_authority_envelopes
2960                    .iter()
2961                    .any(|e| hash(&[e]) == b.creator_authority_envelope_digest)
2962            {
2963                return Err(Reject::Scope);
2964            }
2965            // Every published branch needs its original admission, not merely its
2966            // binding. Proof-only retirement lookup cannot recover a payload.
2967            if require_admissions
2968                && bundle.operations.iter().any(|o| {
2969                    o.body
2970                        .as_ref()
2971                        .is_some_and(|o| o.genesis_digest == b.genesis_digest)
2972                })
2973                && !bundle.genesis_witnesses.iter().any(|payload| {
2974                    payload.binding.as_ref() == Some(g)
2975                        && payload.original_genesis.as_ref().is_some_and(|o| {
2976                            native_id(o) == b.genesis_digest && bundle.original_geneses.contains(o)
2977                        })
2978                        && hash(&[&payload.creator_authority_envelope])
2979                            == b.creator_authority_envelope_digest
2980                        && canonical(payload).is_ok_and(|bytes| {
2981                            bundle.statements.iter().any(|s| {
2982                                s.body
2983                                    .as_ref()
2984                                    .is_some_and(|s| s.purpose == 1 && s.canonical_payload == bytes)
2985                            })
2986                        })
2987                })
2988            {
2989                return Err(Reject::Scope);
2990            }
2991        }
2992    }
2993    for manifest in &bundle.manifests {
2994        validate_manifest(manifest)?;
2995        if manifest.logical_job_id != terminal.logical_job_id
2996            || manifest.retry_lineage_id != terminal.retry_lineage_id
2997        {
2998            return Err(Reject::Scope);
2999        }
3000        for slot in &manifest.slots {
3001            let operation = bundle
3002                .operations
3003                .iter()
3004                .find(|o| {
3005                    signed_operation_digest(o).is_ok_and(|d| d == slot.signed_operation_digest)
3006                })
3007                .ok_or(Reject::Scope)?;
3008            if !check_slot_replay(manifest, operation)? || !check_slot_replay(terminal, operation)?
3009            {
3010                return Err(Reject::Scope);
3011            }
3012        }
3013    }
3014    for operation in &bundle.operations {
3015        let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
3016        if !bundle
3017            .delegations
3018            .iter()
3019            .any(|d| signed_delegation_digest(d).is_ok_and(|h| h == o.delegation_digest))
3020            || !check_slot_replay(terminal, operation)?
3021        {
3022            return Err(Reject::Scope);
3023        }
3024        if !bundle.manifests.iter().any(|m| {
3025            check_slot_replay(m, operation) == Ok(true)
3026                && publication_payload(operation, m)
3027                    .and_then(|p| canonical(&p))
3028                    .is_ok_and(|p| {
3029                        bundle.statements.iter().any(|s| {
3030                            s.body
3031                                .as_ref()
3032                                .is_some_and(|s| s.purpose == 3 && s.canonical_payload == p)
3033                        })
3034                    })
3035        }) {
3036            return Err(Reject::Scope);
3037        }
3038    }
3039    for statement in &bundle.statements {
3040        let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
3041        let found = match s.purpose {
3042            1 => bundle
3043                .genesis_witnesses
3044                .iter()
3045                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
3046            2 => bundle
3047                .authority_witnesses
3048                .iter()
3049                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
3050            3 => bundle.operations.iter().any(|o| {
3051                bundle.manifests.iter().any(|m| {
3052                    publication_payload(o, m)
3053                        .and_then(|p| canonical(&p))
3054                        .is_ok_and(|p| p == s.canonical_payload)
3055                })
3056            }),
3057            4 => bundle
3058                .landing_witnesses
3059                .iter()
3060                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
3061            _ => return Err(Reject::Version),
3062        };
3063        if !found {
3064            return Err(Reject::Scope);
3065        }
3066    }
3067    Ok(())
3068}
3069/// Authenticate policy contents against their committed hashes and enforce the
3070/// governance contract's grow-only revocations. All carried bodies are checked
3071/// before the zero-head shortcut, so no writer lookup can read unchecked data.
3072/// Native verification still
3073/// authenticates owner signatures/context and the receipt observation time.
3074pub(crate) fn require_policy_history(
3075    policies: &[SignedSpoolPolicyRecord],
3076    spool: &[u8],
3077    mut sequence: u64,
3078    state_hash: &[u8],
3079) -> Result<(), Reject> {
3080    let mut previous: Option<(&[u8], u64)> = None;
3081    for signed in policies {
3082        let policy = signed.body.as_ref().ok_or(Reject::Canonical)?;
3083        let digest = policy_state_digest(policy)?;
3084        if digest != policy.policy_state_hash {
3085            return Err(Reject::Canonical);
3086        }
3087        let current = (policy.spool_uuid.as_slice(), policy.sequence);
3088        if previous.is_some_and(|prev| prev >= current) {
3089            return Err(Reject::Canonical);
3090        }
3091        previous = Some(current);
3092    }
3093    let mut state_hash = state_hash.to_vec();
3094    let mut successor_revoked: Option<&[Vec<u8>]> = None;
3095    for _ in 0..=policies.len() {
3096        width(&state_hash, 32)?;
3097        if sequence == 0 {
3098            return if state_hash == [0; 32] {
3099                Ok(())
3100            } else {
3101                Err(Reject::Scope)
3102            };
3103        }
3104        let mut matches = policies.iter().filter_map(|p| p.body.as_ref()).filter(|p| {
3105            p.spool_uuid == spool && p.sequence == sequence && p.policy_state_hash == state_hash
3106        });
3107        let policy = matches.next().ok_or(Reject::Scope)?;
3108        // policy_state_digest above already requires an authenticated body.
3109        let revoked = policy
3110            .policy
3111            .as_ref()
3112            .map(|p| p.revoked_key_ids.as_slice())
3113            .unwrap_or(&[]);
3114        if successor_revoked.is_some_and(|next| revoked.iter().any(|id| !next.contains(id))) {
3115            return Err(Reject::Scope);
3116        }
3117        successor_revoked = Some(revoked);
3118        let head = policy.expected_head.as_ref().ok_or(Reject::Canonical)?;
3119        if head.sequence.checked_add(1) != Some(sequence) {
3120            return Err(Reject::Scope);
3121        }
3122        sequence = head.sequence;
3123        state_hash = head.state_hash.clone();
3124    }
3125    Err(Reject::Scope)
3126}
3127/// Canonical SignedPolicyBody fields 1–10; protobuf is never the signed encoding.
3128pub fn policy_state_digest(p: &SignedPolicyBody) -> Result<Vec<u8>, Reject> {
3129    use crate::hybrid_codec::counted;
3130    if p.format_version != 1 || !p.merge_parent_state_hashes.is_empty() {
3131        return Err(Reject::Version);
3132    }
3133    width(&p.spool_uuid, 16)?;
3134    width(&p.owner_id, 32)?;
3135    width(&p.owner_state_hash, 32)?;
3136    let head = p.expected_head.as_ref().ok_or(Reject::Canonical)?;
3137    width(&head.state_hash, 32)?;
3138    let policy = p.policy.as_ref().ok_or(Reject::Canonical)?;
3139    if p.merge_policies.len() != 2
3140        || p.merge_policies[0].setting_key != "max_audience"
3141        || p.merge_policies[0].semantics != 1
3142        || p.merge_policies[1].setting_key != "revoked_key_ids"
3143        || p.merge_policies[1].semantics != 2
3144    {
3145        return Err(Reject::Canonical);
3146    }
3147    let mut out = p.format_version.to_be_bytes().to_vec();
3148    counted(&mut out, &p.spool_uuid)?;
3149    counted(&mut out, &head.state_hash)?;
3150    out.extend_from_slice(&head.sequence.to_be_bytes());
3151    out.extend_from_slice(&p.sequence.to_be_bytes());
3152    out.extend_from_slice(&0u32.to_be_bytes());
3153    out.extend_from_slice(&(policy.revoked_key_ids.len() as u32).to_be_bytes());
3154    for (i, id) in policy.revoked_key_ids.iter().enumerate() {
3155        width(id, 32)?;
3156        if i > 0 && policy.revoked_key_ids[i - 1] >= *id {
3157            return Err(Reject::Canonical);
3158        }
3159        counted(&mut out, id)?;
3160    }
3161    out.push(u8::from(policy.max_audience.is_some()));
3162    if let Some(audience) = policy.max_audience {
3163        if !(1..=3).contains(&audience) {
3164            return Err(Reject::Canonical);
3165        }
3166        out.extend_from_slice(&(audience as u32).to_be_bytes());
3167    }
3168    out.extend_from_slice(&2u32.to_be_bytes());
3169    for rule in &p.merge_policies {
3170        counted(&mut out, rule.setting_key.as_bytes())?;
3171        out.extend_from_slice(&(rule.semantics as u32).to_be_bytes());
3172    }
3173    counted(&mut out, &p.owner_id)?;
3174    counted(&mut out, &p.owner_state_hash)?;
3175    out.extend_from_slice(&p.ownership_transfer_sequence.to_be_bytes());
3176    Ok(hash(&[b"heddle-spool-signed-policy-v2", &out]))
3177}
3178pub(crate) fn native_id(record: &SignedRecord) -> Vec<u8> {
3179    let mut h = blake3::Hasher::new();
3180    h.update(record.format.as_bytes());
3181    h.update(&(record.canonical_record.len() as u64).to_le_bytes());
3182    h.update(b"\0");
3183    h.update(&record.canonical_record);
3184    h.finalize().as_bytes().to_vec()
3185}
3186
3187/// Caller-generated non-nil UUID, reserved as the first physical operation ID.
3188/// Occupancy is checked under the host's reservation/activation transaction.
3189pub fn initial_operation_id(lineage: &[u8], occupied: bool) -> Result<String, Reject> {
3190    width(lineage, 16)?;
3191    if lineage.iter().all(|b| *b == 0) {
3192        return Err(Reject::Canonical);
3193    }
3194    if occupied {
3195        return Err(Reject::OperationIdReused);
3196    }
3197    let h = hex::encode(lineage);
3198    Ok(format!(
3199        "{}-{}-{}-{}-{}",
3200        &h[..8],
3201        &h[8..12],
3202        &h[12..16],
3203        &h[16..20],
3204        &h[20..]
3205    ))
3206}
3207
3208/// Evaluate inside the cancellation transaction, after caller-scoped replay lookup.
3209/// The selector names only the ACTIVE delegation. Parent revocation is independent.
3210pub fn check_cancel_request(
3211    request: &CancelImportJobRequest,
3212    active: &SignedImportJobDelegationV1,
3213    durable_epoch: u64,
3214    cancelled: bool,
3215) -> Result<(), Reject> {
3216    let d = active.body.as_ref().ok_or(Reject::Canonical)?;
3217    width(&request.logical_job_id, 16)?;
3218    width(&request.cancellation_id, 32)?;
3219    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
3220    if request.client_operation_id.is_empty()
3221        || request.destination.as_ref().is_none_or(|s| {
3222            initial_operation_id(&id.spool_uuid, false).map_or(true, |uuid| s.id != uuid)
3223        })
3224        || request.logical_job_id != d.logical_job_id
3225    {
3226        return Err(Reject::Scope);
3227    }
3228    if durable_epoch == 0 || request.expected_authority_epoch != durable_epoch {
3229        return Err(Reject::StaleContext);
3230    }
3231    if request.cancellation_id != d.cancellation_id {
3232        return Err(Reject::Scope);
3233    }
3234    if cancelled {
3235        return Err(Reject::Revoked);
3236    }
3237    Ok(())
3238}
3239/// Exact replay acknowledges the stored cancellation without advancing the epoch.
3240pub fn check_cancel_replay(
3241    request: &CancelImportJobRequest,
3242    stored: &CancelImportJobRequest,
3243) -> Result<(), Reject> {
3244    if request != stored {
3245        return Err(Reject::OperationIdReused);
3246    }
3247    Ok(())
3248}
3249/// Check both independent revocation selectors, regardless of Cancel's selector.
3250pub fn check_import_revocations(
3251    delegation: &SignedImportJobDelegationV1,
3252    member: Option<&SignedImportMemberPermissionV1>,
3253    revoked: &[Vec<u8>],
3254) -> Result<(), Reject> {
3255    let d = delegation.body.as_ref().ok_or(Reject::Canonical)?;
3256    width(&d.cancellation_id, 32)?;
3257    if revoked.contains(&d.cancellation_id) {
3258        return Err(Reject::Revoked);
3259    }
3260    if let Some(parent) = member {
3261        let p = parent.body.as_ref().ok_or(Reject::ImportPermission)?;
3262        width(&p.cancellation_id, 32)?;
3263        if revoked.contains(&p.cancellation_id) {
3264            return Err(Reject::Revoked);
3265        }
3266    }
3267    Ok(())
3268}
3269
3270/// Compute the exact remaining total and slots from an authenticated manifest.
3271/// An empty result is completed work and cannot pass new-scope validation.
3272pub fn remaining_import_scope(
3273    scope: &ImportPermissionScopeV1,
3274    committed: &ImportResultManifestV1,
3275) -> Result<ImportPermissionScopeV1, Reject> {
3276    validate_scope(scope)?;
3277    validate_manifest(committed)?;
3278    let mut remaining = scope.clone();
3279    for slot in &committed.slots {
3280        if !scope
3281            .branches
3282            .iter()
3283            .any(|b| b.ref_name == slot.ref_name && b.slot_id == slot.slot_id)
3284        {
3285            return Err(Reject::Scope);
3286        }
3287        {
3288            remaining.max_result_bytes = remaining
3289                .max_result_bytes
3290                .checked_sub(slot.result_bytes)
3291                .ok_or(Reject::Scope)?;
3292            remaining.max_operations = remaining
3293                .max_operations
3294                .checked_sub(1)
3295                .ok_or(Reject::Scope)?;
3296        }
3297    }
3298    remaining.branches.retain(|b| {
3299        !committed
3300            .slots
3301            .iter()
3302            .any(|s| s.ref_name == b.ref_name && s.slot_id == b.slot_id)
3303    });
3304    Ok(remaining)
3305}
3306
3307/// Validate discovery metadata only; a well-shaped selector grants no authority.
3308pub fn validate_hybrid_import_job_selector(
3309    selector: &HybridImportJobSelector,
3310) -> Result<(), Reject> {
3311    width(&selector.logical_job_id, 16)?;
3312    if selector.logical_job_id.iter().all(|byte| *byte == 0) {
3313        return Err(Reject::Canonical);
3314    }
3315    Ok(())
3316}
3317
3318/// Project a visible operation's durable HYBRID association into the existing
3319/// writer-only state read. Missing/unknown subject or selector is unavailable.
3320/// Malformed present metadata is rejected; never substitute an attempt ID.
3321/// This validates shape, not operation visibility, writer access or signatures.
3322pub fn import_job_state_request_from_operation(
3323    operation: &OperationRecord,
3324) -> Result<Option<GetImportJobStateRequest>, Reject> {
3325    let Some(operation_subject::Subject::Import(subject)) = operation
3326        .subject
3327        .as_ref()
3328        .and_then(|subject| subject.subject.as_ref())
3329    else {
3330        return Ok(None);
3331    };
3332    let Some(selector) = subject.hybrid_job.as_ref() else {
3333        return Ok(None);
3334    };
3335    validate_hybrid_import_job_selector(selector)?;
3336    let request = GetImportJobStateRequest {
3337        destination: operation
3338            .r#ref
3339            .as_ref()
3340            .and_then(|record| record.spool.clone()),
3341        logical_job_id: selector.logical_job_id.clone(),
3342    };
3343    validate_job_state_request(&request)?;
3344    Ok(Some(request))
3345}
3346
3347/// Finite destination-writer read; transport authentication/authorization belongs
3348/// to the generated RPC contract. Validate before any storage lookup.
3349pub fn validate_job_state_request(request: &GetImportJobStateRequest) -> Result<(), Reject> {
3350    use prost::Message;
3351    if request.encoded_len() > 4096 {
3352        return Err(Reject::Bounds);
3353    }
3354    initial_operation_id(&request.logical_job_id, false)?;
3355    let destination = request.destination.as_ref().ok_or(Reject::Scope)?;
3356    let compact = destination.id.replace('-', "");
3357    let raw = hex::decode(&compact).map_err(|_| Reject::Scope)?;
3358    if initial_operation_id(&raw, false).map_or(true, |id| id != destination.id) {
3359        return Err(Reject::Scope);
3360    }
3361    Ok(())
3362}
3363
3364/// Supply the authenticated read, never an incoming untrusted state assertion.
3365pub fn validate_job_state_response(
3366    request: &GetImportJobStateRequest,
3367    response: &GetImportJobStateResponse,
3368) -> Result<(), Reject> {
3369    use prost::Message;
3370    validate_job_state_request(request)?;
3371    if response.encoded_len() > 4096 {
3372        return Err(Reject::Bounds);
3373    }
3374    if !(1..=5).contains(&response.status) || response.authority_epoch == 0 {
3375        return Err(Reject::Canonical);
3376    }
3377    width(&response.active_cancellation_id, 32)?;
3378    width(&response.active_delegation_digest, 32)?;
3379    validate_retry_availability(response, request.destination.as_ref().ok_or(Reject::Scope)?)
3380}
3381
3382/// Independently installed descriptor pin; never learned from a response.
3383#[derive(Debug, Clone, PartialEq)]
3384pub struct ImportWitnessRootPin {
3385    pub authority: String,
3386    pub root_id: String,
3387    pub public_key: Vec<u8>,
3388    pub epoch: u64,
3389}
3390/// Receiver-owned durable data, committed atomically with accepted history.
3391/// Do not deserialize this from the incoming proof or lower its clock floor.
3392#[derive(Debug, Clone, PartialEq)]
3393pub struct ImportWitnessSnapshot {
3394    pub root: ImportWitnessRootPin,
3395    pub witness_set: crate::heddle::api::common::SignedHostedWitnessSetV1,
3396    pub clock_floor_unix_millis: i64,
3397    pub job_associations: Vec<(Vec<u8>, Vec<u8>)>,
3398    pub accepted_history: Vec<ImportPublicProofBundleV1>,
3399}
3400/// Witnessed requires an authenticated observation for every accepted delegation.
3401/// Recovery can retain witnessed history but grants no admission for the active tail.
3402#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3403pub enum ImportBundleEvidence {
3404    Recovery,
3405    Witnessed,
3406}
3407#[derive(Debug, Clone, PartialEq)]
3408pub struct VerifiedImportBundleWitnesses {
3409    pub evidence: ImportBundleEvidence,
3410    pub accepted_history: ImportResultManifestV1,
3411    pub snapshot: Option<ImportWitnessSnapshot>,
3412    pub snapshot_advanced: bool,
3413    pub owner_check_time_unix_seconds: Option<i64>,
3414}
3415/// Check the host's signed same-transaction assertion for a P1 and its consumed P3.
3416/// Receivers verify consistency; only the issuing host can ensure atomic visibility.
3417pub fn check_import_genesis_publication_pair(
3418    delegation: &VerifiedImportDelegation,
3419    admission: &crate::heddle::api::common::HostedWitnessStatementV1,
3420    publication: &crate::heddle::api::common::HostedWitnessStatementV1,
3421) -> Result<(), Reject> {
3422    validity(
3423        delegation.body.not_before_unix_seconds,
3424        delegation.body.expires_at_unix_seconds,
3425        admission.observed_at_unix_millis / 1000,
3426        true,
3427    )?;
3428    if admission.purpose != 1
3429        || publication.purpose != 3
3430        || admission.observed_at_unix_millis != publication.observed_at_unix_millis
3431        || admission.host_transaction_id != publication.host_transaction_id
3432        || admission.executor_id != publication.executor_id
3433        || admission.admission_order >= publication.admission_order
3434    {
3435        return Err(Reject::Transition);
3436    }
3437    Ok(())
3438}
3439
3440/// Verify public Fetch/export evidence. Owner contexts
3441/// are resolved independently at each authenticated time. The verifier derives times
3442/// from the first authenticated publication of the sole delegation.
3443/// Unwitnessed certificates are time-free recovery only.
3444/// The mandatory hook verifies the selected policy chain and owner/native context
3445/// at each authenticated statement time (heddle capability-verifier / WASM).
3446/// With no statements it receives None and verifies time-free policy closure.
3447/// No result is returned on failure. Recovery without a witnessed prefix or new
3448/// authenticated set returns the input snapshot unchanged. A witnessed prefix
3449/// extends rollback protection; recovery cannot persist job history. A new set
3450/// can additionally advance set trust and clock. None remains None when no
3451/// set is carried and no input exists. Persist under the trust lock; inspect
3452/// snapshot_advanced for actual durable change, independently of evidence.
3453pub fn verify_import_bundle_witnesses<'a>(
3454    bundle: &ImportPublicProofBundleV1,
3455    pin: &ImportWitnessRootPin,
3456    snapshot: Option<&ImportWitnessSnapshot>,
3457    now_ms: i64,
3458    mut owner_at: impl FnMut(Option<i64>) -> Result<ImportBundleOwnerExpectation<'a>, Reject>,
3459    mut verify_policy: impl FnMut(
3460        &ImportPublicProofBundleV1,
3461        Option<&crate::heddle::api::common::HostedWitnessStatementV1>,
3462    ) -> Result<(), Reject>,
3463) -> Result<VerifiedImportBundleWitnesses, Reject> {
3464    use crate::witness_trust as trust;
3465    width(&pin.public_key, 32)?;
3466    if pin.epoch == 0 {
3467        return Err(Reject::StaleContext);
3468    }
3469    validate_bundle_bounds(bundle)?;
3470    let terminal = bundle.terminal_manifest.as_ref().ok_or(Reject::Canonical)?;
3471    validate_bundle_history(bundle, !terminal.slots.is_empty())?;
3472    let mut associations = snapshot.map_or_else(Vec::new, |s| s.job_associations.clone());
3473    for d in &bundle.delegations {
3474        let b = d.body.as_ref().ok_or(Reject::Canonical)?;
3475        if associations
3476            .iter()
3477            .any(|(key, job)| *key == b.job_public_key && *job != b.logical_job_id)
3478        {
3479            return Err(Reject::KeyRole);
3480        }
3481        if !associations.iter().any(|(key, _)| *key == b.job_public_key) {
3482            associations.push((b.job_public_key.clone(), b.logical_job_id.clone()));
3483        }
3484    }
3485    let job_keys = associations
3486        .iter()
3487        .map(|(key, _)| key.clone())
3488        .collect::<Vec<_>>();
3489    fn expectation<'a>(
3490        root: &'a ImportWitnessRootPin,
3491        floor: i64,
3492        now: i64,
3493        keys: &'a [Vec<u8>],
3494    ) -> trust::SetExpectation<'a> {
3495        trust::SetExpectation {
3496            authority: &root.authority,
3497            root_id: &root.root_id,
3498            root_public_key: &root.public_key,
3499            root_epoch: root.epoch,
3500            now_unix_millis: now,
3501            clock_floor_unix_millis: floor,
3502            known_job_keys: keys,
3503        }
3504    }
3505    let previous = if let Some(s) = snapshot {
3506        if s.root.authority != pin.authority {
3507            return Err(Reject::Root);
3508        }
3509        let restored = trust::restore_history_snapshot(
3510            &s.witness_set,
3511            &expectation(&s.root, 0, now_ms, &job_keys),
3512        )?;
3513        Some(restored)
3514    } else {
3515        None
3516    };
3517    let carried = bundle.witness_set.as_ref();
3518    if carried.is_none() && !bundle.statements.is_empty() {
3519        return Err(Reject::Canonical);
3520    }
3521    let new_set =
3522        carried.is_some_and(|c| snapshot.is_none_or(|s| s.root != *pin || s.witness_set != *c));
3523    let selected = expectation(
3524        pin,
3525        snapshot.map_or(0, |s| s.clock_floor_unix_millis),
3526        now_ms,
3527        &job_keys,
3528    );
3529    let set = carried
3530        .map(|carried| {
3531            if snapshot.is_some_and(|s| s.root != *pin) {
3532                trust::verify_set_after_root_replacement(
3533                    carried,
3534                    &selected,
3535                    previous.as_ref().ok_or(Reject::StaleContext)?,
3536                )
3537            } else {
3538                trust::verify_set(carried, &selected, previous.as_ref())
3539            }
3540        })
3541        .transpose()?;
3542    // Authenticate statements before using their observation times or policies.
3543    let mut resolved = Vec::new();
3544    for signed in &bundle.statements {
3545        let s = signed.body.as_ref().ok_or(Reject::Canonical)?;
3546        let set = set.as_ref().ok_or(Reject::Canonical)?;
3547        let entry = set
3548            .body()
3549            .entries
3550            .iter()
3551            .find(|e| e.executor_id == s.executor_id)
3552            .ok_or(Reject::Root)?;
3553        let proof = if entry.state == 2 {
3554            let leaf = trust::leaf_digest(s.purpose, &canonical(s)?, &signed.signature)?;
3555            bundle.history_proofs.iter().find(|p| {
3556                p.purpose == s.purpose && trust::verify_inclusion(&leaf, p, entry).is_ok()
3557            })
3558        } else {
3559            None
3560        };
3561        let context = trust::resolve_statement(set, signed, proof, false, now_ms)?;
3562        verify_policy(bundle, Some(s))?;
3563        resolved.push((context, proof));
3564    }
3565    if bundle.statements.is_empty() {
3566        // Time-free policy/owner/native closure; this asserts no admission event.
3567        verify_policy(bundle, None)?;
3568    }
3569    // Derive owner-selection times only from authenticated receipts. Prefer the
3570    // first publication for each delegation; initial admissions stand alone too.
3571    // Select precisely the P3 each progressive prefix consumes, independent of
3572    // statement array order. Every carried P3 must be consumed exactly once.
3573    let mut prefix = ImportResultManifestV1 {
3574        slots: vec![],
3575        ..terminal.clone()
3576    };
3577    let mut selected_publications = Vec::new();
3578    for operation in &bundle.operations {
3579        let digest = signed_operation_digest(operation)?;
3580        let slot = terminal
3581            .slots
3582            .iter()
3583            .find(|slot| slot.signed_operation_digest == digest)
3584            .ok_or(Reject::Scope)?;
3585        prefix.slots.push(slot.clone());
3586        prefix
3587            .slots
3588            .sort_by(|a, b| (&a.ref_name, a.slot_id).cmp(&(&b.ref_name, b.slot_id)));
3589        let payload = canonical(&publication_payload(operation, &prefix)?)?;
3590        let matches = bundle
3591            .statements
3592            .iter()
3593            .filter(|signed| {
3594                signed
3595                    .body
3596                    .as_ref()
3597                    .is_some_and(|s| s.purpose == 3 && s.canonical_payload == payload)
3598            })
3599            .collect::<Vec<_>>();
3600        if matches.len() != 1 {
3601            return Err(Reject::Transition);
3602        }
3603        selected_publications.push(matches[0]);
3604    }
3605    if bundle.statements.iter().any(|signed| {
3606        signed
3607            .body
3608            .as_ref()
3609            .is_some_and(|s| s.purpose == 3 && !selected_publications.contains(&signed))
3610    }) {
3611        return Err(Reject::Transition);
3612    }
3613    let mut admitted_geneses = std::collections::BTreeSet::<Vec<u8>>::new();
3614    let mut times = vec![None; bundle.delegations.len()];
3615    let mut publications = Vec::new();
3616    for signed in &bundle.statements {
3617        let statement = signed.body.as_ref().ok_or(Reject::Canonical)?;
3618        if statement.purpose != 3 {
3619            continue;
3620        }
3621        let (operation, manifest) = bundle
3622            .operations
3623            .iter()
3624            .flat_map(|o| bundle.manifests.iter().map(move |m| (o, m)))
3625            .find(|(o, m)| {
3626                publication_payload(o, m)
3627                    .and_then(|p| canonical(&p))
3628                    .is_ok_and(|b| b == statement.canonical_payload)
3629            })
3630            .ok_or(Reject::Scope)?;
3631        let digest = &operation
3632            .body
3633            .as_ref()
3634            .ok_or(Reject::Canonical)?
3635            .delegation_digest;
3636        let index = bundle
3637            .delegations
3638            .iter()
3639            .position(|d| signed_delegation_digest(d).is_ok_and(|h| h == *digest))
3640            .ok_or(Reject::Scope)?;
3641        let time = statement.observed_at_unix_millis / 1000;
3642        // First means accepted publication order, independent of array order.
3643        if times[index].is_none_or(|(order, _)| statement.admission_order < order) {
3644            times[index] = Some((statement.admission_order, time));
3645        }
3646        publications.push((signed, operation, manifest, index));
3647    }
3648    // Resolve only after authentication. Facts must cover the selected time;
3649    // current claimed state cannot stand in for an earlier deferred state.
3650    let mut resolve_owner = |time: Option<i64>| {
3651        let facts = owner_at(time)?;
3652        if facts.effective_from_unix_seconds < 0
3653            || facts
3654                .effective_until_unix_seconds
3655                .is_some_and(|end| end <= facts.effective_from_unix_seconds)
3656            || time.is_some_and(|t| {
3657                t < facts.effective_from_unix_seconds
3658                    || facts
3659                        .effective_until_unix_seconds
3660                        .is_some_and(|end| t >= end)
3661            })
3662        {
3663            return Err(Reject::Scope);
3664        }
3665        for (key, job) in facts.known_job_associations {
3666            if associations.iter().any(|(k, j)| k == key && j != job) {
3667                return Err(Reject::KeyRole);
3668            }
3669            if !associations.iter().any(|(k, _)| k == key) {
3670                associations.push((key.clone(), job.clone()));
3671            }
3672        }
3673        if facts
3674            .forbidden_job_keys
3675            .iter()
3676            .any(|key| associations.iter().any(|(known, _)| known == key))
3677        {
3678            return Err(Reject::KeyRole);
3679        }
3680        // Also keep newly supplied known job keys disjoint from witness roles.
3681        if set.as_ref().is_some_and(|set| {
3682            facts.known_job_associations.iter().any(|(key, _)| {
3683                *key == pin.public_key || set.body().entries.iter().any(|e| e.public_key == *key)
3684            })
3685        }) {
3686            return Err(Reject::KeyRole);
3687        }
3688        Ok((facts, associations.clone()))
3689    };
3690    let mut verified = Vec::new();
3691    for (i, d) in bundle.delegations.iter().enumerate() {
3692        let body = d.body.as_ref().ok_or(Reject::Canonical)?;
3693        let parent = resolve_bundle_permission(bundle, &body.parent_permission_digest)?;
3694        let time = times[i].map(|(_, time)| time);
3695        let (facts, selected_associations) = resolve_owner(time)?;
3696        let owner = facts.at(time.unwrap_or(0), &selected_associations);
3697        let token = verify_delegation_inner(d, parent, &owner, times[i].is_some())?;
3698        verified.push(token);
3699    }
3700    let initial = verified.first().ok_or(Reject::Canonical)?;
3701    for branch in &initial.body.branch_manifest {
3702        let limit = branch.limit.as_ref().ok_or(Reject::Canonical)?;
3703        let binding = bundle
3704            .genesis_authorities
3705            .iter()
3706            .find(|g| signed_genesis_digest(g).is_ok_and(|h| h == branch.genesis_authority_digest))
3707            .ok_or(Reject::Scope)?;
3708        let original = bundle
3709            .original_geneses
3710            .iter()
3711            .find(|o| native_id(o) == limit.genesis_digest)
3712            .ok_or(Reject::Scope)?;
3713        let g = binding.body.as_ref().ok_or(Reject::Canonical)?;
3714        let envelope = bundle
3715            .creator_authority_envelopes
3716            .iter()
3717            .find(|e| hash(&[e]) == g.creator_authority_envelope_digest)
3718            .ok_or(Reject::Scope)?;
3719        verify_native(original, "heddle-thread-genesis-v1")?;
3720        let signature = original
3721            .signatures
3722            .iter()
3723            .find(|s| s.public_key == g.creator_public_key)
3724            .ok_or(Reject::Scope)?;
3725        verify_genesis_authority(
3726            binding,
3727            initial,
3728            &limit.genesis_digest,
3729            &signature.signature,
3730            &hash(&[envelope]),
3731        )?;
3732    }
3733    for ((context, proof), signed) in resolved.iter().zip(&bundle.statements) {
3734        let s = signed.body.as_ref().ok_or(Reject::Canonical)?;
3735        if !verified.iter().any(|d| {
3736            d.body.identity.as_ref().is_some_and(|id| {
3737                s.spool_uuid == id.spool_uuid
3738                    && s.spool_genesis_digest == id.spool_genesis_digest
3739                    && s.owner_id == id.owner_id
3740                    && s.owner_state_hash == id.owner_state_hash
3741                    && s.ownership_transfer_sequence == id.ownership_transfer_sequence
3742            })
3743        }) {
3744            return Err(Reject::Scope);
3745        }
3746        match s.purpose {
3747            1 => {
3748                // Every original admission must independently satisfy [N,E).
3749                let time = s.observed_at_unix_millis / 1000;
3750                let (facts, selected_associations) = resolve_owner(Some(time))?;
3751                let owner = facts.at(time, &selected_associations);
3752                verify_delegation(
3753                    &bundle.delegations[0],
3754                    resolve_bundle_permission(bundle, &initial.body.parent_permission_digest)?,
3755                    &owner,
3756                )?;
3757                let payload = bundle
3758                    .genesis_witnesses
3759                    .iter()
3760                    .find(|p| canonical(*p).is_ok_and(|b| b == s.canonical_payload))
3761                    .ok_or(Reject::Scope)?;
3762                let genesis = &payload
3763                    .binding
3764                    .as_ref()
3765                    .and_then(|g| g.body.as_ref())
3766                    .ok_or(Reject::Canonical)?
3767                    .genesis_digest;
3768                if !admitted_geneses.insert(genesis.clone()) {
3769                    return Err(Reject::Transition);
3770                }
3771                let publication = bundle
3772                    .operations
3773                    .iter()
3774                    .zip(&selected_publications)
3775                    .find(|(o, _)| {
3776                        o.body
3777                            .as_ref()
3778                            .is_some_and(|o| &o.genesis_digest == genesis)
3779                    })
3780                    .and_then(|(_, s)| s.body.as_ref())
3781                    .ok_or(Reject::Transition)?;
3782                check_import_genesis_publication_pair(initial, s, publication)?;
3783                verify_witness_payload(
3784                    s,
3785                    WitnessPayload::Genesis(
3786                        bundle
3787                            .genesis_witnesses
3788                            .iter()
3789                            .find(|p| canonical(*p).is_ok_and(|b| b == s.canonical_payload))
3790                            .ok_or(Reject::Scope)?,
3791                    ),
3792                )?;
3793            }
3794            2 => verify_witness_payload(
3795                s,
3796                WitnessPayload::Authority(
3797                    bundle
3798                        .authority_witnesses
3799                        .iter()
3800                        .find(|p| canonical(*p).is_ok_and(|b| b == s.canonical_payload))
3801                        .ok_or(Reject::Scope)?,
3802                ),
3803            )?,
3804            4 => {
3805                let payload = bundle
3806                    .landing_witnesses
3807                    .iter()
3808                    .find(|p| canonical(*p).is_ok_and(|b| b == s.canonical_payload))
3809                    .ok_or(Reject::Scope)?;
3810                let (facts, selected_associations) =
3811                    resolve_owner(Some(s.observed_at_unix_millis / 1000))?;
3812                let known = selected_associations
3813                    .iter()
3814                    .map(|(key, _)| key.clone())
3815                    .collect::<Vec<_>>();
3816                verify_landing_key_roles(payload, &known, facts.forbidden_landing_keys)?;
3817                verify_witness_payload(s, WitnessPayload::Landing(payload))?;
3818            }
3819            3 => {
3820                let (_, operation, manifest, index) = publications
3821                    .iter()
3822                    .find(|(statement, _, _, _)| *statement == signed)
3823                    .ok_or(Reject::Scope)?;
3824                let d = &bundle.delegations[*index];
3825                let body = d.body.as_ref().ok_or(Reject::Canonical)?;
3826                let time = s.observed_at_unix_millis / 1000;
3827                let (facts, selected_associations) = resolve_owner(Some(time))?;
3828                let owner = facts.at(time, &selected_associations);
3829                let delegation = verify_delegation(
3830                    d,
3831                    resolve_bundle_permission(bundle, &body.parent_permission_digest)?,
3832                    &owner,
3833                )?;
3834                verify_publication(
3835                    operation,
3836                    &delegation,
3837                    manifest,
3838                    signed,
3839                    set.as_ref().ok_or(Reject::Canonical)?,
3840                    *proof,
3841                    now_ms,
3842                )?;
3843            }
3844            _ => return Err(Reject::Version),
3845        }
3846        trust::recheck_context(
3847            context,
3848            set.as_ref().ok_or(Reject::Canonical)?,
3849            signed,
3850            now_ms,
3851        )?;
3852    }
3853    // Operations are accepted publication order, while manifest slots are sorted.
3854    let mut progressive = ImportResultManifestV1 {
3855        slots: vec![],
3856        ..terminal.clone()
3857    };
3858    let mut order = 0;
3859    let mut observed = 0;
3860    let mut consumed = std::collections::BTreeMap::<(Vec<u8>, Vec<u8>), (u32, u64)>::new();
3861    let mut total_bytes = 0_u64;
3862    for o in &bundle.operations {
3863        let digest = signed_operation_digest(o)?;
3864        let slot = terminal
3865            .slots
3866            .iter()
3867            .find(|s| s.signed_operation_digest == digest)
3868            .ok_or(Reject::Scope)?;
3869        progressive.slots.push(slot.clone());
3870        progressive
3871            .slots
3872            .sort_by(|a, b| (&a.ref_name, a.slot_id).cmp(&(&b.ref_name, b.slot_id)));
3873        let payload = canonical(&publication_payload(o, &progressive)?)?;
3874        let s = bundle
3875            .statements
3876            .iter()
3877            .filter_map(|s| s.body.as_ref())
3878            .find(|s| s.purpose == 3 && s.canonical_payload == payload)
3879            .ok_or(Reject::Transition)?;
3880        if s.admission_order <= order || s.observed_at_unix_millis < observed {
3881            return Err(Reject::Transition);
3882        }
3883        order = s.admission_order;
3884        observed = s.observed_at_unix_millis;
3885        let digest = &o.body.as_ref().ok_or(Reject::Canonical)?.delegation_digest;
3886        let index = verified
3887            .iter()
3888            .position(|d| d.digest == *digest)
3889            .ok_or(Reject::Scope)?;
3890        let result_bytes = o.body.as_ref().ok_or(Reject::Canonical)?.result_bytes;
3891        total_bytes = total_bytes
3892            .checked_add(result_bytes)
3893            .ok_or(Reject::Bounds)?;
3894        let original_scope = initial.body.scope.as_ref().ok_or(Reject::Canonical)?;
3895        if total_bytes > original_scope.max_result_bytes
3896            || progressive.slots.len() > original_scope.max_operations as usize
3897        {
3898            return Err(Reject::Scope);
3899        }
3900        let delegation = &verified[index];
3901        let mut committed_before = progressive.clone();
3902        let operation_digest = signed_operation_digest(o)?;
3903        committed_before
3904            .slots
3905            .retain(|s| s.signed_operation_digest != operation_digest);
3906        check_import_publication_budget(o, delegation, &committed_before)?;
3907        let scope = delegation.body.scope.as_ref().ok_or(Reject::Canonical)?;
3908        let mut budgets = vec![(&delegation.digest, scope)];
3909        if let Some(parent) = &delegation.member {
3910            budgets.push((
3911                &delegation.body.parent_permission_digest,
3912                parent
3913                    .body
3914                    .as_ref()
3915                    .and_then(|p| p.scope.as_ref())
3916                    .ok_or(Reject::Canonical)?,
3917            ));
3918        }
3919        for (digest, scope) in budgets {
3920            let (operations, bytes) = consumed
3921                .entry((digest.clone(), delegation.body.logical_job_id.clone()))
3922                .or_default();
3923            *operations = operations.checked_add(1).ok_or(Reject::Bounds)?;
3924            *bytes = bytes.checked_add(result_bytes).ok_or(Reject::Bounds)?;
3925            if *operations > scope.max_operations || *bytes > scope.max_result_bytes {
3926                return Err(Reject::Scope);
3927            }
3928        }
3929    }
3930    if progressive != *terminal {
3931        return Err(Reject::Scope);
3932    }
3933    let mut history = snapshot.map_or_else(Vec::new, |s| s.accepted_history.clone());
3934    if let Some(old) = history.iter_mut().find(|b| {
3935        b.terminal_manifest
3936            .as_ref()
3937            .is_some_and(|m| m.logical_job_id == terminal.logical_job_id)
3938            && b.delegations
3939                .first()
3940                .and_then(|d| d.body.as_ref())
3941                .and_then(|d| d.identity.as_ref())
3942                .map(|id| &id.spool_uuid)
3943                == bundle
3944                    .delegations
3945                    .first()
3946                    .and_then(|d| d.body.as_ref())
3947                    .and_then(|d| d.identity.as_ref())
3948                    .map(|id| &id.spool_uuid)
3949    }) {
3950        if !bundle.delegations.starts_with(&old.delegations)
3951            || !bundle.operations.starts_with(&old.operations)
3952            || bundle.owner_genesis != old.owner_genesis
3953            || !bundle
3954                .ownership_transfers
3955                .starts_with(&old.ownership_transfers)
3956            || !old
3957                .owner_histories
3958                .iter()
3959                .all(|v| bundle.owner_histories.contains(v))
3960            || bundle.member_permission != old.member_permission
3961            || !old
3962                .genesis_authorities
3963                .iter()
3964                .all(|v| bundle.genesis_authorities.contains(v))
3965            || !old
3966                .original_geneses
3967                .iter()
3968                .all(|v| bundle.original_geneses.contains(v))
3969            || !old
3970                .creator_authority_envelopes
3971                .iter()
3972                .all(|v| bundle.creator_authority_envelopes.contains(v))
3973            || !old.manifests.iter().all(|v| bundle.manifests.contains(v))
3974            || !old.statements.iter().all(|v| bundle.statements.contains(v))
3975            || !old.policies.iter().all(|v| bundle.policies.contains(v))
3976            || !old
3977                .genesis_witnesses
3978                .iter()
3979                .all(|v| bundle.genesis_witnesses.contains(v))
3980            || !old
3981                .authority_witnesses
3982                .iter()
3983                .all(|v| bundle.authority_witnesses.contains(v))
3984            || !old
3985                .landing_witnesses
3986                .iter()
3987                .all(|v| bundle.landing_witnesses.contains(v))
3988            || !old
3989                .foreign_dependencies
3990                .iter()
3991                .all(|v| bundle.foreign_dependencies.contains(v))
3992        {
3993            return Err(Reject::HighWater);
3994        }
3995        *old = bundle.clone();
3996    } else {
3997        history.push(bundle.clone());
3998    }
3999    let witnessed = times[0].is_some();
4000    let witnessed_prefix = usize::from(witnessed);
4001    let mut persisted = snapshot.cloned();
4002    if witnessed_prefix > 0 || new_set {
4003        let mut persisted_associations =
4004            snapshot.map_or_else(Vec::new, |s| s.job_associations.clone());
4005        if witnessed_prefix > 0 {
4006            for d in &bundle.delegations[..witnessed_prefix] {
4007                let b = d.body.as_ref().ok_or(Reject::Canonical)?;
4008                if !persisted_associations
4009                    .iter()
4010                    .any(|(key, _)| *key == b.job_public_key)
4011                {
4012                    persisted_associations
4013                        .push((b.job_public_key.clone(), b.logical_job_id.clone()));
4014                }
4015            }
4016        }
4017        persisted = Some(ImportWitnessSnapshot {
4018            root: pin.clone(),
4019            witness_set: carried.ok_or(Reject::Canonical)?.clone(),
4020            clock_floor_unix_millis: now_ms,
4021            job_associations: persisted_associations,
4022            accepted_history: if witnessed_prefix > 0 {
4023                history
4024            } else {
4025                snapshot.map_or_else(Vec::new, |s| s.accepted_history.clone())
4026            },
4027        });
4028    }
4029    Ok(VerifiedImportBundleWitnesses {
4030        evidence: if witnessed {
4031            ImportBundleEvidence::Witnessed
4032        } else {
4033            ImportBundleEvidence::Recovery
4034        },
4035        snapshot_advanced: persisted.as_ref() != snapshot,
4036        owner_check_time_unix_seconds: times[0].map(|(_, time)| time),
4037        accepted_history: terminal.clone(),
4038        snapshot: persisted,
4039    })
4040}
4041
4042/// Independently authenticated current caller facts for source custody checks.
4043pub struct ImportControlCaller<'a> {
4044    pub authenticated_pop: bool,
4045    pub destination_writer: bool,
4046    pub caller_account: &'a str,
4047    pub connection_owner_account: Option<&'a str>,
4048    pub authorized_source: Option<&'a ImportSourceSelectionV1>,
4049    pub exact_grants_current: bool,
4050    pub selected_commits_available: bool,
4051}
4052#[derive(Clone, Copy)]
4053pub enum ImportControlAction {
4054    Cancel,
4055    Retry,
4056}
4057
4058/// Destination control is independent of source custody. Cancel never fetches.
4059/// Run at Retry admission after checking signed authority.
4060pub fn check_import_control_caller(
4061    action: ImportControlAction,
4062    retained: &ImportSourceSelectionV1,
4063    scope: &ImportPermissionScopeV1,
4064    caller: &ImportControlCaller<'_>,
4065) -> Result<(), Reject> {
4066    if !caller.authenticated_pop || !caller.destination_writer || caller.caller_account.is_empty() {
4067        return Err(Reject::Scope);
4068    }
4069    if matches!(action, ImportControlAction::Cancel) {
4070        return Ok(());
4071    }
4072    validate_retained_import_source(retained, scope)?;
4073    if retained.connection.is_some()
4074        && (caller.connection_owner_account != Some(caller.caller_account)
4075            || caller.authorized_source != Some(retained)
4076            || !caller.exact_grants_current)
4077    {
4078        return Err(Reject::SourceSelection);
4079    }
4080    if !caller.selected_commits_available {
4081        return Err(Reject::SourceSelection);
4082    }
4083    Ok(())
4084}
4085
4086fn validate_retry_availability(
4087    response: &GetImportJobStateResponse,
4088    destination: &SpoolRef,
4089) -> Result<(), Reject> {
4090    use get_import_job_state_response::RetryAvailability;
4091    match response
4092        .retry_availability
4093        .as_ref()
4094        .ok_or(Reject::Canonical)?
4095    {
4096        RetryAvailability::EligibleRetryTarget(target) => {
4097            if response.status != 1 {
4098                return Err(Reject::Canonical);
4099            }
4100            let operation = target.operation_ref.as_ref().ok_or(Reject::Canonical)?;
4101            let raw = hex::decode(operation.id.replace('-', "")).map_err(|_| Reject::Canonical)?;
4102            if initial_operation_id(&raw, false)? != operation.id {
4103                return Err(Reject::Canonical);
4104            }
4105            if operation.spool.as_ref() != Some(destination) {
4106                return Err(Reject::Scope);
4107            }
4108            if target.operation_version.is_empty() || target.operation_version.len() > 256 {
4109                return Err(Reject::Bounds);
4110            }
4111        }
4112        RetryAvailability::RetryUnavailable(reason) => {
4113            if !matches!(
4114                (response.status, *reason),
4115                (1, 1 | 2 | 6) | (2, 3) | (3, 4) | (4, 5) | (5, 7)
4116            ) {
4117                return Err(Reject::Canonical);
4118            }
4119        }
4120    }
4121    Ok(())
4122}
4123
4124/// Validate the minimal writer snapshot and its retry target.
4125pub fn validate_retry_state_response(
4126    request: &GetImportJobStateRequest,
4127    response: &GetImportJobStateResponse,
4128) -> Result<(), Reject> {
4129    validate_job_state_response(request, response)?;
4130    validate_retry_availability(response, request.destination.as_ref().ok_or(Reject::Scope)?)
4131}
4132
4133/// Receiver-owned facts locked together with job/authority/source state. The
4134/// operation-lineage association is durable host state, not an ID inference.
4135pub struct ImportRetryAdmission<'a> {
4136    pub read: &'a GetImportJobStateResponse,
4137    pub original: &'a OperationRecord,
4138    pub retry_lineage_id: &'a [u8],
4139    pub logical_job_terminal: bool,
4140    pub retained_source: &'a ImportSourceSelectionV1,
4141    pub committed_manifest: &'a ImportResultManifestV1,
4142    pub now_unix_seconds: i64,
4143}
4144
4145/// Run after caller-scoped frozen replay lookup, under ONE admission transaction.
4146/// Full owner/policy/revocation/lease checks and allocation remain host gates.
4147pub fn check_retry_admission(
4148    request: &RetryImportSourceRequest,
4149    context: &ImportRetryAdmission<'_>,
4150    active: &VerifiedImportDelegation,
4151    caller: &ImportControlCaller<'_>,
4152) -> Result<(), Reject> {
4153    use get_import_job_state_response::RetryAvailability;
4154    let destination = request
4155        .original_operation
4156        .as_ref()
4157        .and_then(|r| r.spool.clone());
4158    let read_request = GetImportJobStateRequest {
4159        destination,
4160        logical_job_id: request.logical_job_id.clone(),
4161    };
4162    validate_retry_state_response(&read_request, context.read)?;
4163    if request.client_operation_id.is_empty() || request.client_operation_id.len() > 128 {
4164        return Err(Reject::Canonical);
4165    }
4166    if context.logical_job_terminal || context.read.status != 1 {
4167        return Err(Reject::Revoked);
4168    }
4169    let Some(RetryAvailability::EligibleRetryTarget(target)) = &context.read.retry_availability
4170    else {
4171        return Err(Reject::StaleContext);
4172    };
4173    let target_ref = target.operation_ref.as_ref().ok_or(Reject::Canonical)?;
4174    let original_ref = context.original.r#ref.as_ref().ok_or(Reject::Scope)?;
4175    if request.original_operation.as_ref() != Some(original_ref)
4176        || original_ref.spool != target_ref.spool
4177        || original_ref.id != target_ref.id
4178        || context.retry_lineage_id != active.body.retry_lineage_id
4179        || import_job_state_request_from_operation(context.original)?.as_ref()
4180            != Some(&read_request)
4181    {
4182        return Err(Reject::Scope);
4183    }
4184    if request.expected_operation_version != target.operation_version
4185        || context.original.version != target.operation_version
4186        || context.original.superseded_by.is_some()
4187        || !matches!(context.original.state, 4 | 5)
4188    {
4189        return Err(Reject::StaleContext);
4190    }
4191    let state = context.read;
4192    if state.active_delegation_digest != active.digest
4193        || state.active_cancellation_id != active.body.cancellation_id
4194    {
4195        return Err(Reject::StaleContext);
4196    }
4197    check_job_fence(
4198        &request.logical_job_id,
4199        &request.active_delegation_digest,
4200        request.expected_authority_epoch,
4201        active,
4202        state.authority_epoch,
4203    )?;
4204    interval(
4205        active.body.not_before_unix_seconds,
4206        active.body.expires_at_unix_seconds,
4207        context.now_unix_seconds,
4208    )?;
4209    let manifest = context.committed_manifest;
4210    validate_manifest(manifest)?;
4211    if manifest.logical_job_id != active.body.logical_job_id
4212        || manifest.retry_lineage_id != active.body.retry_lineage_id
4213    {
4214        return Err(Reject::Scope);
4215    }
4216    let scope = active.body.scope.as_ref().ok_or(Reject::Canonical)?;
4217    if !scope.branches.iter().any(|b| {
4218        !manifest
4219            .slots
4220            .iter()
4221            .any(|s| s.ref_name == b.ref_name && s.slot_id == b.slot_id)
4222    }) {
4223        return Err(Reject::StaleContext);
4224    }
4225    check_import_control_caller(
4226        ImportControlAction::Retry,
4227        context.retained_source,
4228        scope,
4229        caller,
4230    )
4231}
4232
4233/// Check a host-generated UUID against the COMPLETE durable attempt set, including
4234/// the first lineage UUID. Persist the allocation, both direct links and receipt
4235/// atomically; do not generate a UUID from the request idempotency key.
4236pub fn validate_retry_response(
4237    request: &RetryImportSourceRequest,
4238    response: &MutationResponse,
4239    prior_attempt_ids: &[String],
4240) -> Result<(), Reject> {
4241    let receipt = response.receipt.as_ref().ok_or(Reject::PendingOperation)?;
4242    let Some(mutation_receipt::Outcome::PendingOperation(operation)) = &receipt.outcome else {
4243        return Err(Reject::PendingOperation);
4244    };
4245    let original = request
4246        .original_operation
4247        .as_ref()
4248        .ok_or(Reject::PendingOperation)?;
4249    let raw = hex::decode(operation.id.replace('-', "")).map_err(|_| Reject::PendingOperation)?;
4250    if initial_operation_id(&raw, false).map_or(true, |id| id != operation.id)
4251        || request.client_operation_id.is_empty()
4252        || receipt.client_operation_id != request.client_operation_id
4253        || original.spool.is_none()
4254        || operation.spool != original.spool
4255        || operation.id == original.id
4256        || operation.id == request.client_operation_id
4257        || prior_attempt_ids.contains(&operation.id)
4258    {
4259        return Err(Reject::PendingOperation);
4260    }
4261    Ok(())
4262}
4263
4264/// Exact frozen Retry replay returns the stored receipt without allocating.
4265pub fn check_retry_replay(request_bytes: &[u8], stored_bytes: &[u8]) -> Result<(), Reject> {
4266    if request_bytes.len() > 2 * MAX_BUNDLE_BYTES {
4267        return Err(Reject::Bounds);
4268    }
4269    if request_bytes != stored_bytes {
4270        return Err(Reject::OperationIdReused);
4271    }
4272    Ok(())
4273}
4274
4275/// Under the publication transaction, use the complete cumulative manifest of
4276/// this logical job. All operations consume the same single delegation total.
4277pub fn check_import_publication_budget(
4278    signed: &SignedDelegatedImportOperationV1,
4279    active: &VerifiedImportDelegation,
4280    committed_before: &ImportResultManifestV1,
4281) -> Result<(), Reject> {
4282    verify_operation(signed, active)?;
4283    let scope = active.body.scope.as_ref().ok_or(Reject::Canonical)?;
4284    if committed_before.logical_job_id != active.body.logical_job_id
4285        || committed_before.retry_lineage_id != active.body.retry_lineage_id
4286    {
4287        return Err(Reject::Scope);
4288    }
4289    let remaining = remaining_import_scope(scope, committed_before)?;
4290    if check_slot_replay(committed_before, signed)? {
4291        return Ok(());
4292    }
4293    let o = signed.body.as_ref().ok_or(Reject::Canonical)?;
4294    if remaining.max_operations == 0 || o.result_bytes > remaining.max_result_bytes {
4295        return Err(Reject::Scope);
4296    }
4297    Ok(())
4298}