{"record":{"id":"ceb1dd8631686514","repo":"astrid-runtime/astrid","slug":"durable-capsule-id-wasm-hash-differs-between-met","errorCode":null,"errorMessage":"durable capsule {id} WASM hash differs between metadata and archive","messagePattern":"durable capsule (.+?) WASM hash differs between metadata and archive","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":312,"sourceCode":"    if let Some(component) = manifest.components.first() {\n        let Some(relative) = component.path.to_str() else {\n            bail!(\"durable capsule {id} component path is not UTF-8\");\n        };\n        let Some(bytes) = archive_files.get(relative) else {\n            bail!(\"durable capsule {id} component is missing from its archive\");\n        };\n        if Path::new(relative)\n            .extension()\n            .is_some_and(|extension| extension.eq_ignore_ascii_case(\"wasm\"))\n        {\n            let archive_hash = blake3::hash(bytes).to_hex().to_string();\n            if authority.wasm_hash_pinned\n                && authority.approved_wasm_hash.as_deref() != Some(archive_hash.as_str())\n            {\n                bail!(\"durable capsule {id} WASM hash differs between authority and archive\");\n            }\n            if metadata.wasm_hash.as_deref() != Some(archive_hash.as_str()) {\n                bail!(\"durable capsule {id} WASM hash differs between metadata and archive\");\n            }\n        } else if metadata.wasm_hash.is_some() {\n            bail!(\"durable capsule {id} metadata names a hash for a non-WASM component\");\n        }\n    } else if metadata.wasm_hash.is_some() {\n        bail!(\"durable capsule {id} metadata names a component absent from its archive\");\n    }\n    let mut effective_capabilities = manifest.capabilities.clone();\n    for component in &manifest.components {\n        if let Some(capabilities) = &component.capabilities {\n            effective_capabilities.merge_from(capabilities);\n        }\n    }\n    if !effective_capabilities\n        .expansions_from(&authority.approved_capabilities)\n        .is_empty()\n    {\n        bail!(\"durable capsule {id} manifest exceeds its authority receipt\");","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L294-L330","documentation":"During durable capsule identity verification, the BLAKE3 hash of the WASM component actually present in the archive does not equal the wasm_hash recorded in the capsule's metadata record. The library throws this because metadata is supposed to describe exactly the executable bytes being installed; any divergence means one of the two was regenerated or tampered with. Verification is aborted before the package can be read for an owner.","triggerScenarios":"Calling read_verified_durable_package_for_owner (directly or via the mismatch test) when metadata.wasm_hash differs from blake3(archive component bytes) for a component with a .wasm extension, e.g. after the archive was rebuilt while the metadata row kept an old hash.","commonSituations":"Publishing a rebuilt WASM binary without refreshing CapsuleMeta; hand-editing metadata rows; partial/corrupted writes where the archive landed but metadata update failed; mirroring or importing a capsule from another environment where the binary was recompiled.","solutions":["Republish the capsule so metadata and archive are regenerated together (publish_directory_package recomputes hashes atomically).","Recompute blake3 of the archive's WASM component and update metadata.wasm_hash to that value.","If the wrong archive was stored, restore the archive matching the pinned metadata hash from a trusted source.","Check for interrupted publication/concurrent writers that left metadata and archive out of sync."],"exampleFix":"// before: metadata published with stale hash\nCapsuleMeta { wasm_hash: Some(\"old_blake3_hex\".into()), .. }\n// after: regenerate metadata from the archive being published\nlet bytes = std::fs::read(\"component.wasm\")?;\nmeta.wasm_hash = Some(blake3::hash(&bytes).to_hex().to_string());","handlingStrategy":"validation","validationCode":"let bytes = archive_files.get(component_path)?;\nlet archive_hash = blake3::hash(bytes).to_hex().to_string();\nif metadata.wasm_hash.as_deref() != Some(archive_hash.as_str()) {\n    return Err(\"metadata wasm_hash does not match archive component\");\n}","typeGuard":"fn metadata_matches_archive(meta: &CapsuleMeta, archive_hash: &str) -> bool {\n    meta.wasm_hash.as_deref() == Some(archive_hash)\n}","tryCatchPattern":"match read_verified_durable_package_for_owner(&store, owner, id).await {\n    Ok(pkg) => pkg,\n    Err(e) if e.to_string().contains(\"WASM hash differs between metadata and archive\") => {\n        // republish or restore consistent metadata/archive\n    },\n    Err(e) => return Err(e),\n}","preventionTips":["Always republish via publish_directory_package so metadata and archive are generated together.","Never hand-edit metadata rows after publication.","Store wasm_hash only as blake3 of the exact bytes shipped in the archive."],"tags":["integrity","hash-mismatch","wasm","storage"],"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"}