mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-15 23:32:46 +00:00
Reject colliding inline workflow paths during tool validation
Two inline file paths that differ only by case, or a file that is also an ancestor directory of another, used to surface as platform-dependent low-level I/O errors naming a private temporary directory, and the case-only case succeeded on Linux while failing on macOS. Validate both shapes in fabro_run_create input validation so callers get a clear message before any staging or registration happens. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
fefc236ab9
commit
001ed111fb
1 changed files with 48 additions and 0 deletions
|
|
@ -552,6 +552,9 @@ fn validate_workflow_source(
|
|||
source.entrypoint
|
||||
)));
|
||||
}
|
||||
fabro_types::validate_workflow_path_collisions(source.files.keys())
|
||||
.map_err(|err| ToolError::message(format!("inline workflow {err}")))?;
|
||||
validate_inline_paths_distinct_ignoring_case(&source.files)?;
|
||||
let mut total_bytes = 0usize;
|
||||
for (path, content) in &source.files {
|
||||
let bytes = content.len();
|
||||
|
|
@ -577,6 +580,24 @@ fn validate_workflow_source(
|
|||
}
|
||||
}
|
||||
|
||||
/// Inline files are staged on, and later checked out to, filesystems that may
|
||||
/// be case-insensitive, so two paths that differ only by case would silently
|
||||
/// overwrite each other there. Reject them up front with a clear message
|
||||
/// instead of surfacing a platform-dependent I/O error later.
|
||||
fn validate_inline_paths_distinct_ignoring_case(
|
||||
files: &BTreeMap<WorkflowPath, String>,
|
||||
) -> ToolResult<()> {
|
||||
let mut seen: HashMap<String, &WorkflowPath> = HashMap::with_capacity(files.len());
|
||||
for path in files.keys() {
|
||||
if let Some(existing) = seen.insert(path.as_str().to_lowercase(), path) {
|
||||
return Err(ToolError::message(format!(
|
||||
"inline workflow files `{existing}` and `{path}` differ only by case; workflow files must stay distinct on case-insensitive filesystems"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct CreateRunsResult {
|
||||
pub runs: Vec<CreatedRunResult>,
|
||||
|
|
@ -925,6 +946,33 @@ mod tests {
|
|||
.contains("entrypoint")
|
||||
);
|
||||
|
||||
for (files, expected) in [
|
||||
(json!({ "a": "x", "a/b.md": "y" }), "paths collide"),
|
||||
(
|
||||
json!({ "main.fabro": "digraph W {}", "Prompt.md": "x", "prompt.md": "y" }),
|
||||
"differ only by case",
|
||||
),
|
||||
] {
|
||||
let entrypoint = files
|
||||
.as_object()
|
||||
.and_then(|files| files.keys().next().cloned())
|
||||
.unwrap();
|
||||
let colliding: FabroRunCreateParams = serde_json::from_value(json!({
|
||||
"runs": [{
|
||||
"workflow": {
|
||||
"kind": "inline",
|
||||
"entrypoint": entrypoint,
|
||||
"files": files
|
||||
}
|
||||
}]
|
||||
}))
|
||||
.unwrap();
|
||||
let error = ValidatedCreateRuns::try_from(colliding)
|
||||
.expect_err("colliding inline paths must fail validation before any staging")
|
||||
.to_string();
|
||||
assert!(error.contains(expected), "unexpected error: {error}");
|
||||
}
|
||||
|
||||
let too_many = (0..=fabro_types::MAX_WORKFLOW_VERSION_FILES)
|
||||
.map(|index| (format!("files/{index}.md"), json!("x")))
|
||||
.collect::<serde_json::Map<_, _>>();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue