feat(skills): declare and enforce skill-to-skill dependencies (depends_on) - #75782
feat(skills): declare and enforce skill-to-skill dependencies (depends_on)#75782mehmetkr-31 wants to merge 1 commit into
Conversation
|
CI note: the single red job is not this PR. The only failing test is |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for adding a focused install-time dependency declaration and an end-to-end install-path test. The premise is real: current do_install moves from the security-policy check at hermes_cli/skills_hub.py:691-701 directly to confirmation at :708-710, without dependency enforcement.
Problems
installed_skill_names()in the PR (tools/skills_hub.py:3373) only scans_skills_dir()and recordsskill_md.parent.name. Hermes treats configuredskills.external_dirsas active skill roots (agent/skill_utils.py:566-574;tools/skills_tool.py:696-721), so an externally supplied required dependency would be falsely reported missing.- Runtime discovery uses
frontmatter.namebefore falling back to the directory name (tools/skills_tool.py:737). A hand-placed skill in a differently named directory is therefore usable at runtime but absent from this gate. The existingtools/skills_sync.py:84-110indexes external skills by both forms.
Suggested changes
- Enumerate the same local and external roots as runtime discovery, and index parsed frontmatter names with a directory-name fallback.
- Add tests for an external-root dependency and a frontmatter-name/directory-name mismatch.
Automated hermes-sweeper review.
| try: | ||
| for skill_md in _skills_dir().rglob("SKILL.md"): | ||
| parent = skill_md.parent | ||
| if any(part.startswith(".") for part in parent.parts): |
There was a problem hiding this comment.
This only scans the active profile root. Runtime skill discovery also scans configured skills.external_dirs (agent/skill_utils.py:566-574, tools/skills_tool.py:696-721), so a usable dependency installed there is falsely blocked. Please enumerate the same roots and parse each SKILL.md's declared name as well; runtime discovery treats that name as canonical (tools/skills_tool.py:737).
|
Both correct — the gate was checking a narrower notion of "installed" than the agent's own, which is the wrong direction for a check that blocks. External roots
Declared name vs directory nameAlso right. Verification
Both cases you asked for have their own tests, plus one for a root that doesn't exist (a stale
Separately: the one red CI job is |
333af6e to
fb4ae34
Compare
|
I built on top of this PR and implemented the remaining features requested in #71853:
32 tests all green, including new coverage for transitive resolution, cycle detection, max-depth capping, --with-optional, and auto-install behavior. Would you prefer I open a separate PR that supersedes this one with the full implementation, or should I wait for this to land first and then follow up? Branch: https://github.com/chancelu/hermes-agent/tree/pr75782 |
…s_on) Skills carry `prerequisites` (env vars, commands) and `related_skills` (advisory), but neither says "this skill does not work without that one", and nothing was enforced at install time (NousResearch#71853). A skill that drives another skill's commands installed cleanly on its own and only failed once the agent reached for the missing piece. Adds a `depends_on` front-matter field, accepted either as a bare list (all required) or as mappings with `required` and `reason`. `hermes skill install` resolves it after the security verdict and before the confirm prompt, so a blocked install never reaches "Install 'x'?" and the user is told what to install first. Required blocks, optional warns and proceeds, --force downgrades the block the same way it already overrides the security verdict. Installed-ness has to agree with what the agent can actually load, or the gate blocks a dependency the user genuinely has. It is resolved from the hub lockfile plus every root get_all_skills_dirs() returns -- the local skills dir AND the configured skills.external_dirs, which runtime discovery treats as active skill roots -- and each SKILL.md contributes both its directory name and its declared `name:`, because tools/skills_tool.py resolves a skill as frontmatter.get("name", skill_dir.name) and tools/skills_sync.py indexes both forms for the same reason. Malformed entries are skipped rather than raised: a bad `depends_on` must not make an otherwise installable skill uninstallable. Auto-installing missing dependencies is deliberately out of scope -- it would run the security scan and confirmation flow recursively, which deserves its own review. The error names the exact command instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fb4ae34 to
531e0c3
Compare
|
@chancelu Thanks for picking this up — and for carrying my commit unsquashed rather than rewriting it. I pulled your branch and reviewed it properly. Supersede this PR, for a reason specific to this repo, but three things to fix first. Answer to your question: supersede, don't sequenceI measured the merge dynamics here recently: of the last 120 merged PRs, 120 merged within 72 hours, median 30 minutes, and none took more than a week. There are ~17.9k open PRs, ~88% of them older than a week. So "land mine, then follow up" means your work waits on a PR that is already outside the window that actually merges — both age out. One complete PR answering all of #71853 is the better target, and the salvage-with-authorship-preserved shape you've used is exactly what landed for #75345. 1.
|
|
Superseded by #76021, which carries this commit unsquashed and completes the rest of #71853 (transitive resolution, I reviewed that PR and confirmed it fixes the two blocking issues I raised — Closing here so there's a single target for the issue. |
…ptional, --auto-install Supersedes PR NousResearch#75782. ### What's new vs NousResearch#75782 - **Recursive dependency resolution** (_resolve_transitive): - Walks the full dependency graph (A → B → C reports C too) - Detects cycles and caps depth at 10 - Reads SKILL.md from disk for installed skills; attempts source lookup for missing ones - **--with-optional CLI flag** (both hermes skills install and /skills): - Treats optional dependencies as required - **--auto-install CLI flag**: - Automatically installs missing transitive dependencies in topological order (deepest first), each going through quarantine + scan - Default is False (opt-in) - **Merged front-matter parsers**: - parse_skill_frontmatter now accepts str | bytes | Path - Removed duplicate ead_skill_frontmatter ### Design note: skip_confirm & third-party disclaimers When --auto-install is used, recursive dependency installs set skip_confirm=True to avoid hanging in TUI/gateway contexts. This skips the per-skill third-party disclaimer panel. The security floor still holds (quarantine + scan runs for every skill, blocked verdicts still block), but users won't see the disclaimer for auto-installed deps. This is intentional — a single upfront prompt listing the whole resolved set is the alternative, but it's not implemented yet. See discussion in NousResearch#75782. Co-authored-by: mehmetkr-31 <mehmetkr-31@users.noreply.github.com>
Closes #71853.
Skills carry two kinds of prerequisite metadata today —
prerequisites(env vars, commands) andrelated_skills(advisory cross-references) — but neither says "this skill does not work without that one", and nothing was enforced at install time. A skill that drives another skill's commands installs cleanly on its own; the failure surfaces only when the agent reaches for the missing piece, or the skill silently runs degraded.The field
The gate
hermes skill installresolves it after the security verdict and before the confirmation prompt, so a blocked install never reaches "Install 'x'?" and the user is told what to install first rather than discovering it at runtime:Required blocks, optional warns and proceeds,
--forcedowngrades the block — matching how--forcealready overrides the security verdict.Two details worth review
Installed-ness is read from the lockfile and from disk. The hub lockfile only knows hub installs, but official skills shipped with Hermes and hand-placed directories are equally installed from the agent's point of view. A dependency the user genuinely has must never be reported missing, so
installed_skill_names()unions both (skipping.hub/.trash).Malformed entries are skipped, never raised. A bad
depends_onmust not make an otherwise installable skill uninstallable, so junk list entries and unparseable front-matter fall through to today's behaviour.Verification — each piece verified to fail when removed
do_installcall siteTestDoInstallActuallyCallsTheGateexists specifically because every other test calls_check_skill_dependenciesdirectly and would pass with the call site deleted — the feature would ship declared and unenforced, which is the exact thing the issue is about. It drives the realdo_installover a loopback-served skill, reusing the scaffolding fromtests/tools/test_skill_bundle_provenance.py.tests/tools -k skill: 14 failures on this branch, 14 on a pristineorigin/mainworktree at the same base — identical set.tests/hermes_cli -k skill: 104 passed.Deliberately out of scope
Auto-installing missing dependencies would mean running the security scan and confirmation flow recursively, which deserves its own review. The error names the exact command instead. Happy to follow up with
--with-optionaland prompted auto-install if you want that shape.