klieo-tools 0.41.2

Tool dispatch + JSON-schema arg validation + timeout enforcement for klieo-core.
Documentation
//! JSON-schema argument validation.
//!
//! [`validate_args`] takes a tool's declared schema and a candidate JSON
//! arg payload (typically produced by an LLM tool call) and reports
//! whether the payload conforms.
//!
//! Schema-compile failures are mapped to [`ToolError::Permanent`] — the
//! schema is authored by the tool implementer, not the LLM, so a bad
//! schema is a programmer bug. Validation failures are mapped to
//! [`ToolError::InvalidArgs`] — the LLM produced malformed args and the
//! runtime should reject without invoking the tool.

use jsonschema::JSONSchema;
use klieo_core::error::ToolError;

/// Validate `args` against `schema`. Returns `Ok(())` on success.
pub fn validate_args(
    schema: &serde_json::Value,
    args: &serde_json::Value,
) -> Result<(), ToolError> {
    let compiled = JSONSchema::compile(schema)
        .map_err(|e| ToolError::Permanent(format!("invalid tool schema: {e}")))?;
    let result = compiled.validate(args);
    if let Err(errors) = result {
        let messages: Vec<String> = errors.map(|e| format!("{e}")).collect();
        return Err(ToolError::InvalidArgs(messages.join("; ")));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn schema() -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "query": { "type": "string" },
                "limit": { "type": "integer", "minimum": 0 },
            },
            "required": ["query"],
            "additionalProperties": false,
        })
    }

    #[test]
    fn valid_args_pass() {
        let s = schema();
        let args = serde_json::json!({ "query": "hello", "limit": 5 });
        validate_args(&s, &args).unwrap();
    }

    #[test]
    fn missing_required_field_rejected() {
        let s = schema();
        let args = serde_json::json!({ "limit": 5 });
        let err = validate_args(&s, &args).unwrap_err();
        assert!(matches!(err, ToolError::InvalidArgs(_)));
    }

    #[test]
    fn wrong_type_rejected() {
        let s = schema();
        let args = serde_json::json!({ "query": 42 });
        let err = validate_args(&s, &args).unwrap_err();
        assert!(matches!(err, ToolError::InvalidArgs(_)));
    }

    #[test]
    fn extra_field_rejected_when_additional_props_false() {
        let s = schema();
        let args = serde_json::json!({ "query": "hi", "junk": true });
        let err = validate_args(&s, &args).unwrap_err();
        assert!(matches!(err, ToolError::InvalidArgs(_)));
    }

    #[test]
    fn invalid_schema_returns_permanent() {
        let s = serde_json::json!({ "type": "not-a-real-type" });
        let args = serde_json::json!({});
        let err = validate_args(&s, &args).unwrap_err();
        assert!(matches!(err, ToolError::Permanent(_)));
    }

    #[test]
    fn out_of_range_integer_rejected() {
        let s = schema();
        let args = serde_json::json!({ "query": "hi", "limit": -1 });
        let err = validate_args(&s, &args).unwrap_err();
        assert!(matches!(err, ToolError::InvalidArgs(_)));
    }
}