astrid-runtime/astrid · error

durable capsule component path is not UTF-8

Error message

durable capsule {id} component path is not UTF-8

What it means

verify_package_identity reads the first component's path from the manifest and converts it to a UTF-8 string via Path::to_str, because archive entries are keyed by string paths. This bail fires when the component path contains bytes that are not valid UTF-8. The library throws it because archive lookup and downstream path handling require string keys, and non-UTF-8 paths cannot be matched safely against archive entries.

Solutions

  1. Rebuild the capsule so the component path in the manifest is valid UTF-8 (ASCII-safe relative path like 'component.wasm').
  2. Fix the manifest's component.path field to a UTF-8 string and repack the archive.
  3. Reject the offending package upstream and regenerate it with normalized (UTF-8) filenames.
  4. Validate every manifest component path with to_str()/String::from_utf8 before packaging to catch this at build time.

Example fix

// before: path from raw OS bytes
let path = PathBuf::from(OsString::from_vec(raw_bytes)); // not UTF-8
// after: normalize to a UTF-8 relative path when building the manifest
let path = PathBuf::from(String::from_utf8(raw_bytes).map_err(|_| "non-UTF-8 component path")?);
Defensive patterns

Strategy: validation

Validate before calling

fn assert_utf8_component_paths(manifest: &CapsuleManifest) -> anyhow::Result<()> {
    for component in &manifest.components {
        component.path.to_str().ok_or_else(|| anyhow!("component path {:?} is not UTF-8", component.path))?;
    }
    Ok(())
}

Type guard

fn has_utf8_path(p: &std::path::Path) -> bool { p.to_str().is_some() }

Prevention

When it happens

Trigger: Installing a durable capsule whose CapsuleManifest declares a component with a non-UTF-8 path (e.g. built on a filesystem with raw-byte filenames or a manifest generated by a tool writing arbitrary bytes into the path field), when read_verified_durable_package_for_owner runs identity verification.

Common situations: Packages built on legacy/non-UTF-8 locales (e.g. Latin-1 filenames) then moved into archives; hand-crafted or programmatically generated manifests with escaped/mojibake path bytes; tar tools preserving odd encodings on extraction or repack.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/ed0b4b866fa2a7ba. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-capsule-install/src/storage.rs:296

    }
    let expected_imports = crate::wit::version_map_to_strings(&manifest.imports, |definition| {
        definition.version.to_string()
    });
    if metadata.imports != expected_imports {
        bail!("durable capsule {id} imports differ between metadata and archive");
    }
    let expected_exports = crate::wit::version_map_to_strings(&manifest.exports, |definition| {
        definition.version.to_string()
    });
    if metadata.exports != expected_exports {
        bail!("durable capsule {id} exports differ between metadata and archive");
    }
    if authority.wasm_hash_pinned && metadata.wasm_hash != authority.approved_wasm_hash {
        bail!("durable capsule {id} metadata executable hash differs from authority receipt");
    }
    if let Some(component) = manifest.components.first() {
        let Some(relative) = component.path.to_str() else {
            bail!("durable capsule {id} component path is not UTF-8");
        };
        let Some(bytes) = archive_files.get(relative) else {
            bail!("durable capsule {id} component is missing from its archive");
        };
        if Path::new(relative)
            .extension()
            .is_some_and(|extension| extension.eq_ignore_ascii_case("wasm"))
        {
            let archive_hash = blake3::hash(bytes).to_hex().to_string();
            if authority.wasm_hash_pinned
                && authority.approved_wasm_hash.as_deref() != Some(archive_hash.as_str())
            {
                bail!("durable capsule {id} WASM hash differs between authority and archive");
            }
            if metadata.wasm_hash.as_deref() != Some(archive_hash.as_str()) {
                bail!("durable capsule {id} WASM hash differs between metadata and archive");
            }
        } else if metadata.wasm_hash.is_some() {

View on GitHub (pinned to affd8760f4)