package bundle import ( "path/filepath" "strings" "huggingface.co/turenlabs/Vigil/source/pkg/types" ) // resolveReferences marks each File.Referenced by searching the SKILL.md for any // form of its name. The indirection layer (context-loader / simple-formatter) // is not defeated by verb omission: we match the basename, the bundle-relative // path, ./name, markdown-link forms ([x](./payload.sh)), and code-span forms // (`./payload.sh`), recording which surface named the file in RefSources. // // Symlinked siblings are matched on their OWN basename/relpath too, so a // directive like "run ./helper" where helper -> /bin/sh resolves to the File // entry for helper. func resolveReferences(b *Bundle) { for _, f := range b.Files { referenced, sources := fileIsReferenced(b.Skill, b.AllowedTools, f) f.Referenced = referenced f.RefSources = sources } } // fileIsReferenced reports whether the SKILL.md (body/raw/description/triggers) // or the allowed-tools frontmatter names f, and via which surfaces. A nil skill // yields no references (e.g. when SKILL.md failed to parse). func fileIsReferenced(skill *types.SkillFile, allowedTools []string, f *File) (bool, []string) { forms := referenceForms(f.RelPath) if len(forms) == 0 { return false, nil } var sources []string seen := map[string]bool{} addSource := func(s string) { if !seen[s] { seen[s] = true sources = append(sources, s) } } // allowed-tools frontmatter list (e.g. Bash(./setup.sh)). for _, tool := range allowedTools { if containsAnyForm(tool, forms) { addSource("allowed-tools") break } } if skill != nil { // Body, raw content, and description are scanned for plain, code-span, and // markdown-link forms. RawContent is a superset of Body+frontmatter, so it // is the primary surface; Body is checked separately so a body-only mention // is attributed to "body" rather than the broader "frontmatter". bodyHit := containsAnyForm(skill.Body, forms) if bodyHit { addSource("body") if hasMarkdownLink(skill.Body, forms) { addSource("markdown-link") } if hasCodeSpan(skill.Body, forms) { addSource("code-span") } } // Frontmatter-region mention: present in RawContent but not in Body and not // already attributed to allowed-tools. if !bodyHit && containsAnyForm(skill.RawContent, forms) { addSource("frontmatter") if hasMarkdownLink(skill.RawContent, forms) { addSource("markdown-link") } if hasCodeSpan(skill.RawContent, forms) { addSource("code-span") } } if containsAnyForm(skill.Description, forms) { addSource("body") } for _, t := range skill.Triggers { if containsAnyForm(t, forms) { addSource("frontmatter") break } } } return len(sources) > 0, sources } // referenceForms returns the distinct textual surfaces under which relPath might // be named in SKILL.md: the basename, the bundle-relative path (slash form), and // the ./-prefixed relative path. Empty/degenerate names yield no forms so a // stray "." cannot match everything. func referenceForms(relPath string) []string { rel := filepath.ToSlash(strings.TrimSpace(relPath)) if rel == "" || rel == "." || rel == ".." { return nil } base := pathBase(rel) forms := []string{} add := func(s string) { s = strings.TrimSpace(s) if s == "" || s == "." { return } for _, existing := range forms { if existing == s { return } } forms = append(forms, s) } add(base) add(rel) add("./" + rel) if base != rel { add("./" + base) } return forms } // pathBase returns the final slash-separated segment of a slash path. func pathBase(slashPath string) string { if i := strings.LastIndex(slashPath, "/"); i >= 0 { return slashPath[i+1:] } return slashPath } // containsAnyForm reports whether haystack contains any reference form as a // whole-token match. A bare basename must not match as a substring of an // unrelated longer word (e.g. "go" inside "category"), so we require the match // to be bounded by a non-identifier character (or string edge) on each side. func containsAnyForm(haystack string, forms []string) bool { if haystack == "" { return false } for _, form := range forms { if tokenMatch(haystack, form) { return true } } return false } // tokenMatch reports whether needle occurs in haystack bounded by non-filename // characters. Filename characters are letters, digits, '_', '-', '.', '/'. // A form that itself starts with "./" or contains "/" is already specific // enough that a plain substring check is safe and is used directly. func tokenMatch(haystack, needle string) bool { if needle == "" { return false } // Path-ish forms are specific; substring is sufficient and avoids missing a // match adjacent to quotes/parens. if strings.ContainsAny(needle, "/") || strings.HasPrefix(needle, "./") { return strings.Contains(haystack, needle) } from := 0 for { idx := strings.Index(haystack[from:], needle) if idx < 0 { return false } start := from + idx end := start + len(needle) leftOK := start == 0 || !isFilenameByte(haystack[start-1]) rightOK := end == len(haystack) || !isFilenameByte(haystack[end]) if leftOK && rightOK { return true } from = start + 1 if from >= len(haystack) { return false } } } // isFilenameByte reports whether b can appear inside an unquoted filename token. func isFilenameByte(b byte) bool { switch { case b >= 'a' && b <= 'z': return true case b >= 'A' && b <= 'Z': return true case b >= '0' && b <= '9': return true case b == '_' || b == '-' || b == '.' || b == '/': return true } return false } // hasMarkdownLink reports whether any form appears inside a markdown link target // "](...form...)". This is a heuristic surface attribution, not a strict parse. func hasMarkdownLink(haystack string, forms []string) bool { for _, form := range forms { // Look for the closing "](" of a link whose target contains the form. from := 0 for { idx := strings.Index(haystack[from:], "](") if idx < 0 { break } start := from + idx + 2 closeIdx := strings.Index(haystack[start:], ")") if closeIdx < 0 { break } target := haystack[start : start+closeIdx] if strings.Contains(target, form) { return true } from = start + closeIdx + 1 if from >= len(haystack) { break } } } return false } // hasCodeSpan reports whether any form appears inside a backtick code span. func hasCodeSpan(haystack string, forms []string) bool { for _, form := range forms { from := 0 for { open := strings.Index(haystack[from:], "`") if open < 0 { break } openAbs := from + open + 1 close := strings.Index(haystack[openAbs:], "`") if close < 0 { break } span := haystack[openAbs : openAbs+close] if strings.Contains(span, form) { return true } from = openAbs + close + 1 if from >= len(haystack) { break } } } return false } // parseAllowedTools extracts the allowed-tools list from raw frontmatter. The // existing parser does not capture this field. Supported forms: // // allowed-tools: [Bash, Read, Write] # inline flow list // allowed-tools: Bash, Read # inline comma list // allowed-tools: # block list // - Bash(./setup.sh) // - Read // // Only the frontmatter region (the first --- ... --- block) is considered. func parseAllowedTools(rawContent string) []string { fm := frontmatterRegion(rawContent) if fm == "" { return nil } lines := strings.Split(fm, "\n") var tools []string inBlock := false for _, line := range lines { trimmedRight := strings.TrimRight(line, " \t\r") trimmed := strings.TrimSpace(trimmedRight) if inBlock { if strings.HasPrefix(trimmed, "- ") { tools = append(tools, splitToolList(trimmed[2:])...) continue } // A new top-level key ends the block. if isTopLevelKey(trimmedRight) { inBlock = false // fall through to key handling below } else if trimmed == "" { continue } else { inBlock = false } } key, val, ok := splitKey(trimmedRight) if !ok { continue } if !isAllowedToolsKey(key) { continue } val = strings.TrimSpace(val) switch { case val == "": // Block list follows on subsequent "- " lines. inBlock = true case strings.HasPrefix(val, "[") && strings.HasSuffix(val, "]"): inner := strings.TrimSuffix(strings.TrimPrefix(val, "["), "]") tools = append(tools, splitToolList(inner)...) default: tools = append(tools, splitToolList(val)...) } } return dedupeNonEmpty(tools) } // frontmatterRegion returns the inner text of the leading --- ... --- block, or // "" if there is no frontmatter. func frontmatterRegion(rawContent string) string { trimmed := strings.TrimLeft(rawContent, " \t\r\n") if !strings.HasPrefix(trimmed, "---") { return "" } rest := trimmed[3:] // Skip to end of the opening delimiter line. if nl := strings.IndexByte(rest, '\n'); nl >= 0 { rest = rest[nl+1:] } else { return "" } idx := strings.Index(rest, "\n---") if idx < 0 { // Closing delimiter might be the very first line of rest. if strings.HasPrefix(rest, "---") { return "" } return "" } return rest[:idx] } // isAllowedToolsKey reports whether a frontmatter key is the allowed-tools list // under any of its common spellings. func isAllowedToolsKey(key string) bool { k := strings.ToLower(strings.TrimSpace(key)) return k == "allowed-tools" || k == "allowed_tools" || k == "allowedtools" || k == "tools" } // isTopLevelKey reports whether a raw line is an unindented "key:" pair. func isTopLevelKey(rawLine string) bool { if rawLine == "" { return false } if rawLine[0] == ' ' || rawLine[0] == '\t' { return false } _, _, ok := splitKey(rawLine) return ok } // splitKey splits a "key: value" line. ok is false when there is no colon-led // key. Returns the trimmed key and the raw remainder. func splitKey(line string) (key, val string, ok bool) { colon := strings.Index(line, ":") if colon <= 0 { return "", "", false } return strings.TrimSpace(line[:colon]), line[colon+1:], true } // splitToolList splits a comma/space-separated tool list, trimming quotes and // whitespace. "Bash(./setup.sh)" is preserved whole so the argument path inside // can be matched by reference resolution. func splitToolList(s string) []string { s = strings.TrimSpace(s) if s == "" { return nil } var out []string for _, part := range strings.Split(s, ",") { p := strings.TrimSpace(part) p = strings.Trim(p, "'\"") if p != "" { out = append(out, p) } } return out } // dedupeNonEmpty removes empties and duplicates, preserving order. func dedupeNonEmpty(in []string) []string { if len(in) == 0 { return nil } seen := map[string]bool{} var out []string for _, s := range in { s = strings.TrimSpace(s) if s == "" || seen[s] { continue } seen[s] = true out = append(out, s) } return out }