Skip to content

Commit 2fca58d

Browse files
author
SqlRush
committed
Validate dynamic tool schemas
1 parent d5c783c commit 2fca58d

5 files changed

Lines changed: 48 additions & 2 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ M5/M9 补充:通用 tool schema validator 现在支持 `required` 的 Go `[]st
295295

296296
M5/M9 补充:通用 tool schema validator 现在支持 `const``pattern``maxLength``minItems`/`maxItems``minProperties`/`maxProperties``exclusiveMinimum`/`exclusiveMaximum` 以及 `allOf`/`anyOf`/`oneOf`,并兼容 Go 代码直接构造的 typed schema list,外部 MCP 工具 schema 的基础 JSON Schema 约束会在本地调用前执行。
297297

298+
M4/M5/M10 补充:`FuncTool.Validate` 现在使用与模型 tool definition 同源的动态 `InputSchemaFunc`,本地执行前校验会应用 Task subagent enum 等 runtime metadata 驱动的 schema 约束,避免“模型看到的 schema”和“实际执行校验”分叉。
299+
298300
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。
299301

300302
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
@@ -188,6 +188,7 @@ test/parity/ # golden tests against TS/official behavior
188188
- 本轮补充:通用 tool schema validator 现在支持数字 `minimum`/`maximum`,可直接执行 LSPDiagnostics `limit` 等工具 schema 的数值范围契约。
189189
- 本轮补充:通用 tool schema validator 现在支持 `required` 的 Go `[]string` 形态和 object `additionalProperties` schema 校验,MCP `get_prompt.arguments` 会在 schema 层拒绝非字符串参数值。
190190
- 本轮补充:通用 tool schema validator 现在支持 `const``pattern``maxLength``minItems`/`maxItems``minProperties`/`maxProperties``exclusiveMinimum`/`exclusiveMaximum` 以及 `allOf`/`anyOf`/`oneOf`,并兼容 Go 代码直接构造的 typed schema list,外部 MCP 工具 schema 的基础 JSON Schema 约束会在本地调用前执行。
191+
- 本轮补充:`FuncTool.Validate` 现在使用与模型 tool definition 同源的动态 `InputSchemaFunc`,本地执行前校验会应用 Task subagent enum 等 runtime metadata 驱动的 schema 约束,避免“模型看到的 schema”和“实际执行校验”分叉。
191192
- 本轮补充:WebFetch/WebSearch 输入解码现在兼容 `timeout``max_bytes`/`maxBytes``max_results`/`maxResults` 的 quoted semantic string 数值;WebSearch 也会按官方校验拒绝同一请求同时设置 `allowed_domains``blocked_domains`
192193
- 本轮补充:Grep 现在支持 whole-word 搜索参数 `word_regexp`/`wordRegexp`/`word-regexp`/`-w`,在 regex 和 fixed-string 模式下按词边界过滤匹配,并兼容 quoted boolean 输入。
193194
- 本轮补充:Grep 现在支持反向匹配参数 `invert_match`/`invertMatch`/`invert-match`/`-v``files_with_matches``content``count` 和 multiline 模式都会按非匹配行/未覆盖行输出,并兼容 quoted boolean 输入。

internal/tool/func_tool.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (t FuncTool) Validate(ctx Context, raw json.RawMessage) error {
7373
if err != nil {
7474
return err
7575
}
76-
if err := ValidateSchema(t.DefinitionValue.InputSchema, normalized); err != nil {
76+
if err := ValidateSchema(t.validationSchema(ctx), normalized); err != nil {
7777
return err
7878
}
7979
if t.ValidateFunc != nil {
@@ -82,6 +82,16 @@ func (t FuncTool) Validate(ctx Context, raw json.RawMessage) error {
8282
return nil
8383
}
8484

85+
func (t FuncTool) validationSchema(ctx Context) contracts.JSONSchema {
86+
if t.InputSchemaFunc == nil {
87+
return t.DefinitionValue.InputSchema
88+
}
89+
return t.InputSchema(PromptContext{
90+
WorkingDirectory: ctx.WorkingDirectory,
91+
Metadata: ctx.Metadata,
92+
})
93+
}
94+
8595
func (t FuncTool) CheckPermissions(ctx Context, raw json.RawMessage) (contracts.PermissionDecision, error) {
8696
normalized, err := t.normalizeInput(raw)
8797
if err != nil {

internal/tool/tool_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,39 @@ func TestValidateSchemaAdvancedConstraints(t *testing.T) {
159159
}
160160
}
161161

162+
func TestFuncToolValidateUsesDynamicInputSchema(t *testing.T) {
163+
dynamic := FuncTool{
164+
DefinitionValue: contracts.ToolDefinition{
165+
Name: "Dynamic",
166+
InputSchema: contracts.JSONSchema{
167+
"type": "object",
168+
"properties": map[string]any{
169+
"mode": map[string]any{"type": "string", "enum": []any{"static"}},
170+
},
171+
},
172+
},
173+
InputSchemaFunc: func(ctx PromptContext) contracts.JSONSchema {
174+
allowed, _ := ctx.Metadata["allowed_mode"].(string)
175+
if allowed == "" {
176+
allowed = "dynamic"
177+
}
178+
return contracts.JSONSchema{
179+
"type": "object",
180+
"properties": map[string]any{
181+
"mode": map[string]any{"type": "string", "enum": []any{allowed}},
182+
},
183+
}
184+
},
185+
}
186+
ctx := Context{Metadata: map[string]any{"allowed_mode": "dynamic"}}
187+
if err := dynamic.Validate(ctx, json.RawMessage(`{"mode":"dynamic"}`)); err != nil {
188+
t.Fatal(err)
189+
}
190+
if err := dynamic.Validate(ctx, json.RawMessage(`{"mode":"static"}`)); err == nil || !strings.Contains(err.Error(), "input.mode must be one of dynamic") {
191+
t.Fatalf("err = %v", err)
192+
}
193+
}
194+
162195
func TestExecutorRunsAllowedTool(t *testing.T) {
163196
engine := permissions.NewEngine(contracts.PermissionContext{Mode: contracts.PermissionDefault})
164197
registry, err := NewRegistry(FuncTool{

internal/tools/task/tools_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1497,7 +1497,7 @@ func TestTaskToolUsesAvailableAgentsInPromptSchemaAndValidation(t *testing.T) {
14971497
Name: "Task",
14981498
Input: json.RawMessage(`{"description":"Review API","prompt":"Inspect API changes","subagent_type":"missing:agent"}`),
14991499
}, nil)
1500-
if err == nil || !strings.Contains(err.Error(), `subagent_type "missing:agent" is not available`) {
1500+
if err == nil || !strings.Contains(err.Error(), "input.subagent_type must be one of general-purpose, demo:reviewer") {
15011501
t.Fatalf("err = %v", err)
15021502
}
15031503
}

0 commit comments

Comments
 (0)