astrid-runtime/astrid · error

materialized capsule directory inventory differs from durabl

Error message

materialized capsule directory inventory differs from durable package

What it means

During published-materialization verification, the kernel inventories every directory present on disk under the materialized capsule cache and compares it to the set of directories implied by the durable registry package (archive directories plus ancestor directories of each archived file). This bail fires when the on-disk directory set differs from that expected set, i.e. the projected cache has extra, missing, or renamed directories relative to the immutable package snapshot. It exists to catch tampering, partial extraction, or stale generations of the cache rather than trusting the filesystem layout.

Source

Thrown at crates/astrid-kernel/src/capsule_materialization.rs:91

            anyhow::bail!("materialized capsule file inventory differs from durable package");
        }
        let mut expected_directories = expected_files
            .keys()
            .flat_map(|relative| authenticated_ancestor_directories(relative))
            .collect::<std::collections::BTreeSet<_>>();
        let archive_directories = verified
            .archive_directories()
            .map(ToOwned::to_owned)
            .collect::<Vec<_>>();
        expected_directories.extend(archive_directories.iter().cloned());
        expected_directories.extend(
            archive_directories
                .iter()
                .map(String::as_str)
                .flat_map(authenticated_ancestor_directories),
        );
        if actual.directories != expected_directories {
            anyhow::bail!("materialized capsule directory inventory differs from durable package");
        }
        for (relative, expected) in &expected_files {
            let materialized =
                Self::read_projection_file_nofollow(&dir.join(relative)).map_err(|error| {
                    anyhow::anyhow!("read materialized capsule member {relative}: {error}")
                })?;
            if materialized != *expected {
                anyhow::bail!(
                    "materialized capsule member {relative} differs from durable archive"
                );
            }
        }
        let expansions = manifest
            .capabilities
            .expansions_from(&verified.authority().approved_capabilities);
        if !expansions.is_empty() {
            anyhow::bail!("materialized capsule manifest exceeds durable authority approval");
        }

View on GitHub (pinned to affd8760f4)

Solutions

  1. Let the library repair the projection instead of fixing files by hand: call repair_published_materialization (or the load/ensure path that routes through it), which detects the failed verification, removes the stale tree with remove_dir_all, and re-materializes from the durable snapshot.
  2. Delete the capsule's materialized cache directory for that principal/package and re-run the load so it is re-materialized from the registry snapshot.
  3. Check that nothing external (editors, sync clients, cleanup scripts) mutates the cache directory between runs, then re-materialize.
  4. If the difference is intentional, verify the durable package snapshot is the correct publication; a mismatched snapshot id (checked earlier as 'snapshot differs from caller's publication') means the caller passed the wrong CapsulePackageSnapshot.

Example fix

// before: hand-editing or reusing a stale cache directory
let manifest = kernel.load_capsule(&cached_dir, &principal)?;

// after: force repair/re-materialization from the durable snapshot
let manifest = kernel.repair_published_materialization(
    &runtime_dir,
    &principal,
    &discovery_manifest,
    &snapshot,
)?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check directory inventory before calling the API
let expected_dirs: BTreeSet<String> = snapshot_archive_directories()
    .into_iter()
    .chain(expected_files.iter().flat_map(|p| ancestor_dirs(p)))
    .collect();
let actual_dirs = list_directories(&cache_dir)?;
if actual_dirs != expected_dirs {
    // schedule a repair instead of load
    kernel.repair_published_materialization(&cache_dir, &principal, &manifest, &snapshot)?;
}

Type guard

fn is_plain_directory(path: &Path) -> bool {
    std::fs::symlink_metadata(path)
        .map(|m| m.is_dir())
        .unwrap_or(false)
}

Try / catch

match kernel.load_capsule(&cache_dir, &principal) {
    Ok(m) => m,
    Err(e) if e.to_string().contains("directory inventory differs") => {
        kernel.repair_published_materialization(&cache_dir, &principal, &manifest, &snapshot)?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: verify_published_materialization is called by repair_published_materialization, confirm_published_materialization, or verify_registry_materialization, and Self::inventory_projection_files(dir) returns a `directories` BTreeSet that is not exactly equal to the set built from verified archive directories plus authenticated_ancestor_directories of every expected file path — e.g. an empty directory left behind by a prior generation, a deleted subdirectory, or a symlinked/renamed folder.

Common situations: A previous capsule version's cache was only partially removed before a new generation was projected; a user or tooling manually added or deleted directories inside the cache path; a backup/sync tool (or interrupted process) left extra empty directories; the package snapshot in the durable registry was republished with a different directory layout while the old projection remained on disk.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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