Repository navigation
fix(skills): skip symlinked resources when loading a skill from disk - #7601
behnam1990adel-hash wants to merge 1 commit into
Conversation
_load_dir walked a skill's references/, assets/, and scripts/
directories with pathlib.Path.rglob("*") and, for every entry where
is_file() was true, read its content with read_text()/read_bytes().
Both calls follow symbolic links transparently, so a symlink
committed inside one of those directories, pointing at a file
anywhere else on disk, was read into the skill's resources under the
symlink's own name, indistinguishable from an ordinary file once
loaded. LoadSkillResourceTool then served that content back verbatim,
since it is a plain dict lookup against what _load_dir already
populated, with no further check at request time.
Skip a path that is itself a symlink before is_file() or either read
call runs. rglob("*") does not recurse into a symlinked directory (a
pathlib default), so this closes the only way a symlink inside these
directories could otherwise leak content.
Added test__load_skill_from_dir_skips_symlinked_resources, which
reproduces the previous behavior on a real on-disk filesystem and
confirms an ordinary (non-symlink) resource is still loaded normally.
Somay-kousis
left a comment
There was a problem hiding this comment.
test__load_skill_from_dir_skips_symlinked_resources fails on the base (assert 'top-secret' is None) and passes on the head. tests/unittests/skills: 105 passed on the head.
The check covers symlinks inside references/, assets/ and scripts/, but not those directories being symlinks themselves. _load_dir(skill_dir / "assets") gets the symlink as directory, rglob("*") walks the target, and none of the yielded entries is a symlink, so everything outside the skill is loaded. That goes against the new comment ("keeps a skill from reading a file that lives outside its own directory").
Probe on this branch, Python 3.12.13, with outside/secret.txt next to the skill:
file symlink in assets/ assets={} references={} scripts=[]
dir symlink inside assets/ assets={} references={} scripts=[]
assets/ itself is a symlink assets={'secret.txt': 'top-secret'} references={} scripts=[]
references/ itself is a symlink assets={} references={'secret.txt': 'top-secret'} scripts=[]
scripts/ itself is a symlink assets={} references={} scripts=['run.py', 'secret.txt']
Returning early when directory.is_symlink() at the top of _load_dir closes these and keeps a skill whose own directory is a symlink loading normally (tried on your branch, then reverted):
assets/ itself is a symlink assets={} references={} scripts=[]
references/ itself is a symlink assets={} references={} scripts=[]
scripts/ itself is a symlink assets={} references={} scripts=[]
symlinked skill dir assets= {'plain.txt': 'plain'} references= {'ref.md': 'ref'}
tests/unittests/skills still 105 passed with that. Parametrizing the new test over the file-in-dir and dir-itself shapes would cover both.
Probe script: probe_7601.py (outputs probe_7601_head.out, probe_7601_base.out, probe_7601_suggested.out).
_load_dirwalks a skill'sreferences/,assets/, andscripts/directories withpathlib.Path.rglob("*")and, for every entry whereis_file()is true, reads its content withread_text()/read_bytes(). Both calls follow symbolic links transparently, so a symlink committed inside one of those directories, pointing at a file anywhere else on disk, is read into the skill's resources under the symlink's own name — indistinguishable from an ordinary file once loaded.LoadSkillResourceToolthen serves that content back verbatim, since it is a plain dict lookup against what_load_diralready populated, with no further check at request time — confirmed end-to-end through the realload_skill_resourcetool.This skips a path that is itself a symlink before
is_file()or either read call runs.rglob("*")does not recurse into a symlinked directory (a pathlib default), so this closes the only way a symlink inside these directories could otherwise leak content.Added
test__load_skill_from_dir_skips_symlinked_resources, which reproduces the previous behavior on a real on-disk filesystem and confirms an ordinary (non-symlink) resource is still loaded normally. The existing test suite undertests/unittests/skills/passes unchanged.