Merge PR #3141: fix(skills): use yaml.safe_load for frontmatter parsing to handle multiline descriptions

fix(skills): use yaml.safe_load for frontmatter parsing to handle multiline descriptions
This commit is contained in:
Xubin Ren
2026-04-16 20:07:15 +08:00
committed by GitHub
3 changed files with 117 additions and 14 deletions
+29 -14
View File
@@ -6,6 +6,8 @@ import re
import shutil
from pathlib import Path
import yaml
# Default builtin skills directory (relative to this file)
BUILTIN_SKILLS_DIR = Path(__file__).parent.parent / "skills"
@@ -165,11 +167,19 @@ class SkillsLoader:
return content[match.end():].strip()
return content
def _parse_nanobot_metadata(self, raw: str) -> dict:
"""Parse skill metadata JSON from frontmatter (supports nanobot and openclaw keys)."""
try:
data = json.loads(raw)
except (json.JSONDecodeError, TypeError):
def _parse_nanobot_metadata(self, raw: object) -> dict:
"""Extract nanobot/openclaw metadata from a frontmatter field.
``raw`` may be a dict (already parsed by yaml.safe_load) or a JSON str.
"""
if isinstance(raw, dict):
data = raw
elif isinstance(raw, str):
try:
data = json.loads(raw)
except (json.JSONDecodeError, TypeError):
return {}
else:
return {}
if not isinstance(data, dict):
return {}
@@ -187,8 +197,8 @@ class SkillsLoader:
def _get_skill_meta(self, name: str) -> dict:
"""Get nanobot metadata for a skill (cached in frontmatter)."""
meta = self.get_skill_metadata(name) or {}
return self._parse_nanobot_metadata(meta.get("metadata", ""))
raw_meta = self.get_skill_metadata(name) or {}
return self._parse_nanobot_metadata(raw_meta.get("metadata"))
def get_always_skills(self) -> list[str]:
"""Get skills marked as always=true that meet requirements."""
@@ -197,7 +207,7 @@ class SkillsLoader:
for entry in self.list_skills(filter_unavailable=True)
if (meta := self.get_skill_metadata(entry["name"]) or {})
and (
self._parse_nanobot_metadata(meta.get("metadata", "")).get("always")
self._parse_nanobot_metadata(meta.get("metadata")).get("always")
or meta.get("always")
)
]
@@ -218,10 +228,15 @@ class SkillsLoader:
match = _STRIP_SKILL_FRONTMATTER.match(content)
if not match:
return None
metadata: dict[str, str] = {}
for line in match.group(1).splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)
metadata[key.strip()] = value.strip().strip('"\'')
try:
parsed = yaml.safe_load(match.group(1))
except yaml.YAMLError:
return None
if not isinstance(parsed, dict):
return None
# yaml.safe_load returns native types (int, bool, list, etc.);
# keep values as-is so downstream consumers get correct types.
metadata: dict[str, object] = {}
for key, value in parsed.items():
metadata[str(key)] = value
return metadata