{"record":{"id":"b23d384e43340f6d","repo":"astrid-runtime/astrid","slug":"legacy-capsule-id-metadata-changed-before-retire","errorCode":null,"errorMessage":"legacy capsule {id} metadata changed before retirement","messagePattern":"legacy capsule (.+?) metadata changed before retirement","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage/migration.rs","lineNumber":197,"sourceCode":"        };\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 {\n                uid,\n                capsule_id: id.to_owned(),\n                authority_digest: blake3::hash(&package.authority).to_hex().to_string(),","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage/migration.rs#L179-L215","documentation":"As a final pre-retirement check, migration re-reads meta.json and compares it byte-for-byte to the metadata that was published durably. A difference means the legacy metadata changed during migration, so the legacy directory is not retired and the error is raised to protect the mutated data.","triggerScenarios":"meta.json was rewritten between its initial read and the final retirement check — e.g. the capsule process updated its own metadata, an installer touched it, or a sync tool restored a different copy.","commonSituations":"Running capsule writing version/state updates to meta.json mid-migration; backup/restore tools rewriting metadata; concurrent second migration attempt.","solutions":["Stop any process that could write meta.json, then re-run migration","Serialize migrations so only one runs at a time","Restore meta.json to the version matching the published package, or re-migrate from scratch"],"exampleFix":"// before\nmigrate while capsule service updates meta.json\n// after\nsystemctl --user stop my-capsule; migrate_all_native_capsules(store)","handlingStrategy":"validation","validationCode":"// verify meta.json stability before migrating\nlet before = std::fs::read(capsule_dir.join(\"meta.json\")).map_err(|e| e.to_string())?;\nstd::thread::sleep(Duration::from_secs(1));\nif std::fs::read(capsule_dir.join(\"meta.json\")).map_err(|e| e.to_string())? != before {\n    return Err(\"meta.json is being rewritten\".into());\n}","typeGuard":null,"tryCatchPattern":"if let Err(e) = migrate_native_capsules(home, store) {\n    if e.to_string().contains(\"metadata changed before retirement\") {\n        eprintln!(\"meta.json changed mid-migration; stop the writer and retry\");\n    } else { return Err(e); }\n}","preventionTips":["Stop capsule services that update meta.json before migrating","Run only one migration at a time","Exclude legacy paths from sync/backup tools during migration","Take a snapshot of the legacy tree before migration for safe rollback"],"tags":["migration","toctou","metadata","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-14T00:17:10.932Z"}