{"record":{"id":"7e33faefe5b60432","repo":"astrid-runtime/astrid","slug":"legacy-capsule-id-changed-before-retirement","errorCode":null,"errorMessage":"legacy capsule {id} changed before retirement","messagePattern":"legacy capsule (.+?) changed before retirement","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage/migration.rs","lineNumber":194,"sourceCode":"                CapsuleInstallExpectation::Generation(snapshot.generation())\n            },\n            Some(_) => bail!(\"durable capsule {id} conflicts with legacy native content\"),\n        };\n        registry.install(&owner, id, &package, expectation)?;\n        let readback = registry\n            .get_snapshot(&owner, id)?\n            .ok_or_else(|| anyhow::anyhow!(\"durable capsule {id} disappeared after publish\"))?;\n        if readback.package() != &package {\n            bail!(\"durable capsule {id} failed byte-for-byte readback\");\n        }\n        read_verified_durable_package_for_owner(store, &owner, id)?.ok_or_else(|| {\n            anyhow::anyhow!(\"durable capsule {id} failed authoritative verification\")\n        })?;\n        astrid_core::platform_fs::verify_no_redirects(&target)\n            .with_context(|| format!(\"verify legacy capsule {id} before retirement\"))?;\n        let final_archive = canonical_legacy_archive(home, &target, &meta, &manifest)?;\n        if final_archive != package.archive {\n            bail!(\"legacy capsule {id} changed before retirement\");\n        }\n        if fs::read(target.join(\"meta.json\"))? != package.metadata {\n            bail!(\"legacy capsule {id} metadata changed before retirement\");\n        }\n        if read_installed_authority_bytes(home, &target)?.as_deref()\n            != Some(source_authority_bytes.as_slice())\n        {\n            bail!(\"legacy capsule {id} authority changed before retirement\");\n        }\n        astrid_core::platform_fs::verify_no_redirects(&target)\n            .with_context(|| format!(\"verify legacy capsule {id} retirement boundary\"))?;\n        astrid_core::dirs::retire_legacy_source_tree(&target)\n            .with_context(|| format!(\"retire migrated legacy capsule {id}\"))?;\n        retire_legacy_authority_receipt(home, &target, &source_authority_bytes)\n            .with_context(|| format!(\"retire migrated legacy capsule {id} authority\"))?;\n        report\n            .retired_authorities\n            .push(LegacyCapsuleAuthorityReceipt {","sourceCodeStart":176,"sourceCodeEnd":212,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage/migration.rs#L176-L212","documentation":"Just before retiring the legacy directory, migration rebuilds the canonical archive from the legacy files and compares it to the archive that was published durably. If they differ, the legacy capsule mutated during migration (after its contents were verified and copied), so retirement is refused to avoid deleting changed data.","triggerScenarios":"Files in the legacy capsule directory changed between the initial canonical_legacy_archive call and the final re-check — e.g. a running capsule process rewrote its files, or a user/sync tool edited them mid-migration.","commonSituations":"The migrated capsule's executable was still running and wrote state; cloud-sync touched the directory; developer edited files during migration.","solutions":["Ensure the capsule is not running and nothing writes into the legacy directory, then re-run migration","Exclude the legacy path from sync tools during migration","Revert or commit any local changes to the legacy capsule, then migrate the stable version"],"exampleFix":"// before\nmigrate while my-capsule binary is running\n// after\npkill my-capsule; migrate_all_native_capsules(store)","handlingStrategy":"validation","validationCode":"// ensure nothing can write into the legacy tree during migration\nif process_running(&capsule_executable) { return Err(\"stop the capsule before migrating\"); }\nlet h1 = hash_tree(&legacy_dir);\nstd::thread::sleep(Duration::from_secs(1));\nif hash_tree(&legacy_dir) != h1 { return Err(\"legacy tree is changing\"); }","typeGuard":null,"tryCatchPattern":"if let Err(e) = migrate_native_capsules(home, store) {\n    if e.to_string().contains(\"changed before retirement\") {\n        eprintln!(\"legacy capsule mutated mid-migration; quiesce it and retry\");\n    } else { return Err(e); }\n}","preventionTips":["Stop capsule processes before migrating their directories","Pause cloud-sync/indexing on the legacy directory during migration","Do not edit installed capsules while migration runs","Migrate once at a stable point (after install/upgrade, before launch)"],"tags":["migration","toctou","race-condition","capsule"],"backgroundTag":"checksum-mismatch","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}