internal/mcp: validate tool input schemas
A tool now validates its input with its InputSchema.
Change schema inference to allow explicit nulls for fields of
pointer type.
We assume the schema has no external references. For example, the
following schema cannot be handled:
{
"$ref": "https://example.com/other.json"
}
Schemas with internal references, like to a "$defs", are fine.
Change-Id: I6ee7c18c2c5cb609df0b22a66da986f7ea64bbe4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/670676
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
parent
0d0c48a468
commit
a07ce11b37
5 changed files with 160 additions and 17 deletions
|
|
@ -42,15 +42,21 @@ func For[T any]() (*Schema, error) {
|
|||
// - complex numbers
|
||||
// - unsafe pointers
|
||||
//
|
||||
// The cannot be any cycles in the types.
|
||||
// TODO(rfindley): we could perhaps just skip these incompatible fields.
|
||||
func ForType(t reflect.Type) (*Schema, error) {
|
||||
return typeSchema(t)
|
||||
}
|
||||
|
||||
func typeSchema(t reflect.Type) (*Schema, error) {
|
||||
if t.Kind() == reflect.Pointer {
|
||||
// Follow pointers: the schema for *T is almost the same as for T, except that
|
||||
// an explicit JSON "null" is allowed for the pointer.
|
||||
allowNull := false
|
||||
for t.Kind() == reflect.Pointer {
|
||||
allowNull = true
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
var (
|
||||
s = new(Schema)
|
||||
err error
|
||||
|
|
@ -121,6 +127,10 @@ func typeSchema(t reflect.Type) (*Schema, error) {
|
|||
default:
|
||||
return nil, fmt.Errorf("type %v is unsupported by jsonschema", t)
|
||||
}
|
||||
if allowNull && s.Type != "" {
|
||||
s.Types = []string{"null", s.Type}
|
||||
s.Type = ""
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func TestForType(t *testing.T) {
|
|||
Properties: map[string]*schema{
|
||||
"f": {Type: "integer"},
|
||||
"G": {Type: "array", Items: &schema{Type: "number"}},
|
||||
"P": {Type: "boolean"},
|
||||
"P": {Types: []string{"null", "boolean"}},
|
||||
"NoSkip": {Type: "string"},
|
||||
},
|
||||
Required: []string{"f", "G", "P"},
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ type Resolved struct {
|
|||
resolvedURIs map[string]*Schema
|
||||
}
|
||||
|
||||
// Schema returns the schema that was resolved.
|
||||
// It must not be modified.
|
||||
func (r *Resolved) Schema() *Schema { return r.root }
|
||||
|
||||
// A Loader reads and unmarshals the schema at uri, if any.
|
||||
type Loader func(uri *url.URL) (*Schema, error)
|
||||
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ func jsonType(v reflect.Value) (string, bool) {
|
|||
return "string", true
|
||||
case reflect.Slice, reflect.Array:
|
||||
return "array", true
|
||||
case reflect.Map:
|
||||
case reflect.Map, reflect.Struct:
|
||||
return "object", true
|
||||
default:
|
||||
return "", false
|
||||
|
|
|
|||
|
|
@ -662,8 +662,8 @@ func property(v reflect.Value, name string) reflect.Value {
|
|||
case reflect.Struct:
|
||||
props := structPropertiesOf(v.Type())
|
||||
// Ignore nonexistent properties.
|
||||
if index, ok := props[name]; ok {
|
||||
return v.FieldByIndex(index)
|
||||
if sf, ok := props[name]; ok {
|
||||
return v.FieldByIndex(sf.Index)
|
||||
}
|
||||
return reflect.Value{}
|
||||
default:
|
||||
|
|
@ -673,6 +673,8 @@ func property(v reflect.Value, name string) reflect.Value {
|
|||
|
||||
// properties returns an iterator over the names and values of all properties
|
||||
// in v, which must be a map or a struct.
|
||||
// If a struct, zero-valued properties that are marked omitempty or omitzero
|
||||
// are excluded.
|
||||
func properties(v reflect.Value) iter.Seq2[string, reflect.Value] {
|
||||
return func(yield func(string, reflect.Value) bool) {
|
||||
switch v.Kind() {
|
||||
|
|
@ -683,8 +685,14 @@ func properties(v reflect.Value) iter.Seq2[string, reflect.Value] {
|
|||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
for name, index := range structPropertiesOf(v.Type()) {
|
||||
if !yield(name, v.FieldByIndex(index)) {
|
||||
for name, sf := range structPropertiesOf(v.Type()) {
|
||||
val := v.FieldByIndex(sf.Index)
|
||||
if val.IsZero() {
|
||||
if tag, ok := sf.Tag.Lookup("json"); ok && (strings.Contains(tag, "omitempty") || strings.Contains(tag, "omitzero")) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !yield(name, val) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -707,8 +715,8 @@ func numPropertiesBounds(v reflect.Value, isRequired map[string]bool) (int, int)
|
|||
case reflect.Struct:
|
||||
sp := structPropertiesOf(v.Type())
|
||||
min := 0
|
||||
for prop, index := range sp {
|
||||
if !v.FieldByIndex(index).IsZero() || isRequired[prop] {
|
||||
for prop, sf := range sp {
|
||||
if !v.FieldByIndex(sf.Index).IsZero() || isRequired[prop] {
|
||||
min++
|
||||
}
|
||||
}
|
||||
|
|
@ -719,7 +727,7 @@ func numPropertiesBounds(v reflect.Value, isRequired map[string]bool) (int, int)
|
|||
}
|
||||
|
||||
// A propertyMap is a map from property name to struct field index.
|
||||
type propertyMap = map[string][]int
|
||||
type propertyMap = map[string]reflect.StructField
|
||||
|
||||
var structProperties sync.Map // from reflect.Type to propertyMap
|
||||
|
||||
|
|
@ -730,10 +738,10 @@ func structPropertiesOf(t reflect.Type) propertyMap {
|
|||
if props, ok := structProperties.Load(t); ok {
|
||||
return props.(propertyMap)
|
||||
}
|
||||
props := map[string][]int{}
|
||||
props := map[string]reflect.StructField{}
|
||||
for _, sf := range reflect.VisibleFields(t) {
|
||||
if name, ok := jsonName(sf); ok {
|
||||
props[name] = sf.Index
|
||||
props[name] = sf
|
||||
}
|
||||
}
|
||||
structProperties.Store(t, props)
|
||||
|
|
|
|||
Loading…
Reference in a new issue