{"record":{"id":"326ae1b116aaeac2","repo":"astrid-runtime/astrid","slug":"materialized-capsule-digest-does-not-match-durable","errorCode":null,"errorMessage":"materialized capsule digest does not match durable registry","messagePattern":"materialized capsule digest does not match durable registry","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/lib.rs","lineNumber":1610,"sourceCode":"                    \"capsule cache path contains unsafe components\"\n                )),\n            })\n            .collect::<anyhow::Result<_>>()?;\n        if components.len() != 3 {\n            anyhow::bail!(\"capsule cache path does not contain owner/id/digest components\");\n        }\n        let uid = self\n            .principal_directory\n            .uid_for(principal)\n            .map_err(|error| anyhow::anyhow!(\"resolve capsule cache owner UID: {error}\"))?;\n        if components[0] != uid.to_string() || components[1] != manifest.package.name {\n            anyhow::bail!(\"capsule cache owner or id does not match authenticated registry scope\");\n        }\n        let digest = blake3::hash(&snapshot.package().archive)\n            .to_hex()\n            .to_string();\n        if components[2] != digest {\n            anyhow::bail!(\"materialized capsule digest does not match durable registry\");\n        }\n        Ok(())\n    }\n\n    /// Inventory a projection without traversing redirects or special files.\n    #[cfg(not(all(target_arch = \"wasm32\", target_os = \"unknown\")))]\n    fn inventory_projection_files(root: &Path) -> anyhow::Result<ProjectionInventory> {\n        fn walk(\n            root: &Path,\n            directory: &Path,\n            inventory: &mut ProjectionInventory,\n        ) -> anyhow::Result<()> {\n            for entry in std::fs::read_dir(directory).map_err(|error| {\n                anyhow::anyhow!(\"read capsule projection {}: {error}\", directory.display())\n            })? {\n                let entry = entry\n                    .map_err(|error| anyhow::anyhow!(\"read capsule projection entry: {error}\"))?;\n                let path = entry.path();","sourceCodeStart":1592,"sourceCodeEnd":1628,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/lib.rs#L1592-L1628","documentation":"This error means the digest recorded in the cache path (components[2]) does not match the blake3 hash of the package archive bytes in the loaded snapshot. The kernel throws it as an integrity check: the cached bytes on disk differ from what the cache key claims, so the materialized capsule may be corrupted, tampered with, or stale relative to the durable registry content.","triggerScenarios":"During cache validation after materialization: blake3::hash(snapshot.package().archive) differs from the digest component of the cache directory name — e.g. truncated or partially-written archive, an archive overwritten after the cache dir was created, bit-rot on disk, or a cache entry seeded with the wrong digest label.","commonSituations":"Interrupted download/materialization leaving a partial archive; antivirus or backup tooling modifying files in the cache; manually editing or replacing cache contents; disk corruption; reusing a cache volume copied inconsistently (digest dir name from one version, bytes from another).","solutions":["Delete the mismatched cache entry (<cache-root>/<uid>/<name>/<wrong-digest>) and re-materialize the capsule from the registry","Re-download/re-fetch the capsule to get a pristine archive, then retry","Check disk health and exclude the cache directory from backup/AV mutation if files were modified externally","If the cache is seeded by tooling, compute the blake3 digest of the final archive bytes and use that as the directory name"],"exampleFix":"// before: labeling cache with a digest from metadata\nlet dir = cache_root.join(name).join(manifest_digest.clone());\n\n// after: digest must hash the actual archive bytes\nlet digest = blake3::hash(&archive_bytes).to_hex().to_string();\nlet dir = cache_root.join(name).join(digest);","handlingStrategy":"validation","validationCode":"let digest = blake3::hash(&snapshot.package().archive).to_hex().to_string();\nif digest != expected_digest { return Err(\"archive digest mismatch before materialization\"); }","typeGuard":null,"tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"digest does not match\") => {\n        // purge the bad cache entry and re-fetch from the registry\n    }\n    other => other?,\n}","preventionTips":["Verify digests immediately after download, before caching","Exclude cache dirs from AV/backup tools that modify files","Check disk health if mismatches recur","Always derive cache keys from the hash of the actual bytes"],"tags":["integrity","checksum","cache","capsule","digest-mismatch"],"backgroundTag":"checksum-mismatch","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}