astrid-runtime/astrid · error

capsule source symlink

Error message

capsule source symlink {} resolves outside source root

What it means

Raised by collect_entries during canonical_capsule_archive when a symlink inside the capsule source canonicalizes to a path outside the source root. The library permits file symlinks (to keep npm-style node_modules/.bin working) only if their targets stay within the capsule, preventing archive content that escapes the source tree.

Solutions

  1. Move the symlink target inside the capsule source directory and update the link to a relative in-tree path.
  2. Replace the symlink with a real copy of the target file inside the capsule.
  3. If the content belongs to another package, publish it separately and depend on it rather than linking.

Example fix

// before: escapes the source root
vendor/lib.wit -> ../../shared/lib.wit

// after: keep the target inside the capsule
vendor/lib.wit (regular file copy of shared/lib.wit)
Defensive patterns

Strategy: validation

Validate before calling

fn symlink_escapes_root(link: &Path, root: &Path) -> std::io::Result<bool> {
    let target = std::fs::canonicalize(link)?;
    let root = std::fs::canonicalize(root)?;
    Ok(!target.starts_with(root))
}

Type guard

fn is_in_tree_link(link: &Path, root: &Path) -> bool {
    std::fs::canonicalize(link).ok()
        .zip(std::fs::canonicalize(root).ok())
        .map_or(false, |(t, r)| t.starts_with(r))
}

Try / catch

match publish(...) {
    Err(e) if e.to_string().contains("resolves outside source root") => { eprintln!("copy linked assets into the capsule"); exit(1); }
    other => other?,
}

Prevention

When it happens

Trigger: Archiving a capsule whose directory contains a symlink pointing to a file outside the capsule directory (e.g. ../shared/lib.wit or /usr/share/foo); fs::canonicalize resolves it and the starts_with(canonical_root) check fails.

Common situations: Sharing a common asset via ../ links during development; node_modules links hoisted above the package root in monorepos; linking to a globally installed tool or config file.

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/ab0e722bef4f9123. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-capsule-install/src/storage.rs:789

    root: &Path,
    current: &Path,
    entries: &mut Vec<(PathBuf, Metadata)>,
) -> anyhow::Result<()> {
    let mut children = read_dir_sorted(current)?;
    for (name, metadata) in children.drain(..) {
        let relative = name
            .strip_prefix(root)
            .map_err(|_| anyhow::anyhow!("capsule path escaped source root"))?
            .to_path_buf();
        let file_type = metadata.file_type();
        if file_type.is_symlink() {
            let resolved = fs::canonicalize(&name).with_context(|| {
                format!("canonicalize capsule source symlink {}", relative.display())
            })?;
            let canonical_root = fs::canonicalize(root)
                .with_context(|| format!("canonicalize capsule source root {}", root.display()))?;
            if !resolved.starts_with(&canonical_root) {
                bail!(
                    "capsule source symlink {} resolves outside source root",
                    relative.display()
                );
            }
            let resolved_metadata = fs::metadata(&resolved).with_context(|| {
                format!("stat capsule source symlink target {}", relative.display())
            })?;
            if !resolved_metadata.is_file() {
                bail!(
                    "capsule source symlink {} does not resolve to a regular file",
                    relative.display()
                );
            }
            // File links are materialized as regular archive entries. This
            // preserves npm's node_modules/.bin links without ever storing a
            // redirect in the durable package.
            entries.push((relative, resolved_metadata));
            continue;

View on GitHub (pinned to affd8760f4)