astrid-runtime/astrid · error

Distro.lock capsule '{capsule}' requires a canonical lowerca

Error message

Distro.lock capsule '{capsule}' requires a canonical lowercase BLAKE3 hash

What it means

Even a parseable blake3 hash must be in canonical form: exactly 64 hex characters whose lowercase hex round-trips through blake3::Hash::to_hex. Uppercase letters or extra characters are rejected so lockfiles stay byte-comparable and diff-stable.

Source

Thrown at crates/astrid-cli/src/commands/init_grant.rs:432

            )
        })?
    };
    let actual = blake3::hash(&bytes);
    if actual != locked {
        bail!("Distro.lock capsule '{capsule}' content blob bytes do not match hash {locked_hash}");
    }
    Ok(())
}

fn parse_locked_blake3(capsule: &CapsuleId, value: &str) -> anyhow::Result<blake3::Hash> {
    let Some(hex) = value.strip_prefix("blake3:") else {
        bail!("Distro.lock capsule '{capsule}' requires a canonical blake3:<hex> WASM hash");
    };
    let hash = blake3::Hash::from_hex(hex).map_err(|_| {
        anyhow::anyhow!("Distro.lock capsule '{capsule}' has an invalid BLAKE3 hash")
    })?;
    if hex.len() != 64 || hash.to_hex().as_str() != hex {
        bail!("Distro.lock capsule '{capsule}' requires a canonical lowercase BLAKE3 hash");
    }
    Ok(hash)
}

fn manifest_declares_wasm(manifest: &CapsuleManifest) -> bool {
    manifest
        .components
        .iter()
        .any(|component| component.path.extension().and_then(|ext| ext.to_str()) == Some("wasm"))
}

/// Apply capsule-access grants for the installed set (opt-in), or print
/// the discoverability hint when the flag was omitted.
///
/// On the grant path the capsules are already installed and the lock is
/// written; a failure here (daemon unreachable, caller lacks `agent:modify`)
/// returns `Err` so `init` exits non-zero, but always prints the exact
/// manual command to finish. The kernel applies the whole `add_capsules`

View on GitHub (pinned to affd8760f4)

Solutions

  1. Lowercase the hex portion of the hash in Distro.lock and confirm it is exactly 64 characters.
  2. Regenerate Distro.lock with the CLI so it writes the canonical lowercase hash.
  3. Normalize hashes in any tooling that writes the lockfile (lowercase, no padding).

Example fix

// before (Distro.lock)
hash = "blake3:9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"
// after
hash = "blake3:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
Defensive patterns

Strategy: validation

Validate before calling

let hex = value.strip_prefix("blake3:")?;
let h = blake3::Hash::from_hex(hex).ok()?;
if hex.len() != 64 || h.to_hex().as_str() != hex { return Err(anyhow!("non-canonical hash")); }

Type guard

fn is_canonical_blake3(value: &str) -> bool {
    match value.strip_prefix("blake3:") {
        Some(h) => h.len() == 64
            && h.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
            && blake3::Hash::from_hex(h).map(|x| x.to_hex().as_str() == h).unwrap_or(false),
        None => false,
    }
}

Try / catch

match parse_locked_blake3(capsule, value) {
    Err(e) if e.to_string().contains("canonical lowercase") => normalize_hash_case(capsule)?,
    other => other?,
}

Prevention

When it happens

Trigger: parse_locked_blake3 parses the hex successfully but hex.len() != 64 or hash.to_hex().as_str() != hex — e.g. uppercase hex, or a hex string with a stray character that still parses.

Common situations: Uppercase hex from another tool's output, padded or trimmed hex from manual editing, script-generated lockfile emitting non-canonical casing.

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/cc2f6eba94772067. Report an issue: GitHub.