Skip to content

Commit 823022a

Browse files
author
SqlRush
committed
Validate conditional schema constraints
1 parent caddb9d commit 823022a

4 files changed

Lines changed: 92 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ M4/M5/M10 补充:`FuncTool.Validate` 现在使用与模型 tool definition 同
299299

300300
M5/M9 补充:通用 tool schema validator 继续补齐 `not``multipleOf``uniqueItems``prefixItems`/`items:false``contains`/`minContains`/`maxContains``patternProperties``dependentRequired``additionalProperties:false` 会正确把 pattern-matched 字段视为已定义,Go typed nested schema map 也会被执行。
301301

302+
M5/M9 补充:通用 tool schema validator 现在支持条件类 JSON Schema 约束 `propertyNames``dependentSchemas``if`/`then`/`else`,外部 MCP/动态工具可用 schema 表达字段名规则、属性依赖和条件必填逻辑。
303+
302304
M7 补充:scripted permission payload、dialog expectation、event、cancel-permission 和 dialog-result expectation 现在接受 `ID`/`ToolName`/`Actions``permissionID``requestID``toolUseID``operationID``operation``commandName``resourcePath``body``reasonText``allowedActions``buttons` 等相邻字段,并支持数字 request ID。
303305

304306
M6 补充:microcompact disk cache loader 和 prune 现在接受 digest 缺失但文件名已 keyed 的 cache entry,会用 `<digest>.json` 文件名作为 digest fallback,同时保留显式 digest mismatch 的 invalid-cache guard。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ test/parity/ # golden tests against TS/official behavior
190190
- 本轮补充:通用 tool schema validator 现在支持 `const``pattern``maxLength``minItems`/`maxItems``minProperties`/`maxProperties``exclusiveMinimum`/`exclusiveMaximum` 以及 `allOf`/`anyOf`/`oneOf`,并兼容 Go 代码直接构造的 typed schema list,外部 MCP 工具 schema 的基础 JSON Schema 约束会在本地调用前执行。
191191
- 本轮补充:`FuncTool.Validate` 现在使用与模型 tool definition 同源的动态 `InputSchemaFunc`,本地执行前校验会应用 Task subagent enum 等 runtime metadata 驱动的 schema 约束,避免“模型看到的 schema”和“实际执行校验”分叉。
192192
- 本轮补充:通用 tool schema validator 继续补齐 `not``multipleOf``uniqueItems``prefixItems`/`items:false``contains`/`minContains`/`maxContains``patternProperties``dependentRequired``additionalProperties:false` 会正确把 pattern-matched 字段视为已定义,Go typed nested schema map 也会被执行。
193+
- 本轮补充:通用 tool schema validator 现在支持条件类 JSON Schema 约束 `propertyNames``dependentSchemas``if`/`then`/`else`,外部 MCP/动态工具可用 schema 表达字段名规则、属性依赖和条件必填逻辑。
193194
- 本轮补充:WebFetch/WebSearch 输入解码现在兼容 `timeout``max_bytes`/`maxBytes``max_results`/`maxResults` 的 quoted semantic string 数值;WebSearch 也会按官方校验拒绝同一请求同时设置 `allowed_domains``blocked_domains`
194195
- 本轮补充:Grep 现在支持 whole-word 搜索参数 `word_regexp`/`wordRegexp`/`word-regexp`/`-w`,在 regex 和 fixed-string 模式下按词边界过滤匹配,并兼容 quoted boolean 输入。
195196
- 本轮补充:Grep 现在支持反向匹配参数 `invert_match`/`invertMatch`/`invert-match`/`-v``files_with_matches``content``count` 和 multiline 模式都会按非匹配行/未覆盖行输出,并兼容 quoted boolean 输入。

internal/tool/schema.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ func validateValue(schema contracts.JSONSchema, value any, path string) error {
4848
return fmt.Errorf("%s must not match disallowed schema", path)
4949
}
5050
}
51+
if ifSchema, ok := schemaMap(schema["if"]); ok {
52+
if err := validateValue(ifSchema, value, path); err == nil {
53+
if thenSchema, ok := schemaMap(schema["then"]); ok {
54+
if err := validateValue(thenSchema, value, path); err != nil {
55+
return err
56+
}
57+
}
58+
} else if elseSchema, ok := schemaMap(schema["else"]); ok {
59+
if err := validateValue(elseSchema, value, path); err != nil {
60+
return err
61+
}
62+
}
63+
}
5164
if types := stringOrStrings(schema["type"]); len(types) > 0 {
5265
if !matchesAnyType(types, value) {
5366
return fmt.Errorf("%s must be %s", path, strings.Join(types, " or "))
@@ -200,6 +213,13 @@ func validateValue(schema contracts.JSONSchema, value any, path string) error {
200213
if maxProperties, ok := intSchemaConstraint(schema["maxProperties"]); ok && len(obj) > maxProperties {
201214
return fmt.Errorf("%s must contain at most %d properties", path, maxProperties)
202215
}
216+
if propertyNameSchema, ok := schemaMap(schema["propertyNames"]); ok {
217+
for key := range obj {
218+
if err := validateValue(propertyNameSchema, key, fmt.Sprintf("%s property name %q", path, key)); err != nil {
219+
return err
220+
}
221+
}
222+
}
203223
if ok {
204224
for key, propertySchema := range properties {
205225
child, ok := obj[key]
@@ -215,6 +235,20 @@ func validateValue(schema contracts.JSONSchema, value any, path string) error {
215235
}
216236
}
217237
}
238+
if dependentSchemas, ok := objectMap(schema["dependentSchemas"]); ok {
239+
for key, rawSchema := range dependentSchemas {
240+
if _, present := obj[key]; !present {
241+
continue
242+
}
243+
dependencySchema, ok := schemaMap(rawSchema)
244+
if !ok {
245+
continue
246+
}
247+
if err := validateValue(dependencySchema, value, path); err != nil {
248+
return err
249+
}
250+
}
251+
}
218252
patternDefined, err := validatePatternProperties(schema["patternProperties"], obj, path)
219253
if err != nil {
220254
return err

internal/tool/tool_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,61 @@ func TestValidateSchemaAdditionalAdvancedConstraints(t *testing.T) {
219219
}
220220
}
221221

222+
func TestValidateSchemaConditionalConstraints(t *testing.T) {
223+
schema := contracts.JSONSchema{
224+
"type": "object",
225+
"propertyNames": map[string]any{
226+
"pattern": "^[a-z_]+$",
227+
},
228+
"properties": map[string]any{
229+
"mode": map[string]any{"type": "string"},
230+
"note": map[string]any{"type": "string"},
231+
"secret": map[string]any{"type": "string"},
232+
"token": map[string]any{"type": "string"},
233+
"token_secret": map[string]any{"type": "string"},
234+
},
235+
"if": map[string]any{
236+
"required": []string{"mode"},
237+
"properties": map[string]any{
238+
"mode": map[string]any{"const": "secure"},
239+
},
240+
},
241+
"then": map[string]any{
242+
"required": []string{"secret"},
243+
},
244+
"else": map[string]any{
245+
"required": []string{"note"},
246+
},
247+
"dependentSchemas": map[string]any{
248+
"token": contracts.JSONSchema{"required": []string{"token_secret"}},
249+
},
250+
}
251+
cases := []struct {
252+
name string
253+
input string
254+
want string
255+
}{
256+
{"property-names", `{"Bad":1,"note":"ok"}`, `input property name "Bad" must match pattern ^[a-z_]+$`},
257+
{"then", `{"mode":"secure"}`, "input.secret is required"},
258+
{"else", `{"mode":"plain"}`, "input.note is required"},
259+
{"dependent-schemas", `{"note":"ok","token":"abc"}`, "input.token_secret is required"},
260+
}
261+
for _, tc := range cases {
262+
t.Run(tc.name, func(t *testing.T) {
263+
err := ValidateSchema(schema, json.RawMessage(tc.input))
264+
if err == nil || !strings.Contains(err.Error(), tc.want) {
265+
t.Fatalf("err = %v, want %q", err, tc.want)
266+
}
267+
})
268+
}
269+
if err := ValidateSchema(schema, json.RawMessage(`{"mode":"secure","secret":"s"}`)); err != nil {
270+
t.Fatal(err)
271+
}
272+
if err := ValidateSchema(schema, json.RawMessage(`{"mode":"plain","note":"ok","token":"abc","token_secret":"def"}`)); err != nil {
273+
t.Fatal(err)
274+
}
275+
}
276+
222277
func TestFuncToolValidateUsesDynamicInputSchema(t *testing.T) {
223278
dynamic := FuncTool{
224279
DefinitionValue: contracts.ToolDefinition{

0 commit comments

Comments
 (0)