{"record":{"id":"683fc080ffd17ac9","repo":"astrid-runtime/astrid","slug":"workspace-capsule-portal-is-not-a-regular-director","errorCode":null,"errorMessage":"workspace capsule portal is not a regular directory: {}","messagePattern":"workspace capsule portal is not a regular directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs","lineNumber":144,"sourceCode":"                ),\n            ));\n        }\n    }\n    Ok(())\n}\n\n/// Collect capsule directories below a workspace portal without following\n/// redirects.  The migration barrier uses this inventory when checking that\n/// no legacy authority receipts remain attached to a workspace capsule.\npub(super) fn collect_workspace_targets(root: &Path) -> io::Result<Vec<PathBuf>> {\n    const MAX_WORKSPACE_TARGETS: usize = 4096;\n    let metadata = match fs::symlink_metadata(root) {\n        Ok(metadata) => metadata,\n        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),\n        Err(error) => return Err(error),\n    };\n    if metadata.file_type().is_symlink() || !metadata.is_dir() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"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();","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs#L126-L162","documentation":"The workspace capsule portal (the root passed to collect_workspace_targets) is not a plain directory: it is a symlink or some non-directory entry. The migration barrier refuses to traverse portals that could redirect the walk, so inventory of workspace capsules fails with io::ErrorKind::InvalidData. This is a deliberate anti-symlink safety check, not a lookup failure.","triggerScenarios":"Calling collect_workspace_targets (via workspace_portal_targets) when the root path is a symlink to a directory, a regular file, a FIFO/socket/device node, or otherwise fails the symlink_metadata is_dir check. NotFound returns empty instead, so the path must exist to trigger this.","commonSituations":"Deployments that replaced the workspace portal with a symlink for redirection or NFS mounting; packaging scripts that bind a single capsule file where a directory is expected; restoring the home from an archive tool that materialized symlinks.","solutions":["Remove the symlink and create a real directory at the portal path (mv the symlink aside, mkdir, copy contents).","Point the application's workspace-root configuration at the actual physical directory instead of a symlinked path.","Check the entry type with `ls -ld <root>`; if it is a file or other special node, move it out of the way and recreate it as a directory.","Re-run migration; note NotFound is treated as an empty inventory, so ensure the path exists."],"exampleFix":"// before\nln -s /data/workspaces ~/.astrid/workspaces\n// after\nrm ~/.astrid/workspaces\nmkdir ~/.astrid/workspaces\ncp -a /data/workspaces/. ~/.astrid/workspaces/","handlingStrategy":"validation","validationCode":"use std::fs;\nfn portal_is_regular_dir(root: &std::path::Path) -> std::io::Result<bool> {\n    match fs::symlink_metadata(root) {\n        Ok(m) => Ok(!m.file_type().is_symlink() && m.is_dir()),\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(true), // absent is OK\n        Err(e) => Err(e),\n    }\n}","typeGuard":"fn is_real_dir(m: &std::fs::Metadata) -> bool {\n    !m.file_type().is_symlink() && m.is_dir()\n}","tryCatchPattern":"match workspace_portal_targets(root) {\n    Ok(targets) => targets,\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData\n        && e.to_string().contains(\"not a regular directory\") =>\n    {\n        eprintln!(\"portal must be a real directory, not a symlink: {e}\");\n        Vec::new()\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never symlink the workspace portal; if redirection is needed, use the app's own configuration for the path","After restoring from backups, audit for symlinks: `find ~/.astrid -type l`","Use archive tools that dereference or preserve directories as directories (tar --dereference)"],"tags":["filesystem","symlink","io","rust"],"backgroundTag":"path-is-not-a-directory","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"}