Skip to content

Commit bf946c9

Browse files
fix(skills): return no skills when the skills directory cannot be read (#2083)
discover_skills raised when dir pointed to a file or could not be listed, which broke client setup. It now warns and returns an empty list, matching how unreadable lockfiles and entries are handled. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Hassieb Pakzad <hassiebp@users.noreply.github.com>
1 parent 92666c2 commit bf946c9

2 files changed

Lines changed: 30 additions & 6 deletions

File tree

‎langfuse/_client/skills.py‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,23 @@ def discover_skills(*, dir: str) -> list[SkillReference]:
2121
Unlocked skills use their directory name without a version. Lock records
2222
do not verify local edits.
2323
24-
Missing directories return an empty list. Unreadable or malformed lockfiles
25-
warn and fall back to directory names. Entries that cannot be inspected,
26-
invalid lock entries, and duplicate names are skipped with a warning.
24+
Missing directories return an empty list; unreadable ones warn and return
25+
an empty list. Unreadable or malformed lockfiles warn and fall back to
26+
directory names. Entries that cannot be inspected, invalid lock entries,
27+
and duplicate names are skipped with a warning.
2728
"""
2829
root = Path(os.path.abspath(dir))
29-
if _stat_if_exists(root) is None:
30+
try:
31+
if _stat_if_exists(root) is None:
32+
return []
33+
with os.scandir(root) as entries:
34+
candidates = sorted(entries, key=lambda entry: entry.name)
35+
except OSError as error:
36+
langfuse_logger.warning(
37+
"Could not read skills directory %s. No skills discovered: %s",
38+
root,
39+
error,
40+
)
3041
return []
3142

3243
locked: dict[Path, SkillReference] = {}
@@ -39,8 +50,6 @@ def discover_skills(*, dir: str) -> list[SkillReference]:
3950
error,
4051
)
4152
available: dict[str, SkillReference] = {}
42-
with os.scandir(root) as entries:
43-
candidates = sorted(entries, key=lambda entry: entry.name)
4453
for candidate in candidates:
4554
if candidate.name.startswith("."):
4655
continue

‎tests/unit/test_skills.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,21 @@ def test_discovery_skips_entries_with_filesystem_errors(
150150
assert "missing-target" not in caplog.text
151151

152152

153+
@pytest.mark.parametrize("kind", ["file", "symlink-loop"])
154+
def test_unreadable_skills_directory_returns_empty_list(
155+
tmp_path, monkeypatch, kind, caplog
156+
):
157+
monkeypatch.chdir(tmp_path)
158+
root = tmp_path / "skills"
159+
if kind == "file":
160+
root.write_text("not a directory", encoding="utf-8")
161+
else:
162+
root.symlink_to(root)
163+
164+
assert discover_skills(dir=str(root)) == []
165+
assert "Could not read skills directory" in caplog.text
166+
167+
153168
def test_serialization_preserves_extra_fields():
154169
skills = [{"name": "customer", "version": 2, "futureField": {"nested": True}}]
155170
assert json.loads(serialize_available_skills(skills)) == skills

0 commit comments

Comments
 (0)