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 || protocol.mandatory_features != [1] {
2022        return Err(Reject::Protocol);
2023    }
2024    Ok(())
2025}
2026
2027/// Producer-owned logical-job/lease fence for RetryImportSource and final
2028/// publication. The physical retry row never supplies a new logical identity.
2029pub fn check_job_fence(
2030    logical_job_id: &[u8],
2031    active_delegation_digest: &[u8],
2032    expected_epoch: u64,
2033    active: &VerifiedImportDelegation,
2034    durable_epoch: u64,
2035) -> Result<(), Reject> {
2036    if logical_job_id != active.body.logical_job_id {
2037        return Err(Reject::Scope);
2038    }
2039    if expected_epoch != durable_epoch || active_delegation_digest != active.digest {
2040        return Err(Reject::StaleContext);
2041    }
2042    Ok(())
2043}
2044pub fn validate_public_bundle(bundle: &ImportPublicProofBundleV1) -> Result<(), Reject> {
2045    validate_bundle_bounds(bundle)?;
2046    validate_bundle_history(bundle, true)
2047}
2048fn validate_bundle_bounds(bundle: &ImportPublicProofBundleV1) -> Result<(), Reject> {
2049    crate::writer_authority::validate_owner_histories(&bundle.owner_histories)?;
2050    use prost::Message;
2051    if bundle.format_version != 1 {
2052        return Err(Reject::Version);
2053    }
2054    if bundle.encoded_len() > MAX_BUNDLE_BYTES
2055        || bundle.owner_histories.len() > 64
2056        || bundle.ownership_transfers.len() > 64
2057        || bundle.genesis_authorities.len() > MAX_BRANCHES
2058        || bundle.delegations.len() != 1
2059        || bundle.operations.len() > MAX_BRANCHES
2060        || bundle.statements.len() > 1024
2061        || bundle.history_proofs.len() > 1024
2062        || bundle.policies.len() > 256
2063        || bundle.original_geneses.len() > MAX_BRANCHES
2064        || bundle.creator_authority_envelopes.len() > MAX_BRANCHES
2065        || bundle.manifests.len() > MAX_BRANCHES
2066        || bundle.genesis_witnesses.len() > 256
2067        || bundle.authority_witnesses.len() > 256
2068        || bundle.landing_witnesses.len() > 256
2069    {
2070        return Err(Reject::Bounds);
2071    }
2072    Ok(())
2073}
2074
2075/// Typed adapter boundary: an authentic unrelated capability/online role must
2076/// never be selected as the parent of an import certificate.
2077pub enum ImportPermissionEvidence<'a> {
2078    Import(&'a SignedImportMemberPermissionV1),
2079    OwnerCapability(&'a SignedOwnerCapability),
2080    OnlineRole(&'a str),
2081}
2082pub fn select_import_permission(
2083    evidence: ImportPermissionEvidence<'_>,
2084) -> Result<&SignedImportMemberPermissionV1, Reject> {
2085    match evidence {
2086        ImportPermissionEvidence::Import(p) => Ok(p),
2087        ImportPermissionEvidence::OwnerCapability(_) | ImportPermissionEvidence::OnlineRole(_) => {
2088            Err(Reject::ImportPermission)
2089        }
2090    }
2091}
2092/// Hybrid dispatch has no legacy execution arm, even for an authentic witness.
2093pub fn require_import_operation_format(format: &str) -> Result<(), Reject> {
2094    if format != OPERATION_DOMAIN {
2095        return Err(Reject::Protocol);
2096    }
2097    Ok(())
2098}
2099pub fn frontier_digest(frontier: &ImportFrontierV1) -> Result<Vec<u8>, Reject> {
2100    if frontier.format_version != 1 {
2101        return Err(Reject::Version);
2102    }
2103    width(&frontier.thread_id, 32)?;
2104    if frontier.operation_ids.len() > 128 {
2105        return Err(Reject::Bounds);
2106    }
2107    for id in &frontier.operation_ids {
2108        width(id, 32)?;
2109    }
2110    if frontier.operation_ids.windows(2).any(|w| w[0] >= w[1]) {
2111        return Err(Reject::Canonical);
2112    }
2113    signing_digest("heddle-import-frontier-v1", frontier)
2114}
2115pub fn content_digest(content: &ImportContentV1) -> Result<Vec<u8>, Reject> {
2116    if content.format_version != 1 {
2117        return Err(Reject::Version);
2118    }
2119    if content.canonical_capture.is_empty() {
2120        return Err(Reject::Bounds);
2121    }
2122    signing_digest("heddle-import-content-v1", content)
2123}
2124pub fn signed_native_digest(record: &SignedRecord) -> Result<Vec<u8>, Reject> {
2125    signing_digest("heddle-signed-native-record-v1", record)
2126}
2127pub(crate) fn verify_native(record: &SignedRecord, format: &str) -> Result<(), Reject> {
2128    if record.format != format {
2129        return Err(Reject::Version);
2130    }
2131    if record.canonical_record.is_empty()
2132        || record.canonical_record.len() > MAX_RECORD_BYTES
2133        || record.signatures.is_empty()
2134        || record.signatures.len() > 16
2135    {
2136        return Err(Reject::Bounds);
2137    }
2138    let mut previous: Option<&[u8]> = None;
2139    let input = [format.as_bytes(), b"\0", &record.canonical_record].concat();
2140    for s in &record.signatures {
2141        if previous.is_some_and(|p| p >= s.public_key.as_slice()) {
2142            return Err(Reject::Canonical);
2143        }
2144        verify(&s.public_key, &input, &s.signature)?;
2145        previous = Some(&s.public_key);
2146    }
2147    Ok(())
2148}
2149/// Recompute transport commitments from exact native evidence. The caller's
2150/// native verifier additionally authenticates manifest membership, receipt
2151/// subjects/bases, accepting authority and canonical native encoding.
2152pub fn verify_boundary_acceptance(e: &ImportBoundaryAcceptanceV1) -> Result<(), Reject> {
2153    let binding = e.binding.as_ref().ok_or(Reject::BoundaryAcceptance)?;
2154    validate_boundary_binding(binding)?;
2155    let acceptance = e
2156        .signed_acceptance
2157        .as_ref()
2158        .ok_or(Reject::BoundaryAcceptance)?;
2159    verify_native(acceptance, "heddle-original-boundary-acceptance-v1")?;
2160    if acceptance.signatures.len() != 1 {
2161        return Err(Reject::Signature);
2162    }
2163    if e.originals_manifest.is_empty()
2164        || e.publication_intent.is_empty()
2165        || e.originals_manifest.len() > MAX_RECORD_BYTES
2166        || e.publication_intent.len() > MAX_RECORD_BYTES
2167        || e.original_receipts.is_empty()
2168        || e.original_receipts.len() > 128
2169    {
2170        return Err(Reject::Bounds);
2171    }
2172    if binding.acceptance_id != native_id(acceptance)
2173        || binding.signed_acceptance_digest != signed_native_digest(acceptance)?
2174        || binding.originals_manifest_digest
2175            != boundary_octets_digest(
2176                "heddle-boundary-originals-manifest-v1",
2177                &e.originals_manifest,
2178            )
2179        || binding.publication_intent_digest
2180            != boundary_octets_digest(
2181                "heddle-boundary-publication-intent-v1",
2182                &e.publication_intent,
2183            )
2184    {
2185        return Err(Reject::BoundaryAcceptance);
2186    }
2187    let native: NativeBoundarySelection =
2188        rmp_serde::from_slice(&acceptance.canonical_record).map_err(|_| Reject::Canonical)?;
2189    if native.originals_manifest.as_slice()
2190        != native_octets_id(
2191            "heddle-original-publication-manifest-v1",
2192            &e.originals_manifest,
2193        )
2194        || native.publication_intent.as_slice()
2195            != native_octets_id(
2196                "heddle-original-publication-intent-v1",
2197                &e.publication_intent,
2198            )
2199    {
2200        return Err(Reject::BoundaryAcceptance);
2201    }
2202    let mut digests = Vec::new();
2203    for receipt in &e.original_receipts {
2204        if ![
2205            "heddle-thread-genesis-admission-v2",
2206            "heddle-thread-authority-admission-v3",
2207        ]
2208        .contains(&receipt.format.as_str())
2209        {
2210            return Err(Reject::Version);
2211        }
2212        verify_native(receipt, &receipt.format)?;
2213        if receipt.signatures.len() != 1 {
2214            return Err(Reject::Signature);
2215        }
2216        let native: NativeBoundaryReceipt =
2217            rmp_serde::from_slice(&receipt.canonical_record).map_err(|_| Reject::Canonical)?;
2218        if native.basis
2219            != (NativeBoundaryBasis::BoundaryAcceptance {
2220                acceptance: binding
2221                    .acceptance_id
2222                    .as_slice()
2223                    .try_into()
2224                    .map_err(|_| Reject::Canonical)?,
2225            })
2226        {
2227            return Err(Reject::BoundaryAcceptance);
2228        }
2229        digests.push(signed_native_digest(receipt)?);
2230    }
2231    if digests != binding.original_receipt_digests {
2232        return Err(Reject::BoundaryAcceptance);
2233    }
2234    Ok(())
2235}
2236// These readers extract only the native commitment selectors. Full native
2237// canonicality, model validity, membership and authority remain the native gate.
2238#[derive(serde::Deserialize)]
2239struct NativeBoundarySelection {
2240    originals_manifest: [u8; 32],
2241    publication_intent: [u8; 32],
2242}
2243#[derive(serde::Deserialize, PartialEq)]
2244enum NativeBoundaryBasis {
2245    OriginalAuthority,
2246    BoundaryAcceptance { acceptance: [u8; 32] },
2247}
2248#[derive(serde::Deserialize)]
2249struct NativeBoundaryReceipt {
2250    basis: NativeBoundaryBasis,
2251    thread: [u8; 32],
2252    subject: Option<NativeBoundarySubject>,
2253}
2254#[derive(serde::Deserialize)]
2255enum NativeBoundarySubject {
2256    Operation([u8; 32]),
2257    OwnershipClaim([u8; 32]),
2258    OwnershipResolution([u8; 32]),
2259}
2260pub(crate) fn native_octets_id(format: &str, bytes: &[u8]) -> Vec<u8> {
2261    let mut h = blake3::Hasher::new();
2262    h.update(format.as_bytes());
2263    h.update(&(bytes.len() as u64).to_le_bytes());
2264    h.update(b"\0");
2265    h.update(bytes);
2266    h.finalize().as_bytes().to_vec()
2267}
2268pub(crate) fn boundary_original(
2269    e: &ImportBoundaryAcceptanceV1,
2270    original: &SignedRecord,
2271) -> Result<(), Reject> {
2272    let id = native_id(original);
2273    for receipt in &e.original_receipts {
2274        let value: NativeBoundaryReceipt =
2275            rmp_serde::from_slice(&receipt.canonical_record).map_err(|_| Reject::Canonical)?;
2276        let (format, subject) = match value.subject {
2277            None if receipt.format == "heddle-thread-genesis-admission-v2" => {
2278                ("heddle-thread-genesis-v1", value.thread)
2279            }
2280            Some(NativeBoundarySubject::Operation(id)) => ("heddle-thread-operation-v1", id),
2281            Some(NativeBoundarySubject::OwnershipClaim(id)) => {
2282                ("heddle-thread-ownership-claim-v1", id)
2283            }
2284            Some(NativeBoundarySubject::OwnershipResolution(id)) => {
2285                ("heddle-thread-ownership-resolution-v1", id)
2286            }
2287            _ => return Err(Reject::BoundaryAcceptance),
2288        };
2289        if original.format == format && id == subject {
2290            return Ok(());
2291        }
2292    }
2293    Err(Reject::BoundaryAcceptance)
2294}
2295pub fn boundary_octets_digest(domain: &str, bytes: &[u8]) -> Vec<u8> {
2296    hash(&[
2297        domain.as_bytes(),
2298        &(bytes.len() as u32).to_be_bytes(),
2299        bytes,
2300    ])
2301}
2302pub fn validate_boundary_binding(
2303    b: &crate::heddle::api::common::HostedWitnessBoundaryAcceptanceV1,
2304) -> Result<(), Reject> {
2305    if b.format_version != 1 {
2306        return Err(Reject::Version);
2307    }
2308    for digest in [
2309        &b.acceptance_id,
2310        &b.signed_acceptance_digest,
2311        &b.originals_manifest_digest,
2312        &b.publication_intent_digest,
2313    ] {
2314        width(digest, 32)?;
2315    }
2316    if b.original_receipt_digests.is_empty() || b.original_receipt_digests.len() > 128 {
2317        return Err(Reject::Bounds);
2318    }
2319    for digest in &b.original_receipt_digests {
2320        width(digest, 32)?;
2321    }
2322    if b.original_receipt_digests.windows(2).any(|w| w[0] >= w[1]) {
2323        return Err(Reject::Canonical);
2324    }
2325    Ok(())
2326}
2327pub fn validate_statement_boundary(
2328    s: &crate::heddle::api::common::HostedWitnessStatementV1,
2329) -> Result<(), Reject> {
2330    match (s.basis, s.boundary_acceptance.as_ref()) {
2331        (1, None) => Ok(()),
2332        (2, Some(b)) if s.purpose == 1 || s.purpose == 2 => validate_boundary_binding(b),
2333        _ => Err(Reject::BoundaryAcceptance),
2334    }
2335}
2336pub(crate) fn match_boundary(
2337    s: &crate::heddle::api::common::HostedWitnessStatementV1,
2338    evidence: &[ImportBoundaryAcceptanceV1],
2339) -> Result<(), Reject> {
2340    validate_statement_boundary(s)?;
2341    let mut previous = None;
2342    for e in evidence {
2343        verify_boundary_acceptance(e)?;
2344        let b = e.binding.as_ref().ok_or(Reject::BoundaryAcceptance)?;
2345        if previous.is_some_and(|p: &[u8]| p >= b.acceptance_id.as_slice()) {
2346            return Err(Reject::Canonical);
2347        }
2348        previous = Some(b.acceptance_id.as_slice());
2349    }
2350    if let Some(binding) = &s.boundary_acceptance
2351        && !evidence.iter().any(|e| e.binding.as_ref() == Some(binding))
2352    {
2353        return Err(Reject::BoundaryAcceptance);
2354    }
2355    Ok(())
2356}
2357fn native_dependencies(
2358    records: &[SignedRecord],
2359    evidence: &[ImportBoundaryAcceptanceV1],
2360    reviews_only: bool,
2361) -> Result<(), Reject> {
2362    if records.len() > 128 {
2363        return Err(Reject::Bounds);
2364    }
2365    let mut previous = None;
2366    for record in records {
2367        if reviews_only {
2368            require_review_operation(record)?;
2369        }
2370        match record.format.as_str() {
2371            "heddle-thread-genesis-v1"
2372            | "heddle-thread-operation-v1"
2373            | "heddle-thread-ownership-claim-v1"
2374            | "heddle-thread-ownership-resolution-v1" => (),
2375            "heddle-original-boundary-acceptance-v1"
2376            | "heddle-thread-genesis-admission-v2"
2377            | "heddle-thread-authority-admission-v3" => {
2378                if !evidence.iter().any(|e| {
2379                    e.signed_acceptance.as_ref() == Some(record)
2380                        || e.original_receipts.contains(record)
2381                }) {
2382                    return Err(Reject::BoundaryAcceptance);
2383                }
2384            }
2385            _ => return Err(Reject::Version),
2386        }
2387        verify_native(record, &record.format)?;
2388        let digest = signed_native_digest(record)?;
2389        if previous.as_ref().is_some_and(|p| p >= &digest) {
2390            return Err(Reject::Canonical);
2391        }
2392        previous = Some(digest);
2393    }
2394    Ok(())
2395}
2396#[derive(serde::Deserialize)]
2397struct ReviewOperation {
2398    body: ReviewBody,
2399}
2400#[derive(serde::Deserialize)]
2401struct ReviewBody {
2402    kind: String,
2403    canonical: Vec<u8>,
2404}
2405#[derive(serde::Deserialize)]
2406struct ReviewControl {
2407    control: ReviewKind,
2408}
2409#[derive(serde::Deserialize)]
2410struct ReviewKind {
2411    kind: String,
2412}
2413fn require_review_operation(record: &SignedRecord) -> Result<(), Reject> {
2414    if record.format != "heddle-thread-operation-v1" {
2415        return Err(Reject::Semantic);
2416    }
2417    let operation: ReviewOperation =
2418        rmp_serde::from_slice(&record.canonical_record).map_err(|_| Reject::Semantic)?;
2419    if operation.body.kind != "metadata" {
2420        return Err(Reject::Semantic);
2421    }
2422    let control: ReviewControl =
2423        rmp_serde::from_slice(&operation.body.canonical).map_err(|_| Reject::Semantic)?;
2424    if control.control.kind != "review" {
2425        return Err(Reject::Semantic);
2426    }
2427    Ok(())
2428}
2429pub(crate) fn original_signatures(
2430    records: &[&SignedRecord],
2431    extra: &[RecordSignature],
2432) -> Result<Vec<u8>, Reject> {
2433    let signatures = records
2434        .iter()
2435        .flat_map(|r| r.signatures.iter())
2436        .chain(extra.iter())
2437        .collect::<Vec<_>>();
2438    let mut out = (signatures.len() as u32).to_be_bytes().to_vec();
2439    for s in signatures {
2440        s.write(&mut out)?;
2441    }
2442    Ok(hash(&[b"heddle-hosted-original-signatures-v1", &out]))
2443}
2444
2445#[cfg(test)]
2446#[test]
2447fn sha2_golden_original_signatures() {
2448    let first = SignedRecord {
2449        signatures: vec![RecordSignature {
2450            public_key: vec![1; 32],
2451            signature: vec![2; 64],
2452        }],
2453        ..Default::default()
2454    };
2455    let second = SignedRecord {
2456        signatures: vec![RecordSignature {
2457            public_key: vec![3; 32],
2458            signature: vec![4; 64],
2459        }],
2460        ..Default::default()
2461    };
2462    let extra = RecordSignature {
2463        public_key: vec![5; 32],
2464        signature: vec![6; 64],
2465    };
2466    assert_eq!(
2467        hex::encode(original_signatures(&[&first, &second], &[extra]).expect("fixed signatures")),
2468        "04bcf713a6a5fdec2bbfae89ee71894db72db54ca75597831fea7192ef9bf3e1"
2469    );
2470}
2471use crate::hybrid_codec::Canonical;
2472/// Portable request-role refusal. Receiver facts include known job and forbidden request keys.
2473pub fn verify_landing_key_roles(
2474    payload: &HostedLandingWitnessV1,
2475    known_job_keys: &[Vec<u8>],
2476    forbidden_keys: &[Vec<u8>],
2477) -> Result<(), Reject> {
2478    let key = &payload
2479        .request
2480        .as_ref()
2481        .and_then(|r| r.signature.as_ref())
2482        .ok_or(Reject::Signature)?
2483        .public_key;
2484    if known_job_keys.contains(key) || forbidden_keys.contains(key) {
2485        return Err(Reject::KeyRole);
2486    }
2487    Ok(())
2488}
2489
2490/// Exact matching and original signatures do not replace native causal/authority/landing checks.
2491#[derive(Clone, Copy)]
2492pub enum WitnessPayload<'a> {
2493    Genesis(&'a ImportGenesisWitnessV1),
2494    Authority(&'a ImportAuthorityWitnessV1),
2495    Landing(&'a HostedLandingWitnessV1),
2496}
2497pub fn verify_witness_payload(
2498    statement: &crate::heddle::api::common::HostedWitnessStatementV1,
2499    payload: WitnessPayload<'_>,
2500) -> Result<(), Reject> {
2501    let (purpose, bytes, authority, signatures, publisher) = match payload {
2502        WitnessPayload::Genesis(p) => {
2503            if p.format_version != 1 {
2504                return Err(Reject::Version);
2505            }
2506            let original = p.original_genesis.as_ref().ok_or(Reject::Canonical)?;
2507            let binding = p.binding.as_ref().ok_or(Reject::Canonical)?;
2508            let b = binding.body.as_ref().ok_or(Reject::Canonical)?;
2509            match_boundary(
2510                statement,
2511                &p.boundary_acceptance.iter().cloned().collect::<Vec<_>>(),
2512            )?;
2513            if let Some(e) = &p.boundary_acceptance {
2514                boundary_original(e, original)?;
2515            }
2516            if (statement.basis == 2) != p.boundary_acceptance.is_some() {
2517                return Err(Reject::BoundaryAcceptance);
2518            }
2519            verify_native(original, "heddle-thread-genesis-v1")?;
2520            if native_id(original) != b.genesis_digest {
2521                return Err(Reject::Scope);
2522            }
2523            if let Some(id) = &b.identity {
2524                if statement.spool_uuid != id.spool_uuid
2525                    || statement.spool_genesis_digest != id.spool_genesis_digest
2526                    || statement.owner_id != id.owner_id
2527                    || statement.owner_state_hash != id.owner_state_hash
2528                    || statement.ownership_transfer_sequence != id.ownership_transfer_sequence
2529                {
2530                    return Err(Reject::Scope);
2531                }
2532            } else {
2533                return Err(Reject::Canonical);
2534            }
2535            let creator = original
2536                .signatures
2537                .iter()
2538                .find(|s| s.public_key == b.creator_public_key)
2539                .ok_or(Reject::Signature)?;
2540            if b.original_creator_signature != creator.signature
2541                || b.creator_authority_envelope_digest != hash(&[&p.creator_authority_envelope])
2542            {
2543                return Err(Reject::Scope);
2544            }
2545            verify_authorization_signature(
2546                &b.creator_public_key,
2547                GENESIS_DOMAIN,
2548                b,
2549                binding
2550                    .creator_signature
2551                    .as_ref()
2552                    .ok_or(Reject::Signature)?,
2553            )?;
2554            (
2555                1,
2556                canonical(p)?,
2557                signed_genesis_digest(binding)?,
2558                original_signatures(&[original], &[])?,
2559                key_id(&b.creator_public_key),
2560            )
2561        }
2562        WitnessPayload::Authority(p) => {
2563            if p.format_version != 1 {
2564                return Err(Reject::Version);
2565            }
2566            let original = p.original.as_ref().ok_or(Reject::Canonical)?;
2567            let format = match p.kind {
2568                1 => "heddle-thread-operation-v1",
2569                2 => "heddle-thread-ownership-claim-v1",
2570                3 => "heddle-thread-ownership-resolution-v1",
2571                _ => return Err(Reject::Version),
2572            };
2573            verify_native(original, format)?;
2574            if (p.kind == 2 || p.kind == 3) && original.signatures.len() != 2 {
2575                return Err(Reject::Signature);
2576            }
2577            if !original
2578                .signatures
2579                .iter()
2580                .any(|s| key_id(&s.public_key) == statement.publisher_key_id)
2581            {
2582                return Err(Reject::Signature);
2583            }
2584            if p.authority_envelope.is_empty() || p.authority_envelope.len() > MAX_RECORD_BYTES {
2585                return Err(Reject::Bounds);
2586            }
2587            match_boundary(statement, &p.boundary_acceptances)?;
2588            if let Some(b) = &statement.boundary_acceptance {
2589                let e = p
2590                    .boundary_acceptances
2591                    .iter()
2592                    .find(|e| e.binding.as_ref() == Some(b))
2593                    .ok_or(Reject::BoundaryAcceptance)?;
2594                boundary_original(e, original)?;
2595            }
2596            native_dependencies(&p.dependencies, &p.boundary_acceptances, false)?;
2597            let records = std::iter::once(original)
2598                .chain(p.dependencies.iter())
2599                .collect::<Vec<_>>();
2600            (
2601                2,
2602                canonical(p)?,
2603                hash(&[
2604                    b"heddle-hosted-authority-envelope-v1",
2605                    &(p.authority_envelope.len() as u32).to_be_bytes(),
2606                    &p.authority_envelope,
2607                ]),
2608                original_signatures(&records, &[])?,
2609                statement.publisher_key_id.clone(),
2610            )
2611        }
2612        WitnessPayload::Landing(p) => {
2613            if p.format_version != 1 {
2614                return Err(Reject::Version);
2615            }
2616            let execution = p.execution.as_ref().ok_or(Reject::Canonical)?;
2617            let source = p.source_operation.as_ref().ok_or(Reject::Canonical)?;
2618            let request = p.request.as_ref().ok_or(Reject::Canonical)?;
2619            if request.format_version != 1
2620                || request.method_path != "/heddle.api.v1alpha2.ThreadService/LandThread"
2621            {
2622                return Err(Reject::Version);
2623            }
2624            verify_native(execution, "heddle-thread-operation-v1")?;
2625            verify_native(source, "heddle-thread-operation-v1")?;
2626            match_boundary(statement, &[])?;
2627            native_dependencies(&p.review_evidence, &[], true)?;
2628            let signature = request.signature.as_ref().ok_or(Reject::Signature)?;
2629            if request.signing_identity
2630                != format!(
2631                    "principal:device-key:{}",
2632                    hex::encode(&signature.public_key)
2633                )
2634            {
2635                return Err(Reject::Signature);
2636            }
2637            width(&request.nonce, 16)?;
2638            if request.timestamp_millis <= 0
2639                || request.request_body.is_empty()
2640                || request.request_body.len() > MAX_RECORD_BYTES
2641                || p.authority_envelope.is_empty()
2642                || p.authority_envelope.len() > MAX_RECORD_BYTES
2643            {
2644                return Err(Reject::Bounds);
2645            }
2646            let input = crate::signing::unary_bytes(
2647                &request.signing_identity,
2648                &request.method_path,
2649                request.timestamp_millis,
2650                &request.nonce,
2651                &request.request_body,
2652            );
2653            verify(&signature.public_key, &input, &signature.signature)?;
2654            let records = [execution, source]
2655                .into_iter()
2656                .chain(p.review_evidence.iter())
2657                .collect::<Vec<_>>();
2658            (
2659                4,
2660                canonical(p)?,
2661                hash(&[
2662                    b"heddle-hosted-authority-envelope-v1",
2663                    &(p.authority_envelope.len() as u32).to_be_bytes(),
2664                    &p.authority_envelope,
2665                ]),
2666                original_signatures(&records, std::slice::from_ref(signature))?,
2667                key_id(&signature.public_key),
2668            )
2669        }
2670    };
2671    if bytes.len() > MAX_RECORD_BYTES {
2672        return Err(Reject::Bounds);
2673    }
2674    if statement.purpose != purpose
2675        || statement.canonical_payload != bytes
2676        || statement.authority_digest != authority
2677        || statement.original_signatures_digest != signatures
2678        || statement.publisher_key_id != publisher
2679    {
2680        return Err(Reject::Scope);
2681    }
2682    Ok(())
2683}
2684
2685pub fn resolve_bundle_permission<'a>(
2686    bundle: &'a ImportPublicProofBundleV1,
2687    digest: &[u8],
2688) -> Result<Option<&'a SignedImportMemberPermissionV1>, Reject> {
2689    width(digest, 32)?;
2690    if digest == [0; 32] {
2691        return Ok(None);
2692    }
2693    bundle
2694        .member_permission
2695        .iter()
2696        .find(|p| signed_permission_digest(p).is_ok_and(|d| d == digest))
2697        .map(Some)
2698        .ok_or(Reject::ImportPermission)
2699}
2700pub fn resolve_bundle_manifest<'a>(
2701    bundle: &'a ImportPublicProofBundleV1,
2702    digest: &[u8],
2703) -> Result<&'a ImportResultManifestV1, Reject> {
2704    width(digest, 32)?;
2705    bundle
2706        .manifests
2707        .iter()
2708        .find(|m| manifest_digest(m).is_ok_and(|d| d == digest))
2709        .ok_or(Reject::StaleManifest)
2710}
2711pub fn publication_payload(
2712    operation: &SignedDelegatedImportOperationV1,
2713    manifest: &ImportResultManifestV1,
2714) -> Result<ImportPublicationWitnessV1, Reject> {
2715    let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
2716    Ok(ImportPublicationWitnessV1 {
2717        format_version: 1,
2718        signed_operation_digest: signed_operation_digest(operation)?,
2719        delegation_digest: o.delegation_digest.clone(),
2720        logical_job_id: o.logical_job_id.clone(),
2721        retry_lineage_id: o.retry_lineage_id.clone(),
2722        physical_operation_id: o.physical_operation_id.clone(),
2723        ref_name: o.ref_name.clone(),
2724        slot_id: o.slot_id,
2725        hash_algorithm: o.hash_algorithm,
2726        observed_commit_oid: o.observed_commit_oid.clone(),
2727        expected_frontier_digest: o.expected_frontier_digest.clone(),
2728        resulting_frontier_digest: o.resulting_frontier_digest.clone(),
2729        terminal_manifest_digest: manifest_digest(manifest)?,
2730    })
2731}
2732/// Completeness and digest addressing only. Trust/signature verification still
2733/// uses independently selected owner contexts at each witnessed historical time.
2734fn validate_bundle_history(
2735    bundle: &ImportPublicProofBundleV1,
2736    require_admissions: bool,
2737) -> Result<(), Reject> {
2738    fn sorted<T>(
2739        values: &[T],
2740        digest: impl Fn(&T) -> Result<Vec<u8>, Reject>,
2741    ) -> Result<(), Reject> {
2742        let mut previous = None;
2743        for value in values {
2744            let d = digest(value)?;
2745            if previous.as_ref().is_some_and(|p| p >= &d) {
2746                return Err(Reject::Canonical);
2747            }
2748            previous = Some(d);
2749        }
2750        Ok(())
2751    }
2752    let mut foreign = crate::foreign_dependencies::References::new(
2753        &bundle.foreign_dependencies,
2754        ForeignDependencyOrigin::Import,
2755    )?;
2756    for p in &bundle.authority_witnesses {
2757        let subject_thread =
2758            crate::foreign_dependencies::thread(p.original.as_ref().ok_or(Reject::Canonical)?)?;
2759        if !bundle.genesis_witnesses.iter().any(|g| {
2760            g.original_genesis
2761                .as_ref()
2762                .is_some_and(|g| native_id(g) == subject_thread)
2763        }) {
2764            return Err(Reject::Scope);
2765        }
2766    }
2767    let job_keys = bundle
2768        .delegations
2769        .iter()
2770        .filter_map(|d| d.body.as_ref().map(|d| d.job_public_key.clone()))
2771        .collect::<Vec<_>>();
2772    for p in &bundle.landing_witnesses {
2773        verify_landing_key_roles(p, &job_keys, &[])?;
2774        let execution = p.execution.as_ref().ok_or(Reject::Canonical)?;
2775        let thread = crate::foreign_dependencies::thread(execution)?;
2776        if !bundle.genesis_witnesses.iter().any(|p| {
2777            p.original_genesis
2778                .as_ref()
2779                .is_some_and(|g| native_id(g) == thread)
2780        }) {
2781            return Err(Reject::Scope);
2782        }
2783    }
2784    // In-carrier genesis witnesses establish origin membership, never permission.
2785    for original in bundle
2786        .authority_witnesses
2787        .iter()
2788        .flat_map(|p| p.original.iter().chain(p.dependencies.iter()))
2789        .chain(bundle.landing_witnesses.iter().flat_map(|p| {
2790            p.execution
2791                .iter()
2792                .chain(p.source_operation.iter())
2793                .chain(p.review_evidence.iter())
2794        }))
2795    {
2796        if [
2797            "heddle-original-boundary-acceptance-v1",
2798            "heddle-thread-genesis-admission-v2",
2799            "heddle-thread-authority-admission-v3",
2800        ]
2801        .contains(&original.format.as_str())
2802        {
2803            continue;
2804        }
2805        let thread = crate::foreign_dependencies::thread(original)?;
2806        if !bundle.genesis_witnesses.iter().any(|p| {
2807            p.original_genesis
2808                .as_ref()
2809                .is_some_and(|g| native_id(g) == thread)
2810        }) {
2811            foreign.require(original)?;
2812        } else if original.format == "heddle-thread-genesis-v1"
2813            && !bundle
2814                .genesis_witnesses
2815                .iter()
2816                .any(|p| p.original_genesis.as_ref() == Some(original))
2817        {
2818            return Err(Reject::Scope);
2819        }
2820    }
2821    foreign.finish()?;
2822    for operation in &bundle.operations {
2823        if operation.body.is_none() {
2824            return Err(Reject::Canonical);
2825        }
2826    }
2827    for statement in &bundle.statements {
2828        let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
2829        validate_statement_boundary(s)?;
2830        require_policy_history(
2831            &bundle.policies,
2832            &s.spool_uuid,
2833            s.policy_sequence,
2834            &s.policy_state_hash,
2835        )?;
2836        let envelope = match s.purpose {
2837            1 if s.basis == 2 => Some(
2838                bundle
2839                    .genesis_witnesses
2840                    .iter()
2841                    .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
2842                    .ok_or(Reject::Scope)?
2843                    .creator_authority_envelope
2844                    .as_slice(),
2845            ),
2846            2 => Some(
2847                bundle
2848                    .authority_witnesses
2849                    .iter()
2850                    .find(|p| canonical(*p).is_ok_and(|bytes| bytes == s.canonical_payload))
2851                    .ok_or(Reject::Scope)?
2852                    .authority_envelope
2853                    .as_slice(),
2854            ),
2855            4 => Some(
2856                bundle
2857                    .landing_witnesses
2858                    .iter()
2859                    .find(|p| canonical(*p).is_ok_and(|bytes| bytes == s.canonical_payload))
2860                    .ok_or(Reject::Scope)?
2861                    .authority_envelope
2862                    .as_slice(),
2863            ),
2864            _ => None,
2865        };
2866        if let Some(envelope) = envelope {
2867            crate::writer_authority::check_witness_writer(
2868                s,
2869                envelope,
2870                &bundle.owner_histories,
2871                &bundle.policies,
2872                crate::writer_authority::spool_account_for_statement(
2873                    s,
2874                    bundle
2875                        .delegations
2876                        .iter()
2877                        .filter_map(|d| d.body.as_ref()?.identity.as_ref())
2878                        .chain(
2879                            bundle
2880                                .genesis_authorities
2881                                .iter()
2882                                .filter_map(|g| g.body.as_ref()?.identity.as_ref()),
2883                        ),
2884                    &bundle.ownership_transfers,
2885                )?,
2886                bundle
2887                    .authority_witnesses
2888                    .iter()
2889                    .find(|p| {
2890                        (p.kind == 2 || p.kind == 3)
2891                            && canonical(*p).is_ok_and(|v| v == s.canonical_payload)
2892                    })
2893                    .and_then(|p| p.original.as_ref())
2894                    .map(|r| r.signatures.as_slice())
2895                    .unwrap_or(&[]),
2896                if s.purpose == 1 {
2897                    bundle
2898                        .genesis_witnesses
2899                        .iter()
2900                        .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
2901                        .and_then(|p| p.boundary_acceptance.as_ref())
2902                } else {
2903                    bundle
2904                        .authority_witnesses
2905                        .iter()
2906                        .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
2907                        .and_then(|p| {
2908                            p.boundary_acceptances
2909                                .iter()
2910                                .find(|e| e.binding == s.boundary_acceptance)
2911                        })
2912                },
2913            )?;
2914        }
2915    }
2916    sorted(&bundle.manifests, manifest_digest)?;
2917    if let Some(p) = &bundle.member_permission
2918        && resolve_bundle_permission(bundle, &signed_permission_digest(p)?)? != Some(p)
2919    {
2920        return Err(Reject::ImportPermission);
2921    }
2922    let terminal = bundle.terminal_manifest.as_ref().ok_or(Reject::Canonical)?;
2923    if resolve_bundle_manifest(bundle, &manifest_digest(terminal)?)? != terminal {
2924        return Err(Reject::Canonical);
2925    }
2926    if bundle.delegations.len() != 1 {
2927        return Err(Reject::Canonical);
2928    }
2929    for d in &bundle.delegations {
2930        let body = d.body.as_ref().ok_or(Reject::Canonical)?;
2931        resolve_bundle_permission(bundle, &body.parent_permission_digest)?;
2932        for branch in &body.branch_manifest {
2933            let g = bundle
2934                .genesis_authorities
2935                .iter()
2936                .find(|g| {
2937                    signed_genesis_digest(g).is_ok_and(|h| h == branch.genesis_authority_digest)
2938                })
2939                .ok_or(Reject::Scope)?;
2940            let b = g.body.as_ref().ok_or(Reject::Canonical)?;
2941            resolve_bundle_permission(bundle, &b.parent_permission_digest)?;
2942            if !bundle
2943                .original_geneses
2944                .iter()
2945                .any(|o| native_id(o) == b.genesis_digest)
2946                || !bundle
2947                    .creator_authority_envelopes
2948                    .iter()
2949                    .any(|e| hash(&[e]) == b.creator_authority_envelope_digest)
2950            {
2951                return Err(Reject::Scope);
2952            }
2953            // Every published branch needs its original admission, not merely its
2954            // binding. Proof-only retirement lookup cannot recover a payload.
2955            if require_admissions
2956                && bundle.operations.iter().any(|o| {
2957                    o.body
2958                        .as_ref()
2959                        .is_some_and(|o| o.genesis_digest == b.genesis_digest)
2960                })
2961                && !bundle.genesis_witnesses.iter().any(|payload| {
2962                    payload.binding.as_ref() == Some(g)
2963                        && payload.original_genesis.as_ref().is_some_and(|o| {
2964                            native_id(o) == b.genesis_digest && bundle.original_geneses.contains(o)
2965                        })
2966                        && hash(&[&payload.creator_authority_envelope])
2967                            == b.creator_authority_envelope_digest
2968                        && canonical(payload).is_ok_and(|bytes| {
2969                            bundle.statements.iter().any(|s| {
2970                                s.body
2971                                    .as_ref()
2972                                    .is_some_and(|s| s.purpose == 1 && s.canonical_payload == bytes)
2973                            })
2974                        })
2975                })
2976            {
2977                return Err(Reject::Scope);
2978            }
2979        }
2980    }
2981    for manifest in &bundle.manifests {
2982        validate_manifest(manifest)?;
2983        if manifest.logical_job_id != terminal.logical_job_id
2984            || manifest.retry_lineage_id != terminal.retry_lineage_id
2985        {
2986            return Err(Reject::Scope);
2987        }
2988        for slot in &manifest.slots {
2989            let operation = bundle
2990                .operations
2991                .iter()
2992                .find(|o| {
2993                    signed_operation_digest(o).is_ok_and(|d| d == slot.signed_operation_digest)
2994                })
2995                .ok_or(Reject::Scope)?;
2996            if !check_slot_replay(manifest, operation)? || !check_slot_replay(terminal, operation)?
2997            {
2998                return Err(Reject::Scope);
2999            }
3000        }
3001    }
3002    for operation in &bundle.operations {
3003        let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
3004        if !bundle
3005            .delegations
3006            .iter()
3007            .any(|d| signed_delegation_digest(d).is_ok_and(|h| h == o.delegation_digest))
3008            || !check_slot_replay(terminal, operation)?
3009        {
3010            return Err(Reject::Scope);
3011        }
3012        if !bundle.manifests.iter().any(|m| {
3013            check_slot_replay(m, operation) == Ok(true)
3014                && publication_payload(operation, m)
3015                    .and_then(|p| canonical(&p))
3016                    .is_ok_and(|p| {
3017                        bundle.statements.iter().any(|s| {
3018                            s.body
3019                                .as_ref()
3020                                .is_some_and(|s| s.purpose == 3 && s.canonical_payload == p)
3021                        })
3022                    })
3023        }) {
3024            return Err(Reject::Scope);
3025        }
3026    }
3027    for statement in &bundle.statements {
3028        let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
3029        let found = match s.purpose {
3030            1 => bundle
3031                .genesis_witnesses
3032                .iter()
3033                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
3034            2 => bundle
3035                .authority_witnesses
3036                .iter()
3037                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
3038            3 => bundle.operations.iter().any(|o| {
3039                bundle.manifests.iter().any(|m| {
3040                    publication_payload(o, m)
3041                        .and_then(|p| canonical(&p))
3042                        .is_ok_and(|p| p == s.canonical_payload)
3043                })
3044            }),
3045            4 => bundle
3046                .landing_witnesses
3047                .iter()
3048                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
3049            _ => return Err(Reject::Version),
3050        };
3051        if !found {
3052            return Err(Reject::Scope);
3053        }
3054    }
3055    Ok(())
3056}
3057/// Authenticate policy contents against their committed hashes and enforce the
3058/// governance contract's grow-only revocations. All carried bodies are checked
3059/// before the zero-head shortcut, so no writer lookup can read unchecked data.
3060/// Native verification still
3061/// authenticates owner signatures/context and the receipt observation time.
3062pub(crate) fn require_policy_history(
3063    policies: &[SignedSpoolPolicyRecord],
3064    spool: &[u8],
3065    mut sequence: u64,
3066    state_hash: &[u8],
3067) -> Result<(), Reject> {
3068    let mut previous: Option<(&[u8], u64)> = None;
3069    for signed in policies {
3070        let policy = signed.body.as_ref().ok_or(Reject::Canonical)?;
3071        let digest = policy_state_digest(policy)?;
3072        if digest != policy.policy_state_hash {
3073            return Err(Reject::Canonical);
3074        }
3075        let current = (policy.spool_uuid.as_slice(), policy.sequence);
3076        if previous.is_some_and(|prev| prev >= current) {
3077            return Err(Reject::Canonical);
3078        }
3079        previous = Some(current);
3080    }
3081    let mut state_hash = state_hash.to_vec();
3082    let mut successor_revoked: Option<&[Vec<u8>]> = None;
3083    for _ in 0..=policies.len() {
3084        width(&state_hash, 32)?;
3085        if sequence == 0 {
3086            return if state_hash == [0; 32] {
3087                Ok(())
3088            } else {
3089                Err(Reject::Scope)
3090            };
3091        }
3092        let mut matches = policies.iter().filter_map(|p| p.body.as_ref()).filter(|p| {
3093            p.spool_uuid == spool && p.sequence == sequence && p.policy_state_hash == state_hash
3094        });
3095        let policy = matches.next().ok_or(Reject::Scope)?;
3096        // policy_state_digest above already requires an authenticated body.
3097        let revoked = policy
3098            .policy
3099            .as_ref()
3100            .map(|p| p.revoked_key_ids.as_slice())
3101            .unwrap_or(&[]);
3102        if successor_revoked.is_some_and(|next| revoked.iter().any(|id| !next.contains(id))) {
3103            return Err(Reject::Scope);
3104        }
3105        successor_revoked = Some(revoked);
3106        let head = policy.expected_head.as_ref().ok_or(Reject::Canonical)?;
3107        if head.sequence.checked_add(1) != Some(sequence) {
3108            return Err(Reject::Scope);
3109        }
3110        sequence = head.sequence;
3111        state_hash = head.state_hash.clone();
3112    }
3113    Err(Reject::Scope)
3114}
3115/// Canonical SignedPolicyBody fields 1–10; protobuf is never the signed encoding.
3116pub fn policy_state_digest(p: &SignedPolicyBody) -> Result<Vec<u8>, Reject> {
3117    use crate::hybrid_codec::counted;
3118    if p.format_version != 1 || !p.merge_parent_state_hashes.is_empty() {
3119        return Err(Reject::Version);
3120    }
3121    width(&p.spool_uuid, 16)?;
3122    width(&p.owner_id, 32)?;
3123    width(&p.owner_state_hash, 32)?;
3124    let head = p.expected_head.as_ref().ok_or(Reject::Canonical)?;
3125    width(&head.state_hash, 32)?;
3126    let policy = p.policy.as_ref().ok_or(Reject::Canonical)?;
3127    if p.merge_policies.len() != 2
3128        || p.merge_policies[0].setting_key != "max_audience"
3129        || p.merge_policies[0].semantics != 1
3130        || p.merge_policies[1].setting_key != "revoked_key_ids"
3131        || p.merge_policies[1].semantics != 2
3132    {
3133        return Err(Reject::Canonical);
3134    }
3135    let mut out = p.format_version.to_be_bytes().to_vec();
3136    counted(&mut out, &p.spool_uuid)?;
3137    counted(&mut out, &head.state_hash)?;
3138    out.extend_from_slice(&head.sequence.to_be_bytes());
3139    out.extend_from_slice(&p.sequence.to_be_bytes());
3140    out.extend_from_slice(&0u32.to_be_bytes());
3141    out.extend_from_slice(&(policy.revoked_key_ids.len() as u32).to_be_bytes());
3142    for (i, id) in policy.revoked_key_ids.iter().enumerate() {
3143        width(id, 32)?;
3144        if i > 0 && policy.revoked_key_ids[i - 1] >= *id {
3145            return Err(Reject::Canonical);
3146        }
3147        counted(&mut out, id)?;
3148    }
3149    out.push(u8::from(policy.max_audience.is_some()));
3150    if let Some(audience) = policy.max_audience {
3151        if !(1..=3).contains(&audience) {
3152            return Err(Reject::Canonical);
3153        }
3154        out.extend_from_slice(&(audience as u32).to_be_bytes());
3155    }
3156    out.extend_from_slice(&2u32.to_be_bytes());
3157    for rule in &p.merge_policies {
3158        counted(&mut out, rule.setting_key.as_bytes())?;
3159        out.extend_from_slice(&(rule.semantics as u32).to_be_bytes());
3160    }
3161    counted(&mut out, &p.owner_id)?;
3162    counted(&mut out, &p.owner_state_hash)?;
3163    out.extend_from_slice(&p.ownership_transfer_sequence.to_be_bytes());
3164    Ok(hash(&[b"heddle-spool-signed-policy-v2", &out]))
3165}
3166pub(crate) fn native_id(record: &SignedRecord) -> Vec<u8> {
3167    let mut h = blake3::Hasher::new();
3168    h.update(record.format.as_bytes());
3169    h.update(&(record.canonical_record.len() as u64).to_le_bytes());
3170    h.update(b"\0");
3171    h.update(&record.canonical_record);
3172    h.finalize().as_bytes().to_vec()
3173}
3174
3175/// Caller-generated non-nil UUID, reserved as the first physical operation ID.
3176/// Occupancy is checked under the host's reservation/activation transaction.
3177pub fn initial_operation_id(lineage: &[u8], occupied: bool) -> Result<String, Reject> {
3178    width(lineage, 16)?;
3179    if lineage.iter().all(|b| *b == 0) {
3180        return Err(Reject::Canonical);
3181    }
3182    if occupied {
3183        return Err(Reject::OperationIdReused);
3184    }
3185    let h = hex::encode(lineage);
3186    Ok(format!(
3187        "{}-{}-{}-{}-{}",
3188        &h[..8],
3189        &h[8..12],
3190        &h[12..16],
3191        &h[16..20],
3192        &h[20..]
3193    ))
3194}
3195
3196/// Evaluate inside the cancellation transaction, after caller-scoped replay lookup.
3197/// The selector names only the ACTIVE delegation. Parent revocation is independent.
3198pub fn check_cancel_request(
3199    request: &CancelImportJobRequest,
3200    active: &SignedImportJobDelegationV1,
3201    durable_epoch: u64,
3202    cancelled: bool,
3203) -> Result<(), Reject> {
3204    let d = active.body.as_ref().ok_or(Reject::Canonical)?;
3205    width(&request.logical_job_id, 16)?;
3206    width(&request.cancellation_id, 32)?;
3207    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
3208    if request.client_operation_id.is_empty()
3209        || request.destination.as_ref().is_none_or(|s| {
3210            initial_operation_id(&id.spool_uuid, false).map_or(true, |uuid| s.id != uuid)
3211        })
3212        || request.logical_job_id != d.logical_job_id
3213    {
3214        return Err(Reject::Scope);
3215    }
3216    if durable_epoch == 0 || request.expected_authority_epoch != durable_epoch {
3217        return Err(Reject::StaleContext);
3218    }
3219    if request.cancellation_id != d.cancellation_id {
3220        return Err(Reject::Scope);
3221    }
3222    if cancelled {
3223        return Err(Reject::Revoked);
3224    }
3225    Ok(())
3226}
3227/// Exact replay acknowledges the stored cancellation without advancing the epoch.
3228pub fn check_cancel_replay(
3229    request: &CancelImportJobRequest,
3230    stored: &CancelImportJobRequest,
3231) -> Result<(), Reject> {
3232    if request != stored {
3233        return Err(Reject::OperationIdReused);
3234    }
3235    Ok(())
3236}
3237/// Check both independent revocation selectors, regardless of Cancel's selector.
3238pub fn check_import_revocations(
3239    delegation: &SignedImportJobDelegationV1,
3240    member: Option<&SignedImportMemberPermissionV1>,
3241    revoked: &[Vec<u8>],
3242) -> Result<(), Reject> {
3243    let d = delegation.body.as_ref().ok_or(Reject::Canonical)?;
3244    width(&d.cancellation_id, 32)?;
3245    if revoked.contains(&d.cancellation_id) {
3246        return Err(Reject::Revoked);
3247    }
3248    if let Some(parent) = member {
3249        let p = parent.body.as_ref().ok_or(Reject::ImportPermission)?;
3250        width(&p.cancellation_id, 32)?;
3251        if revoked.contains(&p.cancellation_id) {
3252            return Err(Reject::Revoked);
3253        }
3254    }
3255    Ok(())
3256}
3257
3258/// Compute the exact remaining total and slots from an authenticated manifest.
3259/// An empty result is completed work and cannot pass new-scope validation.
3260pub fn remaining_import_scope(
3261    scope: &ImportPermissionScopeV1,
3262    committed: &ImportResultManifestV1,
3263) -> Result<ImportPermissionScopeV1, Reject> {
3264    validate_scope(scope)?;
3265    validate_manifest(committed)?;
3266    let mut remaining = scope.clone();
3267    for slot in &committed.slots {
3268        if !scope
3269            .branches
3270            .iter()
3271            .any(|b| b.ref_name == slot.ref_name && b.slot_id == slot.slot_id)
3272        {
3273            return Err(Reject::Scope);
3274        }
3275        {
3276            remaining.max_result_bytes = remaining
3277                .max_result_bytes
3278                .checked_sub(slot.result_bytes)
3279                .ok_or(Reject::Scope)?;
3280            remaining.max_operations = remaining
3281                .max_operations
3282                .checked_sub(1)
3283                .ok_or(Reject::Scope)?;
3284        }
3285    }
3286    remaining.branches.retain(|b| {
3287        !committed
3288            .slots
3289            .iter()
3290            .any(|s| s.ref_name == b.ref_name && s.slot_id == b.slot_id)
3291    });
3292    Ok(remaining)
3293}
3294
3295/// Validate discovery metadata only; a well-shaped selector grants no authority.
3296pub fn validate_hybrid_import_job_selector(
3297    selector: &HybridImportJobSelector,
3298) -> Result<(), Reject> {
3299    width(&selector.logical_job_id, 16)?;
3300    if selector.logical_job_id.iter().all(|byte| *byte == 0) {
3301        return Err(Reject::Canonical);
3302    }
3303    Ok(())
3304}
3305
3306/// Project a visible operation's durable HYBRID association into the existing
3307/// writer-only state read. Missing/unknown subject or selector is unavailable.
3308/// Malformed present metadata is rejected; never substitute an attempt ID.
3309/// This validates shape, not operation visibility, writer access or signatures.
3310pub fn import_job_state_request_from_operation(
3311    operation: &OperationRecord,
3312) -> Result<Option<GetImportJobStateRequest>, Reject> {
3313    let Some(operation_subject::Subject::Import(subject)) = operation
3314        .subject
3315        .as_ref()
3316        .and_then(|subject| subject.subject.as_ref())
3317    else {
3318        return Ok(None);
3319    };
3320    let Some(selector) = subject.hybrid_job.as_ref() else {
3321        return Ok(None);
3322    };
3323    validate_hybrid_import_job_selector(selector)?;
3324    let request = GetImportJobStateRequest {
3325        destination: operation
3326            .r#ref
3327            .as_ref()
3328            .and_then(|record| record.spool.clone()),
3329        logical_job_id: selector.logical_job_id.clone(),
3330    };
3331    validate_job_state_request(&request)?;
3332    Ok(Some(request))
3333}
3334
3335/// Finite destination-writer read; transport authentication/authorization belongs
3336/// to the generated RPC contract. Validate before any storage lookup.
3337pub fn validate_job_state_request(request: &GetImportJobStateRequest) -> Result<(), Reject> {
3338    use prost::Message;
3339    if request.encoded_len() > 4096 {
3340        return Err(Reject::Bounds);
3341    }
3342    initial_operation_id(&request.logical_job_id, false)?;
3343    let destination = request.destination.as_ref().ok_or(Reject::Scope)?;
3344    let compact = destination.id.replace('-', "");
3345    let raw = hex::decode(&compact).map_err(|_| Reject::Scope)?;
3346    if initial_operation_id(&raw, false).map_or(true, |id| id != destination.id) {
3347        return Err(Reject::Scope);
3348    }
3349    Ok(())
3350}
3351
3352/// Supply the authenticated read, never an incoming untrusted state assertion.
3353pub fn validate_job_state_response(
3354    request: &GetImportJobStateRequest,
3355    response: &GetImportJobStateResponse,
3356) -> Result<(), Reject> {
3357    use prost::Message;
3358    validate_job_state_request(request)?;
3359    if response.encoded_len() > 4096 {
3360        return Err(Reject::Bounds);
3361    }
3362    if !(1..=5).contains(&response.status) || response.authority_epoch == 0 {
3363        return Err(Reject::Canonical);
3364    }
3365    width(&response.active_cancellation_id, 32)?;
3366    width(&response.active_delegation_digest, 32)?;
3367    validate_retry_availability(response, request.destination.as_ref().ok_or(Reject::Scope)?)
3368}
3369
3370/// Independently installed descriptor pin; never learned from a response.
3371#[derive(Debug, Clone, PartialEq)]
3372pub struct ImportWitnessRootPin {
3373    pub authority: String,
3374    pub root_id: String,
3375    pub public_key: Vec<u8>,
3376    pub epoch: u64,
3377}
3378/// Receiver-owned durable data, committed atomically with accepted history.
3379/// Do not deserialize this from the incoming proof or lower its clock floor.
3380#[derive(Debug, Clone, PartialEq)]
3381pub struct ImportWitnessSnapshot {
3382    pub root: ImportWitnessRootPin,
3383    pub witness_set: crate::heddle::api::common::SignedHostedWitnessSetV1,
3384    pub clock_floor_unix_millis: i64,
3385    pub job_associations: Vec<(Vec<u8>, Vec<u8>)>,
3386    pub accepted_history: Vec<ImportPublicProofBundleV1>,
3387}
3388/// Witnessed requires an authenticated observation for every accepted delegation.
3389/// Recovery can retain witnessed history but grants no admission for the active tail.
3390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3391pub enum ImportBundleEvidence {
3392    Recovery,
3393    Witnessed,
3394}
3395#[derive(Debug, Clone, PartialEq)]
3396pub struct VerifiedImportBundleWitnesses {
3397    pub evidence: ImportBundleEvidence,
3398    pub accepted_history: ImportResultManifestV1,
3399    pub snapshot: Option<ImportWitnessSnapshot>,
3400    pub snapshot_advanced: bool,
3401    pub owner_check_time_unix_seconds: Option<i64>,
3402}
3403/// Check the host's signed same-transaction assertion for a P1 and its consumed P3.
3404/// Receivers verify consistency; only the issuing host can ensure atomic visibility.
3405pub fn check_import_genesis_publication_pair(
3406    delegation: &VerifiedImportDelegation,
3407    admission: &crate::heddle::api::common::HostedWitnessStatementV1,
3408    publication: &crate::heddle::api::common::HostedWitnessStatementV1,
3409) -> Result<(), Reject> {
3410    validity(
3411        delegation.body.not_before_unix_seconds,
3412        delegation.body.expires_at_unix_seconds,
3413        admission.observed_at_unix_millis / 1000,
3414        true,
3415    )?;
3416    if admission.purpose != 1
3417        || publication.purpose != 3
3418        || admission.observed_at_unix_millis != publication.observed_at_unix_millis
3419        || admission.host_transaction_id != publication.host_transaction_id
3420        || admission.executor_id != publication.executor_id
3421        || admission.admission_order >= publication.admission_order
3422    {
3423        return Err(Reject::Transition);
3424    }
3425    Ok(())
3426}
3427
3428/// Verify public Fetch/export evidence. Owner contexts
3429/// are resolved independently at each authenticated time. The verifier derives times
3430/// from the first authenticated publication of the sole delegation.
3431/// Unwitnessed certificates are time-free recovery only.
3432/// The mandatory hook verifies the selected policy chain and owner/native context
3433/// at each authenticated statement time (heddle capability-verifier / WASM).
3434/// With no statements it receives None and verifies time-free policy closure.
3435/// No result is returned on failure. Recovery without a witnessed prefix or new
3436/// authenticated set returns the input snapshot unchanged. A witnessed prefix
3437/// extends rollback protection; recovery cannot persist job history. A new set
3438/// can additionally advance set trust and clock. None remains None when no
3439/// set is carried and no input exists. Persist under the trust lock; inspect
3440/// snapshot_advanced for actual durable change, independently of evidence.
3441pub fn verify_import_bundle_witnesses<'a>(
3442    bundle: &ImportPublicProofBundleV1,
3443    pin: &ImportWitnessRootPin,
3444    snapshot: Option<&ImportWitnessSnapshot>,
3445    now_ms: i64,
3446    mut owner_at: impl FnMut(Option<i64>) -> Result<ImportBundleOwnerExpectation<'a>, Reject>,
3447    mut verify_policy: impl FnMut(
3448        &ImportPublicProofBundleV1,
3449        Option<&crate::heddle::api::common::HostedWitnessStatementV1>,
3450    ) -> Result<(), Reject>,
3451) -> Result<VerifiedImportBundleWitnesses, Reject> {
3452    use crate::witness_trust as trust;
3453    width(&pin.public_key, 32)?;
3454    if pin.epoch == 0 {
3455        return Err(Reject::StaleContext);
3456    }
3457    validate_bundle_bounds(bundle)?;
3458    let terminal = bundle.terminal_manifest.as_ref().ok_or(Reject::Canonical)?;
3459    validate_bundle_history(bundle, !terminal.slots.is_empty())?;
3460    let mut associations = snapshot.map_or_else(Vec::new, |s| s.job_associations.clone());
3461    for d in &bundle.delegations {
3462        let b = d.body.as_ref().ok_or(Reject::Canonical)?;
3463        if associations
3464            .iter()
3465            .any(|(key, job)| *key == b.job_public_key && *job != b.logical_job_id)
3466        {
3467            return Err(Reject::KeyRole);
3468        }
3469        if !associations.iter().any(|(key, _)| *key == b.job_public_key) {
3470            associations.push((b.job_public_key.clone(), b.logical_job_id.clone()));
3471        }
3472    }
3473    let job_keys = associations
3474        .iter()
3475        .map(|(key, _)| key.clone())
3476        .collect::<Vec<_>>();
3477    fn expectation<'a>(
3478        root: &'a ImportWitnessRootPin,
3479        floor: i64,
3480        now: i64,
3481        keys: &'a [Vec<u8>],
3482    ) -> trust::SetExpectation<'a> {
3483        trust::SetExpectation {
3484            authority: &root.authority,
3485            root_id: &root.root_id,
3486            root_public_key: &root.public_key,
3487            root_epoch: root.epoch,
3488            now_unix_millis: now,
3489            clock_floor_unix_millis: floor,
3490            known_job_keys: keys,
3491        }
3492    }
3493    let previous = if let Some(s) = snapshot {
3494        if s.root.authority != pin.authority {
3495            return Err(Reject::Root);
3496        }
3497        let restored = trust::restore_history_snapshot(
3498            &s.witness_set,
3499            &expectation(&s.root, 0, now_ms, &job_keys),
3500        )?;
3501        Some(restored)
3502    } else {
3503        None
3504    };
3505    let carried = bundle.witness_set.as_ref();
3506    if carried.is_none() && !bundle.statements.is_empty() {
3507        return Err(Reject::Canonical);
3508    }
3509    let new_set =
3510        carried.is_some_and(|c| snapshot.is_none_or(|s| s.root != *pin || s.witness_set != *c));
3511    let selected = expectation(
3512        pin,
3513        snapshot.map_or(0, |s| s.clock_floor_unix_millis),
3514        now_ms,
3515        &job_keys,
3516    );
3517    let set = carried
3518        .map(|carried| {
3519            if snapshot.is_some_and(|s| s.root != *pin) {
3520                trust::verify_set_after_root_replacement(
3521                    carried,
3522                    &selected,
3523                    previous.as_ref().ok_or(Reject::StaleContext)?,
3524                )
3525            } else {
3526                trust::verify_set(carried, &selected, previous.as_ref())
3527            }
3528        })
3529        .transpose()?;
3530    // Authenticate statements before using their observation times or policies.
3531    let mut resolved = Vec::new();
3532    for signed in &bundle.statements {
3533        let s = signed.body.as_ref().ok_or(Reject::Canonical)?;
3534        let set = set.as_ref().ok_or(Reject::Canonical)?;
3535        let entry = set
3536            .body()
3537            .entries
3538            .iter()
3539            .find(|e| e.executor_id == s.executor_id)
3540            .ok_or(Reject::Root)?;
3541        let proof = if entry.state == 2 {
3542            let leaf = trust::leaf_digest(s.purpose, &canonical(s)?, &signed.signature)?;
3543            bundle.history_proofs.iter().find(|p| {
3544                p.purpose == s.purpose && trust::verify_inclusion(&leaf, p, entry).is_ok()
3545            })
3546        } else {
3547            None
3548        };
3549        let context = trust::resolve_statement(set, signed, proof, false, now_ms)?;
3550        verify_policy(bundle, Some(s))?;
3551        resolved.push((context, proof));
3552    }
3553    if bundle.statements.is_empty() {
3554        // Time-free policy/owner/native closure; this asserts no admission event.
3555        verify_policy(bundle, None)?;
3556    }
3557    // Derive owner-selection times only from authenticated receipts. Prefer the
3558    // first publication for each delegation; initial admissions stand alone too.
3559    // Select precisely the P3 each progressive prefix consumes, independent of
3560    // statement array order. Every carried P3 must be consumed exactly once.
3561    let mut prefix = ImportResultManifestV1 {
3562        slots: vec![],
3563        ..terminal.clone()
3564    };
3565    let mut selected_publications = Vec::new();
3566    for operation in &bundle.operations {
3567        let digest = signed_operation_digest(operation)?;
3568        let slot = terminal
3569            .slots
3570            .iter()
3571            .find(|slot| slot.signed_operation_digest == digest)
3572            .ok_or(Reject::Scope)?;
3573        prefix.slots.push(slot.clone());
3574        prefix
3575            .slots
3576            .sort_by(|a, b| (&a.ref_name, a.slot_id).cmp(&(&b.ref_name, b.slot_id)));
3577        let payload = canonical(&publication_payload(operation, &prefix)?)?;
3578        let matches = bundle
3579            .statements
3580            .iter()
3581            .filter(|signed| {
3582                signed
3583                    .body
3584                    .as_ref()
3585                    .is_some_and(|s| s.purpose == 3 && s.canonical_payload == payload)
3586            })
3587            .collect::<Vec<_>>();
3588        if matches.len() != 1 {
3589            return Err(Reject::Transition);
3590        }
3591        selected_publications.push(matches[0]);
3592    }
3593    if bundle.statements.iter().any(|signed| {
3594        signed
3595            .body
3596            .as_ref()
3597            .is_some_and(|s| s.purpose == 3 && !selected_publications.contains(&signed))
3598    }) {
3599        return Err(Reject::Transition);
3600    }
3601    let mut admitted_geneses = std::collections::BTreeSet::<Vec<u8>>::new();
3602    let mut times = vec![None; bundle.delegations.len()];
3603    let mut publications = Vec::new();
3604    for signed in &bundle.statements {
3605        let statement = signed.body.as_ref().ok_or(Reject::Canonical)?;
3606        if statement.purpose != 3 {
3607            continue;
3608        }
3609        let (operation, manifest) = bundle
3610            .operations
3611            .iter()
3612            .flat_map(|o| bundle.manifests.iter().map(move |m| (o, m)))
3613            .find(|(o, m)| {
3614                publication_payload(o, m)
3615                    .and_then(|p| canonical(&p))
3616                    .is_ok_and(|b| b == statement.canonical_payload)
3617            })
3618            .ok_or(Reject::Scope)?;
3619        let digest = &operation
3620            .body
3621            .as_ref()
3622            .ok_or(Reject::Canonical)?
3623            .delegation_digest;
3624        let index = bundle
3625            .delegations
3626            .iter()
3627            .position(|d| signed_delegation_digest(d).is_ok_and(|h| h == *digest))
3628            .ok_or(Reject::Scope)?;
3629        let time = statement.observed_at_unix_millis / 1000;
3630        // First means accepted publication order, independent of array order.
3631        if times[index].is_none_or(|(order, _)| statement.admission_order < order) {
3632            times[index] = Some((statement.admission_order, time));
3633        }
3634        publications.push((signed, operation, manifest, index));
3635    }
3636    // Resolve only after authentication. Facts must cover the selected time;
3637    // current claimed state cannot stand in for an earlier deferred state.
3638    let mut resolve_owner = |time: Option<i64>| {
3639        let facts = owner_at(time)?;
3640        if facts.effective_from_unix_seconds < 0
3641            || facts
3642                .effective_until_unix_seconds
3643                .is_some_and(|end| end <= facts.effective_from_unix_seconds)
3644            || time.is_some_and(|t| {
3645                t < facts.effective_from_unix_seconds
3646                    || facts
3647                        .effective_until_unix_seconds
3648                        .is_some_and(|end| t >= end)
3649            })
3650        {
3651            return Err(Reject::Scope);
3652        }
3653        for (key, job) in facts.known_job_associations {
3654            if associations.iter().any(|(k, j)| k == key && j != job) {
3655                return Err(Reject::KeyRole);
3656            }
3657            if !associations.iter().any(|(k, _)| k == key) {
3658                associations.push((key.clone(), job.clone()));
3659            }
3660        }
3661        if facts
3662            .forbidden_job_keys
3663            .iter()
3664            .any(|key| associations.iter().any(|(known, _)| known == key))
3665        {
3666            return Err(Reject::KeyRole);
3667        }
3668        // Also keep newly supplied known job keys disjoint from witness roles.
3669        if set.as_ref().is_some_and(|set| {
3670            facts.known_job_associations.iter().any(|(key, _)| {
3671                *key == pin.public_key || set.body().entries.iter().any(|e| e.public_key == *key)
3672            })
3673        }) {
3674            return Err(Reject::KeyRole);
3675        }
3676        Ok((facts, associations.clone()))
3677    };
3678    let mut verified = Vec::new();
3679    for (i, d) in bundle.delegations.iter().enumerate() {
3680        let body = d.body.as_ref().ok_or(Reject::Canonical)?;
3681        let parent = resolve_bundle_permission(bundle, &body.parent_permission_digest)?;
3682        let time = times[i].map(|(_, time)| time);
3683        let (facts, selected_associations) = resolve_owner(time)?;
3684        let owner = facts.at(time.unwrap_or(0), &selected_associations);
3685        let token = verify_delegation_inner(d, parent, &owner, times[i].is_some())?;
3686        verified.push(token);
3687    }
3688    let initial = verified.first().ok_or(Reject::Canonical)?;
3689    for branch in &initial.body.branch_manifest {
3690        let limit = branch.limit.as_ref().ok_or(Reject::Canonical)?;
3691        let binding = bundle
3692            .genesis_authorities
3693            .iter()
3694            .find(|g| signed_genesis_digest(g).is_ok_and(|h| h == branch.genesis_authority_digest))
3695            .ok_or(Reject::Scope)?;
3696        let original = bundle
3697            .original_geneses
3698            .iter()
3699            .find(|o| native_id(o) == limit.genesis_digest)
3700            .ok_or(Reject::Scope)?;
3701        let g = binding.body.as_ref().ok_or(Reject::Canonical)?;
3702        let envelope = bundle
3703            .creator_authority_envelopes
3704            .iter()
3705            .find(|e| hash(&[e]) == g.creator_authority_envelope_digest)
3706            .ok_or(Reject::Scope)?;
3707        verify_native(original, "heddle-thread-genesis-v1")?;
3708        let signature = original
3709            .signatures
3710            .iter()
3711            .find(|s| s.public_key == g.creator_public_key)
3712            .ok_or(Reject::Scope)?;
3713        verify_genesis_authority(
3714            binding,
3715            initial,
3716            &limit.genesis_digest,
3717            &signature.signature,
3718            &hash(&[envelope]),
3719        )?;
3720    }
3721    for ((context, proof), signed) in resolved.iter().zip(&bundle.statements) {
3722        let s = signed.body.as_ref().ok_or(Reject::Canonical)?;
3723        if !verified.iter().any(|d| {
3724            d.body.identity.as_ref().is_some_and(|id| {
3725                s.spool_uuid == id.spool_uuid
3726                    && s.spool_genesis_digest == id.spool_genesis_digest
3727                    && s.owner_id == id.owner_id
3728                    && s.owner_state_hash == id.owner_state_hash
3729                    && s.ownership_transfer_sequence == id.ownership_transfer_sequence
3730            })
3731        }) {
3732            return Err(Reject::Scope);
3733        }
3734        match s.purpose {
3735            1 => {
3736                // Every original admission must independently satisfy [N,E).
3737                let time = s.observed_at_unix_millis / 1000;
3738                let (facts, selected_associations) = resolve_owner(Some(time))?;
3739                let owner = facts.at(time, &selected_associations);
3740                verify_delegation(
3741                    &bundle.delegations[0],
3742                    resolve_bundle_permission(bundle, &initial.body.parent_permission_digest)?,
3743                    &owner,
3744                )?;
3745                let payload = bundle
3746                    .genesis_witnesses
3747                    .iter()
3748                    .find(|p| canonical(*p).is_ok_and(|b| b == s.canonical_payload))
3749                    .ok_or(Reject::Scope)?;
3750                let genesis = &payload
3751                    .binding
3752                    .as_ref()
3753                    .and_then(|g| g.body.as_ref())
3754                    .ok_or(Reject::Canonical)?
3755                    .genesis_digest;
3756                if !admitted_geneses.insert(genesis.clone()) {
3757                    return Err(Reject::Transition);
3758                }
3759                let publication = bundle
3760                    .operations
3761                    .iter()
3762                    .zip(&selected_publications)
3763                    .find(|(o, _)| {
3764                        o.body
3765                            .as_ref()
3766                            .is_some_and(|o| &o.genesis_digest == genesis)
3767                    })
3768                    .and_then(|(_, s)| s.body.as_ref())
3769                    .ok_or(Reject::Transition)?;
3770                check_import_genesis_publication_pair(initial, s, publication)?;
3771                verify_witness_payload(
3772                    s,
3773                    WitnessPayload::Genesis(
3774                        bundle
3775                            .genesis_witnesses
3776                            .iter()
3777                            .find(|p| canonical(*p).is_ok_and(|b| b == s.canonical_payload))
3778                            .ok_or(Reject::Scope)?,
3779                    ),
3780                )?;
3781            }
3782            2 => verify_witness_payload(
3783                s,
3784                WitnessPayload::Authority(
3785                    bundle
3786                        .authority_witnesses
3787                        .iter()
3788                        .find(|p| canonical(*p).is_ok_and(|b| b == s.canonical_payload))
3789                        .ok_or(Reject::Scope)?,
3790                ),
3791            )?,
3792            4 => {
3793                let payload = bundle
3794                    .landing_witnesses
3795                    .iter()
3796                    .find(|p| canonical(*p).is_ok_and(|b| b == s.canonical_payload))
3797                    .ok_or(Reject::Scope)?;
3798                let (facts, selected_associations) =
3799                    resolve_owner(Some(s.observed_at_unix_millis / 1000))?;
3800                let known = selected_associations
3801                    .iter()
3802                    .map(|(key, _)| key.clone())
3803                    .collect::<Vec<_>>();
3804                verify_landing_key_roles(payload, &known, facts.forbidden_landing_keys)?;
3805                verify_witness_payload(s, WitnessPayload::Landing(payload))?;
3806            }
3807            3 => {
3808                let (_, operation, manifest, index) = publications
3809                    .iter()
3810                    .find(|(statement, _, _, _)| *statement == signed)
3811                    .ok_or(Reject::Scope)?;
3812                let d = &bundle.delegations[*index];
3813                let body = d.body.as_ref().ok_or(Reject::Canonical)?;
3814                let time = s.observed_at_unix_millis / 1000;
3815                let (facts, selected_associations) = resolve_owner(Some(time))?;
3816                let owner = facts.at(time, &selected_associations);
3817                let delegation = verify_delegation(
3818                    d,
3819                    resolve_bundle_permission(bundle, &body.parent_permission_digest)?,
3820                    &owner,
3821                )?;
3822                verify_publication(
3823                    operation,
3824                    &delegation,
3825                    manifest,
3826                    signed,
3827                    set.as_ref().ok_or(Reject::Canonical)?,
3828                    *proof,
3829                    now_ms,
3830                )?;
3831            }
3832            _ => return Err(Reject::Version),
3833        }
3834        trust::recheck_context(
3835            context,
3836            set.as_ref().ok_or(Reject::Canonical)?,
3837            signed,
3838            now_ms,
3839        )?;
3840    }
3841    // Operations are accepted publication order, while manifest slots are sorted.
3842    let mut progressive = ImportResultManifestV1 {
3843        slots: vec![],
3844        ..terminal.clone()
3845    };
3846    let mut order = 0;
3847    let mut observed = 0;
3848    let mut consumed = std::collections::BTreeMap::<(Vec<u8>, Vec<u8>), (u32, u64)>::new();
3849    let mut total_bytes = 0_u64;
3850    for o in &bundle.operations {
3851        let digest = signed_operation_digest(o)?;
3852        let slot = terminal
3853            .slots
3854            .iter()
3855            .find(|s| s.signed_operation_digest == digest)
3856            .ok_or(Reject::Scope)?;
3857        progressive.slots.push(slot.clone());
3858        progressive
3859            .slots
3860            .sort_by(|a, b| (&a.ref_name, a.slot_id).cmp(&(&b.ref_name, b.slot_id)));
3861        let payload = canonical(&publication_payload(o, &progressive)?)?;
3862        let s = bundle
3863            .statements
3864            .iter()
3865            .filter_map(|s| s.body.as_ref())
3866            .find(|s| s.purpose == 3 && s.canonical_payload == payload)
3867            .ok_or(Reject::Transition)?;
3868        if s.admission_order <= order || s.observed_at_unix_millis < observed {
3869            return Err(Reject::Transition);
3870        }
3871        order = s.admission_order;
3872        observed = s.observed_at_unix_millis;
3873        let digest = &o.body.as_ref().ok_or(Reject::Canonical)?.delegation_digest;
3874        let index = verified
3875            .iter()
3876            .position(|d| d.digest == *digest)
3877            .ok_or(Reject::Scope)?;
3878        let result_bytes = o.body.as_ref().ok_or(Reject::Canonical)?.result_bytes;
3879        total_bytes = total_bytes
3880            .checked_add(result_bytes)
3881            .ok_or(Reject::Bounds)?;
3882        let original_scope = initial.body.scope.as_ref().ok_or(Reject::Canonical)?;
3883        if total_bytes > original_scope.max_result_bytes
3884            || progressive.slots.len() > original_scope.max_operations as usize
3885        {
3886            return Err(Reject::Scope);
3887        }
3888        let delegation = &verified[index];
3889        let mut committed_before = progressive.clone();
3890        let operation_digest = signed_operation_digest(o)?;
3891        committed_before
3892            .slots
3893            .retain(|s| s.signed_operation_digest != operation_digest);
3894        check_import_publication_budget(o, delegation, &committed_before)?;
3895        let scope = delegation.body.scope.as_ref().ok_or(Reject::Canonical)?;
3896        let mut budgets = vec![(&delegation.digest, scope)];
3897        if let Some(parent) = &delegation.member {
3898            budgets.push((
3899                &delegation.body.parent_permission_digest,
3900                parent
3901                    .body
3902                    .as_ref()
3903                    .and_then(|p| p.scope.as_ref())
3904                    .ok_or(Reject::Canonical)?,
3905            ));
3906        }
3907        for (digest, scope) in budgets {
3908            let (operations, bytes) = consumed
3909                .entry((digest.clone(), delegation.body.logical_job_id.clone()))
3910                .or_default();
3911            *operations = operations.checked_add(1).ok_or(Reject::Bounds)?;
3912            *bytes = bytes.checked_add(result_bytes).ok_or(Reject::Bounds)?;
3913            if *operations > scope.max_operations || *bytes > scope.max_result_bytes {
3914                return Err(Reject::Scope);
3915            }
3916        }
3917    }
3918    if progressive != *terminal {
3919        return Err(Reject::Scope);
3920    }
3921    let mut history = snapshot.map_or_else(Vec::new, |s| s.accepted_history.clone());
3922    if let Some(old) = history.iter_mut().find(|b| {
3923        b.terminal_manifest
3924            .as_ref()
3925            .is_some_and(|m| m.logical_job_id == terminal.logical_job_id)
3926            && b.delegations
3927                .first()
3928                .and_then(|d| d.body.as_ref())
3929                .and_then(|d| d.identity.as_ref())
3930                .map(|id| &id.spool_uuid)
3931                == bundle
3932                    .delegations
3933                    .first()
3934                    .and_then(|d| d.body.as_ref())
3935                    .and_then(|d| d.identity.as_ref())
3936                    .map(|id| &id.spool_uuid)
3937    }) {
3938        if !bundle.delegations.starts_with(&old.delegations)
3939            || !bundle.operations.starts_with(&old.operations)
3940            || bundle.owner_genesis != old.owner_genesis
3941            || !bundle
3942                .ownership_transfers
3943                .starts_with(&old.ownership_transfers)
3944            || !old
3945                .owner_histories
3946                .iter()
3947                .all(|v| bundle.owner_histories.contains(v))
3948            || bundle.member_permission != old.member_permission
3949            || !old
3950                .genesis_authorities
3951                .iter()
3952                .all(|v| bundle.genesis_authorities.contains(v))
3953            || !old
3954                .original_geneses
3955                .iter()
3956                .all(|v| bundle.original_geneses.contains(v))
3957            || !old
3958                .creator_authority_envelopes
3959                .iter()
3960                .all(|v| bundle.creator_authority_envelopes.contains(v))
3961            || !old.manifests.iter().all(|v| bundle.manifests.contains(v))
3962            || !old.statements.iter().all(|v| bundle.statements.contains(v))
3963            || !old.policies.iter().all(|v| bundle.policies.contains(v))
3964            || !old
3965                .genesis_witnesses
3966                .iter()
3967                .all(|v| bundle.genesis_witnesses.contains(v))
3968            || !old
3969                .authority_witnesses
3970                .iter()
3971                .all(|v| bundle.authority_witnesses.contains(v))
3972            || !old
3973                .landing_witnesses
3974                .iter()
3975                .all(|v| bundle.landing_witnesses.contains(v))
3976            || !old
3977                .foreign_dependencies
3978                .iter()
3979                .all(|v| bundle.foreign_dependencies.contains(v))
3980        {
3981            return Err(Reject::HighWater);
3982        }
3983        *old = bundle.clone();
3984    } else {
3985        history.push(bundle.clone());
3986    }
3987    let witnessed = times[0].is_some();
3988    let witnessed_prefix = usize::from(witnessed);
3989    let mut persisted = snapshot.cloned();
3990    if witnessed_prefix > 0 || new_set {
3991        let mut persisted_associations =
3992            snapshot.map_or_else(Vec::new, |s| s.job_associations.clone());
3993        if witnessed_prefix > 0 {
3994            for d in &bundle.delegations[..witnessed_prefix] {
3995                let b = d.body.as_ref().ok_or(Reject::Canonical)?;
3996                if !persisted_associations
3997                    .iter()
3998                    .any(|(key, _)| *key == b.job_public_key)
3999                {
4000                    persisted_associations
4001                        .push((b.job_public_key.clone(), b.logical_job_id.clone()));
4002                }
4003            }
4004        }
4005        persisted = Some(ImportWitnessSnapshot {
4006            root: pin.clone(),
4007            witness_set: carried.ok_or(Reject::Canonical)?.clone(),
4008            clock_floor_unix_millis: now_ms,
4009            job_associations: persisted_associations,
4010            accepted_history: if witnessed_prefix > 0 {
4011                history
4012            } else {
4013                snapshot.map_or_else(Vec::new, |s| s.accepted_history.clone())
4014            },
4015        });
4016    }
4017    Ok(VerifiedImportBundleWitnesses {
4018        evidence: if witnessed {
4019            ImportBundleEvidence::Witnessed
4020        } else {
4021            ImportBundleEvidence::Recovery
4022        },
4023        snapshot_advanced: persisted.as_ref() != snapshot,
4024        owner_check_time_unix_seconds: times[0].map(|(_, time)| time),
4025        accepted_history: terminal.clone(),
4026        snapshot: persisted,
4027    })
4028}
4029
4030/// Independently authenticated current caller facts for source custody checks.
4031pub struct ImportControlCaller<'a> {
4032    pub authenticated_pop: bool,
4033    pub destination_writer: bool,
4034    pub caller_account: &'a str,
4035    pub connection_owner_account: Option<&'a str>,
4036    pub authorized_source: Option<&'a ImportSourceSelectionV1>,
4037    pub exact_grants_current: bool,
4038    pub selected_commits_available: bool,
4039}
4040#[derive(Clone, Copy)]
4041pub enum ImportControlAction {
4042    Cancel,
4043    Retry,
4044}
4045
4046/// Destination control is independent of source custody. Cancel never fetches.
4047/// Run at Retry admission after checking signed authority.
4048pub fn check_import_control_caller(
4049    action: ImportControlAction,
4050    retained: &ImportSourceSelectionV1,
4051    scope: &ImportPermissionScopeV1,
4052    caller: &ImportControlCaller<'_>,
4053) -> Result<(), Reject> {
4054    if !caller.authenticated_pop || !caller.destination_writer || caller.caller_account.is_empty() {
4055        return Err(Reject::Scope);
4056    }
4057    if matches!(action, ImportControlAction::Cancel) {
4058        return Ok(());
4059    }
4060    validate_retained_import_source(retained, scope)?;
4061    if retained.connection.is_some()
4062        && (caller.connection_owner_account != Some(caller.caller_account)
4063            || caller.authorized_source != Some(retained)
4064            || !caller.exact_grants_current)
4065    {
4066        return Err(Reject::SourceSelection);
4067    }
4068    if !caller.selected_commits_available {
4069        return Err(Reject::SourceSelection);
4070    }
4071    Ok(())
4072}
4073
4074fn validate_retry_availability(
4075    response: &GetImportJobStateResponse,
4076    destination: &SpoolRef,
4077) -> Result<(), Reject> {
4078    use get_import_job_state_response::RetryAvailability;
4079    match response
4080        .retry_availability
4081        .as_ref()
4082        .ok_or(Reject::Canonical)?
4083    {
4084        RetryAvailability::EligibleRetryTarget(target) => {
4085            if response.status != 1 {
4086                return Err(Reject::Canonical);
4087            }
4088            let operation = target.operation_ref.as_ref().ok_or(Reject::Canonical)?;
4089            let raw = hex::decode(operation.id.replace('-', "")).map_err(|_| Reject::Canonical)?;
4090            if initial_operation_id(&raw, false)? != operation.id {
4091                return Err(Reject::Canonical);
4092            }
4093            if operation.spool.as_ref() != Some(destination) {
4094                return Err(Reject::Scope);
4095            }
4096            if target.operation_version.is_empty() || target.operation_version.len() > 256 {
4097                return Err(Reject::Bounds);
4098            }
4099        }
4100        RetryAvailability::RetryUnavailable(reason) => {
4101            if !matches!(
4102                (response.status, *reason),
4103                (1, 1 | 2 | 6) | (2, 3) | (3, 4) | (4, 5) | (5, 7)
4104            ) {
4105                return Err(Reject::Canonical);
4106            }
4107        }
4108    }
4109    Ok(())
4110}
4111
4112/// Validate the minimal writer snapshot and its retry target.
4113pub fn validate_retry_state_response(
4114    request: &GetImportJobStateRequest,
4115    response: &GetImportJobStateResponse,
4116) -> Result<(), Reject> {
4117    validate_job_state_response(request, response)?;
4118    validate_retry_availability(response, request.destination.as_ref().ok_or(Reject::Scope)?)
4119}
4120
4121/// Receiver-owned facts locked together with job/authority/source state. The
4122/// operation-lineage association is durable host state, not an ID inference.
4123pub struct ImportRetryAdmission<'a> {
4124    pub read: &'a GetImportJobStateResponse,
4125    pub original: &'a OperationRecord,
4126    pub retry_lineage_id: &'a [u8],
4127    pub logical_job_terminal: bool,
4128    pub retained_source: &'a ImportSourceSelectionV1,
4129    pub committed_manifest: &'a ImportResultManifestV1,
4130    pub now_unix_seconds: i64,
4131}
4132
4133/// Run after caller-scoped frozen replay lookup, under ONE admission transaction.
4134/// Full owner/policy/revocation/lease checks and allocation remain host gates.
4135pub fn check_retry_admission(
4136    request: &RetryImportSourceRequest,
4137    context: &ImportRetryAdmission<'_>,
4138    active: &VerifiedImportDelegation,
4139    caller: &ImportControlCaller<'_>,
4140) -> Result<(), Reject> {
4141    use get_import_job_state_response::RetryAvailability;
4142    let destination = request
4143        .original_operation
4144        .as_ref()
4145        .and_then(|r| r.spool.clone());
4146    let read_request = GetImportJobStateRequest {
4147        destination,
4148        logical_job_id: request.logical_job_id.clone(),
4149    };
4150    validate_retry_state_response(&read_request, context.read)?;
4151    if request.client_operation_id.is_empty() || request.client_operation_id.len() > 128 {
4152        return Err(Reject::Canonical);
4153    }
4154    if context.logical_job_terminal || context.read.status != 1 {
4155        return Err(Reject::Revoked);
4156    }
4157    let Some(RetryAvailability::EligibleRetryTarget(target)) = &context.read.retry_availability
4158    else {
4159        return Err(Reject::StaleContext);
4160    };
4161    let target_ref = target.operation_ref.as_ref().ok_or(Reject::Canonical)?;
4162    let original_ref = context.original.r#ref.as_ref().ok_or(Reject::Scope)?;
4163    if request.original_operation.as_ref() != Some(original_ref)
4164        || original_ref.spool != target_ref.spool
4165        || original_ref.id != target_ref.id
4166        || context.retry_lineage_id != active.body.retry_lineage_id
4167        || import_job_state_request_from_operation(context.original)?.as_ref()
4168            != Some(&read_request)
4169    {
4170        return Err(Reject::Scope);
4171    }
4172    if request.expected_operation_version != target.operation_version
4173        || context.original.version != target.operation_version
4174        || context.original.superseded_by.is_some()
4175        || !matches!(context.original.state, 4 | 5)
4176    {
4177        return Err(Reject::StaleContext);
4178    }
4179    let state = context.read;
4180    if state.active_delegation_digest != active.digest
4181        || state.active_cancellation_id != active.body.cancellation_id
4182    {
4183        return Err(Reject::StaleContext);
4184    }
4185    check_job_fence(
4186        &request.logical_job_id,
4187        &request.active_delegation_digest,
4188        request.expected_authority_epoch,
4189        active,
4190        state.authority_epoch,
4191    )?;
4192    interval(
4193        active.body.not_before_unix_seconds,
4194        active.body.expires_at_unix_seconds,
4195        context.now_unix_seconds,
4196    )?;
4197    let manifest = context.committed_manifest;
4198    validate_manifest(manifest)?;
4199    if manifest.logical_job_id != active.body.logical_job_id
4200        || manifest.retry_lineage_id != active.body.retry_lineage_id
4201    {
4202        return Err(Reject::Scope);
4203    }
4204    let scope = active.body.scope.as_ref().ok_or(Reject::Canonical)?;
4205    if !scope.branches.iter().any(|b| {
4206        !manifest
4207            .slots
4208            .iter()
4209            .any(|s| s.ref_name == b.ref_name && s.slot_id == b.slot_id)
4210    }) {
4211        return Err(Reject::StaleContext);
4212    }
4213    check_import_control_caller(
4214        ImportControlAction::Retry,
4215        context.retained_source,
4216        scope,
4217        caller,
4218    )
4219}
4220
4221/// Check a host-generated UUID against the COMPLETE durable attempt set, including
4222/// the first lineage UUID. Persist the allocation, both direct links and receipt
4223/// atomically; do not generate a UUID from the request idempotency key.
4224pub fn validate_retry_response(
4225    request: &RetryImportSourceRequest,
4226    response: &MutationResponse,
4227    prior_attempt_ids: &[String],
4228) -> Result<(), Reject> {
4229    let receipt = response.receipt.as_ref().ok_or(Reject::PendingOperation)?;
4230    let Some(mutation_receipt::Outcome::PendingOperation(operation)) = &receipt.outcome else {
4231        return Err(Reject::PendingOperation);
4232    };
4233    let original = request
4234        .original_operation
4235        .as_ref()
4236        .ok_or(Reject::PendingOperation)?;
4237    let raw = hex::decode(operation.id.replace('-', "")).map_err(|_| Reject::PendingOperation)?;
4238    if initial_operation_id(&raw, false).map_or(true, |id| id != operation.id)
4239        || request.client_operation_id.is_empty()
4240        || receipt.client_operation_id != request.client_operation_id
4241        || original.spool.is_none()
4242        || operation.spool != original.spool
4243        || operation.id == original.id
4244        || operation.id == request.client_operation_id
4245        || prior_attempt_ids.contains(&operation.id)
4246    {
4247        return Err(Reject::PendingOperation);
4248    }
4249    Ok(())
4250}
4251
4252/// Exact frozen Retry replay returns the stored receipt without allocating.
4253pub fn check_retry_replay(request_bytes: &[u8], stored_bytes: &[u8]) -> Result<(), Reject> {
4254    if request_bytes.len() > 2 * MAX_BUNDLE_BYTES {
4255        return Err(Reject::Bounds);
4256    }
4257    if request_bytes != stored_bytes {
4258        return Err(Reject::OperationIdReused);
4259    }
4260    Ok(())
4261}
4262
4263/// Under the publication transaction, use the complete cumulative manifest of
4264/// this logical job. All operations consume the same single delegation total.
4265pub fn check_import_publication_budget(
4266    signed: &SignedDelegatedImportOperationV1,
4267    active: &VerifiedImportDelegation,
4268    committed_before: &ImportResultManifestV1,
4269) -> Result<(), Reject> {
4270    verify_operation(signed, active)?;
4271    let scope = active.body.scope.as_ref().ok_or(Reject::Canonical)?;
4272    if committed_before.logical_job_id != active.body.logical_job_id
4273        || committed_before.retry_lineage_id != active.body.retry_lineage_id
4274    {
4275        return Err(Reject::Scope);
4276    }
4277    let remaining = remaining_import_scope(scope, committed_before)?;
4278    if check_slot_replay(committed_before, signed)? {
4279        return Ok(());
4280    }
4281    let o = signed.body.as_ref().ok_or(Reject::Canonical)?;
4282    if remaining.max_operations == 0 || o.result_bytes > remaining.max_result_bytes {
4283        return Err(Reject::Scope);
4284    }
4285    Ok(())
4286}