astrid-runtime/astrid · error

legacy source is not an empty regular directory

Error message

legacy source is not an empty regular directory: {}

What it means

`retire_empty_directory` removes an empty legacy source directory only if it is a real, non-symlink directory. Anything else (symlink, file) is rejected with `InvalidData` to guarantee retirement never deletes through a redirect.

Solutions

  1. Remove the symlink or file at the path and create/verify a real empty directory before migration.
  2. Point the migration at the genuine legacy directory path.
  3. Clean up the legacy layout so only real directories remain.

Example fix

# before
legacy-secrets -> /mnt/backup/legacy-secrets
# after
rm legacy-secrets && mkdir legacy-secrets && copy files in
Defensive patterns

Strategy: validation

Validate before calling

fn retire_ok(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p)
        .map(|m| !m.file_type().is_symlink() && m.is_dir())
        .unwrap_or(false)
}

Type guard

fn is_plain_directory(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p).map(|m| m.is_dir() && !m.file_type().is_symlink()).unwrap_or(false)
}

Try / catch

if is_plain_directory(legacy_dir) {
    retire_empty_directory(legacy_dir)?;
} else {
    eprintln!("legacy dir is a symlink/file; normalize before migration");
}

Prevention

When it happens

Trigger: Called from `ensure_legacy_secret_aliases`, `import_legacy_system_secrets`, or `handle_non_default_audit_source` when the path's `symlink_metadata` reports `is_symlink()` or not `is_dir()`.

Common situations: Legacy secret dir was symlinked to a dotfiles repo; the 'directory' was replaced by a file; a sync client substituted a placeholder file.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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

Appendix: source

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

            sources,
            format!("principal:{uid}:env:{capsule}"),
            home.principal_home(alias)
                .env_dir()
                .join(format!("{capsule}.env.json")),
        )?;
        add_source(
            sources,
            format!("principal:{uid}:secret:{capsule}"),
            home.secrets_dir().join(alias.as_str()).join(capsule),
        )?;
    }
    Ok(())
}

pub(super) fn retire_empty_directory(path: &Path) -> io::Result<()> {
    let metadata = fs::symlink_metadata(path)?;
    if metadata.file_type().is_symlink() || !metadata.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "legacy source is not an empty regular directory: {}",
                path.display()
            ),
        ));
    }
    astrid_core::platform_fs::verify_no_redirects(path)?;
    fs::remove_dir(path).map_err(io::Error::other)?;
    sync_parent(path)
}

/// Check every alias-keyed child of the released `secrets/` root.  The
/// barrier passes `allow_empty_cleanup=false` while resuming a completed
/// ledger, so a deleted or renamed principal cannot leave a reappeared empty
/// directory that is silently swept on restart.
pub(super) fn ensure_legacy_secret_aliases(
    root: &Path,

View on GitHub (pinned to affd8760f4)