86 lines
2.2 KiB
Go
86 lines
2.2 KiB
Go
// Package argschema checks tool-call arguments against the small JSON-schema
|
|
// subset that tool declarations use. It has no dependencies beyond the
|
|
// standard library so both the tool registry and the LLM client can share it.
|
|
package argschema
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Validate reports whether args satisfy schema: the top-level type is object,
|
|
// required keys are present and not blank strings, declared properties have
|
|
// their primitive type, and unknown keys are rejected when properties are
|
|
// declared.
|
|
func Validate(schema, args json.RawMessage) error {
|
|
var spec struct {
|
|
Type string `json:"type"`
|
|
Required []string `json:"required"`
|
|
Properties map[string]json.RawMessage `json:"properties"`
|
|
}
|
|
if len(schema) > 0 {
|
|
if err := json.Unmarshal(schema, &spec); err != nil {
|
|
return fmt.Errorf("invalid tool schema")
|
|
}
|
|
}
|
|
var obj map[string]any
|
|
if err := json.Unmarshal(args, &obj); err != nil {
|
|
return fmt.Errorf("arguments must be a JSON object")
|
|
}
|
|
if spec.Type != "" && spec.Type != "object" {
|
|
return fmt.Errorf("unsupported schema type %s", spec.Type)
|
|
}
|
|
for _, req := range spec.Required {
|
|
v, ok := obj[req]
|
|
if !ok || v == nil {
|
|
return fmt.Errorf("missing required argument %s", req)
|
|
}
|
|
if s, ok := v.(string); ok && strings.TrimSpace(s) == "" {
|
|
return fmt.Errorf("missing required argument %s", req)
|
|
}
|
|
}
|
|
for key, raw := range spec.Properties {
|
|
v, ok := obj[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
var prop struct {
|
|
Type string `json:"type"`
|
|
}
|
|
if err := json.Unmarshal(raw, &prop); err != nil || prop.Type == "" {
|
|
continue
|
|
}
|
|
if !matchesType(prop.Type, v) {
|
|
return fmt.Errorf("argument %s must be %s", key, prop.Type)
|
|
}
|
|
}
|
|
if len(spec.Properties) > 0 {
|
|
for key := range obj {
|
|
if _, ok := spec.Properties[key]; !ok {
|
|
return fmt.Errorf("unknown argument %s", key)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func matchesType(kind string, v any) bool {
|
|
switch kind {
|
|
case "string":
|
|
_, ok := v.(string)
|
|
return ok
|
|
case "number":
|
|
_, ok := v.(float64)
|
|
return ok
|
|
case "integer":
|
|
f, ok := v.(float64)
|
|
return ok && f == float64(int64(f))
|
|
case "boolean":
|
|
_, ok := v.(bool)
|
|
return ok
|
|
default:
|
|
return true
|
|
}
|
|
}
|