Harden SKILL.md parser — error logging, flexible indent, CRLF support, type validation

This commit is contained in:
2026-04-08 01:51:34 +00:00
parent d838fe08cf
commit 2d371ceb86

View File

@@ -67,13 +67,22 @@ except FileNotFoundError:
def parse_skill_md(path): def parse_skill_md(path):
"""Parse a SKILL.md frontmatter into a tool definition.""" """Parse a SKILL.md frontmatter into a tool definition.
with open(path) as f: Returns tool definition dict or None on failure."""
content = f.read() try:
with open(path) as f:
content = f.read()
except Exception as e:
log(f"Cannot read {path}: {e}")
return None
# Normalize line endings
content = content.replace("\r\n", "\n")
# Extract YAML frontmatter between --- # Extract YAML frontmatter between ---
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL) match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not match: if not match:
log(f"No frontmatter in {path}")
return None return None
# Simple YAML-like parser (no pyyaml dependency) # Simple YAML-like parser (no pyyaml dependency)
@@ -87,21 +96,24 @@ def parse_skill_md(path):
if not stripped or stripped.startswith("#"): if not stripped or stripped.startswith("#"):
continue continue
if line.startswith(" ") and current_key == "parameters": # Detect indent level (flexible — 2+ spaces counts as nested)
# Inside parameters block indent = len(line) - len(line.lstrip())
if line.startswith(" ") and current_param:
if indent >= 2 and current_key == "parameters":
if indent >= 4 and current_param:
# Parameter property # Parameter property
k, _, v = stripped.partition(":") k, _, v = stripped.partition(":")
k = k.strip()
v = v.strip().strip('"').strip("'") v = v.strip().strip('"').strip("'")
if k.strip() == "required": if k == "required":
v = v.lower() == "true" v = v.lower() in ("true", "yes", "1")
params[current_param][k.strip()] = v params[current_param][k] = v
elif ":" in stripped: elif ":" in stripped:
# New parameter # New parameter name
param_name = stripped.rstrip(":").strip() param_name = stripped.rstrip(":").strip()
current_param = param_name current_param = param_name
params[param_name] = {} params[param_name] = {}
elif ":" in line and not line.startswith(" "): elif ":" in line and indent == 0:
k, _, v = line.partition(":") k, _, v = line.partition(":")
k = k.strip() k = k.strip()
v = v.strip().strip('"').strip("'") v = v.strip().strip('"').strip("'")
@@ -111,14 +123,21 @@ def parse_skill_md(path):
current_param = None current_param = None
if "name" not in fm: if "name" not in fm:
log(f"No 'name' field in {path}")
return None return None
if "description" not in fm:
log(f"Warning: no 'description' in {path}")
# Build Ollama tool definition # Build Ollama tool definition
properties = {} properties = {}
required = [] required = []
for pname, pdata in params.items(): for pname, pdata in params.items():
ptype = pdata.get("type", "string")
if ptype not in ("string", "integer", "number", "boolean", "array", "object"):
log(f"Warning: unknown type '{ptype}' for param '{pname}' in {path}")
properties[pname] = { properties[pname] = {
"type": pdata.get("type", "string"), "type": ptype,
"description": pdata.get("description", ""), "description": pdata.get("description", ""),
} }
if pdata.get("required", False): if pdata.get("required", False):