返回 DeepSeek-Reasonix
tool_diagnostics.go
根目录 / internal / skill / tool_diagnostics.go
1 package skill
2
3 import (
4 "fmt"
5 "path"
6 "strings"
7
8 "reasonix/internal/tool"
9 )
10
11 // ToolReferenceOptions separates known identities from a session snapshot.
12 // Registered must include hidden tools, not only provider-visible schemas.
13 type ToolReferenceOptions struct {
14 Known []string
15 Registered []tool.ContractEntry
16 Bindings []tool.MCPBinding
17 }
18
19 // ToolReferenceDiagnostic describes a reference without granting any permission.
20 type ToolReferenceDiagnostic struct {
21 Skill, Reference, Code, Severity, Message string
22 }
23
24 // CheckToolReferences is an offline check over the supplied inventory.
25 func CheckToolReferences(skills []Skill, opts ToolReferenceOptions) []ToolReferenceDiagnostic {
26 names := make(map[string]bool)
27 for _, name := range opts.Known {
28 names[name] = true
29 }
30 for _, entry := range opts.Registered {
31 names[entry.Name] = true
32 }
33 for _, binding := range opts.Bindings {
34 if binding.CallableName != "" {
35 names[binding.CallableName] = true
36 }
37 if binding.CapabilityID != "" {
38 names[binding.CapabilityID] = true
39 }
40 }
41 var out []ToolReferenceDiagnostic
42 for _, sk := range skills {
43 bindings := ToolBindingsForSkill(sk, opts.Bindings)
44 for _, ref := range sk.AllowedTools {
45 ref = strings.TrimSpace(ref)
46 if ref == "" {
47 continue
48 }
49 code, severity, reason := checkToolReference(ref, names, bindings)
50 if code != "" {
51 out = append(out, ToolReferenceDiagnostic{sk.Name, ref, code, severity,
52 fmt.Sprintf("skill %q allowed-tools reference %q %s", sk.Name, ref, reason)})
53 }
54 }
55 }
56 return out
57 }
58
59 func checkToolReference(ref string, names map[string]bool, bindings []tool.MCPBinding) (string, string, string) {
60 pattern := strings.ContainsAny(ref, "*?[")
61 if pattern {
62 if _, err := path.Match(ref, ""); err != nil {
63 return "skill.tool_reference_invalid", "warning", "has invalid glob syntax"
64 }
65 }
66 if names[ref] {
67 return "", "", ""
68 }
69 if !pattern && invalidMCPReference(ref) {
70 return "skill.tool_reference_invalid", "warning", "has an incomplete or invalid MCP reference"
71 }
72 matched := func(name string) bool {
73 if !pattern {
74 return ref == name
75 }
76 ok, _ := path.Match(ref, name)
77 return ok
78 }
79 for name := range names {
80 if matched(name) {
81 return "", "", ""
82 }
83 }
84 targets := map[string]bool{}
85 for _, b := range bindings {
86 for _, alias := range append(tool.MCPBindingAliases(b), b.CallableName) {
87 if matched(alias) {
88 targets[b.CallableName] = true
89 }
90 }
91 }
92 if len(targets) == 1 || pattern && len(targets) > 0 {
93 return "", "", ""
94 }
95 if len(targets) > 1 {
96 return "skill.tool_reference_ambiguous", "warning", "matches multiple MCP tools; use a qualified reference"
97 }
98 if pattern || dynamicToolReference(ref) {
99 return "skill.tool_reference_unverified", "info", "is unverified by the offline inventory; resolve it in the target session"
100 }
101 return "skill.tool_reference_unknown", "warning", "is not a known tool identity"
102 }
103
104 func dynamicToolReference(ref string) bool {
105 return strings.HasPrefix(ref, "mcp__") || strings.HasPrefix(ref, "mcp-tool:") ||
106 strings.HasPrefix(ref, "mcp-server:") || strings.HasPrefix(ref, "mcp_connect__") ||
107 strings.HasPrefix(ref, "tool:") || strings.HasPrefix(ref, "skill:") ||
108 strings.HasPrefix(ref, "session:") || strings.HasPrefix(ref, "task:") ||
109 strings.HasPrefix(ref, "workflow:") || strings.HasPrefix(ref, "source:") ||
110 strings.HasPrefix(ref, "web:") || strings.HasPrefix(ref, "lsp:") ||
111 strings.HasPrefix(ref, "memory:") || strings.Contains(ref, "/")
112 }
113
114 func invalidMCPReference(ref string) bool {
115 switch {
116 case strings.HasPrefix(ref, "mcp-tool:"):
117 _, _, ok := tool.ParseMCPToolReference(ref)
118 return !ok
119 case strings.HasPrefix(ref, "mcp-server:"):
120 _, ok := tool.ParseMCPServerReference(ref)
121 return !ok
122 case strings.HasPrefix(ref, "mcp__"):
123 _, _, ok := tool.SplitMCPName(ref)
124 return !ok
125 default:
126 return false
127 }
128 }
129
129 lines GO