registry.go
1 package skills 2 3 // SecretSpec describes a single secret (API key) that a skill requires. 4 type SecretSpec struct { 5 Key string `json:"key"` 6 Label string `json:"label"` 7 Required bool `json:"required"` 8 } 9 10 // Skill is a composable capability loaded from a SKILL.md file. 11 // Follows the Anthropic Agent Skills spec (agentskills.io/specification). 12 // 13 // Name vs Slug: 14 // - Name comes from frontmatter.name — the human-readable / LLM activation 15 // identifier the skill author declares. Shown to the model in the 16 // "Available Skills" list; used as the argument to `use_skill`. 17 // - Slug is the on-disk directory name, which also serves as the 18 // marketplace URL identifier and the key for stored API secrets. 19 // Always URL-safe (^[a-z0-9][a-z0-9-]*$). 20 // 21 // The two are normally equal. ClawHub allows them to diverge (e.g. 22 // `name: Docker` / directory `docker`, or ClawHub slug 23 // `xiaohongshu-mcp-skills` with frontmatter `name: xiaohongshu`), so we 24 // track them separately instead of enforcing equality at load time. 25 type Skill struct { 26 Name string `json:"name"` 27 Slug string `json:"slug"` 28 Description string `json:"description"` 29 Prompt string `json:"prompt,omitempty"` 30 License string `json:"license,omitempty"` 31 Compatibility string `json:"compatibility,omitempty"` 32 // Metadata uses `map[string]any` to preserve nested YAML structures. 33 // ClawHub exposes a structured schema under any of three 34 // interchangeable parent keys: `openclaw`, `clawdbot`, and `clawdis`. 35 // See skillFrontmatter in loader.go for rationale. 36 Metadata map[string]any `json:"metadata,omitempty"` 37 AllowedTools []string `json:"allowed_tools,omitempty"` 38 // StickyInstructions, when true, opts the skill into a short 39 // <system-reminder> reinjection on activation and on skill-filter 40 // drift. Set via frontmatter `sticky-instructions: true`. Intended for 41 // policy skills (e.g. kocoro) whose guidance must survive compaction. 42 StickyInstructions bool `json:"sticky_instructions,omitempty"` 43 // Hidden excludes the skill from the default GET /skills listing so 44 // frontends hide it from users. Display-only: loader, discovery, and 45 // use_skill are unaffected. Opt-in via frontmatter `hidden: true`. 46 Hidden bool `json:"hidden,omitempty"` 47 // StickySnippet is the RESOLVED reinjection body used at runtime. It 48 // comes from StickySnippetOverride when set, else from the heuristic 49 // body extractor, else from Description. Capped to 400 chars. Not 50 // persisted — recomputed on every load. 51 StickySnippet string `json:"-"` 52 // StickySnippetOverride is the author-pinned frontmatter value 53 // (`sticky-snippet:`). Separate from the resolved StickySnippet so the 54 // save path can round-trip author intent without accidentally freezing 55 // a heuristic choice into the SKILL.md file. Empty means "let the 56 // heuristic pick". 57 StickySnippetOverride string `json:"-"` 58 Source string `json:"-"` 59 InstallSource string `json:"-"` 60 MarketplaceSlug string `json:"-"` 61 Dir string `json:"-"` 62 } 63 64 // SkillMeta is the lightweight representation for API responses (no body/prompt). 65 type SkillMeta struct { 66 Name string `json:"name"` 67 Slug string `json:"slug"` 68 Description string `json:"description"` 69 Source string `json:"source,omitempty"` 70 InstallSource string `json:"install_source"` 71 MarketplaceSlug string `json:"marketplace_slug,omitempty"` 72 Hidden bool `json:"hidden,omitempty"` 73 RequiredSecrets []SecretSpec `json:"required_secrets,omitempty"` 74 ConfiguredSecrets []string `json:"configured_secrets,omitempty"` 75 } 76 77 // ToMeta returns API-safe metadata without the full prompt body. 78 func (s *Skill) ToMeta() SkillMeta { 79 return SkillMeta{ 80 Name: s.Name, 81 Slug: s.Slug, 82 Description: s.Description, 83 Source: s.Source, 84 InstallSource: s.InstallSource, 85 MarketplaceSlug: s.MarketplaceSlug, 86 Hidden: s.Hidden, 87 } 88 } 89 90 // RequiredSecrets parses requires.env from ClawHub metadata. The spec 91 // accepts three interchangeable parent keys (openclaw / clawdbot / clawdis) 92 // pointing at the same ClawdisSkillMetadataSchema — see 93 // https://github.com/openclaw/clawhub/blob/main/docs/spec.md. Returns 94 // nil if no secrets are declared or metadata is malformed. 95 func (s *Skill) RequiredSecrets() []SecretSpec { 96 if len(s.Metadata) == 0 { 97 return nil 98 } 99 seen := map[string]bool{} 100 var result []SecretSpec 101 for _, parentKey := range []string{"openclaw", "clawdbot", "clawdis"} { 102 envKeys := extractRequiresEnv(s.Metadata, parentKey) 103 for _, key := range envKeys { 104 if seen[key] { 105 continue 106 } 107 seen[key] = true 108 result = append(result, SecretSpec{ 109 Key: key, 110 Label: key, 111 Required: true, 112 }) 113 } 114 } 115 return result 116 } 117 118 // extractRequiresEnv safely navigates metadata[parentKey].requires.env 119 // and returns the string slice. Returns nil on any type mismatch. 120 func extractRequiresEnv(metadata map[string]any, parentKey string) []string { 121 parent, ok := metadata[parentKey].(map[string]any) 122 if !ok { 123 return nil 124 } 125 requires, ok := parent["requires"].(map[string]any) 126 if !ok { 127 return nil 128 } 129 envList, ok := requires["env"].([]any) 130 if !ok { 131 return nil 132 } 133 var result []string 134 for _, v := range envList { 135 if s, ok := v.(string); ok && s != "" { 136 result = append(result, s) 137 } 138 } 139 return result 140 }