astrid-runtime/astrid · error
refusing to seed canonical from a non-content-address contra
Error message
refusing to seed canonical from a non-content-address contracts pin
What it means
seed_canonical_contracts_if_absent copies a supplied pin-named contracts blob to the canonical path (wit/astrid-contracts.wit). Because the pin is used to build the wit/store/<pin>.wit lookup path, a non-BLAKE3-hex pin could traverse out of the store (e.g. "../evil"). As defense in depth the function validates the pin's BLAKE3-hex shape at the boundary and refuses anything else, mirroring daemon_fleet_contracts_pin.
Source
Thrown at crates/astrid-capsule-install/src/contracts.rs:323
/// superseding whatever this seeded. Seeding is first-writer-wins and never
/// overwrites, so a running daemon's baseline is never clobbered by a later
/// side-loaded install (that install warns via [`contracts_skew`] instead).
///
/// Best-effort: the caller logs any failure and proceeds; retention of
/// the canonical must never break an otherwise-successful install.
pub fn seed_canonical_contracts_if_absent<S: BuildHasher>(
home: &AstridHome,
wit_files: &HashMap<String, String, S>,
) -> anyhow::Result<()> {
let Some(pin) = contracts_pin(wit_files) else {
return Ok(());
};
// Defense in depth: `pin` builds the `wit/store/<pin>.wit` lookup path
// below. The install caller passes a freshly content-addressed pin, but
// validate the BLAKE3-hex shape at the boundary so no caller can traverse
// out of the store (mirrors `daemon_fleet_contracts_pin`).
if !is_blake3_pin(pin) {
anyhow::bail!("refusing to seed canonical from a non-content-address contracts pin");
}
let canonical = canonical_contracts_path(home);
if canonical.exists() {
return Ok(());
}
let blob = home.wit_store_dir().join(format!("{pin}.wit"));
let content = std::fs::read(&blob)
.with_context(|| format!("failed to read contracts blob {}", blob.display()))?;
// Create-if-absent (atomic), not write-or-replace: the `exists()` fast path
// above is a cheap common-case skip, but two installs racing that check
// must not clobber each other's canonical. `create_canonical_if_absent`
// returning `Ok(false)` means a racing installer won — first-writer-wins
// holds either way, so both outcomes are `Ok`.
create_canonical_if_absent(&canonical, &content)
.map(|_created| ())View on GitHub (pinned to affd8760f4)
Solutions
- Pass the full 64-character BLAKE3 hex pin as computed by blake3::hash(..).to_hex() for the contracts blob.
- If you only have short_hash output, retrieve the full pin from the capsule's meta.json wit_files entry before seeding.
- Guard your call site with the same is_blake3_pin check and surface a clear caller-side error instead of reaching this bail.
Example fix
// before seed_canonical_contracts_if_absent(&home, short_hash(&pin), &blob)?; // after seed_canonical_contracts_if_absent(&home, &pin, &blob)?; // full blake3 hex
Defensive patterns
Strategy: validation
Validate before calling
fn is_blake3_pin(pin: &str) -> bool {
pin.len() == 64 && pin.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
}
// guard your call:
assert!(is_blake3_pin(&pin), "seed requires full blake3 hex pin"); Try / catch
match seed_canonical_contracts_if_absent(&home, &pin, &blob) {
Err(e) if e.to_string().contains("non-content-address") => {
eprintln!("pin {:?} is not blake3 hex; fetch the full pin from meta.json", short_hash(&pin));
}
other => other?,
} Prevention
- Pass the full pin from blake3::hash(..).to_hex(), never short_hash output.
- Treat display prefixes as presentation-only.
- Sanitize any pin sourced from external input before seeding.
When it happens
Trigger: Calling seed_canonical_contracts_if_absent (directly, or via the first-writer-wins / vendor-no-contracts test flows) with a pin argument that is not a valid 64-char lowercase BLAKE3 hex string — a short hash, a sha256, a path, or an empty/placeholder string.
Common situations: Passing short_hash()'s 12-char display prefix instead of the full pin; passing a filename or relative path as the pin; wiring a legacy pin format from an older store into the seeding path.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- durable capsule {} has malformed contracts pin
- capsule archive already contains {PROVENANCE_FILE}
- WASM capsule has no BLAKE3 hash in meta.json
- durable capsule {} contracts blob digest mismatch
- symlink {} resolves outside the capsule source root ({}); re
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/fe27b12c0c3289c0.
Report an issue: GitHub.