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

  1. Pass the full 64-character BLAKE3 hex pin as computed by blake3::hash(..).to_hex() for the contracts blob.
  2. If you only have short_hash output, retrieve the full pin from the capsule's meta.json wit_files entry before seeding.
  3. 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

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


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