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
- Set pointer.release.source_commit to the exact 40-char lowercase hex commit embedded in the version prerelease (after the 'g').
- Regenerate the channel pointer with the official release tooling instead of hand-editing so version and commit are stamped from the same build.
- 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
- Generate nightly pointers only from CI at build time
- Assert version prerelease commit == source_commit before signing
- Never hand-edit signed channel files
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
- opaque asset path must be a directory: {}
- WASM binary not found at {}
- capsule archive already contains {PROVENANCE_FILE}
- capsule archive contains duplicate entry '{requested}'
- capsule archive contains duplicate entry '{path}'
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/f1d41c6c2c012f8d.
Report an issue: GitHub.