mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 12:34:17 +02:00
Block out-of-skill symlink hashing in skills scan (#3410)
🤖 Generated with GPT-5.2-Codex
This commit is contained in:
parent
163c16876f
commit
ea19a2a5f5
2 changed files with 30 additions and 2 deletions
|
|
@ -86,16 +86,22 @@ def compute_file_hash(path: Path) -> str:
|
|||
def scan_skill_files(skill_dir: Path) -> list[SkillFileInfo]:
|
||||
"""Scan a skill directory for all files."""
|
||||
files = []
|
||||
resolved_skill_dir = skill_dir.resolve()
|
||||
|
||||
# Sort for deterministic ordering across platforms
|
||||
for file_path in sorted(skill_dir.rglob("*")):
|
||||
if file_path.is_file():
|
||||
resolved_file_path = file_path.resolve()
|
||||
if not resolved_file_path.is_relative_to(resolved_skill_dir):
|
||||
continue
|
||||
|
||||
rel_path = file_path.relative_to(skill_dir)
|
||||
files.append(
|
||||
SkillFileInfo(
|
||||
# Use POSIX paths for cross-platform URI consistency
|
||||
path=rel_path.as_posix(),
|
||||
size=file_path.stat().st_size,
|
||||
hash=compute_file_hash(file_path),
|
||||
size=resolved_file_path.stat().st_size,
|
||||
hash=compute_file_hash(resolved_file_path),
|
||||
)
|
||||
)
|
||||
return files
|
||||
|
|
|
|||
|
|
@ -169,6 +169,28 @@ This is my skill content.
|
|||
assert "reference.md" in paths
|
||||
assert "scripts/helper.py" in paths
|
||||
|
||||
async def test_manifest_ignores_symlink_target_outside_skill(self, tmp_path: Path):
|
||||
skill_dir = tmp_path / "symlinked-skill"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text("# Skill\n")
|
||||
|
||||
outside_file = tmp_path / "outside.txt"
|
||||
outside_file.write_text("secret")
|
||||
(skill_dir / "leak.txt").symlink_to(outside_file)
|
||||
|
||||
mcp = FastMCP("Test")
|
||||
mcp.add_provider(SkillProvider(skill_path=skill_dir))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.read_resource(
|
||||
AnyUrl("skill://symlinked-skill/_manifest")
|
||||
)
|
||||
manifest = json.loads(result[0].text)
|
||||
|
||||
paths = {f["path"] for f in manifest["files"]}
|
||||
assert "SKILL.md" in paths
|
||||
assert "leak.txt" not in paths
|
||||
|
||||
async def test_read_supporting_file_via_template(self, single_skill_dir: Path):
|
||||
mcp = FastMCP("Test")
|
||||
mcp.add_provider(SkillProvider(skill_path=single_skill_dir))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue