feat: project-local skill discovery with per-repo trust gate
Sessions started inside a git checkout now source skills from <root>/.hermes/skills/ and <root>/.agents/skills/ (the cross-tool convention shared with other agent harnesses) as the highest-precedence skill tier: project > local > external_dirs. Loading is trust-gated per repo (skills.trusted_project_dirs, managed by 'hermes skills trust'/'untrust') because skills are executable procedure documents — auto-sourcing them from any cloned repo is a prompt-injection vector. Untrusted repos with skills get a one-line banner notice instead. - agent/skill_utils.py: find_project_root, get_project_skills_dirs, get_untrusted_project_skills_root, get_scan_ordered_skills_dirs; project dirs join the curator read-only ownership boundary - agent/prompt_builder.py: project tier scanned first, entries tagged [project], same-named local entries shadowed; cache key extended - tools/skills_tool.py: skills_list scans project dirs first (first-wins); skill_view resolves cross-tier collisions in favor of the project tier (same-tier ambiguity still refuses); security warning recognizes the tier - agent/skill_commands.py + hermes_cli/commands.py: /skill-name slash commands and gateway slash menus include project skills - tools/credential_files.py: project dirs mounted into remote backends - cli.py: banner notice (loaded count / trust hint) - hermes_cli/main.py + subcommands/skills.py: hermes skills trust/untrust - config: skills.project_discovery (default on), skills.trusted_project_dirs - docs: Project-Local Skills section in skills.md - tests: tests/agent/test_project_skills.py (18 cases) Session cwd is fixed at agent build time, so the resolved tier is stable for the conversation and the system prompt stays byte-stable (cache-safe).
This commit is contained in:
@@ -8328,6 +8328,35 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
"[dim] Switch with: /model sonnet or /model gpt5[/]"
|
||||
)
|
||||
|
||||
# Project-local skills: one-line status. Trusted → show count;
|
||||
# untrusted-with-skills → point at `hermes skills trust`. Never raises.
|
||||
try:
|
||||
from agent.skill_utils import (
|
||||
get_project_skills_dirs,
|
||||
get_untrusted_project_skills_root,
|
||||
iter_skill_index_files,
|
||||
)
|
||||
_proj_dirs = get_project_skills_dirs()
|
||||
if _proj_dirs:
|
||||
_n = sum(
|
||||
sum(1 for _ in iter_skill_index_files(d, "SKILL.md"))
|
||||
for d in _proj_dirs
|
||||
)
|
||||
if _n:
|
||||
self._console_print(
|
||||
f"[dim]◆ {_n} project skill(s) loaded from this repo[/]"
|
||||
)
|
||||
else:
|
||||
_untrusted = get_untrusted_project_skills_root()
|
||||
if _untrusted is not None:
|
||||
_root, _n = _untrusted
|
||||
self._console_print(
|
||||
f"[yellow]◆ {_n} project skill(s) found in {_root} but not "
|
||||
f"loaded — run `hermes skills trust` to enable them.[/]"
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("project skills banner notice failed", exc_info=True)
|
||||
|
||||
self._console_print()
|
||||
|
||||
def _restore_session_cwd(self, session_meta: dict, *, quiet: bool = False) -> None:
|
||||
|
||||
Reference in New Issue
Block a user