validate.go
1 package skills 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 "unicode/utf8" 8 ) 9 10 var skillNameRegex = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`) 11 12 // builtinCommands mirrors agents.BuiltinCommands to avoid an import cycle. 13 // Keep in sync with internal/agents/validate.go. 14 var builtinCommands = map[string]bool{ 15 "quit": true, "exit": true, "help": true, "clear": true, 16 "sessions": true, "session": true, "model": true, "config": true, 17 "setup": true, "update": true, "copy": true, "research": true, 18 "swarm": true, "search": true, 19 } 20 21 func ValidateSkillName(name string) error { 22 if name == "" { 23 return fmt.Errorf("skill name is required") 24 } 25 if len(name) > 64 { 26 return fmt.Errorf("skill name %q exceeds 64 characters", name) 27 } 28 if !skillNameRegex.MatchString(name) { 29 return fmt.Errorf("skill name %q must contain only lowercase letters, numbers, and hyphens (no underscores), and must not start or end with a hyphen", name) 30 } 31 if strings.Contains(name, "--") { 32 return fmt.Errorf("skill name %q must not contain consecutive hyphens", name) 33 } 34 if builtinCommands[name] { 35 return fmt.Errorf("skill name %q conflicts with built-in slash command", name) 36 } 37 return nil 38 } 39 40 // validateFrontmatterName bounds the frontmatter.name field so it can't 41 // smuggle newlines or control chars into LLM-visible contexts (skill 42 // catalog, use_skill output, sticky reinjection). Unlike slugs, frontmatter 43 // name is a free-form display label and may contain uppercase / mixed 44 // case / CJK / spaces; we only reject hostile formatting. 45 func validateFrontmatterName(name string) error { 46 if name == "" { 47 return fmt.Errorf("skill name is required in frontmatter") 48 } 49 // Count runes, not bytes, so CJK names (3 bytes per character in UTF-8) 50 // are not wrongly rejected at ~33 characters. 51 if utf8.RuneCountInString(name) > 100 { 52 return fmt.Errorf("skill frontmatter name exceeds 100 characters") 53 } 54 for _, r := range name { 55 // Reject ASCII control chars and the DEL character. U+0000–U+001F 56 // and U+007F can break the "Available Skills" list formatting or 57 // inject markers into <system-reminder> content. 58 if r < 0x20 || r == 0x7f { 59 return fmt.Errorf("skill frontmatter name contains a control character") 60 } 61 } 62 return nil 63 }