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
- Fix Capsule.toml / asset layout so every content file has a unique path
- Rebuild the capsule; duplicate paths indicate a packaging bug
- 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)