package tools import ( "sort" "strings" ) type ToolAllowlistGroup struct { Name string `json:"name"` Description string `json:"description"` Aliases []string `json:"aliases,omitempty"` Tools []string `json:"tools"` } var defaultToolAllowlistGroups = []ToolAllowlistGroup{ { Name: "files_read", Description: "Read-only workspace file tools", Aliases: []string{"file_read", "readonly_files"}, Tools: []string{"read_file", "list_dir", "repo_map", "read"}, }, { Name: "files_write", Description: "Workspace file modification tools", Aliases: []string{"file_write"}, Tools: []string{"write_file", "edit_file", "write", "edit"}, }, { Name: "memory_read", Description: "Read-only memory tools", Aliases: []string{"mem_read"}, Tools: []string{"memory_search", "memory_get"}, }, { Name: "memory_write", Description: "Memory write tools", Aliases: []string{"mem_write"}, Tools: []string{"memory_write"}, }, { Name: "memory_all", Description: "All memory tools", Aliases: []string{"memory"}, Tools: []string{"memory_search", "memory_get", "memory_write"}, }, { Name: "subagents", Description: "Subagent management tools", Aliases: []string{"subagent", "agent_runtime"}, Tools: []string{"spawn", "subagents", "subagent_profile"}, }, { Name: "skills", Description: "Skill script execution tools", Aliases: []string{"skill", "skill_scripts"}, Tools: []string{"skill_exec"}, }, } func ToolAllowlistGroups() []ToolAllowlistGroup { out := make([]ToolAllowlistGroup, 0, len(defaultToolAllowlistGroups)) for _, g := range defaultToolAllowlistGroups { item := ToolAllowlistGroup{ Name: strings.ToLower(strings.TrimSpace(g.Name)), Description: strings.TrimSpace(g.Description), Aliases: normalizeAllowlistTokenList(g.Aliases), Tools: normalizeAllowlistTokenList(g.Tools), } if item.Name == "" { continue } out = append(out, item) } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out } func ExpandToolAllowlistEntries(entries []string) []string { if len(entries) == 0 { return nil } groups := ToolAllowlistGroups() resolved := make(map[string][]string, len(groups)) for _, g := range groups { if g.Name != "" { resolved[g.Name] = g.Tools } for _, alias := range g.Aliases { resolved[alias] = g.Tools } } out := map[string]struct{}{} for _, raw := range entries { token := normalizeAllowlistToken(raw) if token == "" { continue } if token == "*" || token == "all" { out[token] = struct{}{} continue } if groupName, isGroupToken := parseAllowlistGroupToken(token); isGroupToken { if members, ok := resolved[groupName]; ok { for _, name := range members { out[name] = struct{}{} } continue } // Keep unknown group token as-is to preserve user intent and avoid silent mutation. out[token] = struct{}{} continue } if members, ok := resolved[token]; ok { for _, name := range members { out[name] = struct{}{} } continue } out[token] = struct{}{} } if len(out) == 0 { return nil } result := make([]string, 0, len(out)) for name := range out { result = append(result, name) } sort.Strings(result) return result } func parseAllowlistGroupToken(token string) (string, bool) { token = normalizeAllowlistToken(token) if token == "" { return "", false } if strings.HasPrefix(token, "group:") { v := normalizeAllowlistToken(strings.TrimPrefix(token, "group:")) if v != "" { return v, true } return "", false } if strings.HasPrefix(token, "@") { v := normalizeAllowlistToken(strings.TrimPrefix(token, "@")) if v != "" { return v, true } return "", false } return "", false } func normalizeAllowlistTokenList(in []string) []string { if len(in) == 0 { return nil } seen := map[string]struct{}{} out := make([]string, 0, len(in)) for _, item := range in { v := normalizeAllowlistToken(item) if v == "" { continue } if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} out = append(out, v) } sort.Strings(out) return out } func normalizeAllowlistToken(in string) string { return strings.ToLower(strings.TrimSpace(in)) }