astrid-runtime/astrid · error

opaque capsule asset directories cannot be symlinks: {}

Error message

opaque capsule asset directories cannot be symlinks: {}

What it means

During signed channel pointer validation, a nightly-channel version's prerelease segment (nightly.YYYYMMDD.g<commit>) must embed the exact 40-hex source commit that the pointer's release.source_commit field declares. The library throws this when the commit decoded from the semver prerelease does not equal the pointer's source_commit, i.e. the version and the signed commit disagree. This is an integrity check so a nightly binary can always be traced to its exact source revision.

Source

Thrown at crates/astrid-build/src/archiver.rs:33

///
/// `assets/` is the generic surface. `skills/` remains packable as opaque data
/// so existing capsule sources do not lose files when the old `[[skill]]`
/// protocol is removed. Symlinks are rejected so recursive discovery cannot
/// escape the capsule source tree.
pub(crate) fn discover_opaque_assets(base_dir: &Path) -> Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    for name in OPAQUE_ASSET_DIRS {
        let root = base_dir.join(name);
        let metadata = match fs::symlink_metadata(&root) {
            Ok(metadata) => metadata,
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
            Err(error) => {
                return Err(error)
                    .with_context(|| format!("Failed to inspect asset path: {}", root.display()));
            },
        };
        if metadata.file_type().is_symlink() {
            anyhow::bail!(
                "opaque capsule asset directories cannot be symlinks: {}",
                root.display()
            );
        }
        if !metadata.is_dir() {
            anyhow::bail!("opaque asset path must be a directory: {}", root.display());
        }
        let mut pending = vec![root];
        while let Some(dir) = pending.pop() {
            for entry in fs::read_dir(&dir)
                .with_context(|| format!("Failed to read asset directory: {}", dir.display()))?
            {
                let entry = entry?;
                let path = entry.path();
                let file_type = entry.file_type()?;
                if file_type.is_symlink() {
                    anyhow::bail!(
                        "opaque capsule assets cannot be symlinks: {}",

View on GitHub (pinned to affd8760f4)

Solutions

  1. Set pointer.release.source_commit to the exact 40-char lowercase hex commit embedded in the version prerelease (after the 'g').
  2. Regenerate the channel pointer with the official release tooling instead of hand-editing so version and commit are stamped from the same build.
  3. Re-run parse_channel on the corrected, re-signed pointer.

Example fix

# before
version = "0.5.0-nightly.20260901.gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
source_commit = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
# after
version = "0.5.0-nightly.20260901.gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
source_commit = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
Defensive patterns

Strategy: validation

Validate before calling

fn nightly_commit_matches(version: &str, source_commit: &str) -> bool {
    version.rsplit(".g").next()
        .map(|c| c.len() == 40 && c == source_commit)
        .unwrap_or(false)
}

Type guard

fn is_lower_hex_n(s: &str, n: usize) -> bool {
    s.len() == n && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
}

Try / catch

match parse_channel(&bytes, UpdateChannel::Nightly, now) {
    Err(e) if e.to_string().contains("does not embed its source commit") => eprintln!("re-fetch signed nightly pointer"),
    other => other,
}

Prevention

When it happens

Trigger: parse_channel or enforce_continuity calls validate_pointer on a ChannelPointer whose channel is 'nightly' and whose release.version prerelease commit (the g<40-hex> part) differs from pointer.release.source_commit.

Common situations: Hand-edited or template-generated channel TOML where the version string was bumped but source_commit was not updated; publishing tooling that stamps the wrong commit; a replayed pointer from an older nightly paired with a newer version field.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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