astrid-runtime/astrid · error

workspace capsule portal is not a regular directory: {}

Error message

workspace capsule portal is not a regular directory: {}

What it means

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.

Source

Thrown at crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs:144

                ),
            ));
        }
    }
    Ok(())
}

/// Collect capsule directories below a workspace portal without following
/// redirects.  The migration barrier uses this inventory when checking that
/// no legacy authority receipts remain attached to a workspace capsule.
pub(super) fn collect_workspace_targets(root: &Path) -> io::Result<Vec<PathBuf>> {
    const MAX_WORKSPACE_TARGETS: usize = 4096;
    let metadata = match fs::symlink_metadata(root) {
        Ok(metadata) => metadata,
        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
        Err(error) => return Err(error),
    };
    if metadata.file_type().is_symlink() || !metadata.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "workspace capsule portal is not a regular directory: {}",
                root.display()
            ),
        ));
    }
    astrid_core::platform_fs::verify_no_redirects(root)?;
    let mut targets = Vec::new();
    let mut stack = vec![root.to_path_buf()];
    while let Some(dir) = stack.pop() {
        let mut entries = fs::read_dir(&dir)
            .map_err(io::Error::other)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(io::Error::other)?;
        entries.sort_by_key(std::fs::DirEntry::file_name);
        for entry in entries {
            let path = entry.path();

View on GitHub (pinned to affd8760f4)

Solutions

  1. Remove the symlink and create a real directory at the portal path (mv the symlink aside, mkdir, copy contents).
  2. Point the application's workspace-root configuration at the actual physical directory instead of a symlinked path.
  3. 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.
  4. Re-run migration; note NotFound is treated as an empty inventory, so ensure the path exists.

Example fix

// before
ln -s /data/workspaces ~/.astrid/workspaces
// after
rm ~/.astrid/workspaces
mkdir ~/.astrid/workspaces
cp -a /data/workspaces/. ~/.astrid/workspaces/
Defensive patterns

Strategy: validation

Validate before calling

use std::fs;
fn portal_is_regular_dir(root: &std::path::Path) -> std::io::Result<bool> {
    match fs::symlink_metadata(root) {
        Ok(m) => Ok(!m.file_type().is_symlink() && m.is_dir()),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(true), // absent is OK
        Err(e) => Err(e),
    }
}

Type guard

fn is_real_dir(m: &std::fs::Metadata) -> bool {
    !m.file_type().is_symlink() && m.is_dir()
}

Try / catch

match workspace_portal_targets(root) {
    Ok(targets) => targets,
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData
        && e.to_string().contains("not a regular directory") =>
    {
        eprintln!("portal must be a real directory, not a symlink: {e}");
        Vec::new()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/683fc080ffd17ac9. Report an issue: GitHub.