mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-16 09:45:24 -04:00
fix(skills): scope skill reads to caller owner (#777)
read_skill_md and read_skill_reference walk all skill files via _iter_skill_files and return the first match by slug, regardless of owner. In a multi-user deployment where two users have skills with the same slug under different categories, a caller scoped to owner='alice' can read Bob's skill content. This is the same cross-tenant leak class as the update_skill / delete_skill fix (PR #755, merged), but on the read path. Changes: - read_skill_md / read_skill_reference accept owner= param (default None = match ownerless only, matching the write-path convention). - 7 callers updated: tool_implementations.py (view, view_ref, patch), builtin_actions.py (test_skills), skills_routes.py (audit, source, test routes). - Tests: read scoping (alice reads hers, not bob's), positive update scoping (alice can mutate her own), ownerless-match default.
This commit is contained in:
@@ -472,24 +472,29 @@ class SkillsManager:
|
||||
# Reading a single skill (used by the skill_view tool)
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
def read_skill_md(self, name: str) -> Optional[str]:
|
||||
def read_skill_md(self, name: str, owner: Optional[str] = None) -> Optional[str]:
|
||||
for path in self._iter_skill_files():
|
||||
sk = self._read_skill(path)
|
||||
if sk and sk.name == name:
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return None
|
||||
if not sk or sk.name != name:
|
||||
continue
|
||||
if (sk.owner or "") != (owner or ""):
|
||||
continue
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def read_skill_reference(self, name: str, ref_path: str) -> Optional[str]:
|
||||
def read_skill_reference(self, name: str, ref_path: str, owner: Optional[str] = None) -> Optional[str]:
|
||||
"""Read a sub-file under the skill's directory (references/, etc).
|
||||
Refuses path traversal."""
|
||||
for path in self._iter_skill_files():
|
||||
sk = self._read_skill(path)
|
||||
if not sk or sk.name != name:
|
||||
continue
|
||||
if (sk.owner or "") != (owner or ""):
|
||||
continue
|
||||
base = os.path.realpath(os.path.dirname(path))
|
||||
target = os.path.realpath(os.path.join(base, ref_path))
|
||||
if os.path.commonpath([base, target]) != base or target == os.path.dirname(path):
|
||||
|
||||
Reference in New Issue
Block a user