{"record":{"id":"f1d41c6c2c012f8d","repo":"astrid-runtime/astrid","slug":"opaque-capsule-asset-directories-cannot-be-symlink","errorCode":null,"errorMessage":"opaque capsule asset directories cannot be symlinks: {}","messagePattern":"opaque capsule asset directories cannot be symlinks: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-build/src/archiver.rs","lineNumber":33,"sourceCode":"///\n/// `assets/` is the generic surface. `skills/` remains packable as opaque data\n/// so existing capsule sources do not lose files when the old `[[skill]]`\n/// protocol is removed. Symlinks are rejected so recursive discovery cannot\n/// escape the capsule source tree.\npub(crate) fn discover_opaque_assets(base_dir: &Path) -> Result<Vec<PathBuf>> {\n    let mut files = Vec::new();\n    for name in OPAQUE_ASSET_DIRS {\n        let root = base_dir.join(name);\n        let metadata = match fs::symlink_metadata(&root) {\n            Ok(metadata) => metadata,\n            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,\n            Err(error) => {\n                return Err(error)\n                    .with_context(|| format!(\"Failed to inspect asset path: {}\", root.display()));\n            },\n        };\n        if metadata.file_type().is_symlink() {\n            anyhow::bail!(\n                \"opaque capsule asset directories cannot be symlinks: {}\",\n                root.display()\n            );\n        }\n        if !metadata.is_dir() {\n            anyhow::bail!(\"opaque asset path must be a directory: {}\", root.display());\n        }\n        let mut pending = vec![root];\n        while let Some(dir) = pending.pop() {\n            for entry in fs::read_dir(&dir)\n                .with_context(|| format!(\"Failed to read asset directory: {}\", dir.display()))?\n            {\n                let entry = entry?;\n                let path = entry.path();\n                let file_type = entry.file_type()?;\n                if file_type.is_symlink() {\n                    anyhow::bail!(\n                        \"opaque capsule assets cannot be symlinks: {}\",","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-build/src/archiver.rs#L15-L51","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nversion = \"0.5.0-nightly.20260901.gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\nsource_commit = \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"\n# after\nversion = \"0.5.0-nightly.20260901.gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\nsource_commit = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"","handlingStrategy":"validation","validationCode":"fn nightly_commit_matches(version: &str, source_commit: &str) -> bool {\n    version.rsplit(\".g\").next()\n        .map(|c| c.len() == 40 && c == source_commit)\n        .unwrap_or(false)\n}","typeGuard":"fn is_lower_hex_n(s: &str, n: usize) -> bool {\n    s.len() == n && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))\n}","tryCatchPattern":"match parse_channel(&bytes, UpdateChannel::Nightly, now) {\n    Err(e) if e.to_string().contains(\"does not embed its source commit\") => eprintln!(\"re-fetch signed nightly pointer\"),\n    other => other,\n}","preventionTips":["Generate nightly pointers only from CI at build time","Assert version prerelease commit == source_commit before signing","Never hand-edit signed channel files"],"tags":["validation","signing","semver","integrity"],"backgroundTag":"invalid-argument-value","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}