diff --git a/agent/skills/frontmatter_source_test.go b/agent/skills/frontmatter_source_test.go index fe0ebf67..25754892 100644 --- a/agent/skills/frontmatter_source_test.go +++ b/agent/skills/frontmatter_source_test.go @@ -107,6 +107,33 @@ func TestValidateCompatibility_Unset_ReturnsNil(t *testing.T) { } } +func TestExportedFieldValidators(t *testing.T) { + for _, name := range []string{"my-skill", "a", "a1"} { + if err := skills.ValidateName(name); err != nil { + t.Errorf("ValidateName(%q) = %v", name, err) + } + } + for _, name := range []string{"", " ", "-a", "a-", "a--b", "Abc", strings.Repeat("a", 65)} { + if err := skills.ValidateName(name); err == nil { + t.Errorf("ValidateName(%q) succeeded", name) + } + } + if err := skills.ValidateDescription("ok"); err != nil { + t.Errorf("ValidateDescription: %v", err) + } + for _, d := range []string{"", " ", strings.Repeat("x", 1025)} { + if err := skills.ValidateDescription(d); err == nil { + t.Errorf("ValidateDescription(len %d) succeeded", len(d)) + } + } + if err := skills.ValidateCompatibility(""); err != nil { + t.Errorf("ValidateCompatibility empty: %v", err) + } + if err := skills.ValidateCompatibility(strings.Repeat("x", 501)); err == nil { + t.Error("ValidateCompatibility accepted 501 characters") + } +} + func TestFrontmatter_UnicodeLengthLimits(t *testing.T) { for _, tc := range []struct { name string diff --git a/agent/skills/skills.go b/agent/skills/skills.go index 2e716e57..d4d3e505 100644 --- a/agent/skills/skills.go +++ b/agent/skills/skills.go @@ -34,13 +34,13 @@ type Frontmatter struct { // Validate validates the frontmatter according to the Agent Skills specification. // Description and compatibility limits count UTF-16 code units. func (f Frontmatter) Validate() error { - if err := validateName(f.Name); err != nil { + if err := ValidateName(f.Name); err != nil { return err } - if err := validateDescription(f.Description); err != nil { + if err := ValidateDescription(f.Description); err != nil { return err } - if err := validateCompatibility(f.Compatibility); err != nil { + if err := ValidateCompatibility(f.Compatibility); err != nil { return err } return nil @@ -122,8 +122,10 @@ type Script struct { // The runner is responsible for decoding and validating its argument format. type ScriptRunner func(context.Context, *Skill, *Script, json.RawMessage) (any, error) -// validateName validates a skill name. -func validateName(name string) error { +// ValidateName validates a skill name. It must be non-blank, at most 64 +// characters, and use only lowercase letters, digits, and single interior +// hyphens. +func ValidateName(name string) error { if strings.TrimSpace(name) == "" { return fmt.Errorf("skill name is required") } @@ -157,8 +159,9 @@ func isValidSkillName(name string) bool { return !previousHyphen } -// validateDescription validates a skill description. -func validateDescription(description string) error { +// ValidateDescription validates a skill description. It must be non-blank and +// at most 1024 UTF-16 code units. +func ValidateDescription(description string) error { if strings.TrimSpace(description) == "" { return fmt.Errorf("skill description is required") } @@ -168,8 +171,9 @@ func validateDescription(description string) error { return nil } -// validateCompatibility validates an optional compatibility value. -func validateCompatibility(compatibility string) error { +// ValidateCompatibility validates an optional compatibility value. An empty +// value is valid; otherwise it must be at most 500 UTF-16 code units. +func ValidateCompatibility(compatibility string) error { if utf16Length(compatibility) > maxCompatibilityLength { return fmt.Errorf("skill compatibility must be %d characters or fewer", maxCompatibilityLength) }