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

  1. Validate that all path components are UTF-8 (and match the run-id charset) before writing
  2. Use validate_run_id for ids so generated paths are always ASCII
  3. Normalize/convert external names with to_string_lossy checks and reject on lossy conversion
  4. 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

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


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/48e0e8f9b9d767c4. Report an issue: GitHub.