mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fcc20ec72c
Validate tool call arguments against each tool's Parameters() JSON Schema
in ExecuteWithContext() before calling Execute(). This prevents type
confusion, argument injection, and missing-field errors from reaching tools.
Validates: required fields, type matching (string/integer/number/boolean/
array/object), enum membership, nested objects (recursive), array element
types. Rejects unexpected extra properties unless additionalProperties is
set to true (for MCP tool compatibility).
Returns ToolResult{IsError: true} on failure so the LLM can self-correct.
Ref: Security Hardening > Tool abuse prevention via strict parameter validation
210 lines
4.5 KiB
Go
210 lines
4.5 KiB
Go
package tools
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// validateToolArgs validates args against a JSON Schema-like map.
|
|
// schema is expected to have optional keys: "properties", "required", "additionalProperties".
|
|
func validateToolArgs(schema map[string]any, args map[string]any) error {
|
|
if len(schema) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if args == nil {
|
|
args = map[string]any{}
|
|
}
|
|
|
|
if err := checkRequired(schema, args); err != nil {
|
|
return err
|
|
}
|
|
|
|
propsRaw, ok := schema["properties"]
|
|
if !ok {
|
|
return nil // no properties defined — accept any args
|
|
}
|
|
|
|
props, ok := propsRaw.(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
additional := allowsAdditional(schema)
|
|
|
|
for key, val := range args {
|
|
propSchemaRaw, known := props[key]
|
|
if !known {
|
|
if !additional {
|
|
return fmt.Errorf("unexpected property %q", key)
|
|
}
|
|
continue
|
|
}
|
|
propSchema, ok := propSchemaRaw.(map[string]any)
|
|
if !ok {
|
|
continue // can't validate without a proper schema map
|
|
}
|
|
if err := checkType(key, val, propSchema); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkRequired verifies that every field listed in schema["required"] is present in args.
|
|
func checkRequired(schema map[string]any, args map[string]any) error {
|
|
reqRaw, ok := schema["required"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var required []string
|
|
|
|
switch r := reqRaw.(type) {
|
|
case []string:
|
|
required = r
|
|
case []any:
|
|
for _, v := range r {
|
|
s, ok := v.(string)
|
|
if ok {
|
|
required = append(required, s)
|
|
}
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
for _, field := range required {
|
|
if _, present := args[field]; !present {
|
|
return fmt.Errorf("missing required property %q", field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// allowsAdditional returns true when the schema explicitly sets
|
|
// "additionalProperties" to true, or when the key is absent (default: reject extras).
|
|
func allowsAdditional(schema map[string]any) bool {
|
|
v, ok := schema["additionalProperties"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
b, ok := v.(bool)
|
|
return ok && b
|
|
}
|
|
|
|
// checkType validates that val matches the JSON Schema type declared in propSchema.
|
|
func checkType(key string, val any, propSchema map[string]any) error {
|
|
typeRaw, ok := propSchema["type"]
|
|
if !ok {
|
|
return nil // no type constraint
|
|
}
|
|
typeName, ok := typeRaw.(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
switch typeName {
|
|
case "string":
|
|
if _, ok := val.(string); !ok {
|
|
return fmt.Errorf("property %q: expected string, got %T", key, val)
|
|
}
|
|
case "integer":
|
|
switch v := val.(type) {
|
|
case float64:
|
|
if v != math.Trunc(v) {
|
|
return fmt.Errorf("property %q: expected integer, got float64 with fractional part", key)
|
|
}
|
|
case int:
|
|
// ok
|
|
case int64:
|
|
// ok
|
|
default:
|
|
return fmt.Errorf("property %q: expected integer, got %T", key, val)
|
|
}
|
|
case "number":
|
|
switch val.(type) {
|
|
case float64, int, int64:
|
|
// ok
|
|
default:
|
|
return fmt.Errorf("property %q: expected number, got %T", key, val)
|
|
}
|
|
case "boolean":
|
|
if _, ok := val.(bool); !ok {
|
|
return fmt.Errorf("property %q: expected boolean, got %T", key, val)
|
|
}
|
|
case "array":
|
|
arr, ok := val.([]any)
|
|
if !ok {
|
|
return fmt.Errorf("property %q: expected array, got %T", key, val)
|
|
}
|
|
if err := checkArrayItems(key, arr, propSchema); err != nil {
|
|
return err
|
|
}
|
|
case "object":
|
|
obj, ok := val.(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("property %q: expected object, got %T", key, val)
|
|
}
|
|
if err := validateToolArgs(propSchema, obj); err != nil {
|
|
return fmt.Errorf("property %q: %w", key, err)
|
|
}
|
|
}
|
|
|
|
if err := checkEnum(key, val, propSchema); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkArrayItems validates each element of arr against the "items" sub-schema.
|
|
func checkArrayItems(key string, arr []any, propSchema map[string]any) error {
|
|
itemsRaw, ok := propSchema["items"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
itemSchema, ok := itemsRaw.(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for i, elem := range arr {
|
|
elemKey := fmt.Sprintf("%s[%d]", key, i)
|
|
if err := checkType(elemKey, elem, itemSchema); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkEnum validates that val is one of the allowed enum values in propSchema.
|
|
func checkEnum(key string, val any, propSchema map[string]any) error {
|
|
enumRaw, ok := propSchema["enum"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
switch ev := enumRaw.(type) {
|
|
case []any:
|
|
for _, allowed := range ev {
|
|
if val == allowed {
|
|
return nil
|
|
}
|
|
}
|
|
case []string:
|
|
s, ok := val.(string)
|
|
if ok {
|
|
for _, allowed := range ev {
|
|
if s == allowed {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
return nil // unknown enum format, skip
|
|
}
|
|
|
|
return fmt.Errorf("property %q: value %v is not in enum", key, val)
|
|
}
|