astrid-runtime/astrid · error

legacy capsule {id} authority receipt disappeared

Error message

legacy capsule {id} authority receipt disappeared

What it means

Immediately after reading and validating the authority receipt, migration reads the raw receipt bytes with `read_installed_authority_bytes` to embed into the durable record. If that second read returns `None`, the receipt that existed moments earlier has disappeared — either deleted concurrently or due to a state mismatch between the two read functions. The error protects against building a durable authority from mismatched or vanishing data.

Source

Thrown at crates/astrid-capsule-install/src/storage/migration.rs:162

            bail!("legacy capsule metadata version differs for {id}");
        }
        // Released pre-authority installs are admitted only through the
        // existing one-time verifier. It pins their exact manifest,
        // capabilities, and executable before any durable publication.
        // Relocated homes keep receipts hashed from a previous absolute
        // path; rebind a unique leftover onto this target first.
        rebind_relocated_legacy_authority_receipt(home, &target, &manifest, workspace_targets)
            .with_context(|| format!("rebind relocated leftover authority for {id}"))?;
        verify_installed_authority(home, &target, &manifest)
            .with_context(|| format!("verify legacy capsule authority {id}"))?;
        let authority = read_installed_authority(home, &target)?.ok_or_else(|| {
            anyhow::anyhow!("legacy capsule {id} authority verification produced no receipt")
        })?;
        if authority.capsule_id != id || authority.version != manifest.package.version {
            bail!("legacy capsule authority identity differs for {id}");
        }
        let source_authority_bytes = read_installed_authority_bytes(home, &target)?
            .ok_or_else(|| anyhow::anyhow!("legacy capsule {id} authority receipt disappeared"))?;
        let archive = canonical_legacy_archive(home, &target, &meta, &manifest)?;
        let verification = artifact::verify_archive_bytes(&archive)
            .with_context(|| format!("verify canonical legacy capsule archive {id}"))?;
        let mut durable_authority = authority;
        verification
            .content_digest()
            .clone_into(&mut durable_authority.content_digest);
        let durable_authority_bytes = serde_json::to_vec_pretty(&durable_authority)
            .with_context(|| format!("serialize durable legacy capsule authority {id}"))?;
        let package = CapsulePackage::new(archive, meta_bytes, durable_authority_bytes);
        let expectation = match registry.get_snapshot(&owner, id)? {
            None => CapsuleInstallExpectation::Absent,
            Some(snapshot) if snapshot.package() == &package => {
                CapsuleInstallExpectation::Generation(snapshot.generation())
            },
            Some(_) => bail!("durable capsule {id} conflicts with legacy native content"),
        };
        registry.install(&owner, id, &package, expectation)?;

View on GitHub (pinned to affd8760f4)

Solutions

  1. Ensure only one migration/install process runs against a given capsule home at a time (file lock or single-flight wrapper)
  2. Re-run the migration once the concurrent writer has finished; the transient race resolves on retry
  3. Check that `read_installed_authority` and `read_installed_authority_bytes` resolve the same receipt path for your install layout (especially after path-hashed receipt relocation changes) and align them

Example fix

// before
let (a, b) = tokio::join!(migrate_native_capsules(home), reinstall_capsule(home)); // concurrent delete/recreate

// after
let lock = home.lock("migration")?;
let report = migrate_native_capsules(home)?;
drop(lock);
Defensive patterns

Strategy: try-catch

Validate before calling

fn assert_no_concurrent_writers(home: &Path) -> anyhow::Result<()> {
    let lock_path = home.join(".migration.lock");
    anyhow::ensure!(!lock_path.exists(),
        "another migration/install appears to be running (lock present)");
    std::fs::write(&lock_path, std::process::id().to_string())?;
    Ok(())
}

Type guard

fn receipt_readable_twice(home: &Path, target: &Path) -> bool {
    matches!(read_installed_authority(home, target), Ok(Some(_)))
        && matches!(read_installed_authority_bytes(home, target), Ok(Some(_)))
}

Try / catch

match migrate_native_capsules(home) {
    Err(e) if e.to_string().contains("receipt disappeared") => {
        // transient race or path-layout mismatch: serialize access, then retry once
        let _lock = acquire_home_lock(home)?;
        migrate_native_capsules(home)?
    }
    Err(e) => return Err(e),
    Ok(r) => r,
}

Prevention

When it happens

Trigger: Calling `migrate_native_capsules_with_report` when a concurrent process deletes or rewrites the authority receipt between `read_installed_authority` and `read_installed_authority_bytes`; or when the two functions disagree on the receipt's location/format so the byte read finds nothing where the structured read succeeded.

Common situations: Two capsule migrations or an install running in parallel on the same home directory; an antivirus/cleanup tool quarantining files mid-migration; a customized receipt path layout confusing one of the read helpers after a version change.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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