Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions openkb/skill/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,20 @@ def validate_skill(skill_dir: Path, *, strict: bool = False) -> ValidationResult
# references/ wikilink resolution
wikilinks = WIKILINK_RE.findall(text)
for link in wikilinks:
# link may or may not include .md suffix
target = refs_dir / link
if not target.suffix:
target = target.with_suffix(".md")
# The link may already include the .md suffix; append it otherwise.
# Test the literal ".md" rather than Path.suffix — a dotted stem like
# "api.v2" has a truthy suffix (".v2"), so Path.suffix would skip the
# ".md" and then look for a non-existent extension-less file.
target = refs_dir / (link if link.lower().endswith(".md") else f"{link}.md")
# A reference must resolve to a file *under* references/ — reject any
# that escape it (e.g. "[[references/../SKILL]]"), which would also
# break the relative_to(skill_dir) call in the not-found message below.
if not target.resolve().is_relative_to(refs_dir.resolve()):
result.errors.append(
f"SKILL.md references [[references/{link}]] which resolves "
f"outside the references/ directory."
)
continue
if not target.exists():
result.errors.append(
f"SKILL.md references [[references/{link}]] but "
Expand Down
26 changes: 26 additions & 0 deletions tests/test_skill_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,32 @@ def test_wikilink_without_md_suffix_resolves(tmp_path):
assert result.passed, result.errors


def test_wikilink_dotted_stem_without_md_suffix_resolves(tmp_path):
# A reference name whose stem contains a dot (e.g. "api.v2") must still get
# the implicit ".md". Path.suffix would treat ".v2" as the suffix and skip
# appending ".md", falsely reporting the existing api.v2.md as missing.
sd = _write_skill(
tmp_path, "ref-dotted-stem",
body="See [[references/api.v2]] for details.\n",
refs={"api.v2.md": "# api v2\n"},
)
result = validate_skill(sd)
assert result.passed, result.errors


def test_wikilink_escaping_references_dir_is_rejected(tmp_path):
# A link that resolves outside references/ (e.g. "../SKILL") must error —
# not be accepted just because the resolved target (here SKILL.md) exists.
sd = _write_skill(
tmp_path, "ref-escape",
body="See [[references/../SKILL]] for details.\n",
refs={"topic.md": "# topic\n"},
)
result = validate_skill(sd)
assert not result.passed
assert any("outside the references/" in e for e in result.errors)


# ---------------------------------------------------------------------------
# scripts/ imports — strict mode only
# ---------------------------------------------------------------------------
Expand Down