xai-org/grok-build · error · std::io::Error
workflow path is not UTF-8
Error message
workflow path is not UTF-8
What it means
atomic_write derives the temp file name from the destination's file_name().to_str(). If the file name is not valid UTF-8 (non-UTF8 bytes on the filesystem), it returns InvalidInput with this message, since the tmp naming scheme requires a str.
Source
Thrown at crates/codegen/xai-grok-shell/src/session/workflow/store.rs:378
format!("immutable workflow file already exists: {}", path.display()),
));
}
atomic_write(path, bytes, false)
}
fn atomic_write_replace(path: &Path, bytes: &[u8]) -> io::Result<()> {
atomic_write(path, bytes, true)
}
fn atomic_write(path: &Path, bytes: &[u8], replace: bool) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "workflow path has no parent")
})?;
std::fs::create_dir_all(parent)?;
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "workflow path is not UTF-8"))?;
let tmp = parent.join(format!(
".{file_name}.{}.{}.tmp",
std::process::id(),
uuid::Uuid::now_v7().simple()
));
let result = (|| {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)?;
file.write_all(bytes)?;
file.sync_all()?;
drop(file);
if !replace && path.exists() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("immutable workflow file already exists: {}", path.display()),
));View on GitHub (pinned to bc7f02eddd)
Solutions
- Validate that all path components are UTF-8 (and match the run-id charset) before writing
- Use validate_run_id for ids so generated paths are always ASCII
- Normalize/convert external names with to_string_lossy checks and reject on lossy conversion
- Store ids generated internally (uuid simple) rather than raw filesystem names
Example fix
// before
let dir_name = std::env::args_os().nth(1).unwrap(); // may be non-UTF-8
let path = runs_root.join(dir_name).join("runs.json");
// after
let dir_name = arg.into_string().map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "non-UTF-8 run id"))?;
validate_run_id(&dir_name)?;
let path = runs_root.join(dir_name).join("runs.json"); Defensive patterns
Strategy: validation
Validate before calling
fn utf8_components(p: &std::path::Path) -> bool {
p.components().all(|c| c.as_os_str().to_str().is_some())
} Type guard
fn as_utf8_path(p: std::path::PathBuf) -> Option<std::path::PathBuf> {
if p.components().all(|c| c.as_os_str().to_str().is_some()) { Some(p) } else { None }
} Try / catch
if !utf8_components(&path) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "workflow path contains non-UTF-8 components"));
}
atomic_write(&path, &bytes, true)?; Prevention
- Validate run ids with validate_run_id (ASCII-only) before building paths
- Convert external OsStr input via into_string() and reject on failure
- Avoid os-generated names (readdir results) as workflow path components without revalidation
- Standardize on internally generated ASCII ids for all run directories
When it happens
Trigger: Writing an artifact whose path contains non-UTF-8 bytes — e.g. a run directory or file name built from OsString/os platform data with invalid bytes, then passed to atomic_write_new/atomic_write_replace.
Common situations: Run ids or directory names imported from external systems with non-UTF-8 encodings; user-supplied names on filesystems allowing arbitrary bytes; concatenating unvalidated OsStr input into workflow paths.
Related errors
- workflow path has no parent
- target has no parent
- path contains NUL
- trust store path has no parent
- Local marketplace path not found (or is not a directory): {}
AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31).
Data as JSON: /api/errors/48e0e8f9b9d767c4.
Report an issue: GitHub.