mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(cron): normalize absolute skill paths before skill_view (#59824)
Cron jobs may store absolute paths to skills under HERMES_HOME/skills or external_dirs, but skill_view rejects absolute names for security. Extract the slash-command normalization into agent.skill_utils and reuse it when cron loads job skills.
This commit is contained in:
parent
07d93413e5
commit
62972060ca
3 changed files with 44 additions and 31 deletions
|
|
@ -143,37 +143,9 @@ def _load_skill_payload(skill_identifier: str, task_id: str | None = None) -> tu
|
|||
|
||||
try:
|
||||
from tools.skills_tool import SKILLS_DIR, skill_view
|
||||
from agent.skill_utils import get_external_skills_dirs
|
||||
from agent.skill_utils import normalize_skill_lookup_name
|
||||
|
||||
identifier_path = Path(raw_identifier).expanduser()
|
||||
if identifier_path.is_absolute():
|
||||
normalized = None
|
||||
trusted_roots = [SKILLS_DIR]
|
||||
try:
|
||||
trusted_roots.extend(get_external_skills_dirs())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Prefer the lexical path under a trusted skill root before
|
||||
# resolving symlinks. Slash-command discovery can legitimately
|
||||
# find a skill via ~/.hermes/skills/<name> where <name> is a
|
||||
# symlink to a checked-out skill elsewhere. Resolving first turns
|
||||
# that trusted visible path into an arbitrary absolute path that
|
||||
# skill_view() refuses to load.
|
||||
for root in trusted_roots:
|
||||
try:
|
||||
normalized = str(identifier_path.relative_to(root))
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if normalized is None:
|
||||
try:
|
||||
normalized = str(identifier_path.resolve().relative_to(SKILLS_DIR.resolve()))
|
||||
except Exception:
|
||||
normalized = raw_identifier
|
||||
else:
|
||||
normalized = raw_identifier.lstrip("/")
|
||||
normalized = normalize_skill_lookup_name(raw_identifier)
|
||||
|
||||
loaded_skill = json.loads(
|
||||
skill_view(normalized, task_id=task_id, preprocess=False)
|
||||
|
|
|
|||
|
|
@ -507,6 +507,46 @@ def get_all_skills_dirs() -> List[Path]:
|
|||
return dirs
|
||||
|
||||
|
||||
def normalize_skill_lookup_name(identifier: str) -> str:
|
||||
"""Normalize a skill identifier to a ``skill_view()``-safe relative path.
|
||||
|
||||
Slash commands and cron jobs may store absolute paths to skills that live
|
||||
under ``~/.hermes/skills/`` (including via symlinks) or configured
|
||||
``skills.external_dirs``. ``skill_view()`` rejects absolute names for
|
||||
security, so callers must translate trusted absolute paths to their
|
||||
relative form first.
|
||||
"""
|
||||
raw_identifier = (identifier or "").strip()
|
||||
if not raw_identifier:
|
||||
return raw_identifier
|
||||
|
||||
identifier_path = Path(raw_identifier).expanduser()
|
||||
if not identifier_path.is_absolute():
|
||||
return raw_identifier.lstrip("/")
|
||||
|
||||
trusted_roots = [get_skills_dir()]
|
||||
try:
|
||||
trusted_roots.extend(get_external_skills_dirs())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Prefer the lexical path under a trusted skill root before resolving
|
||||
# symlinks. Slash-command discovery can legitimately find a skill via
|
||||
# ~/.hermes/skills/<name> where <name> is a symlink to a checked-out
|
||||
# skill elsewhere. Resolving first turns that trusted visible path into
|
||||
# an arbitrary absolute path that skill_view() refuses to load.
|
||||
for root in trusted_roots:
|
||||
try:
|
||||
return str(identifier_path.relative_to(root))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
try:
|
||||
return str(identifier_path.resolve().relative_to(get_skills_dir().resolve()))
|
||||
except Exception:
|
||||
return raw_identifier
|
||||
|
||||
|
||||
def _resolve_for_skill_ownership(path) -> Path:
|
||||
path_obj = path if isinstance(path, Path) else Path(str(path))
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -2195,6 +2195,7 @@ def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:
|
|||
from tools.skills_tool import skill_view
|
||||
from tools.skill_usage import bump_use
|
||||
from agent.skill_bundles import build_bundle_invocation_message, resolve_bundle_command_key
|
||||
from agent.skill_utils import normalize_skill_lookup_name
|
||||
|
||||
parts = []
|
||||
skipped: list[str] = []
|
||||
|
|
@ -2225,7 +2226,7 @@ def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:
|
|||
continue
|
||||
|
||||
try:
|
||||
loaded = json.loads(skill_view(skill_name))
|
||||
loaded = json.loads(skill_view(normalize_skill_lookup_name(skill_name)))
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
logger.warning("Cron job '%s': skill '%s' returned invalid JSON, skipping", job.get("name", job.get("id")), skill_name)
|
||||
skipped.append(skill_name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue