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