astrid-runtime/astrid · error

capsule content tree contains duplicate paths

Error message

capsule content tree contains duplicate paths

What it means

Fails the capsule digest computation in digest_records after sorting ContentRecords by path and detecting two adjacent records with the same path. It fires when the content collection stage produced two entries for one file — typically because overlapping include rules or a symlink resolution mapped two inputs to the same normalized path. Callers sign_archive and verify_records abort rather than emit a hash over an ambiguous manifest.

Solutions

  1. Fix Capsule.toml / asset layout so every content file has a unique path
  2. Rebuild the capsule; duplicate paths indicate a packaging bug
  3. Report the duplicate path to the capsule author
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at crates/astrid-build/src/artifact.rs:332 when the library encounters an invalid state.

Common situations: See trigger scenarios.


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

Appendix: source

Thrown at crates/astrid-build/src/artifact.rs:332

            break;
        }
        hasher.update(&buffer[..count]);
        read = read.saturating_add(count as u64);
    }
    if read != size {
        bail!("capsule entry '{path}' size changed while hashing");
    }
    Ok(ContentRecord {
        path,
        size,
        hash: *hasher.finalize().as_bytes(),
    })
}

fn digest_records(mut records: Vec<ContentRecord>) -> anyhow::Result<String> {
    records.sort_unstable_by(|left, right| left.path.cmp(&right.path));
    if records.windows(2).any(|pair| pair[0].path == pair[1].path) {
        bail!("capsule content tree contains duplicate paths");
    }
    let mut hasher = blake3::Hasher::new();
    hasher.update(CONTENT_DOMAIN);
    for record in records {
        let path = record.path.as_bytes();
        hasher.update(&(path.len() as u64).to_le_bytes());
        hasher.update(path);
        hasher.update(&record.size.to_le_bytes());
        hasher.update(&record.hash);
    }
    Ok(hasher.finalize().to_hex().to_string())
}

fn signature_message(content_digest: &str) -> Vec<u8> {
    let mut message =
        Vec::with_capacity(SIGNATURE_DOMAIN.len().saturating_add(content_digest.len()));
    message.extend_from_slice(SIGNATURE_DOMAIN);
    message.extend_from_slice(content_digest.as_bytes());

View on GitHub (pinned to affd8760f4)