fix(bridge): enforce tool schema enum validation

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 15:54:32 +00:00
parent ac21992623
commit 28f9540fa8
2 changed files with 50 additions and 9 deletions

View file

@ -257,7 +257,8 @@ func validateSchemaNode(value any, schema map[string]any, path string) error {
return nil
}
if schemaType, _ := schema["type"].(string); schemaType != "" {
schemaType, _ := schema["type"].(string)
if schemaType != "" {
switch schemaType {
case "object":
obj, ok := value.(map[string]any)
@ -296,8 +297,6 @@ func validateSchemaNode(value any, schema map[string]any, path string) error {
return fmt.Errorf("%s contains unknown field %q", displayPath(path), name)
}
}
return nil
case "array":
arr, ok := value.([]any)
if !ok {
@ -310,31 +309,27 @@ func validateSchemaNode(value any, schema map[string]any, path string) error {
}
}
}
return nil
case "string":
if _, ok := value.(string); !ok {
return typeError(path, "string", value)
}
return nil
case "boolean":
if _, ok := value.(bool); !ok {
return typeError(path, "boolean", value)
}
return nil
case "integer":
if !isIntegerValue(value) {
return typeError(path, "integer", value)
}
return nil
case "number":
if !isNumberValue(value) {
return typeError(path, "number", value)
}
return nil
}
}
if props := schemaMap(schema["properties"]); len(props) > 0 || schema["required"] != nil || schema["additionalProperties"] != nil {
if schemaType == "" && (len(schemaMap(schema["properties"])) > 0 || schema["required"] != nil || schema["additionalProperties"] != nil) {
props := schemaMap(schema["properties"])
return validateSchemaNode(value, map[string]any{
"type": "object",
"properties": props,

View file

@ -362,6 +362,52 @@ func TestToolBridge_Good_ValidatesEnumValues(t *testing.T) {
}
}
func TestToolBridge_Bad_RejectsInvalidEnumValues(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
bridge := api.NewToolBridge("/tools")
bridge.Add(api.ToolDescriptor{
Name: "publish_item",
Description: "Publish an item",
Group: "items",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"status": map[string]any{
"type": "string",
"enum": []any{"draft", "published"},
},
},
"required": []any{"status"},
},
}, func(c *gin.Context) {
c.JSON(http.StatusOK, api.OK("published"))
})
rg := engine.Group(bridge.BasePath())
bridge.RegisterRoutes(rg)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/tools/publish_item", bytes.NewBufferString(`{"status":"archived"}`))
engine.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", w.Code)
}
var resp api.Response[any]
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if resp.Success {
t.Fatal("expected Success=false")
}
if resp.Error == nil || resp.Error.Code != "invalid_request_body" {
t.Fatalf("expected invalid_request_body error, got %#v", resp.Error)
}
}
func TestToolBridge_Bad_RejectsAdditionalProperties(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()