// Package skill handles the markdown representation of skills: parsing, // loading, writing, and the structure shared with the agent runtime. // // A skill file is markdown with a YAML frontmatter block delimited by "---". // The frontmatter carries machine-readable metadata (tools, permissions, // endpoint mappings); the document body is the natural-language system // prompt fed to the LLM. This dual form lets operators read and edit a skill // like a normal doc while the runtime parses a strict contract from it. package skill import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // Skill is the in-memory representation of a skill markdown file. type Skill struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` Version int `yaml:"version" json:"version"` AppSlug string `yaml:"app_slug" json:"app_slug"` ModelConfig string `yaml:"model_config" json:"model_config,omitempty"` Permissions Permissions `yaml:"permissions" json:"permissions"` Tools []Tool `yaml:"tools" json:"tools"` // SystemPrompt is the markdown body (everything after frontmatter). It // is NOT stored in frontmatter; Write places it after the delimiter. SystemPrompt string `yaml:"-" json:"system_prompt"` } // Permissions governs what the agent may do with this skill's tools. type Permissions struct { // DefaultMode is the blanket policy: "readonly" (default) restricts // tools to safe methods; "confirm" requires end-user confirmation for // any tool not explicitly allowed. DefaultMode string `yaml:"default_mode" json:"default_mode"` // RequireConfirmation lists tool names that always need end-user // confirmation before execution, regardless of HTTP method. RequireConfirmation []string `yaml:"require_confirmation" json:"require_confirmation"` // AllowWrite lists tool names explicitly permitted to use mutating HTTP // methods without per-call confirmation. Empty by default. AllowWrite []string `yaml:"allow_write" json:"allow_write"` } // Tool is one callable capability exposed to the LLM. Parameters is a JSON // Schema describing the arguments; Endpoint maps the tool to a concrete HTTP // call against the target site. type Tool struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` Parameters jsonObject `yaml:"parameters" json:"parameters"` // JSON Schema object Endpoint Endpoint `yaml:"endpoint" json:"endpoint"` } // Endpoint maps a tool call to a concrete HTTP request. Path/Query/Header // values may use {{param}} placeholders resolved from the tool arguments at // runtime. Body is a JSON template (also {{param}}-substituted) for writes. type Endpoint struct { Method string `yaml:"method" json:"method"` URL string `yaml:"url" json:"url"` Path map[string]string `yaml:"path" json:"path,omitempty"` Query map[string]string `yaml:"query" json:"query,omitempty"` Header map[string]string `yaml:"header" json:"header,omitempty"` Body string `yaml:"body" json:"body,omitempty"` // JSON template } // MakeParameters wraps a JSON Schema byte slice into the Parameters type used // by Tool. Exported so callers in other packages (and tests) can build a Tool // without referencing the unexported jsonObject type directly. Returns the // same underlying bytes; the conversion is type-level. func MakeParameters(jsonSchema []byte) jsonObject { return jsonObject(jsonSchema) } const frontMatterDelim = "---" // Load reads and parses a skill markdown file from path. func Load(path string) (*Skill, error) { raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read skill %s: %w", path, err) } return Parse(raw) } // Parse decodes a skill markdown document from raw bytes. func Parse(raw []byte) (*Skill, error) { s := &Skill{} // Defaults: skills are read-only and require confirmation for anything // mutating unless the operator explicitly opts in. s.Permissions.DefaultMode = "readonly" body, err := splitFrontMatter(raw) if err != nil { return nil, err } if body.front != nil { if err := yaml.Unmarshal(body.front, s); err != nil { return nil, fmt.Errorf("parse frontmatter yaml: %w", err) } } if s.Permissions.DefaultMode == "" { s.Permissions.DefaultMode = "readonly" } if s.Version == 0 { s.Version = 1 } s.SystemPrompt = strings.TrimSpace(body.body) return s, nil } type parts struct { front []byte body string } // splitFrontMatter separates a leading "---\n...\n---\n" block from the // remainder of the document. If no frontmatter is present the whole input is // returned as the body. func splitFrontMatter(raw []byte) (*parts, error) { text := string(raw) // Trim a leading UTF-8 BOM if present so editors that add one don't // break frontmatter detection. text = strings.TrimPrefix(text, "\uFEFF") text = strings.TrimPrefix(text, "\n") if !strings.HasPrefix(text, frontMatterDelim+"\n") && text != frontMatterDelim { return &parts{body: strings.TrimSpace(text)}, nil } // Skip the opening delimiter line. rest := text[len(frontMatterDelim):] rest = strings.TrimPrefix(rest, "\n") // Find the closing delimiter on its own line. endIdx := indexClosingDelim(rest) if endIdx < 0 { return nil, fmt.Errorf("frontmatter not terminated by a closing %q line", frontMatterDelim) } front := []byte(rest[:endIdx]) // Move past the closing delimiter and its trailing newline. tail := rest[endIdx+len(frontMatterDelim):] tail = strings.TrimPrefix(tail, "\r") tail = strings.TrimPrefix(tail, "\n") return &parts{front: front, body: strings.TrimSpace(tail)}, nil } // indexClosingDelim returns the byte offset of a line equal to "---" within // rest, or -1 if none. A line is matched when it starts with "---" followed // by end-of-line. func indexClosingDelim(rest string) int { lines := strings.SplitN(rest, "\n", -1) off := 0 for _, ln := range lines { if strings.TrimRight(ln, "\r") == frontMatterDelim { return off } // +1 for the '\n' that Split removed. off += len(ln) + 1 } return -1 } // Write serialises a skill to a markdown file at path, creating parent dirs. func Write(path string, s *Skill) error { raw, err := Marshal(s) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return fmt.Errorf("mkdir for skill: %w", err) } if err := os.WriteFile(path, raw, 0o644); err != nil { return fmt.Errorf("write skill %s: %w", path, err) } return nil } // Marshal renders a skill as markdown: frontmatter block + system prompt body. func Marshal(s *Skill) ([]byte, error) { if s == nil { return nil, fmt.Errorf("nil skill") } if s.Permissions.DefaultMode == "" { s.Permissions.DefaultMode = "readonly" } if s.Version == 0 { s.Version = 1 } front, err := yaml.Marshal(s) if err != nil { return nil, fmt.Errorf("marshal frontmatter: %w", err) } var b strings.Builder b.WriteString(frontMatterDelim) b.WriteString("\n") b.Write(front) b.WriteString(frontMatterDelim) b.WriteString("\n\n") b.WriteString(strings.TrimSpace(s.SystemPrompt)) b.WriteString("\n") return []byte(b.String()), nil }