{"record":{"id":"503ef1888b8ff65e","repo":"astrid-runtime/astrid","slug":"workspace-capsule-portal-contains-a-redirect","errorCode":null,"errorMessage":"workspace capsule portal contains a redirect: {}","messagePattern":"workspace capsule portal contains a redirect: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs","lineNumber":165,"sourceCode":"                \"workspace capsule portal is not a regular directory: {}\",\n                root.display()\n            ),\n        ));\n    }\n    astrid_core::platform_fs::verify_no_redirects(root)?;\n    let mut targets = Vec::new();\n    let mut stack = vec![root.to_path_buf()];\n    while let Some(dir) = stack.pop() {\n        let mut entries = fs::read_dir(&dir)\n            .map_err(io::Error::other)?\n            .collect::<Result<Vec<_>, _>>()\n            .map_err(io::Error::other)?;\n        entries.sort_by_key(std::fs::DirEntry::file_name);\n        for entry in entries {\n            let path = entry.path();\n            let metadata = fs::symlink_metadata(&path).map_err(io::Error::other)?;\n            if metadata.file_type().is_symlink() {\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidData,\n                    format!(\n                        \"workspace capsule portal contains a redirect: {}\",\n                        path.display()\n                    ),\n                ));\n            }\n            if !metadata.is_dir() {\n                continue;\n            }\n            astrid_core::platform_fs::verify_no_redirects(&path)?;\n            if path.join(\"Capsule.toml\").is_file() {\n                targets.push(path.clone());\n                if targets.len() > MAX_WORKSPACE_TARGETS {\n                    return Err(io::Error::other(\n                        \"workspace capsule portal exceeds target limit\",\n                    ));\n                }","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs#L147-L183","documentation":"While walking the workspace capsule portal, an entry inside the tree is itself a symlink. The barrier treats any redirect inside the portal as a potential path-traversal vector and aborts the inventory with InvalidData rather than following it. The message names the offending child path.","triggerScenarios":"collect_workspace_targets iterates sorted directory entries and calls symlink_metadata on each child; any child whose file type is a symlink raises this immediately, before recursion. Also triggered if the portal root's own verify_no_redirects pass misses a deeper entry and the walk reaches it.","commonSituations":"Editors or package managers creating convenience symlinks (e.g. node_modules links, shared asset links) inside a workspace; users linking a capsule dir into their dotfiles; migration of data between machines that preserved symlinks.","solutions":["Replace the reported symlink with a real copy of its target: rm the link, then `cp -a` or `mv` the target into place.","Exclude symlinks from the portal directory, keeping only physical capsule directories containing Capsule.toml.","If the link is required, relocate the linked content physically under the portal and update consumers to the new path.","Re-run the migration after cleaning; the error names the exact path each time, so fix iteratively."],"exampleFix":"// before\nln -s ~/shared-capsule ~/.astrid/workspaces/team-capsule\n// after\nrm ~/.astrid/workspaces/team-capsule\ncp -a ~/shared-capsule ~/.astrid/workspaces/team-capsule","handlingStrategy":"validation","validationCode":"fn tree_has_symlinks(root: &std::path::Path) -> std::io::Result<Vec<std::path::PathBuf>> {\n    let mut hits = Vec::new();\n    for entry in walkdir_like(root) {\n        if std::fs::symlink_metadata(&entry)?.file_type().is_symlink() {\n            hits.push(entry);\n        }\n    }\n    Ok(hits) // must be empty before migration\n}","typeGuard":"fn is_plain_entry(m: &std::fs::Metadata) -> bool {\n    !m.file_type().is_symlink()\n}","tryCatchPattern":"match collect_workspace_targets(root) {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"contains a redirect\") => {\n        let p = extract_path(&e.to_string());\n        eprintln!(\"replace symlink with a real copy: {p}\");\n        Vec::new()\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Keep convenience symlinks (dotfiles, shared assets) outside the portal tree","Run `find <portal> -type l` as a pre-migration checklist step","Teach package managers/editors to place links elsewhere (e.g. global cache dirs)"],"tags":["filesystem","symlink","io","rust"],"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-14T05:17:10.506Z"}