{"record":{"id":"d6e084e0bbf1d060","repo":"astrid-runtime/astrid","slug":"legacy-capsule-contains-symlink","errorCode":null,"errorMessage":"legacy capsule contains symlink {}","messagePattern":"legacy capsule contains symlink (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":700,"sourceCode":"        let destination = staging.path().join(\"wit\").join(relative);\n        if let Some(parent) = destination.parent() {\n            fs::create_dir_all(parent)?;\n        }\n        fs::copy(&source, &destination)\n            .with_context(|| format!(\"restore content-addressed WIT blob {hash}\"))?;\n    }\n    canonical_capsule_archive(staging.path())\n}\n\nfn copy_legacy_tree(source: &Path, destination: &Path) -> anyhow::Result<()> {\n    fs::create_dir_all(destination)?;\n    for (path, metadata) in read_dir_sorted(source)? {\n        let relative = path\n            .file_name()\n            .ok_or_else(|| anyhow::anyhow!(\"legacy capsule entry has no name\"))?;\n        let destination = destination.join(relative);\n        if metadata.file_type().is_symlink() {\n            bail!(\"legacy capsule contains symlink {}\", path.display());\n        }\n        if metadata.is_dir() {\n            copy_legacy_tree(&path, &destination)?;\n        } else if metadata.is_file() {\n            let name = path.file_name().and_then(|name| name.to_str());\n            if matches!(name, Some(\"meta.json\" | \"authority.json\" | \".env.json\")) {\n                continue;\n            }\n            fs::copy(path, destination)?;\n        } else {\n            bail!(\"legacy capsule contains special file {}\", path.display());\n        }\n    }\n    Ok(())\n}\n\nfn manifest_identity(source_dir: &Path) -> anyhow::Result<(String, String)> {\n    let manifest = fs::read_to_string(source_dir.join(\"Capsule.toml\"))","sourceCodeStart":682,"sourceCodeEnd":718,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L682-L718","documentation":"Raised by copy_legacy_tree while walking a legacy capsule directory during canonical_legacy_archive: an entry is a symlink. The library refuses to copy symlinks into the canonical legacy archive to keep archives deterministic and free of links that could point outside the tree.","triggerScenarios":"Migrating a legacy capsule whose directory tree contains a symlink anywhere under it; copy_legacy_tree recurses via canonical_legacy_archive and also calls itself, so a symlink at any depth triggers it.","commonSituations":"Users replaced large dependency folders with symlinks to save space; node_modules-style links present in legacy layouts; Capsule trees checked out on filesystems or via tools that create links; restored backups that turned directories into symlinks.","solutions":["Find and remove the symlink (the message names its path): replace it with a real directory or a copy of the target content.","Re-copy the capsule tree with symlink dereferencing (e.g. `cp -rL`) to materialize targets as regular files, then retry the migration.","If the link was intentional for out-of-tree content, move the content physically into the capsule directory and link nothing."],"exampleFix":"# before: node_modules points elsewhere\nnode_modules -> /shared/deps\n\n# after: materialize the content as real files\ncp -rL /shared/deps node_modules","handlingStrategy":"validation","validationCode":"fn has_symlinks(dir: &Path) -> std::io::Result<bool> {\n    for e in walkdir(dir) {\n        if e.file_type().is_symlink() { return Ok(true); }\n    }\n    Ok(false)\n}","typeGuard":"fn is_regular_entry(md: &std::fs::Metadata) -> bool {\n    md.is_file() || md.is_dir()\n}","tryCatchPattern":"if let Err(e) = migrate(...) {\n    if e.to_string().contains(\"contains symlink\") {\n        materialize_symlinks(&capsule_dir)?; // cp -rL equivalent\n        migrate(...)?;\n    }\n}","preventionTips":["Don't replace dependency folders with symlinks","Use `cp -rL` when copying capsule trees","Check trees for links with `find . -type l` before migrating"],"tags":["symlink","archive-validation","capsule"],"backgroundTag":"path-traversal-blocked","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}