{"record":{"id":"b71866d0f3768bde","repo":"astrid-runtime/astrid","slug":"durable-capsule-id-exports-differ-between-metada","errorCode":null,"errorMessage":"durable capsule {id} exports differ between metadata and archive","messagePattern":"durable capsule (.+?) exports differ between metadata and archive","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":289,"sourceCode":"    }\n    let manifest_digest = crate::authority::digest_manifest(manifest_bytes);\n    if authority.manifest_digest != manifest_digest {\n        bail!(\"durable capsule {id} manifest digest differs from authority receipt\");\n    }\n    if authority.content_digest != verification.content_digest() {\n        bail!(\"durable capsule {id} content digest differs from authority receipt\");\n    }\n    let expected_imports = crate::wit::version_map_to_strings(&manifest.imports, |definition| {\n        definition.version.to_string()\n    });\n    if metadata.imports != expected_imports {\n        bail!(\"durable capsule {id} imports differ between metadata and archive\");\n    }\n    let expected_exports = crate::wit::version_map_to_strings(&manifest.exports, |definition| {\n        definition.version.to_string()\n    });\n    if metadata.exports != expected_exports {\n        bail!(\"durable capsule {id} exports differ between metadata and archive\");\n    }\n    if authority.wasm_hash_pinned && metadata.wasm_hash != authority.approved_wasm_hash {\n        bail!(\"durable capsule {id} metadata executable hash differs from authority receipt\");\n    }\n    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())","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L271-L307","documentation":"During durable package verification, verify_package_identity recomputes the export list from the manifest's WIT exports (name + version) and compares it with the exports recorded in the capsule metadata. This bail fires when the two differ, meaning the metadata file was hand-edited, regenerated from a different manifest, or paired with the wrong archive. The library throws it to prevent installing a capsule whose advertised API surface does not match what was actually approved and archived.","triggerScenarios":"Calling read_verified_durable_package_for_owner (or the test-path durable_metadata_cross_binding_rejects_manifest_and_archive_mismatches) on a durable capsule where metadata.exports != version_map_to_strings(manifest.exports), i.e. the manifest in the archive declares exports (names or versions) that don't exactly equal the exports list persisted in CapsuleMeta.","commonSituations":"Rebuilding a capsule with added/removed/renamed WIT exports but reusing the old metadata file; bumping an export's version in the manifest without regenerating metadata; mixing archives and metadata from different capsule versions; editing metadata by hand or with a tool that rewrote the exports field.","solutions":["Regenerate the capsule metadata (CapsuleMeta) from the current manifest/archive so the exports field matches manifest.exports versions.","Reinstall the durable capsule from a freshly built, internally consistent package so metadata, manifest, and authority receipt are all produced together.","If the old exports are intended, restore the original manifest/archive that matches the existing metadata instead of changing metadata.","Compare manifest.exports vs metadata.exports (names and version strings) to identify which side is stale before regenerating."],"exampleFix":"// before: metadata written once, manifest later gained an export\nlet metadata = read_metadata(); // exports: [\"astrid:store@1.0.0\"]\nlet manifest = read_manifest(); // exports: [\"astrid:store@1.1.0\"]\n// after: regenerate metadata from the manifest before installing\nlet mut metadata = read_metadata();\nmetadata.exports = crate::wit::version_map_to_strings(&manifest.exports, |d| d.version.to_string());\nwrite_metadata(&metadata);","handlingStrategy":"validation","validationCode":"let expected_exports = crate::wit::version_map_to_strings(&manifest.exports, |d| d.version.to_string());\nif metadata.exports != expected_exports {\n    return Err(anyhow!(\"exports mismatch: metadata {:?} vs manifest {:?}\", metadata.exports, expected_exports));\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always generate metadata and manifest together from a single build step.","Never hand-edit metadata exports; regenerate them from the manifest.","Add a packaging CI check that recomputes exports from the manifest and diffs against metadata."],"tags":["integrity-verification","capsule-metadata","wit-exports","rust"],"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"}