block/buzz · error · std::io::Error

oauth cache path has no parent directory

Error message

oauth cache path has no parent directory

What it means

write_private_cache atomically persists OAuth tokens: it resolves path.parent() to create the containing directory and a same-directory temp file. Path::parent() returns None only when the path is a filesystem root ('/') or empty, so this InvalidInput error means the computed cache path has no directory component at all. It is a defensive guard for an impossible-in-practice configuration rather than a normal runtime condition.

Source

Thrown at crates/buzz-agent/src/auth.rs:1880

    format!("{nanos:x}")
}

/// Write `body` to `path` as an owner-only file via an atomic rename.
///
/// The cache holds both the refresh and access tokens, so it must never be
/// readable by other users. We create a uniquely-named temp file in the same
/// directory with owner-only protection at creation time — mode `0o600` on
/// Unix (see [`create_private_temp_file`]) — so it is never briefly
/// world/other readable, write and fsync it, then rename over the
/// destination. The rename swaps the inode/entry wholesale, so a pre-existing
/// cache file with loose permissions is *replaced* by the new private one;
/// its old mode never survives. `fs::rename` maps to
/// `MOVEFILE_REPLACE_EXISTING` on Windows, so the atomic replace holds on
/// both platforms; the Windows owner-only DACL is pending the unsafe-FFI
/// decision noted at the seam.
fn write_private_cache(path: &Path, body: &[u8]) -> io::Result<()> {
    let parent = path.parent().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            "oauth cache path has no parent directory",
        )
    })?;
    fs::create_dir_all(parent)?;

    let file_name = path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("oauth-cache");
    let tmp = parent.join(format!(".{file_name}.{}.tmp", unique_suffix()));
    let guard = TmpFileGuard(&tmp);

    let mut f = create_private_temp_file(&tmp)?;
    f.write_all(body)?;
    f.sync_all()?;
    drop(f);

View on GitHub (pinned to dad5a33865)

Solutions

  1. Check the effective cache location: log or inspect cache_path_for output — the override dir must be a real directory, not '/'.
  2. Set cache_dir_override to a concrete directory (e.g. $XDG_CACHE_HOME/buzz-agent) instead of a root path.
  3. If you are calling write_private_cache/read_private_cache directly, guard the argument: assert path.parent().is_some() before use.
  4. Once the path is sane, the token save proceeds; the existing cache (if any) keeps working since reads use the same computed path.

Example fix

// before
let path = std::path::PathBuf::from("/");
write_private_cache(&path, body)?; // oauth cache path has no parent directory

// after
let dir = std::env::var_os("XDG_CACHE_HOME")
    .map(PathBuf::from)
    .unwrap_or_else(|| dirs::home_dir().unwrap().join(".cache"));
let path = dir.join("buzz-agent").join("token.json");
Defensive patterns

Strategy: validation

Validate before calling

// guard before persisting
debug_assert!(path.parent().is_some(), "cache path must have a parent dir");
if path.parent().is_none() { return Ok(()); } // skip save, tokens stay in memory

Type guard

fn has_parent(p: &std::path::Path) -> bool {
    p.parent().map(|d| !d.as_os_str().is_empty()).unwrap_or(false)
}

Try / catch

// the existing writer surfaces io::Error; callers already map it to AgentError
.map_err(|e| AgentError::Llm(format!("oauth cache write {:?}: {e}", path)))

Prevention

When it happens

Trigger: write_private_cache at crates/buzz-agent/src/auth.rs:566-573 constructs the cache path from cache_dir_override/cache_namespace + a '<sha256>.json' filename (cache_path_for); parent() can only be None if the joined path degenerates to '/' — e.g. a cache_dir_override of '/' with a namespace that joins to empty on the platform, or a caller passing PathBuf::from("")/'/' directly into the private-cache helper. The error then surfaces to callers as AgentError::Llm("oauth cache write: ...") on token save.

Common situations: Almost never seen in production; realistically triggered by tests or tooling that call write_private_cache with a degenerate path, or by an exotic cache_dir_override like '/' combined with path normalization that collapses the filename away.

Related errors


AI-assisted analysis of block/buzz@dad5a33865 (2026-08-20). Data as JSON: /api/errors/00a8647e49f25c4f. Report an issue: GitHub.