Drop the duplicate-key guard that no tool route can reach

FabroWorkflowVersionCreateParams deserialized `files` through a
duplicate-rejecting map, but both production routes (rmcp Parameters
and the native LLM tool dispatch) deserialize from an already-parsed
serde_json::Value in which duplicate keys have collapsed last-wins. The
only test that exercised the guard used serde_json::from_str, the one
entry point production never uses, so the safeguard was misleading.

Remove the attribute and its byte-level test, and drop the
deserialize_unique_map export that existed only for it. The canonical
WorkflowVersion wire type keeps its own duplicate-key rejection, which
does run on the byte-level HTTP route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-09-11 15:25:34 -06:00
parent 4b9db26027
commit 70b1090f58
3 changed files with 4 additions and 20 deletions

View file

@ -21,8 +21,9 @@ pub struct FabroWorkflowVersionCreateParams {
#[schemars(with = "String")]
pub entrypoint: WorkflowPath,
/// All local dependencies, keyed by package-relative path. Values are text
/// contents.
#[serde(deserialize_with = "fabro_types::deserialize_unique_map")]
/// contents. Tool arguments arrive as an already-parsed JSON value on
/// every production route, so duplicate keys have collapsed (last wins)
/// before this type sees them; there is no byte-level guard to add here.
#[schemars(with = "BTreeMap<String, String>")]
pub files: BTreeMap<WorkflowPath, String>,
}
@ -127,12 +128,6 @@ mod tests {
fn workflow_version_request_rejects_unknown_fields_and_invalid_paths() {
let valid = json!({"entrypoint": "workflow", "files": {"workflow": "digraph W {}"}});
validate(valid.clone()).unwrap();
assert!(
serde_json::from_str::<FabroWorkflowVersionCreateParams>(
r#"{"entrypoint":"workflow","files":{"workflow":"a","workflow":"b"}}"#
)
.is_err()
);
for field in [
"cwd",
"url",

View file

@ -197,7 +197,7 @@ pub use workflow_path::{
};
pub use workflow_version::{
MAX_WORKFLOW_VERSION_BYTES, MAX_WORKFLOW_VERSION_DEPENDENCIES, MAX_WORKFLOW_VERSION_FILE_BYTES,
MAX_WORKFLOW_VERSION_FILES, WorkflowVersion, WorkflowVersionShapeError, deserialize_unique_map,
MAX_WORKFLOW_VERSION_FILES, WorkflowVersion, WorkflowVersionShapeError,
validate_workflow_source_paths,
};
pub use workflow_version_id::{WorkflowVersionId, WorkflowVersionIdParseError};

View file

@ -236,17 +236,6 @@ impl<'de> Deserialize<'de> for WorkflowVersion {
}
}
/// Deserialize a map while rejecting duplicate keys, which serde would
/// otherwise silently collapse to the last value.
pub fn deserialize_unique_map<'de, D, K, V>(deserializer: D) -> Result<BTreeMap<K, V>, D::Error>
where
D: Deserializer<'de>,
K: Deserialize<'de> + Ord + fmt::Display,
V: Deserialize<'de>,
{
UniqueBTreeMap::deserialize(deserializer).map(|map| map.0)
}
struct UniqueBTreeMap<K, V>(BTreeMap<K, V>);
impl<'de, K, V> Deserialize<'de> for UniqueBTreeMap<K, V>