xai-org/grok-build · error · std::io::Error

workflow path has no parent

Error message

workflow path has no parent

What it means

atomic_write needs the parent directory of the destination path to create the temp file and rename. If path.parent() returns None (the path is a bare file name with no directory component), it returns InvalidInput with this message.

Source

Thrown at crates/codegen/xai-grok-shell/src/session/workflow/store.rs:372

}

fn atomic_write_new(path: &Path, bytes: &[u8]) -> io::Result<()> {
    if path.exists() {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            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()?;

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Always pass fully-qualified paths that include the parent directory (e.g. run_dir.join("runs.json"))
  2. Fix callers to build paths from the configured runs root, not bare names
  3. Verify path construction with .parent().is_some() before calling store write APIs

Example fix

// before
let path = Path::new("runs.json"); // no parent
// after
let path = Path::new(RUNS_ROOT).join("run-abc").join("runs.json");
Defensive patterns

Strategy: validation

Validate before calling

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

Type guard

fn with_parent(p: &std::path::Path) -> Option<&std::path::Path> {
    p.parent().filter(|parent| !parent.as_os_str().is_empty())
}

Try / catch

let path = build_full_path(&run_dir, "runs.json");
assert!(path.parent().is_some(), "workflow paths must include a parent dir");
atomic_write_replace(&path, &bytes)?;

Prevention

When it happens

Trigger: Passing a relative bare name like "runs.json" (no directory) to atomic_write via atomic_write_new/atomic_write_replace; constructing a path with Path::new(file_name) instead of joining it to the runs dir.

Common situations: Refactor changed an absolute path into a bare file name; a config value holding just a filename was passed where a full path is required; misuse of Path::file_name output fed back as a path.

Related errors


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