claude-code: read front matter as YAML and refuse a file reference outside the item
A byte-order mark, a flow mapping or a merged key hid a key the line scan looked for, and @path read any file into every session the item reached (hq issue 340 review).
This commit is contained in:
@@ -41,6 +41,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Plugin is the plugin's name, and its marketplace's: what its items are called in a session, `nox-mesh:<name>`.
|
||||
@@ -112,8 +114,14 @@ var runsFrontMatter = map[string]string{
|
||||
// !`command` inline, or a fenced block opened with ```!.
|
||||
var shellLine = regexp.MustCompile("!`[^`\n]+`|(?m)^[ \t]*```!")
|
||||
|
||||
// fileRef is a file the agent may read into the prompt when it loads a skill or a command: `@` and a path, at the
|
||||
// start of a line or after a space, a bracket or a quote. Any such word counts, a path without a dot or a slash
|
||||
// (`@Makefile`) as much as one with: refused unless it is one of the item's own files, rather than guessed at.
|
||||
var fileRef = regexp.MustCompile(`(?:^|[\s(\[{"'<])@([^\s` + "`" + `"')\]}>]+)`)
|
||||
|
||||
// runsProblem says why an item's text would run something in a session by itself, or "": the shell lines the
|
||||
// agent runs when it loads a skill or a command, and the front matter keys that start, run or pre-approve.
|
||||
// agent runs when it loads a skill or a command, the front matter keys that start, run or pre-approve, front
|
||||
// matter the module cannot read for certain, and a file outside the item that the agent would read in.
|
||||
func runsProblem(it Item) string {
|
||||
if it.Kind != KindSkill && it.Kind != KindAgent && it.Kind != KindCommand {
|
||||
return ""
|
||||
@@ -127,13 +135,18 @@ func runsProblem(it Item) string {
|
||||
if !strings.HasSuffix(rel, ".md") {
|
||||
continue // a script beside a skill runs only when the agent runs it, through the session's own rules
|
||||
}
|
||||
content := it.Files[rel]
|
||||
content := strings.TrimPrefix(it.Files[rel], "\ufeff")
|
||||
if shellLine.MatchString(content) {
|
||||
return fmt.Sprintf("%s holds a shell line (!`…` or ```!), which the agent runs when it loads the %s, "+
|
||||
"before any permission is asked; nothing registered through this module runs by itself in a "+
|
||||
"session (novox/hq issue 340)", rel, it.Kind)
|
||||
}
|
||||
for _, key := range frontMatterKeys(content) {
|
||||
keys, problem := frontMatterKeys(content)
|
||||
if problem != "" {
|
||||
return fmt.Sprintf("%s: %s, so what it asks of a session cannot be judged; it is refused rather than "+
|
||||
"guessed at (novox/hq issue 340)", rel, problem)
|
||||
}
|
||||
for _, key := range keys {
|
||||
norm := strings.ToLower(strings.NewReplacer("-", "", "_", "").Replace(key))
|
||||
if why, ok := runsFrontMatter[norm]; ok {
|
||||
return fmt.Sprintf("%s says %q in its front matter: %s. Nothing registered through this module "+
|
||||
@@ -141,30 +154,64 @@ func runsProblem(it Item) string {
|
||||
"terminal (novox/hq issue 340)", rel, key, why)
|
||||
}
|
||||
}
|
||||
if it.Kind == KindSkill || it.Kind == KindCommand {
|
||||
for _, m := range fileRef.FindAllStringSubmatch(content, -1) {
|
||||
ref := strings.TrimRight(m[1], ".,;:!?")
|
||||
if ref == "" {
|
||||
continue
|
||||
}
|
||||
if _, own := it.Files[ref]; !own || pathProblem(ref) != "" {
|
||||
return fmt.Sprintf("%s refers to @%s, a file outside the %s that the agent reads into its "+
|
||||
"prompt when it loads it; a registered item refers only to its own files (novox/hq issue 340)",
|
||||
rel, ref, it.Kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// frontMatterKeys are the top-level keys of a markdown file's front matter: the lines between a first line
|
||||
// `---` and the next, that start a key at the left margin.
|
||||
func frontMatterKeys(content string) []string {
|
||||
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
||||
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
||||
return nil
|
||||
// frontMatterKeys are the top-level keys of a markdown file's front matter, read as YAML: the block between a
|
||||
// first line `---` (after a byte-order mark) and the next. A block that does not close, does not parse, or is not
|
||||
// a mapping is a problem: the agent's own reader may take from it what this one cannot see.
|
||||
func frontMatterKeys(content string) ([]string, string) {
|
||||
content = strings.ReplaceAll(strings.TrimPrefix(content, "\ufeff"), "\r\n", "\n")
|
||||
lines := strings.Split(content, "\n")
|
||||
first := 0
|
||||
for first < len(lines) && strings.TrimSpace(lines[first]) == "" {
|
||||
first++
|
||||
}
|
||||
var keys []string
|
||||
for _, line := range lines[1:] {
|
||||
if strings.TrimSpace(line) == "---" {
|
||||
if first == len(lines) || strings.TrimSpace(lines[first]) != "---" {
|
||||
return nil, ""
|
||||
}
|
||||
end := -1
|
||||
for i := first + 1; i < len(lines); i++ {
|
||||
if t := strings.TrimSpace(lines[i]); t == "---" || t == "..." {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
if line == "" || line[0] == ' ' || line[0] == '\t' || line[0] == '#' || line[0] == '-' {
|
||||
continue
|
||||
}
|
||||
if i := strings.Index(line, ":"); i > 0 {
|
||||
keys = append(keys, strings.Trim(strings.TrimSpace(line[:i]), `"'`))
|
||||
}
|
||||
}
|
||||
return keys
|
||||
if end < 0 {
|
||||
return nil, "its front matter does not close"
|
||||
}
|
||||
block := strings.Join(lines[first+1:end], "\n")
|
||||
if strings.TrimSpace(block) == "" {
|
||||
return nil, ""
|
||||
}
|
||||
var node yaml.Node
|
||||
if err := yaml.Unmarshal([]byte(block), &node); err != nil {
|
||||
return nil, "its front matter is not YAML (" + err.Error() + ")"
|
||||
}
|
||||
var top map[string]any
|
||||
if err := node.Decode(&top); err != nil {
|
||||
return nil, "its front matter is not a mapping of keys (" + err.Error() + ")"
|
||||
}
|
||||
keys := make([]string, 0, len(top))
|
||||
for k := range top {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys, ""
|
||||
}
|
||||
|
||||
// Item is one registration, as the state keeps it.
|
||||
|
||||
Reference in New Issue
Block a user