astrid-runtime/astrid · error

legacy capsule {id} authority changed before retirement

Error message

legacy capsule {id} authority changed before retirement

What it means

This error is raised during legacy capsule migration when the installed capsule at the migration target no longer matches the expected state at retirement time. Specifically, the bytes recorded as the installed authority for the capsule no longer equal the authority bytes from the legacy source being migrated. Migration is a one-shot, verifiable copy: if the target's authority was modified between an earlier partial migration and this pass, the code refuses to retire the legacy source rather than destroy a divergent installation.

Source

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

        if readback.package() != &package {
            bail!("durable capsule {id} failed byte-for-byte readback");
        }
        read_verified_durable_package_for_owner(store, &owner, id)?.ok_or_else(|| {
            anyhow::anyhow!("durable capsule {id} failed authoritative verification")
        })?;
        astrid_core::platform_fs::verify_no_redirects(&target)
            .with_context(|| format!("verify legacy capsule {id} before retirement"))?;
        let final_archive = canonical_legacy_archive(home, &target, &meta, &manifest)?;
        if final_archive != package.archive {
            bail!("legacy capsule {id} changed before retirement");
        }
        if fs::read(target.join("meta.json"))? != package.metadata {
            bail!("legacy capsule {id} metadata changed before retirement");
        }
        if read_installed_authority_bytes(home, &target)?.as_deref()
            != Some(source_authority_bytes.as_slice())
        {
            bail!("legacy capsule {id} authority changed before retirement");
        }
        astrid_core::platform_fs::verify_no_redirects(&target)
            .with_context(|| format!("verify legacy capsule {id} retirement boundary"))?;
        astrid_core::dirs::retire_legacy_source_tree(&target)
            .with_context(|| format!("retire migrated legacy capsule {id}"))?;
        retire_legacy_authority_receipt(home, &target, &source_authority_bytes)
            .with_context(|| format!("retire migrated legacy capsule {id} authority"))?;
        report
            .retired_authorities
            .push(LegacyCapsuleAuthorityReceipt {
                uid,
                capsule_id: id.to_owned(),
                authority_digest: blake3::hash(&package.authority).to_hex().to_string(),
            });
    }
    Ok(report)
}

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the target capsule's installed authority receipt and compare it with the legacy source authority; determine which is current.
  2. If the target is the newer, correct install, remove it (or its receipt) so migration can redo the copy cleanly, or finish retirement manually with retire_legacy_source_tree and retire_legacy_authority_receipt.
  3. If the legacy source is newer, re-copy it over the target so authority bytes match, then re-run migration.
  4. Avoid running concurrent migrations; ensure only one astrid process migrates at a time.

Example fix

// before (target diverged after partial migration)
// migration bails: authority bytes differ

// after: reset diverged target so migration can complete atomically
let target = home.capsules().join("my-capsule");
std::fs::remove_dir_all(&target)?; // only if the legacy source is authoritative
let report = migrate_native_capsules(&home, &directory)?;
Defensive patterns

Strategy: validation

Validate before calling

let target = home.capsules().join(alias);
if target.exists() {
    let installed = read_installed_authority_bytes(home, &target)?;
    if installed.as_deref() != Some(source_authority_bytes.as_slice()) {
        eprintln!("target diverged; reset or reconcile before migrating");
    }
}

Type guard

fn authority_matches(installed: Option<&[u8]>, source: &[u8]) -> bool {
    installed == Some(source)
}

Try / catch

match migrate_native_capsules(&home, &directory) {
    Ok(report) => println!("migrated: {:?}", report),
    Err(e) if e.to_string().contains("authority changed before retirement") => {
        eprintln!("target capsule diverged; reconcile and re-run");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling migrate_native_capsules, migrate_all_native_capsules_with_report, or migrate_legacy_layout when the target capsule directory's installed authority receipt (read via read_installed_authority_bytes) differs from the source package's authority bytes — e.g. the capsule was re-installed or upgraded after a previous interrupted migration, or the receipt file was edited/deleted.

Common situations: A first migration attempt partially completed (copied files, wrote a receipt) but crashed before retiring the legacy tree; the user then upgraded the capsule in the new location; then re-ran migration. Also occurs when users hand-edit install receipts or when two migration processes race.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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