Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
109 lines
2.6 KiB
Go
109 lines
2.6 KiB
Go
package guard
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// validate checks a value against the part of JSON Schema tool parameters
|
|
// are written in: type, properties, required, enum, items, and
|
|
// additionalProperties false. Enough to stop a model calling a tool with
|
|
// arguments the tool never declared; not a full validator.
|
|
func validate(schema json.RawMessage, value any) error {
|
|
if len(schema) == 0 {
|
|
return nil
|
|
}
|
|
var s struct {
|
|
Type any `json:"type"`
|
|
Properties map[string]json.RawMessage `json:"properties"`
|
|
Required []string `json:"required"`
|
|
Enum []any `json:"enum"`
|
|
Items json.RawMessage `json:"items"`
|
|
AdditionalProperties *bool `json:"additionalProperties"`
|
|
}
|
|
if json.Unmarshal(schema, &s) != nil {
|
|
return nil // a schema this cannot read is the engine's to apply
|
|
}
|
|
if t, ok := s.Type.(string); ok && !isType(value, t) {
|
|
return fmt.Errorf("is not %s", t)
|
|
}
|
|
if list, ok := s.Type.([]any); ok {
|
|
match := false
|
|
for _, t := range list {
|
|
if ts, ok := t.(string); ok && isType(value, ts) {
|
|
match = true
|
|
}
|
|
}
|
|
if !match {
|
|
return fmt.Errorf("is none of the types allowed")
|
|
}
|
|
}
|
|
if len(s.Enum) > 0 {
|
|
b, _ := json.Marshal(value)
|
|
found := false
|
|
for _, e := range s.Enum {
|
|
eb, _ := json.Marshal(e)
|
|
if string(eb) == string(b) {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("is not one of the values allowed")
|
|
}
|
|
}
|
|
if obj, ok := value.(map[string]any); ok {
|
|
for _, r := range s.Required {
|
|
if _, ok := obj[r]; !ok {
|
|
return fmt.Errorf("lacks %q", r)
|
|
}
|
|
}
|
|
for k, v := range obj {
|
|
sub, declared := s.Properties[k]
|
|
if !declared {
|
|
if s.AdditionalProperties != nil && !*s.AdditionalProperties {
|
|
return fmt.Errorf("has %q, which is not declared", k)
|
|
}
|
|
continue
|
|
}
|
|
if err := validate(sub, v); err != nil {
|
|
return fmt.Errorf("%s %w", k, err)
|
|
}
|
|
}
|
|
}
|
|
if arr, ok := value.([]any); ok && len(s.Items) > 0 {
|
|
for i, v := range arr {
|
|
if err := validate(s.Items, v); err != nil {
|
|
return fmt.Errorf("[%d] %w", i, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isType(v any, t string) bool {
|
|
switch t {
|
|
case "object":
|
|
_, ok := v.(map[string]any)
|
|
return ok
|
|
case "array":
|
|
_, ok := v.([]any)
|
|
return ok
|
|
case "string":
|
|
_, ok := v.(string)
|
|
return ok
|
|
case "number":
|
|
_, ok := v.(float64)
|
|
return ok
|
|
case "integer":
|
|
f, ok := v.(float64)
|
|
return ok && f == math.Trunc(f)
|
|
case "boolean":
|
|
_, ok := v.(bool)
|
|
return ok
|
|
case "null":
|
|
return v == nil
|
|
}
|
|
return true
|
|
}
|