astrid-runtime/astrid · error

capsule materialization target is redirected or not a direct

Error message

capsule materialization target is redirected or not a directory

What it means

Before reusing or replacing an existing capsule projection, repair_published_materialization inspects the target path with symlink_metadata (following nothing). If the path exists but is a symlink or any non-directory entry, the kernel bails rather than deleting or writing through it — a redirected target could point the removal/re-materialization at arbitrary filesystem locations. Note symlink_metadata does not follow the final component, so a symlinked target is detected directly.

Source

Thrown at crates/astrid-kernel/src/capsule_materialization.rs:186

    /// Replace a canonical stale projection without trusting the old manifest.
    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
    pub(crate) fn repair_published_materialization(
        &self,
        target: &Path,
        principal: &astrid_core::principal::PrincipalId,
        discovery_manifest: &astrid_capsule_types::manifest::CapsuleManifest,
        snapshot: &astrid_storage::CapsulePackageSnapshot,
    ) -> anyhow::Result<astrid_capsule_types::manifest::CapsuleManifest> {
        self.validate_published_cache_path(target, principal, discovery_manifest, snapshot)?;
        let target_metadata = match std::fs::symlink_metadata(target) {
            Ok(metadata) => Some(metadata),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
            Err(error) => return Err(anyhow::anyhow!("inspect capsule materialization: {error}")),
        };
        if let Some(metadata) = target_metadata {
            if metadata.file_type().is_symlink() || !metadata.is_dir() {
                anyhow::bail!("capsule materialization target is redirected or not a directory");
            }
            if let Ok(bound_manifest) =
                astrid_capsule::discovery::load_manifest(&target.join("Capsule.toml"))
                && self
                    .verify_published_materialization(target, principal, &bound_manifest, snapshot)
                    .is_ok()
            {
                return Ok(bound_manifest);
            }
            astrid_core::platform_fs::verify_no_redirects(target).map_err(|error| {
                anyhow::anyhow!("capsule materialization target is redirected: {error}")
            })?;
            std::fs::remove_dir_all(target).map_err(|error| {
                anyhow::anyhow!("remove stale capsule materialization: {error}")
            })?;
        }
        astrid_capsule_install::materialize_capsule_package(snapshot.package(), target)
            .map_err(|error| anyhow::anyhow!("materialize durable capsule package: {error:#}"))?;

View on GitHub (pinned to affd8760f4)

Solutions

  1. Remove the symlink or non-directory entry at the target path yourself (after confirming it is safe), then retry so the kernel can materialize a real directory there.
  2. Reconfigure the cache/state location to its real directory path instead of using a symlink (the kernel intentionally refuses to traverse redirected targets).
  3. If the path should be a directory, inspect what created the file/symlink (dotfile managers, previous tools) and fix that setup before re-running.
  4. Use an alternative target path under the workspace state dir that is a plain directory.

Example fix

// before: cache path is a symlink, kernel refuses
// ~/.astrid/capsules/my-capsule -> /mnt/bigdisk/my-capsule (symlink)

// after: remove the redirect and let the kernel own the directory
std::fs::remove_file(&runtime_dir)?; // removes the symlink itself
std::fs::create_dir_all(&runtime_dir)?;
let manifest = kernel.ensure_published_materialization(
    &runtime_dir, &principal, &discovery_manifest, &snapshot,
)?;
Defensive patterns

Strategy: validation

Validate before calling

// Refuse to proceed if the target is not a plain directory
let meta = std::fs::symlink_metadata(&target)?;
if meta.file_type().is_symlink() || !meta.is_dir() {
    std::fs::remove_file(&target)?; // drop redirect; kernel will re-materialize
}

Type guard

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

Try / catch

match kernel.ensure_published_materialization(&target, &principal, &manifest, &snapshot) {
    Err(e) if e.to_string().contains("redirected or not a directory") => {
    remove_redirect_at(&target)?;
    kernel.ensure_published_materialization(&target, &principal, &manifest, &snapshot)
    }
    other => other,
}

Prevention

When it happens

Trigger: repair_published_materialization (via capture_bound_materialization or ensure_published_materialization) is called with a target that exists and std::fs::symlink_metadata reports file_type().is_symlink() == true or is_dir() == false — e.g. the cache path is a symlink to another location, or a regular file/socket was left at the expected directory path.

Common situations: A user symlinked the cache directory to a bigger disk or to dotfile-managed storage; a previous failed run left a stray file where the directory should be; container image layers replaced the directory with a symlink; the state_dir was relocated and a compat symlink was created manually.

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