Skip to main content

u280_hardware_promoter/
coordination.rs

1//! Complete canonical Phase-B coordination burn-chain verification.
2
3use crate::canonical::{canonical_line, hex_lower, is_lower_hex, parse_canonical_line, sha256_hex};
4use crate::model::RequestFileLocator;
5use crate::{FailureCode, PromotionError, PromotionRequest, PromotionResult};
6use rustix::fs::{CWD, Mode, OFlags, ResolveFlags, openat2};
7use rustix::process::geteuid;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest as _, Sha256};
10use std::collections::BTreeSet;
11use std::fs::{File, Metadata};
12use std::os::unix::fs::{FileExt as _, MetadataExt as _};
13use std::path::{Component, Path, PathBuf};
14
15const BURN_SCHEMA: u64 = 1;
16const BURN_CONTRACT: &str = "hashsigs-u280-phase-b-coordination-burn-v1";
17const GENESIS_SHA256: &str = "0000000000000000000000000000000000000000000000000000000000000000";
18const PROJECT_LEDGER: &str = "hashsigs-project-coordination";
19const ED25519_LEDGER: &str = "ed25519-agent-coordination";
20const MAXIMUM_HISTORY_BYTES: usize = 64 * 1024 * 1024;
21const MAXIMUM_RECORD_BYTES: usize = 64 * 1024;
22const MAXIMUM_MANIFEST_BYTES: u64 = 64 * 1024;
23pub(crate) const MAXIMUM_COORDINATION_INTENT_BYTES: u64 = 64 * 1024;
24
25#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
26#[serde(deny_unknown_fields)]
27struct CoordinationGrantWire {
28    ledger_id: String,
29    entry_id: String,
30    entry_bytes: u64,
31    entry_sha256: String,
32}
33
34impl CoordinationGrantWire {
35    fn validate(&self, expected_ledger: &str) -> PromotionResult<()> {
36        let canonical_id = !self.entry_id.is_empty()
37            && self.entry_id.len() <= 128
38            && self.entry_id.bytes().all(|byte| {
39                byte.is_ascii_lowercase()
40                    || byte.is_ascii_digit()
41                    || matches!(byte, b'.' | b'_' | b':' | b'-')
42            });
43        if self.ledger_id != expected_ledger
44            || !canonical_id
45            || self.entry_bytes == 0
46            || !is_lower_hex(&self.entry_sha256, 64, true)
47        {
48            return Err(PromotionError::new(
49                FailureCode::CoordinationChain,
50                "coordination grant binding is non-canonical or out of order",
51            ));
52        }
53        Ok(())
54    }
55}
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57#[serde(deny_unknown_fields)]
58struct IntentBinding {
59    bytes: u64,
60    sha256: String,
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64#[serde(deny_unknown_fields)]
65struct CoordinationBurnWire {
66    schema: u64,
67    contract: String,
68    sequence: u64,
69    run_id: String,
70    intent: IntentBinding,
71    grants: Vec<CoordinationGrantWire>,
72    previous_sha256: String,
73    record_sha256: String,
74}
75
76impl CoordinationBurnWire {
77    fn validate_shape(&self) -> PromotionResult<()> {
78        if self.schema != BURN_SCHEMA
79            || self.contract != BURN_CONTRACT
80            || !is_lower_hex(&self.run_id, 32, true)
81            || self.intent.bytes == 0
82            || self.intent.bytes > MAXIMUM_COORDINATION_INTENT_BYTES
83            || !is_lower_hex(&self.intent.sha256, 64, false)
84            || self.grants.len() != 2
85            || !is_lower_hex(&self.previous_sha256, 64, false)
86            || !is_lower_hex(&self.record_sha256, 64, false)
87        {
88            return Err(PromotionError::new(
89                FailureCode::CoordinationChain,
90                "coordination burn record has a non-canonical shape",
91            ));
92        }
93        self.grants[0].validate(PROJECT_LEDGER)?;
94        self.grants[1].validate(ED25519_LEDGER)?;
95        if self.grants[0].entry_id == self.grants[1].entry_id {
96            return Err(PromotionError::new(
97                FailureCode::CoordinationChain,
98                "the two coordination grant entry identifiers must differ",
99            ));
100        }
101        Ok(())
102    }
103
104    fn recomputed_record_sha256(&self) -> PromotionResult<String> {
105        let grants = self
106            .grants
107            .iter()
108            .map(|grant| {
109                CoordinationBurnGrantHashInput::new(
110                    &grant.ledger_id,
111                    &grant.entry_id,
112                    grant.entry_bytes,
113                    &grant.entry_sha256,
114                )
115            })
116            .collect::<Vec<_>>();
117        recompute_coordination_burn_record_sha256(
118            self.sequence,
119            &self.run_id,
120            self.intent.bytes,
121            &self.intent.sha256,
122            &grants,
123            &self.previous_sha256,
124        )
125    }
126}
127
128#[derive(Serialize)]
129struct CoordinationBurnHashBody<'a> {
130    schema: u64,
131    contract: &'static str,
132    sequence: u64,
133    run_id: &'a str,
134    intent: CoordinationBurnIntentHashInput<'a>,
135    grants: &'a [CoordinationBurnGrantHashInput],
136    previous_sha256: &'a str,
137}
138
139#[derive(Serialize)]
140struct CoordinationBurnIntentHashInput<'a> {
141    bytes: u64,
142    sha256: &'a str,
143}
144
145#[derive(Serialize)]
146pub(crate) struct CoordinationBurnGrantHashInput {
147    ledger_id: String,
148    entry_id: String,
149    entry_bytes: u64,
150    entry_sha256: String,
151}
152
153impl CoordinationBurnGrantHashInput {
154    pub(crate) fn new(
155        ledger_id: &str,
156        entry_id: &str,
157        entry_bytes: u64,
158        entry_sha256: &str,
159    ) -> Self {
160        Self {
161            ledger_id: ledger_id.to_owned(),
162            entry_id: entry_id.to_owned(),
163            entry_bytes,
164            entry_sha256: entry_sha256.to_owned(),
165        }
166    }
167}
168
169pub(crate) fn recompute_coordination_burn_record_sha256(
170    sequence: u64,
171    run_id: &str,
172    intent_bytes: u64,
173    intent_sha256: &str,
174    grants: &[CoordinationBurnGrantHashInput],
175    previous_sha256: &str,
176) -> PromotionResult<String> {
177    let body = CoordinationBurnHashBody {
178        schema: BURN_SCHEMA,
179        contract: BURN_CONTRACT,
180        sequence,
181        run_id,
182        intent: CoordinationBurnIntentHashInput {
183            bytes: intent_bytes,
184            sha256: intent_sha256,
185        },
186        grants,
187        previous_sha256,
188    };
189    let mut canonical = canonical_line(&body)?;
190    let newline = canonical.pop();
191    debug_assert_eq!(newline, Some(b'\n'));
192    Ok(sha256_hex(&canonical))
193}
194
195/// One grant binding proven by a complete canonical coordination history.
196#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
197pub struct VerifiedCoordinationGrant {
198    ledger_id: String,
199    entry_id: String,
200    entry_bytes: u64,
201    entry_sha256: String,
202}
203
204impl VerifiedCoordinationGrant {
205    /// Stable ledger identifier.
206    #[must_use]
207    pub fn ledger_id(&self) -> &str {
208        &self.ledger_id
209    }
210
211    /// Immutable grant entry identifier.
212    #[must_use]
213    pub fn entry_id(&self) -> &str {
214        &self.entry_id
215    }
216
217    /// Exact byte count of the immutable grant entry.
218    #[must_use]
219    pub const fn entry_bytes(&self) -> u64 {
220        self.entry_bytes
221    }
222
223    /// Immutable grant entry SHA-256.
224    #[must_use]
225    pub fn entry_sha256(&self) -> &str {
226        &self.entry_sha256
227    }
228}
229
230/// Opaque selected burn proven to be the exact last record and receipt for the
231/// requested run. It has no serde implementation or public constructor.
232#[derive(Clone, Debug, Eq, PartialEq)]
233pub struct VerifiedCoordinationBurn {
234    sequence: u64,
235    run_id: String,
236    intent_bytes: u64,
237    intent_sha256: String,
238    grants: Vec<VerifiedCoordinationGrant>,
239    previous_sha256: String,
240    record_sha256: String,
241}
242
243impl VerifiedCoordinationBurn {
244    /// Zero-based position in the complete history.
245    #[must_use]
246    pub const fn sequence(&self) -> u64 {
247        self.sequence
248    }
249
250    /// Phase-B run identity bound to the selected receipt.
251    #[must_use]
252    pub fn run_id(&self) -> &str {
253        &self.run_id
254    }
255
256    /// Exact byte count of the canonical coordination intent.
257    #[must_use]
258    pub const fn intent_bytes(&self) -> u64 {
259        self.intent_bytes
260    }
261
262    /// SHA-256 of the canonical coordination intent.
263    #[must_use]
264    pub fn intent_sha256(&self) -> &str {
265        &self.intent_sha256
266    }
267
268    /// Ordered immutable grant bindings.
269    #[must_use]
270    pub fn grants(&self) -> &[VerifiedCoordinationGrant] {
271        &self.grants
272    }
273
274    /// Previous coordination burn-record SHA-256.
275    #[must_use]
276    pub fn previous_sha256(&self) -> &str {
277        &self.previous_sha256
278    }
279
280    /// SHA-256 of this record's canonical body.
281    #[must_use]
282    pub fn record_sha256(&self) -> &str {
283        &self.record_sha256
284    }
285}
286
287impl From<CoordinationBurnWire> for VerifiedCoordinationBurn {
288    fn from(wire: CoordinationBurnWire) -> Self {
289        Self {
290            sequence: wire.sequence,
291            run_id: wire.run_id,
292            intent_bytes: wire.intent.bytes,
293            intent_sha256: wire.intent.sha256,
294            grants: wire
295                .grants
296                .into_iter()
297                .map(|grant| VerifiedCoordinationGrant {
298                    ledger_id: grant.ledger_id,
299                    entry_id: grant.entry_id,
300                    entry_bytes: grant.entry_bytes,
301                    entry_sha256: grant.entry_sha256,
302                })
303                .collect(),
304            previous_sha256: wire.previous_sha256,
305            record_sha256: wire.record_sha256,
306        }
307    }
308}
309
310/// Verified global replay index for one complete genesis-rooted snapshot.
311#[derive(Clone, Debug, Eq, PartialEq)]
312pub struct CoordinationChain {
313    records: u64,
314    selected_burn: VerifiedCoordinationBurn,
315    run_ids: BTreeSet<String>,
316    intent_sha256s: BTreeSet<String>,
317    grant_replay_keys: BTreeSet<(String, String, String)>,
318    exact_grant_bindings: BTreeSet<(String, String, u64, String)>,
319}
320
321impl CoordinationChain {
322    /// Number of contiguous records, including the selected last burn.
323    #[must_use]
324    pub const fn records(&self) -> u64 {
325        self.records
326    }
327
328    /// Exact last history record, run binding, and selected receipt.
329    #[must_use]
330    pub const fn selected_burn(&self) -> &VerifiedCoordinationBurn {
331        &self.selected_burn
332    }
333
334    /// Reports whether a run identity already exists anywhere in the history.
335    #[must_use]
336    pub fn contains_run_id(&self, run_id: &str) -> bool {
337        self.run_ids.contains(run_id)
338    }
339
340    /// Reports whether an intent digest already exists anywhere in the history.
341    #[must_use]
342    pub fn contains_intent_sha256(&self, sha256: &str) -> bool {
343        self.intent_sha256s.contains(sha256)
344    }
345
346    /// Reports whether an exact ledger/entry/digest triple was already burned.
347    #[must_use]
348    pub fn contains_grant(&self, ledger_id: &str, entry_id: &str, entry_sha256: &str) -> bool {
349        self.grant_replay_keys.contains(&(
350            ledger_id.to_owned(),
351            entry_id.to_owned(),
352            entry_sha256.to_owned(),
353        ))
354    }
355
356    /// Reports whether an exact ledger/entry/byte-count/digest tuple was burned.
357    #[must_use]
358    pub fn contains_exact_grant(
359        &self,
360        ledger_id: &str,
361        entry_id: &str,
362        entry_bytes: u64,
363        entry_sha256: &str,
364    ) -> bool {
365        self.exact_grant_bindings.contains(&(
366            ledger_id.to_owned(),
367            entry_id.to_owned(),
368            entry_bytes,
369            entry_sha256.to_owned(),
370        ))
371    }
372}
373
374struct CoordinationChainBuilder {
375    expected_sequence: u64,
376    previous: String,
377    run_ids: BTreeSet<String>,
378    intents: BTreeSet<String>,
379    grant_replay_keys: BTreeSet<(String, String, String)>,
380    exact_grant_bindings: BTreeSet<(String, String, u64, String)>,
381    last_record: Option<CoordinationBurnWire>,
382    last_line: Vec<u8>,
383}
384
385impl CoordinationChainBuilder {
386    fn new() -> Self {
387        Self {
388            expected_sequence: 0,
389            previous: GENESIS_SHA256.to_owned(),
390            run_ids: BTreeSet::new(),
391            intents: BTreeSet::new(),
392            grant_replay_keys: BTreeSet::new(),
393            exact_grant_bindings: BTreeSet::new(),
394            last_record: None,
395            last_line: Vec::new(),
396        }
397    }
398
399    fn push(&mut self, line: &[u8]) -> PromotionResult<()> {
400        let record: CoordinationBurnWire =
401            parse_canonical_line(line, MAXIMUM_RECORD_BYTES, "coordination burn record").map_err(
402                |error| PromotionError::new(FailureCode::CoordinationChain, error.to_string()),
403            )?;
404        record.validate_shape()?;
405        if record.sequence != self.expected_sequence
406            || record.previous_sha256 != self.previous
407            || record.recomputed_record_sha256()? != record.record_sha256
408        {
409            return Err(PromotionError::new(
410                FailureCode::CoordinationChain,
411                "coordination history is not one contiguous recomputed genesis chain",
412            ));
413        }
414        if !self.run_ids.insert(record.run_id.clone())
415            || !self.intents.insert(record.intent.sha256.clone())
416        {
417            return Err(PromotionError::new(
418                FailureCode::CoordinationReplay,
419                "coordination history repeats a run ID or intent digest",
420            ));
421        }
422        for grant in &record.grants {
423            if !self.grant_replay_keys.insert((
424                grant.ledger_id.clone(),
425                grant.entry_id.clone(),
426                grant.entry_sha256.clone(),
427            )) {
428                return Err(PromotionError::new(
429                    FailureCode::CoordinationReplay,
430                    "coordination history repeats a ledger grant binding",
431                ));
432            }
433            let inserted = self.exact_grant_bindings.insert((
434                grant.ledger_id.clone(),
435                grant.entry_id.clone(),
436                grant.entry_bytes,
437                grant.entry_sha256.clone(),
438            ));
439            debug_assert!(inserted);
440        }
441        self.expected_sequence = self.expected_sequence.checked_add(1).ok_or_else(|| {
442            PromotionError::new(
443                FailureCode::CoordinationChain,
444                "coordination history sequence overflow",
445            )
446        })?;
447        self.previous.clone_from(&record.record_sha256);
448        self.last_line.clear();
449        self.last_line.extend_from_slice(line);
450        self.last_record = Some(record);
451        Ok(())
452    }
453
454    fn finish(
455        self,
456        expected_run_id: &str,
457        selected_wire: CoordinationBurnWire,
458        selected_receipt: &[u8],
459    ) -> PromotionResult<CoordinationChain> {
460        let Some(last_record) = self.last_record else {
461            return Err(PromotionError::new(
462                FailureCode::CoordinationChain,
463                "coordination history contains no canonical burn record",
464            ));
465        };
466        if last_record != selected_wire
467            || last_record.run_id.as_str() != expected_run_id
468            || self.last_line != selected_receipt
469            || canonical_line(&last_record)? != selected_receipt
470        {
471            return Err(PromotionError::new(
472                FailureCode::CoordinationChain,
473                "selected burn receipt is not the exact last record for the requested run",
474            ));
475        }
476        Ok(CoordinationChain {
477            records: self.expected_sequence,
478            selected_burn: last_record.into(),
479            run_ids: self.run_ids,
480            intent_sha256s: self.intents,
481            grant_replay_keys: self.grant_replay_keys,
482            exact_grant_bindings: self.exact_grant_bindings,
483        })
484    }
485}
486
487fn parse_selected_receipt(bytes: &[u8]) -> PromotionResult<CoordinationBurnWire> {
488    let selected: CoordinationBurnWire = parse_canonical_line(
489        bytes,
490        MAXIMUM_RECORD_BYTES,
491        "selected coordination burn receipt",
492    )
493    .map_err(|error| PromotionError::new(FailureCode::CoordinationChain, error.to_string()))?;
494    selected.validate_shape()?;
495    Ok(selected)
496}
497
498/// Exact locator and immutable byte binding supplied by the request model.
499///
500/// This is only an integration seam. It is not evidence until consumed by
501/// [`retain_verified_coordination_snapshot`].
502#[derive(Debug, Eq, PartialEq)]
503struct CoordinationFileBinding {
504    absolute_path: PathBuf,
505    bytes: u64,
506    sha256: String,
507}
508
509impl CoordinationFileBinding {
510    fn from_request_locator(locator: &RequestFileLocator<'_>) -> Self {
511        Self {
512            absolute_path: PathBuf::from(locator.absolute_path()),
513            bytes: locator.bytes(),
514            sha256: locator.sha256().to_owned(),
515        }
516    }
517}
518
519/// The three files making one closed coordination snapshot.
520///
521/// All three request paths must be direct children of the same immutable root.
522/// Deriving that root from the locators prevents an adapter from silently
523/// treating unrelated parent directories as one snapshot.
524#[derive(Debug, Eq, PartialEq)]
525struct CoordinationSnapshotBinding {
526    root: PathBuf,
527    history: CoordinationFileBinding,
528    manifest: CoordinationFileBinding,
529    selected_receipt: CoordinationFileBinding,
530}
531
532impl CoordinationSnapshotBinding {
533    fn from_request(request: &PromotionRequest) -> PromotionResult<Self> {
534        let locators = request.coordination_locators();
535        Self::from_direct_children(
536            CoordinationFileBinding::from_request_locator(
537                locators.complete_burn_history_snapshot(),
538            ),
539            CoordinationFileBinding::from_request_locator(locators.history_snapshot_manifest()),
540            CoordinationFileBinding::from_request_locator(locators.selected_burn_receipt()),
541        )
542    }
543
544    fn from_direct_children(
545        history: CoordinationFileBinding,
546        manifest: CoordinationFileBinding,
547        selected_receipt: CoordinationFileBinding,
548    ) -> PromotionResult<Self> {
549        let root = history
550            .absolute_path
551            .parent()
552            .ok_or_else(|| {
553                coordination_error("coordination history has no absolute parent directory")
554            })?
555            .to_owned();
556        if manifest.absolute_path.parent() != Some(root.as_path())
557            || selected_receipt.absolute_path.parent() != Some(root.as_path())
558        {
559            return Err(coordination_error(
560                "coordination snapshot inputs do not share one direct parent",
561            ));
562        }
563        Ok(Self {
564            root,
565            history,
566            manifest,
567            selected_receipt,
568        })
569    }
570}
571
572#[derive(Clone, Debug, Eq, PartialEq)]
573struct SnapshotFingerprint {
574    dev: u64,
575    ino: u64,
576    mode: u32,
577    uid: u32,
578    nlink: u64,
579    bytes: u64,
580    blocks: u64,
581    mtime: i64,
582    mtime_nsec: i64,
583    ctime: i64,
584    ctime_nsec: i64,
585}
586
587impl SnapshotFingerprint {
588    fn from_metadata(metadata: &Metadata) -> Self {
589        Self {
590            dev: metadata.dev(),
591            ino: metadata.ino(),
592            mode: metadata.mode(),
593            uid: metadata.uid(),
594            nlink: metadata.nlink(),
595            bytes: metadata.size(),
596            blocks: metadata.blocks(),
597            mtime: metadata.mtime(),
598            mtime_nsec: metadata.mtime_nsec(),
599            ctime: metadata.ctime(),
600            ctime_nsec: metadata.ctime_nsec(),
601        }
602    }
603
604    fn same_object(&self, other: &Self) -> bool {
605        self.dev == other.dev && self.ino == other.ino
606    }
607}
608
609#[derive(Debug)]
610struct RetainedCoordinationFile {
611    relative: String,
612    file: File,
613    fingerprint: SnapshotFingerprint,
614    binding: CoordinationFileBinding,
615}
616
617/// Descriptor-retained proof of one complete coordination snapshot.
618///
619/// The type deliberately implements neither `Clone` nor serde. A digest-only
620/// copy cannot stand in for the retained root and file descriptors.
621#[derive(Debug)]
622pub(crate) struct VerifiedCoordinationSnapshot {
623    expected_run_id: String,
624    root_path: PathBuf,
625    root: File,
626    root_fingerprint: SnapshotFingerprint,
627    history: RetainedCoordinationFile,
628    manifest: RetainedCoordinationFile,
629    selected_receipt: RetainedCoordinationFile,
630    chain: CoordinationChain,
631}
632
633#[allow(dead_code)] // Remaining accessors belong to the future complete evidence combiner.
634impl VerifiedCoordinationSnapshot {
635    #[must_use]
636    pub(crate) const fn chain(&self) -> &CoordinationChain {
637        &self.chain
638    }
639
640    #[must_use]
641    pub(crate) const fn selected_burn(&self) -> &VerifiedCoordinationBurn {
642        self.chain.selected_burn()
643    }
644
645    #[must_use]
646    pub(crate) fn root(&self) -> &Path {
647        &self.root_path
648    }
649
650    #[must_use]
651    pub(crate) fn history_binding(&self) -> (u64, &str) {
652        (self.history.binding.bytes, &self.history.binding.sha256)
653    }
654
655    #[must_use]
656    pub(crate) fn manifest_binding(&self) -> (u64, &str) {
657        (self.manifest.binding.bytes, &self.manifest.binding.sha256)
658    }
659
660    #[must_use]
661    pub(crate) fn selected_receipt_binding(&self) -> (u64, &str) {
662        (
663            self.selected_receipt.binding.bytes,
664            &self.selected_receipt.binding.sha256,
665        )
666    }
667
668    /// Re-hashes and reparses all retained inputs, proving that both their
669    /// descriptors and descriptor-rooted pathnames still name the snapshot.
670    pub(crate) fn revalidate(&self) -> PromotionResult<()> {
671        revalidate_root_path(&self.root_path, &self.root, &self.root_fingerprint)?;
672        revalidate_retained_file(&self.root, &self.history)?;
673        revalidate_retained_file(&self.root, &self.manifest)?;
674        revalidate_retained_file(&self.root, &self.selected_receipt)?;
675
676        let manifest = read_small_retained(&self.manifest, MAXIMUM_MANIFEST_BYTES)?;
677        let selected = read_small_retained(
678            &self.selected_receipt,
679            u64::try_from(MAXIMUM_RECORD_BYTES).expect("record bound fits u64"),
680        )?;
681        verify_history_manifest(&manifest, &self.history)?;
682        let reparsed = verify_streaming_history(&self.history, &selected, &self.expected_run_id)?;
683        if reparsed != self.chain {
684            return Err(coordination_error(
685                "coordination snapshot proof changed during final revalidation",
686            ));
687        }
688
689        revalidate_retained_file(&self.root, &self.history)?;
690        revalidate_retained_file(&self.root, &self.manifest)?;
691        revalidate_retained_file(&self.root, &self.selected_receipt)?;
692        revalidate_root_path(&self.root_path, &self.root, &self.root_fingerprint)
693    }
694}
695
696/// Securely opens, consumes, and retains a complete coordination snapshot.
697/// The only locator source is the validated request; callers cannot inject a
698/// parallel path, length, digest, or run identity.
699///
700/// # Errors
701///
702/// Returns [`FailureCode::CoordinationChain`] for an insecure or mutable path,
703/// a malformed manifest, any byte-binding mismatch, or an invalid burn chain.
704#[allow(dead_code)] // Called only by the future complete evidence combiner.
705pub(crate) fn retain_verified_coordination_snapshot(
706    request: &PromotionRequest,
707) -> PromotionResult<VerifiedCoordinationSnapshot> {
708    let expected_run_id = request.run_id();
709    let binding = CoordinationSnapshotBinding::from_request(request)?;
710    if !is_lower_hex(expected_run_id, 32, true) {
711        return Err(coordination_error(
712            "coordination snapshot expected run ID is non-canonical",
713        ));
714    }
715    validate_absolute_path(&binding.root, "coordination snapshot root")?;
716    validate_file_binding(
717        &binding.root,
718        &binding.history,
719        u64::try_from(MAXIMUM_HISTORY_BYTES).expect("history bound fits u64"),
720        "coordination history",
721    )?;
722    validate_file_binding(
723        &binding.root,
724        &binding.manifest,
725        MAXIMUM_MANIFEST_BYTES,
726        "coordination history manifest",
727    )?;
728    validate_file_binding(
729        &binding.root,
730        &binding.selected_receipt,
731        u64::try_from(MAXIMUM_RECORD_BYTES).expect("record bound fits u64"),
732        "selected coordination receipt",
733    )?;
734    if binding.history.absolute_path == binding.manifest.absolute_path
735        || binding.history.absolute_path == binding.selected_receipt.absolute_path
736        || binding.manifest.absolute_path == binding.selected_receipt.absolute_path
737    {
738        return Err(coordination_error(
739            "coordination snapshot inputs must have three distinct pathnames",
740        ));
741    }
742
743    let (root, root_fingerprint) = open_immutable_root(&binding.root)?;
744    revalidate_root_path(&binding.root, &root, &root_fingerprint)?;
745    let history = open_retained_file(&root, binding.history, "coordination history")?;
746    let manifest = open_retained_file(&root, binding.manifest, "coordination history manifest")?;
747    let selected_receipt = open_retained_file(
748        &root,
749        binding.selected_receipt,
750        "selected coordination receipt",
751    )?;
752    if history.fingerprint.same_object(&manifest.fingerprint)
753        || history
754            .fingerprint
755            .same_object(&selected_receipt.fingerprint)
756        || manifest
757            .fingerprint
758            .same_object(&selected_receipt.fingerprint)
759    {
760        return Err(coordination_error(
761            "coordination snapshot inputs alias one retained file object",
762        ));
763    }
764
765    let manifest_bytes = read_small_retained(&manifest, MAXIMUM_MANIFEST_BYTES)?;
766    let selected_bytes = read_small_retained(
767        &selected_receipt,
768        u64::try_from(MAXIMUM_RECORD_BYTES).expect("record bound fits u64"),
769    )?;
770    verify_history_manifest(&manifest_bytes, &history)?;
771    let chain = verify_streaming_history(&history, &selected_bytes, expected_run_id)?;
772    revalidate_root_path(&binding.root, &root, &root_fingerprint)?;
773
774    let snapshot = VerifiedCoordinationSnapshot {
775        expected_run_id: expected_run_id.to_owned(),
776        root_path: binding.root,
777        root,
778        root_fingerprint,
779        history,
780        manifest,
781        selected_receipt,
782        chain,
783    };
784    snapshot.revalidate()?;
785    Ok(snapshot)
786}
787
788fn validate_file_binding(
789    root: &Path,
790    binding: &CoordinationFileBinding,
791    maximum_bytes: u64,
792    label: &str,
793) -> PromotionResult<()> {
794    validate_absolute_path(&binding.absolute_path, label)?;
795    let _ = direct_child_name(root, &binding.absolute_path, label)?;
796    if binding.bytes == 0
797        || binding.bytes > maximum_bytes
798        || !is_lower_hex(&binding.sha256, 64, false)
799    {
800        return Err(coordination_error(format!(
801            "{label} binding is empty, oversized, or has a non-canonical SHA-256"
802        )));
803    }
804    Ok(())
805}
806
807fn validate_absolute_path(path: &Path, label: &str) -> PromotionResult<()> {
808    let Some(text) = path.to_str() else {
809        return Err(coordination_error(format!("{label} path is not UTF-8")));
810    };
811    let canonical = text.starts_with('/')
812        && text.len() > 1
813        && !text.ends_with('/')
814        && !text.contains("//")
815        && !text.contains('\0')
816        && path
817            .components()
818            .all(|component| matches!(component, Component::RootDir | Component::Normal(_)));
819    if !canonical {
820        return Err(coordination_error(format!(
821            "{label} is not a traversal-free canonical absolute path"
822        )));
823    }
824    Ok(())
825}
826
827fn direct_child_name(root: &Path, path: &Path, label: &str) -> PromotionResult<String> {
828    let relative = path
829        .strip_prefix(root)
830        .map_err(|_| coordination_error(format!("{label} is not beneath the snapshot root")))?;
831    let mut components = relative.components();
832    let Some(Component::Normal(name)) = components.next() else {
833        return Err(coordination_error(format!(
834            "{label} is not a direct snapshot child"
835        )));
836    };
837    if components.next().is_some() {
838        return Err(coordination_error(format!(
839            "{label} is not a direct snapshot child"
840        )));
841    }
842    let name = name
843        .to_str()
844        .ok_or_else(|| coordination_error(format!("{label} basename is not UTF-8")))?;
845    if name.is_empty()
846        || name.contains('/')
847        || name.contains('\0')
848        || name.contains('\n')
849        || name.contains('\r')
850    {
851        return Err(coordination_error(format!(
852            "{label} basename is non-canonical"
853        )));
854    }
855    Ok(name.to_owned())
856}
857
858fn open_immutable_root(path: &Path) -> PromotionResult<(File, SnapshotFingerprint)> {
859    let descriptor = openat2(
860        CWD,
861        path,
862        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
863        Mode::empty(),
864        ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
865    )
866    .map_err(|error| coordination_error(format!("cannot securely open snapshot root: {error}")))?;
867    let root = File::from(descriptor);
868    let metadata = root
869        .metadata()
870        .map_err(|error| coordination_error(format!("cannot stat snapshot root: {error}")))?;
871    if !metadata.file_type().is_dir()
872        || metadata.uid() != geteuid().as_raw()
873        || metadata.mode() & 0o222 != 0
874    {
875        return Err(coordination_error(
876            "coordination snapshot root must be owner-retained and non-writable",
877        ));
878    }
879    Ok((root, SnapshotFingerprint::from_metadata(&metadata)))
880}
881
882fn open_retained_file(
883    root: &File,
884    binding: CoordinationFileBinding,
885    label: &str,
886) -> PromotionResult<RetainedCoordinationFile> {
887    let root_path = binding
888        .absolute_path
889        .parent()
890        .ok_or_else(|| coordination_error(format!("{label} has no snapshot parent")))?;
891    let relative = direct_child_name(root_path, &binding.absolute_path, label)?;
892    let descriptor = openat2(
893        root,
894        relative.as_str(),
895        OFlags::RDONLY | OFlags::NONBLOCK | OFlags::CLOEXEC | OFlags::NOFOLLOW,
896        Mode::empty(),
897        ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
898    )
899    .map_err(|error| coordination_error(format!("cannot securely open {label}: {error}")))?;
900    let file = File::from(descriptor);
901    let fingerprint = immutable_file_fingerprint(&file, label)?;
902    if fingerprint.bytes != binding.bytes {
903        return Err(coordination_error(format!(
904            "{label} descriptor length differs from its request binding"
905        )));
906    }
907    Ok(RetainedCoordinationFile {
908        relative,
909        file,
910        fingerprint,
911        binding,
912    })
913}
914
915fn immutable_file_fingerprint(file: &File, label: &str) -> PromotionResult<SnapshotFingerprint> {
916    let metadata = file
917        .metadata()
918        .map_err(|error| coordination_error(format!("cannot stat {label}: {error}")))?;
919    let allocated = metadata.blocks().saturating_mul(512);
920    if !metadata.file_type().is_file()
921        || metadata.nlink() != 1
922        || metadata.uid() != geteuid().as_raw()
923        || metadata.mode() & 0o222 != 0
924        || metadata.size() == 0
925        || allocated < metadata.size()
926    {
927        return Err(coordination_error(format!(
928            "{label} must be a nonempty, non-sparse, unaliased, owner-retained, non-writable regular file"
929        )));
930    }
931    Ok(SnapshotFingerprint::from_metadata(&metadata))
932}
933
934fn revalidate_root_path(
935    root_path: &Path,
936    root: &File,
937    expected: &SnapshotFingerprint,
938) -> PromotionResult<()> {
939    let descriptor_metadata = root
940        .metadata()
941        .map_err(|error| coordination_error(format!("cannot restat snapshot root: {error}")))?;
942    let descriptor_fingerprint = SnapshotFingerprint::from_metadata(&descriptor_metadata);
943    if descriptor_fingerprint != *expected {
944        return Err(coordination_error(
945            "coordination snapshot root descriptor changed",
946        ));
947    }
948    let (path_root, path_fingerprint) = open_immutable_root(root_path)?;
949    drop(path_root);
950    if path_fingerprint != *expected {
951        return Err(coordination_error(
952            "coordination snapshot root pathname changed",
953        ));
954    }
955    Ok(())
956}
957
958fn revalidate_retained_file(
959    root: &File,
960    retained: &RetainedCoordinationFile,
961) -> PromotionResult<()> {
962    let descriptor_fingerprint = immutable_file_fingerprint(&retained.file, &retained.relative)?;
963    if descriptor_fingerprint != retained.fingerprint {
964        return Err(coordination_error(format!(
965            "retained coordination file descriptor changed: {}",
966            retained.relative
967        )));
968    }
969    let descriptor = openat2(
970        root,
971        retained.relative.as_str(),
972        OFlags::RDONLY | OFlags::NONBLOCK | OFlags::CLOEXEC | OFlags::NOFOLLOW,
973        Mode::empty(),
974        ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
975    )
976    .map_err(|error| {
977        coordination_error(format!(
978            "cannot reopen retained coordination path {}: {error}",
979            retained.relative
980        ))
981    })?;
982    let path_file = File::from(descriptor);
983    let path_fingerprint = immutable_file_fingerprint(&path_file, &retained.relative)?;
984    if path_fingerprint != retained.fingerprint {
985        return Err(coordination_error(format!(
986            "coordination pathname no longer identifies its retained file: {}",
987            retained.relative
988        )));
989    }
990    Ok(())
991}
992
993fn read_small_retained(
994    retained: &RetainedCoordinationFile,
995    maximum_bytes: u64,
996) -> PromotionResult<Vec<u8>> {
997    if retained.fingerprint.bytes > maximum_bytes {
998        return Err(coordination_error(format!(
999            "retained coordination file exceeds its strict bound: {}",
1000            retained.relative
1001        )));
1002    }
1003    let capacity = usize::try_from(retained.fingerprint.bytes).map_err(|_| {
1004        coordination_error(format!(
1005            "retained coordination file is too large for this host: {}",
1006            retained.relative
1007        ))
1008    })?;
1009    let mut bytes = vec![0_u8; capacity];
1010    retained
1011        .file
1012        .read_exact_at(&mut bytes, 0)
1013        .map_err(|error| {
1014            coordination_error(format!(
1015                "cannot read retained coordination file {}: {error}",
1016                retained.relative
1017            ))
1018        })?;
1019    let mut extra = [0_u8; 1];
1020    if retained
1021        .file
1022        .read_at(&mut extra, retained.fingerprint.bytes)
1023        .map_err(|error| {
1024            coordination_error(format!(
1025                "cannot check retained coordination EOF {}: {error}",
1026                retained.relative
1027            ))
1028        })?
1029        != 0
1030        || immutable_file_fingerprint(&retained.file, &retained.relative)? != retained.fingerprint
1031        || sha256_hex(&bytes) != retained.binding.sha256
1032    {
1033        return Err(coordination_error(format!(
1034            "retained coordination file changed or differs from its binding: {}",
1035            retained.relative
1036        )));
1037    }
1038    Ok(bytes)
1039}
1040
1041fn verify_history_manifest(
1042    bytes: &[u8],
1043    history: &RetainedCoordinationFile,
1044) -> PromotionResult<()> {
1045    if bytes.is_empty() || bytes.last() != Some(&b'\n') || !bytes.is_ascii() {
1046        return Err(coordination_error(
1047            "coordination history manifest is empty, truncated, or non-ASCII",
1048        ));
1049    }
1050    let body = &bytes[..bytes.len() - 1];
1051    if body.contains(&b'\n')
1052        || body.len() != 66_usize.saturating_add(history.relative.len())
1053        || body.get(64..66) != Some(&b"  "[..])
1054        || body.get(..64) != Some(history.binding.sha256.as_bytes())
1055        || body.get(66..) != Some(history.relative.as_bytes())
1056    {
1057        return Err(coordination_error(
1058            "coordination history manifest does not contain the exact sole history binding",
1059        ));
1060    }
1061    Ok(())
1062}
1063
1064fn verify_streaming_history(
1065    history: &RetainedCoordinationFile,
1066    selected_receipt: &[u8],
1067    expected_run_id: &str,
1068) -> PromotionResult<CoordinationChain> {
1069    let selected_wire = parse_selected_receipt(selected_receipt)?;
1070    let mut builder = CoordinationChainBuilder::new();
1071    let mut hasher = Sha256::new();
1072    let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
1073    let mut line = Vec::with_capacity(MAXIMUM_RECORD_BYTES.min(4096));
1074    let mut offset = 0_u64;
1075    while offset < history.fingerprint.bytes {
1076        let remaining = history.fingerprint.bytes - offset;
1077        let requested = usize::try_from(
1078            remaining.min(u64::try_from(buffer.len()).expect("buffer length fits u64")),
1079        )
1080        .expect("bounded history chunk fits usize");
1081        let amount = history
1082            .file
1083            .read_at(&mut buffer[..requested], offset)
1084            .map_err(|error| {
1085                coordination_error(format!("cannot stream coordination history: {error}"))
1086            })?;
1087        if amount == 0 {
1088            return Err(coordination_error(
1089                "coordination history was truncated during streaming",
1090            ));
1091        }
1092        hasher.update(&buffer[..amount]);
1093        for byte in &buffer[..amount] {
1094            line.push(*byte);
1095            if line.len() > MAXIMUM_RECORD_BYTES {
1096                return Err(coordination_error(
1097                    "coordination history record exceeds its strict bound",
1098                ));
1099            }
1100            if *byte == b'\n' {
1101                builder.push(&line)?;
1102                line.clear();
1103            }
1104        }
1105        offset = offset
1106            .checked_add(u64::try_from(amount).expect("usize fits u64"))
1107            .ok_or_else(|| coordination_error("coordination history byte-count overflow"))?;
1108    }
1109    let mut extra = [0_u8; 1];
1110    if !line.is_empty()
1111        || history
1112            .file
1113            .read_at(&mut extra, history.fingerprint.bytes)
1114            .map_err(|error| {
1115                coordination_error(format!("cannot check coordination history EOF: {error}"))
1116            })?
1117            != 0
1118        || immutable_file_fingerprint(&history.file, &history.relative)? != history.fingerprint
1119        || offset != history.binding.bytes
1120        || hex_lower(&hasher.finalize()) != history.binding.sha256
1121    {
1122        return Err(coordination_error(
1123            "coordination history is truncated, changed, or differs from its exact binding",
1124        ));
1125    }
1126    builder.finish(expected_run_id, selected_wire, selected_receipt)
1127}
1128
1129fn coordination_error(message: impl Into<String>) -> PromotionError {
1130    PromotionError::new(FailureCode::CoordinationChain, message)
1131}
1132
1133/// Verifies a complete, bounded, canonical JSONL history from sequence zero.
1134///
1135/// The expected byte count and digest must come from the separately sealed
1136/// history-snapshot manifest. Every record is reserialized and rehashed; the
1137/// chain rejects gaps, forks represented by a wrong predecessor, and global
1138/// replay of run IDs, intent digests, or grant triples.
1139///
1140/// # Errors
1141///
1142/// Returns [`FailureCode::CoordinationChain`] for malformed or incomplete
1143/// history and [`FailureCode::CoordinationReplay`] for a repeated identity.
1144pub fn verify_complete_burn_history(
1145    request: &PromotionRequest,
1146    bytes: &[u8],
1147    selected_receipt: &[u8],
1148) -> PromotionResult<CoordinationChain> {
1149    let (expected_bytes, expected_sha256) = request.coordination_history_binding();
1150    if bytes.is_empty()
1151        || bytes.len() > MAXIMUM_HISTORY_BYTES
1152        || u64::try_from(bytes.len()).ok() != Some(expected_bytes)
1153        || !is_lower_hex(expected_sha256, 64, false)
1154        || sha256_hex(bytes) != expected_sha256
1155        || bytes.last() != Some(&b'\n')
1156    {
1157        return Err(PromotionError::new(
1158            FailureCode::CoordinationChain,
1159            "coordination history is empty, truncated, oversized, or differs from its manifest binding",
1160        ));
1161    }
1162    let (selected_receipt_expected_bytes, selected_receipt_expected_sha256) =
1163        request.selected_burn_binding();
1164    if selected_receipt.is_empty()
1165        || u64::try_from(selected_receipt.len()).ok() != Some(selected_receipt_expected_bytes)
1166        || !is_lower_hex(selected_receipt_expected_sha256, 64, false)
1167        || sha256_hex(selected_receipt) != selected_receipt_expected_sha256
1168    {
1169        return Err(PromotionError::new(
1170            FailureCode::CoordinationChain,
1171            "selected coordination receipt or run binding is non-canonical or differs from its manifest binding",
1172        ));
1173    }
1174    let selected_wire = parse_selected_receipt(selected_receipt)?;
1175    let mut builder = CoordinationChainBuilder::new();
1176    for line in bytes.split_inclusive(|byte| *byte == b'\n') {
1177        builder.push(line)?;
1178    }
1179    builder.finish(request.run_id(), selected_wire, selected_receipt)
1180}
1181
1182#[cfg(test)]
1183fn synthetic_burn_record(sequence: u64, previous_sha256: &str, salt: u8) -> CoordinationBurnWire {
1184    let mut wire = CoordinationBurnWire {
1185        schema: BURN_SCHEMA,
1186        contract: BURN_CONTRACT.to_owned(),
1187        sequence,
1188        run_id: format!("{salt:02x}{}", "1".repeat(30)),
1189        intent: IntentBinding {
1190            bytes: 100,
1191            sha256: format!("{salt:02x}{}", "2".repeat(62)),
1192        },
1193        grants: vec![
1194            CoordinationGrantWire {
1195                ledger_id: PROJECT_LEDGER.to_owned(),
1196                entry_id: format!("project-{salt}"),
1197                entry_bytes: 10,
1198                entry_sha256: format!("{salt:02x}{}", "3".repeat(62)),
1199            },
1200            CoordinationGrantWire {
1201                ledger_id: ED25519_LEDGER.to_owned(),
1202                entry_id: format!("ed25519-{salt}"),
1203                entry_bytes: 11,
1204                entry_sha256: format!("{salt:02x}{}", "4".repeat(62)),
1205            },
1206        ],
1207        previous_sha256: previous_sha256.to_owned(),
1208        record_sha256: String::new(),
1209    };
1210    wire.record_sha256 = wire.recomputed_record_sha256().unwrap();
1211    wire
1212}
1213
1214#[cfg(test)]
1215pub(crate) fn synthetic_chain(salt: u8) -> CoordinationChain {
1216    let wire = synthetic_burn_record(0, GENESIS_SHA256, salt);
1217    let receipt = canonical_line(&wire).unwrap();
1218    let request =
1219        crate::model::synthetic_request_with_coordination(&wire.run_id, &receipt, &receipt);
1220    verify_complete_burn_history(&request, &receipt, &receipt).unwrap()
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225    use super::{
1226        CoordinationBurnWire, CoordinationSnapshotBinding, GENESIS_SHA256, synthetic_burn_record,
1227        verify_complete_burn_history,
1228    };
1229    use crate::FailureCode;
1230    use crate::canonical::{canonical_line, sha256_hex};
1231    use crate::model::{
1232        synthetic_request_with_coordination, synthetic_request_with_coordination_snapshot,
1233    };
1234    use std::fs;
1235    use std::os::unix::fs::PermissionsExt;
1236    use std::path::{Path, PathBuf};
1237    use std::sync::atomic::{AtomicU64, Ordering};
1238
1239    static NEXT_SNAPSHOT_ROOT: AtomicU64 = AtomicU64::new(0);
1240
1241    struct TestSnapshotRoot {
1242        path: PathBuf,
1243    }
1244
1245    impl TestSnapshotRoot {
1246        fn new() -> Self {
1247            let path = std::env::temp_dir().join(format!(
1248                "hashsigs-promoter-coordination-{}-{}",
1249                std::process::id(),
1250                NEXT_SNAPSHOT_ROOT.fetch_add(1, Ordering::Relaxed)
1251            ));
1252            fs::create_dir(&path).unwrap();
1253            set_mode(&path, 0o700);
1254            Self { path }
1255        }
1256
1257        fn path(&self) -> &Path {
1258            &self.path
1259        }
1260
1261        fn child(&self, name: &str) -> PathBuf {
1262            self.path.join(name)
1263        }
1264
1265        fn write_file(&self, name: &str, bytes: &[u8], mode: u32) -> PathBuf {
1266            let path = self.child(name);
1267            fs::write(&path, bytes).unwrap();
1268            set_mode(&path, mode);
1269            path
1270        }
1271
1272        fn seal(&self) {
1273            set_mode(&self.path, 0o500);
1274        }
1275    }
1276
1277    impl Drop for TestSnapshotRoot {
1278        fn drop(&mut self) {
1279            make_writable(&self.path);
1280            let _ = fs::remove_dir_all(&self.path);
1281        }
1282    }
1283
1284    struct SnapshotFixture {
1285        root: TestSnapshotRoot,
1286        request: crate::PromotionRequest,
1287        selected_path: PathBuf,
1288        selected_bytes: Vec<u8>,
1289    }
1290
1291    fn set_mode(path: &Path, mode: u32) {
1292        fs::set_permissions(path, fs::Permissions::from_mode(mode)).unwrap();
1293    }
1294
1295    fn make_writable(path: &Path) {
1296        let Ok(metadata) = fs::symlink_metadata(path) else {
1297            return;
1298        };
1299        if metadata.file_type().is_symlink() {
1300            let _ = fs::remove_file(path);
1301        } else if metadata.is_dir() {
1302            let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o700));
1303            if let Ok(entries) = fs::read_dir(path) {
1304                for entry in entries.flatten() {
1305                    make_writable(&entry.path());
1306                }
1307            }
1308        } else {
1309            let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o600));
1310        }
1311    }
1312
1313    fn history_manifest(history: &[u8]) -> Vec<u8> {
1314        format!("{}  history.jsonl\n", sha256_hex(history)).into_bytes()
1315    }
1316
1317    fn snapshot_request(
1318        root: &TestSnapshotRoot,
1319        run_id: &str,
1320        history: &[u8],
1321        manifest: &[u8],
1322        selected: &[u8],
1323    ) -> crate::PromotionRequest {
1324        let history_path = root.child("history.jsonl");
1325        let manifest_path = root.child("history.SHA256SUMS");
1326        let selected_path = root.child("selected.json");
1327        synthetic_request_with_coordination_snapshot(
1328            run_id,
1329            [
1330                (history_path.to_str().unwrap(), history),
1331                (manifest_path.to_str().unwrap(), manifest),
1332                (selected_path.to_str().unwrap(), selected),
1333            ],
1334        )
1335    }
1336
1337    fn valid_snapshot_fixture(salt: u8) -> SnapshotFixture {
1338        let burn = record(0, GENESIS_SHA256, salt);
1339        let history = canonical_line(&burn).unwrap();
1340        let selected_bytes = history.clone();
1341        let manifest = history_manifest(&history);
1342        let root = TestSnapshotRoot::new();
1343        root.write_file("history.jsonl", &history, 0o400);
1344        root.write_file("history.SHA256SUMS", &manifest, 0o400);
1345        let selected_path = root.write_file("selected.json", &selected_bytes, 0o400);
1346        let request = snapshot_request(&root, &burn.run_id, &history, &manifest, &selected_bytes);
1347        root.seal();
1348        SnapshotFixture {
1349            root,
1350            request,
1351            selected_path,
1352            selected_bytes,
1353        }
1354    }
1355
1356    fn record(sequence: u64, previous: &str, salt: u8) -> CoordinationBurnWire {
1357        synthetic_burn_record(sequence, previous, salt)
1358    }
1359
1360    fn verify(
1361        history: &[u8],
1362        selected: &CoordinationBurnWire,
1363    ) -> crate::PromotionResult<super::CoordinationChain> {
1364        let selected_bytes = canonical_line(selected).unwrap();
1365        let request =
1366            synthetic_request_with_coordination(&selected.run_id, history, &selected_bytes);
1367        verify_complete_burn_history(&request, history, &selected_bytes)
1368    }
1369
1370    #[test]
1371    fn complete_chain_recomputes_genesis_and_global_uniqueness() {
1372        let first = record(0, GENESIS_SHA256, 1);
1373        let second = record(1, first.record_sha256.as_str(), 2);
1374        let mut history = canonical_line(&first).unwrap();
1375        history.extend(canonical_line(&second).unwrap());
1376        let state = verify(&history, &second).unwrap();
1377        assert_eq!(state.records(), 2);
1378        assert_eq!(state.selected_burn().run_id(), second.run_id.as_str());
1379        assert_eq!(
1380            state.selected_burn().record_sha256(),
1381            second.record_sha256.as_str()
1382        );
1383        assert!(state.contains_run_id(&first.run_id));
1384        assert!(state.contains_grant(
1385            &first.grants[0].ledger_id,
1386            &first.grants[0].entry_id,
1387            &first.grants[0].entry_sha256
1388        ));
1389    }
1390
1391    #[test]
1392    fn gap_and_replay_are_rejected() {
1393        let first = record(0, GENESIS_SHA256, 1);
1394        let gap = record(2, first.record_sha256.as_str(), 2);
1395        let mut gap_history = canonical_line(&first).unwrap();
1396        gap_history.extend(canonical_line(&gap).unwrap());
1397        assert_eq!(
1398            verify(&gap_history, &gap).unwrap_err().code(),
1399            FailureCode::CoordinationChain
1400        );
1401
1402        let mut replay = record(1, first.record_sha256.as_str(), 2);
1403        replay.run_id.clone_from(&first.run_id);
1404        replay.record_sha256 = replay.recomputed_record_sha256().unwrap();
1405        let mut replay_history = canonical_line(&first).unwrap();
1406        replay_history.extend(canonical_line(&replay).unwrap());
1407        assert_eq!(
1408            verify(&replay_history, &replay).unwrap_err().code(),
1409            FailureCode::CoordinationReplay
1410        );
1411    }
1412
1413    #[test]
1414    fn coordination_replay_key_ignores_changed_grant_byte_metadata() {
1415        let first = record(0, GENESIS_SHA256, 1);
1416        let mut second = record(1, &first.record_sha256, 2);
1417        second.grants[0]
1418            .ledger_id
1419            .clone_from(&first.grants[0].ledger_id);
1420        second.grants[0]
1421            .entry_id
1422            .clone_from(&first.grants[0].entry_id);
1423        second.grants[0]
1424            .entry_sha256
1425            .clone_from(&first.grants[0].entry_sha256);
1426        second.grants[0].entry_bytes = first.grants[0].entry_bytes + 1;
1427        second.record_sha256 = second.recomputed_record_sha256().unwrap();
1428        let mut history = canonical_line(&first).unwrap();
1429        history.extend(canonical_line(&second).unwrap());
1430        assert_eq!(
1431            verify(&history, &second).unwrap_err().code(),
1432            FailureCode::CoordinationReplay
1433        );
1434    }
1435
1436    #[test]
1437    fn selected_receipt_and_run_must_bind_the_exact_last_record() {
1438        let first = record(0, GENESIS_SHA256, 1);
1439        let second = record(1, first.record_sha256.as_str(), 2);
1440        let mut history = canonical_line(&first).unwrap();
1441        history.extend(canonical_line(&second).unwrap());
1442        let first_bytes = canonical_line(&first).unwrap();
1443        let request = synthetic_request_with_coordination(&second.run_id, &history, &first_bytes);
1444        assert_eq!(
1445            verify_complete_burn_history(&request, &history, &first_bytes,)
1446                .unwrap_err()
1447                .code(),
1448            FailureCode::CoordinationChain
1449        );
1450    }
1451
1452    #[test]
1453    fn retained_snapshot_binding_is_derived_only_from_request_locators() {
1454        let request =
1455            synthetic_request_with_coordination(&"1".repeat(32), b"history\n", b"receipt\n");
1456        let binding = CoordinationSnapshotBinding::from_request(&request).unwrap();
1457        assert_eq!(binding.root, std::path::Path::new("/evidence"));
1458        assert_eq!(
1459            binding.history.absolute_path,
1460            std::path::Path::new("/evidence/coordination-history.jsonl")
1461        );
1462        assert_eq!(binding.history.bytes, 8);
1463        assert_eq!(
1464            binding.history.sha256,
1465            crate::canonical::sha256_hex(b"history\n")
1466        );
1467        assert_eq!(
1468            binding.manifest.absolute_path,
1469            std::path::Path::new("/evidence/coordination-history.SHA256SUMS")
1470        );
1471        assert_eq!(binding.manifest.bytes, 10);
1472        assert_eq!(binding.manifest.sha256, "8".repeat(64));
1473        assert_eq!(
1474            binding.selected_receipt.absolute_path,
1475            std::path::Path::new("/evidence/coordination-burn.json")
1476        );
1477        assert_eq!(binding.selected_receipt.bytes, 8);
1478        assert_eq!(
1479            binding.selected_receipt.sha256,
1480            crate::canonical::sha256_hex(b"receipt\n")
1481        );
1482    }
1483
1484    #[test]
1485    fn retained_snapshot_rejects_manifest_content_mismatch() {
1486        let burn = record(0, GENESIS_SHA256, 3);
1487        let history = canonical_line(&burn).unwrap();
1488        let manifest = format!("{}  history.jsonl\n", "0".repeat(64)).into_bytes();
1489        let root = TestSnapshotRoot::new();
1490        root.write_file("history.jsonl", &history, 0o400);
1491        root.write_file("history.SHA256SUMS", &manifest, 0o400);
1492        root.write_file("selected.json", &history, 0o400);
1493        let request = snapshot_request(&root, &burn.run_id, &history, &manifest, &history);
1494        root.seal();
1495        assert_eq!(
1496            super::retain_verified_coordination_snapshot(&request)
1497                .unwrap_err()
1498                .code(),
1499            FailureCode::CoordinationChain
1500        );
1501    }
1502
1503    #[test]
1504    fn retained_snapshot_rejects_hardlink_alias() {
1505        let burn = record(0, GENESIS_SHA256, 4);
1506        let history = canonical_line(&burn).unwrap();
1507        let manifest = history_manifest(&history);
1508        let root = TestSnapshotRoot::new();
1509        let history_path = root.write_file("history.jsonl", &history, 0o400);
1510        root.write_file("history.SHA256SUMS", &manifest, 0o400);
1511        fs::hard_link(&history_path, root.child("selected.json")).unwrap();
1512        let request = snapshot_request(&root, &burn.run_id, &history, &manifest, &history);
1513        root.seal();
1514        assert_eq!(
1515            super::retain_verified_coordination_snapshot(&request)
1516                .unwrap_err()
1517                .code(),
1518            FailureCode::CoordinationChain
1519        );
1520    }
1521
1522    #[test]
1523    fn retained_snapshot_rejects_writable_inputs() {
1524        let burn = record(0, GENESIS_SHA256, 5);
1525        let history = canonical_line(&burn).unwrap();
1526        let manifest = history_manifest(&history);
1527        let root = TestSnapshotRoot::new();
1528        root.write_file("history.jsonl", &history, 0o600);
1529        root.write_file("history.SHA256SUMS", &manifest, 0o400);
1530        root.write_file("selected.json", &history, 0o400);
1531        let request = snapshot_request(&root, &burn.run_id, &history, &manifest, &history);
1532        root.seal();
1533        assert_eq!(
1534            super::retain_verified_coordination_snapshot(&request)
1535                .unwrap_err()
1536                .code(),
1537            FailureCode::CoordinationChain
1538        );
1539    }
1540
1541    #[test]
1542    fn retained_snapshot_final_revalidation_detects_path_substitution() {
1543        let fixture = valid_snapshot_fixture(6);
1544        let snapshot = super::retain_verified_coordination_snapshot(&fixture.request).unwrap();
1545        snapshot.revalidate().unwrap();
1546
1547        set_mode(fixture.root.path(), 0o700);
1548        fs::rename(&fixture.selected_path, fixture.root.child("selected.old")).unwrap();
1549        fs::write(&fixture.selected_path, &fixture.selected_bytes).unwrap();
1550        set_mode(&fixture.selected_path, 0o400);
1551        fixture.root.seal();
1552        assert_eq!(
1553            snapshot.revalidate().unwrap_err().code(),
1554            FailureCode::CoordinationChain
1555        );
1556    }
1557
1558    #[test]
1559    fn retained_snapshot_final_revalidation_detects_in_place_change() {
1560        let fixture = valid_snapshot_fixture(7);
1561        let snapshot = super::retain_verified_coordination_snapshot(&fixture.request).unwrap();
1562        snapshot.revalidate().unwrap();
1563
1564        let mut changed = fixture.selected_bytes.clone();
1565        changed[0] ^= 1;
1566        set_mode(&fixture.selected_path, 0o600);
1567        fs::write(&fixture.selected_path, changed).unwrap();
1568        set_mode(&fixture.selected_path, 0o400);
1569        assert_eq!(
1570            snapshot.revalidate().unwrap_err().code(),
1571            FailureCode::CoordinationChain
1572        );
1573    }
1574}