{"record":{"id":"00a8647e49f25c4f","repo":"block/buzz","slug":"oauth-cache-path-has-no-parent-directory","errorCode":null,"errorMessage":"oauth cache path has no parent directory","messagePattern":"oauth cache path has no parent directory","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/buzz-agent/src/auth.rs","lineNumber":1880,"sourceCode":"    format!(\"{nanos:x}\")\n}\n\n/// Write `body` to `path` as an owner-only file via an atomic rename.\n///\n/// The cache holds both the refresh and access tokens, so it must never be\n/// readable by other users. We create a uniquely-named temp file in the same\n/// directory with owner-only protection at creation time — mode `0o600` on\n/// Unix (see [`create_private_temp_file`]) — so it is never briefly\n/// world/other readable, write and fsync it, then rename over the\n/// destination. The rename swaps the inode/entry wholesale, so a pre-existing\n/// cache file with loose permissions is *replaced* by the new private one;\n/// its old mode never survives. `fs::rename` maps to\n/// `MOVEFILE_REPLACE_EXISTING` on Windows, so the atomic replace holds on\n/// both platforms; the Windows owner-only DACL is pending the unsafe-FFI\n/// decision noted at the seam.\nfn write_private_cache(path: &Path, body: &[u8]) -> io::Result<()> {\n    let parent = path.parent().ok_or_else(|| {\n        io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"oauth cache path has no parent directory\",\n        )\n    })?;\n    fs::create_dir_all(parent)?;\n\n    let file_name = path\n        .file_name()\n        .and_then(|n| n.to_str())\n        .unwrap_or(\"oauth-cache\");\n    let tmp = parent.join(format!(\".{file_name}.{}.tmp\", unique_suffix()));\n    let guard = TmpFileGuard(&tmp);\n\n    let mut f = create_private_temp_file(&tmp)?;\n    f.write_all(body)?;\n    f.sync_all()?;\n    drop(f);\n","sourceCodeStart":1862,"sourceCodeEnd":1898,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-agent/src/auth.rs#L1862-L1898","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the effective cache location: log or inspect cache_path_for output — the override dir must be a real directory, not '/'.","Set cache_dir_override to a concrete directory (e.g. $XDG_CACHE_HOME/buzz-agent) instead of a root path.","If you are calling write_private_cache/read_private_cache directly, guard the argument: assert path.parent().is_some() before use.","Once the path is sane, the token save proceeds; the existing cache (if any) keeps working since reads use the same computed path."],"exampleFix":"// before\nlet path = std::path::PathBuf::from(\"/\");\nwrite_private_cache(&path, body)?; // oauth cache path has no parent directory\n\n// after\nlet dir = std::env::var_os(\"XDG_CACHE_HOME\")\n    .map(PathBuf::from)\n    .unwrap_or_else(|| dirs::home_dir().unwrap().join(\".cache\"));\nlet path = dir.join(\"buzz-agent\").join(\"token.json\");","handlingStrategy":"validation","validationCode":"// guard before persisting\ndebug_assert!(path.parent().is_some(), \"cache path must have a parent dir\");\nif path.parent().is_none() { return Ok(()); } // skip save, tokens stay in memory","typeGuard":"fn has_parent(p: &std::path::Path) -> bool {\n    p.parent().map(|d| !d.as_os_str().is_empty()).unwrap_or(false)\n}","tryCatchPattern":"// the existing writer surfaces io::Error; callers already map it to AgentError\n.map_err(|e| AgentError::Llm(format!(\"oauth cache write {:?}: {e}\", path)))","preventionTips":["Always derive cache paths via cache_path_for-style joins (dir + namespace + filename) instead of literals.","Validate cache_dir_override configuration at startup (must be an existing directory, not '/').","Unit-test the write path with a tempdir so degenerate paths never reach production."],"tags":["rust","filesystem","oauth","cache","path-handling"],"backgroundTag":"invalid-path","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-08-20T04:38:24.874Z","contentChangedAt":"2026-08-20T04:38:24.874Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}