astrid-runtime/astrid · critical

capsule ' ' hash mismatch: lock has , archive has

Error message

capsule '{}' hash mismatch: lock has {}, archive has {actual}

What it means

For each locked capsule, verify_capsule_hashes reads the mirrored archive and computes its blake3 hash, comparing it to the hash recorded in the lock. A mismatch means the archive on the shuttle differs from what was sealed — corruption or substitution. The install aborts before any side effects.

Solutions

  1. Re-copy or rebuild the shuttle so each capsule archive matches its locked blake3 hash
  2. Check the source media/storage for corruption and re-transfer
  3. Never replace archive files on a sealed shuttle; repack instead
Defensive patterns

Strategy: validation

Validate before calling

let bytes = std::fs::read(&file)?;
let actual = format!("blake3:{}", blake3::hash(&bytes).to_hex());
if entry.hash != actual {
    return Err(anyhow!("capsule '{}' hash mismatch", entry.name));
}

Prevention

When it happens

Trigger: install_from_shuttle where a mirrored capsule file's blake3 hash differs from lock.capsules[].hash — bit rot on removable media, truncated copy, or a replaced archive file.

Common situations: USB media corruption or incomplete copy; someone swapped an archive on the shuttle; shuttle contents edited after sealing.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/aa521a1d869bf282. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/distro/shuttle_install.rs:311

/// Verify the per-capsule blake3 of every lock entry against the bytes
/// actually present in the mirror. Returns an error on the first
/// mismatch or missing file. Pure (no install side effects) so the
/// integrity gate is unit-testable.
fn verify_capsule_hashes(mirror: &Path, lock: &DistroLock) -> anyhow::Result<()> {
    for entry in &lock.capsules {
        let file = shuttle::capsule_mirror_path(mirror, &entry.name);
        if !file.is_file() {
            bail!(
                "capsule '{}' is missing from the shuttle mirror",
                entry.name
            );
        }
        let bytes = std::fs::read(&file)
            .with_context(|| format!("failed to read mirrored capsule {}", entry.name))?;
        let actual = format!("blake3:{}", blake3::hash(&bytes).to_hex());
        if entry.hash != actual {
            bail!(
                "capsule '{}' hash mismatch: lock has {}, archive has {actual}",
                entry.name,
                entry.hash
            );
        }
    }
    Ok(())
}

fn source_digest(path: &Path) -> anyhow::Result<String> {
    let mut file = std::fs::File::open(path)
        .with_context(|| format!("open capsule source {}", path.display()))?;
    let mut hasher = blake3::Hasher::new();
    let mut buffer = vec![0_u8; 1024 * 1024].into_boxed_slice();
    loop {
        let count = file.read(&mut buffer)?;
        if count == 0 {
            break;

View on GitHub (pinned to affd8760f4)