Fix schema-guided tool parameter coercion (#1143)

* Fix schema-guided tool parameter coercion

* Fix CI checks for coercion regression tests

* Finish panic-scan annotations

* Avoid redundant worker param preparation

* Keep panic-scan annotations rustfmt-stable

* Handle nullable WASM schema review feedback

* Address param coercion review notes
This commit is contained in:
Henry Park
2026-03-14 16:27:18 -07:00
committed by GitHub
parent fda5160940
commit c79754df28
9 changed files with 1025 additions and 193 deletions
+59 -4
View File
@@ -8,7 +8,7 @@ use crate::context::JobContext;
use crate::error::Error;
use crate::llm::ChatMessage;
use crate::safety::SafetyLayer;
use crate::tools::{ToolRegistry, redact_params};
use crate::tools::{ToolRegistry, prepare_tool_params, redact_params};
/// Execute a tool with safety checks: lookup → validate → timeout → execute → serialize.
///
@@ -29,8 +29,10 @@ pub async fn execute_tool_with_safety(
name: tool_name.to_string(),
})?;
let normalized_params = prepare_tool_params(tool.as_ref(), params);
// Validate tool parameters
let validation = safety.validator().validate_tool_params(params);
let validation = safety.validator().validate_tool_params(&normalized_params);
if !validation.is_valid {
let details = validation
.errors
@@ -45,7 +47,7 @@ pub async fn execute_tool_with_safety(
.into());
}
let safe_params = redact_params(params, tool.sensitive_params());
let safe_params = redact_params(&normalized_params, tool.sensitive_params());
tracing::debug!(
tool = %tool_name,
params = %safe_params,
@@ -56,7 +58,7 @@ pub async fn execute_tool_with_safety(
let timeout = tool.execution_timeout();
let start = std::time::Instant::now();
let result = tokio::time::timeout(timeout, async {
tool.execute(params.clone(), job_ctx).await
tool.execute(normalized_params.clone(), job_ctx).await
})
.await;
let elapsed = start.elapsed();
@@ -237,6 +239,39 @@ mod tests {
}
}
struct ArrayEchoTool;
#[async_trait::async_trait]
impl Tool for ArrayEchoTool {
fn name(&self) -> &str {
"array_echo"
}
fn description(&self) -> &str {
"Echoes normalized params"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"values": {
"type": "array",
"items": { "type": "integer" }
}
}
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: &JobContext,
) -> Result<ToolOutput, ToolError> {
Ok(ToolOutput::success(params, Duration::default()))
}
fn requires_sanitization(&self) -> bool {
false
}
}
fn test_safety() -> SafetyLayer {
SafetyLayer::new(&crate::config::SafetyConfig {
max_output_length: 100_000,
@@ -348,6 +383,26 @@ mod tests {
);
}
#[tokio::test]
async fn test_execute_normalizes_stringified_array_params() {
let registry = registry_with(vec![Arc::new(ArrayEchoTool)]).await;
let safety = test_safety();
let result = execute_tool_with_safety(
&registry,
&safety,
"array_echo",
&serde_json::json!({"values": "[\"1\", \"2\", 3]"}),
&test_job_ctx(),
)
.await
.expect("array_echo should succeed"); // safety: test-only assertion
let output: serde_json::Value =
serde_json::from_str(&result).expect("tool result should be valid JSON"); // safety: test-only assertion
assert_eq!(output["values"], serde_json::json!([1, 2, 3])); // safety: test-only assertion
}
#[test]
fn test_process_tool_result_success() {
let safety = test_safety();