| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "strings" |
| 6 | |
| 7 | "reasonix/internal/permission" |
| 8 | "reasonix/internal/plugin" |
| 9 | "reasonix/internal/tool" |
| 10 | ) |
| 11 | |
| 12 | func validateSavedPermissionRule(list, raw string, tools []tool.ContractEntry, servers []string) error { |
| 13 | rule, ok := permission.ParseRule(raw) |
| 14 | if !ok { |
| 15 | return nil // AddPermissionRule reports malformed rules. |
| 16 | } |
| 17 | for _, candidate := range tools { |
| 18 | if permission.RuleMatchesString(rule.Tool, candidate.Name, "") { |
| 19 | return nil |
| 20 | } |
| 21 | } |
| 22 | for _, server := range servers { |
| 23 | prefix := plugin.ToolPrefix(server) |
| 24 | suffix := strings.TrimPrefix(rule.Tool, prefix) |
| 25 | if (suffix != rule.Tool && validConfiguredMCPToolSuffix(suffix)) || rule.Tool == plugin.MCPConnectPermissionName(server) { |
| 26 | return nil |
| 27 | } |
| 28 | } |
| 29 | if strings.HasPrefix(rule.Tool, "mcp__") && strings.ContainsAny(rule.Tool, "*?") { |
| 30 | return fmt.Errorf("permission rule %q uses an MCP tool-name wildcard; MCP permission rules require an exact tool name", raw) |
| 31 | } |
| 32 | if rule.Subject == "" && !strings.ContainsAny(rule.Tool, "()") { |
| 33 | // Allow uses an exact reusable command; ask/deny use a command prefix |
| 34 | // so arguments to the same command remain covered. |
| 35 | if strings.EqualFold(strings.TrimSpace(list), "allow") { |
| 36 | return fmt.Errorf("permission rule %q names no registered tool; if this is a shell command, use Bash(%s)", raw, rule.Tool) |
| 37 | } |
| 38 | return fmt.Errorf("permission rule %q names no registered tool; if this is a shell command, use Bash(%s:*)", raw, rule.Tool) |
| 39 | } |
| 40 | return fmt.Errorf("permission rule %q names no registered tool", raw) |
| 41 | } |
| 42 | |
| 43 | func validConfiguredMCPToolSuffix(suffix string) bool { |
| 44 | if suffix == "" { |
| 45 | return false |
| 46 | } |
| 47 | for _, c := range suffix { |
| 48 | if !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' || c == '_' || c == '-') { |
| 49 | return false |
| 50 | } |
| 51 | } |
| 52 | return true |
| 53 | } |
| 54 |