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
- Ensure only one migration/install process runs against a given capsule home at a time (file lock or single-flight wrapper)
- Re-run the migration once the concurrent writer has finished; the transient race resolves on retry
- 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
- Hold an exclusive lock on the capsule home for the entire migration/install
- Never run a capsule install concurrently with a legacy migration on the same home
- Exclude capsule directories from antivirus/cleanup tools that quarantine files mid-read
- After receipt-path layout changes, confirm read_installed_authority and read_installed_authority_bytes resolve identical paths
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
- legacy capsule {id} authority verification produced no recei
- legacy capsule authority identity differs for {id}
- legacy capsule {id} changed before retirement
- legacy capsule {id} metadata changed before retirement
- corpus input changed while its baseline snapshot was capture
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/ad17e841a9aa36a9.
Report an issue: GitHub.