Repository navigation
fix(skills): follow symlinks in skill management - #67748
americodias wants to merge 1 commit into
Conversation
ae4b910 to
7058706
Compare
7058706 to
719bf5b
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused symlink-lookup work. The underlying defect is still present on current main: tools/skill_manager_tool.py:638 and :702 use Path.rglob("SKILL.md"), while the established reader-side iterator uses os.walk(..., followlinks=True) in agent/skill_utils.py:821.
Problems
- The PR also includes commit
8468aaca446889653029092800de894d0f937da3, which adds the unrelated--oneshot-fileCLI surface inhermes_cli/_parser.py:116,hermes_cli/main.py, andhermes_cli/oneshot.py. Please keep this PR to the stated skill-management fix. - If proposed separately, the new helper's “without following symlinks” claim is not true where
os.O_NOFOLLOWis unavailable:hermes_cli/oneshot.py:49-56then opens and validates the symlink target, andtests/hermes_cli/test_oneshot_file.py:45-47deliberately does not assert rejection on that path.
Suggested changes
- Split out
8468aaca; the first commit is a narrow, relevant salvage candidate. - Make the separate file-input feature's symlink behavior consistent across supported platforms, or document its platform limitation and test it.
Automated hermes-sweeper review.
| @@ -112,6 +113,17 @@ def build_top_level_parser(): | |||
| "auto-bypassed. Intended for scripts / pipes." | |||
| ), | |||
| ) | |||
| oneshot_group.add_argument( | |||
There was a problem hiding this comment.
This introduces a new CLI feature unrelated to symlinked skill management. Please move the --oneshot-file implementation and its tests into a separate PR so this fix remains the narrow traversal change described in the PR body.
| flags = os.O_RDONLY | ||
| if hasattr(os, "O_CLOEXEC"): | ||
| flags |= os.O_CLOEXEC | ||
| if hasattr(os, "O_NOFOLLOW"): |
There was a problem hiding this comment.
When os.O_NOFOLLOW is unavailable, the subsequent os.open() follows a symlink and fstat() validates the target, contradicting this helper's “without following symlinks” contract. Reject links consistently on supported platforms, or narrow the contract and add coverage for this fallback.
8468aac to
719bf5b
Compare
|
Split out 8468aaca4 as requested — this PR now carries only the symlink-lookup fix (719bf5b). The oneshot-file feature will be proposed separately, with the O_NOFOLLOW portability concern addressed (consistent behavior or a documented, tested platform limitation). |
Summary35 PRs address or reference this issue complex: configurable skill write roots, external-directory mutation, and the recurring mismatch where symlink-aware readers can see skills that rglob-based management and auxiliary paths cannot. The diffs range from already-merged external-dir and reader-discovery fixes through competing skill-manager iterator substitutions; #67748 is the recorded best fix for the remaining management lookup defect across #4759, #35184, #44609, and #54195. Related pull requests
DuplicatesManager-lookup duplicates: #35213, #35244, #35299, #44210, #54200, #56420, #56423, #60960, #67748, and #75225, with #67748 the focused superseding implementation for the core issues. Reader-discovery lineage: #8357, #8769, #9253, #11847, #12624, #14230, #14476, #14668, and #14740; configurable-write lineage: #8177, #13996, #21812, #22236, and #23562. Suggested consolidationKeep #67748 open with the salvage path of validating its now-focused head and closing #35213, #35244, #35299, #44210, #54200, #56423, and #60960 as duplicates; this does not disregard their keep_open reviews because #67748 carries the requested shared iterator, platform-safe setup, and cross-profile regression while the unrelated commit was removed. Keep #8177 open for the distinct configurable-write-root feature, and keep #75225 open for author action to add the requested manager-level patch regression and document retained delete refusal before deciding whether its #75130-specific framing should collapse into #67748. Complex graphflowchart TD
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I4759(["issue #4759 (closed)"])
I35184(["issue #35184 (open)"])
I44609(["issue #44609 (closed)"])
I54195(["issue #54195 (open)"])
subgraph Dup35213 ["PRs duplicating each other"]
P35213["PR #35213 (open)"]
P35244["PR #35244 (open)"]
P44210["PR #44210 (open)"]
P54200["PR #54200 (closed)"]
P56420["PR #56420 (closed)"]
P56423["PR #56423 (open)"]
P60960["PR #60960 (open)"]
P67748["PR #67748 (open)"]
P75225["PR #75225 (open)"]
end
P67748 -->|best fix| I4759
P67748 -->|best fix| I35184
P67748 -->|best fix| I44609
P67748 -->|best fix| I54195
class I4759 closed
class I35184 open
class I44609 closed
class I54195 open
class P35213 open
class P35244 open
class P44210 open
class P54200 closed
class P56420 closed
class P56423 open
class P60960 open
class P67748 open
class P75225 open
class P67748 best
class P67748 best
class P67748 best
class P67748 best
class P75225 best
class P67748 target
click I4759 "https://github.com/NousResearch/hermes-agent/issues/4759"
click I35184 "https://github.com/NousResearch/hermes-agent/issues/35184"
click I44609 "https://github.com/NousResearch/hermes-agent/issues/44609"
click I54195 "https://github.com/NousResearch/hermes-agent/issues/54195"
click P35213 "https://github.com/NousResearch/hermes-agent/pull/35213"
click P35244 "https://github.com/NousResearch/hermes-agent/pull/35244"
click P44210 "https://github.com/NousResearch/hermes-agent/pull/44210"
click P54200 "https://github.com/NousResearch/hermes-agent/pull/54200"
click P56420 "https://github.com/NousResearch/hermes-agent/pull/56420"
click P56423 "https://github.com/NousResearch/hermes-agent/pull/56423"
click P60960 "https://github.com/NousResearch/hermes-agent/pull/60960"
click P67748 "https://github.com/NousResearch/hermes-agent/pull/67748"
click P75225 "https://github.com/NousResearch/hermes-agent/pull/75225"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 35 pull requests and 9 issues in this complex. Each diff was read against this issue; Assessment working set: 438 kB of PR diffs, 102 kB of issue/PR text, 46 kB of discussion (81 comments), 69 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
|
I reproduced this lookup approach against current
I built and independently reviewed a current-main fix that keeps the lexical alias for discovery, resolves the target only for batch snapshot/restore, and checks normalized lexical containment before resolved containment for external ownership. Public
Verification: focused 4/4 passed; relevant skill-manager/discovery/ownership suite 168/168 passed; independent read-only review PASS with no findings. Reviewed diff SHA-256: I have not pushed a competing branch or changed this PR. Happy to provide the reviewed four-file patch if useful; the important part is that the lookup fix lands together with the rollback and ownership safeguards. |
|
The reviewed patch offered above is now a current- |
What does this PR do?
skill_manageusedPath.rglob("SKILL.md")in_find_skill()and_find_skill_in_other_profiles(). Unlike the sharediter_skill_index_files()helper,Path.rglob()does not follow directory symlinks. A symlinked skill could therefore appear inskills_listand load throughskill_view, butskill_managereported it as missing.This PR routes both skill-management lookups through the existing shared iterator. The change is deliberately narrow: it keeps current matching, exclusion, precedence, and mutation behavior, while making traversal consistent with the rest of the skills system.
This supersedes #35244 by preserving its minimal approach and adding the two items requested in maintainer review: platform-safe symlink setup and regression coverage for
_find_skill_in_other_profiles(). Credit to @Linux2010 for #35244 and @liuhao1024 for the related #54200 implementation.Related Issue
Fixes #35184
Closes #54195
Supersedes #35244
Type of Change
Changes Made
tools/skill_manager_tool.pyrglob()calls withiter_skill_index_files()tests/tools/test_skill_manager_tool.py_find_skill()discovers a skill installed as a directory symlink in an external skills root_find_skill_in_other_profiles()discovers a skill below a symlinked category in another profileHow to Test
Configure an external skills directory containing a directory symlink to a real skill package.
Call
skill_manage(action="patch", name="<skill>", old_string="__DOES_NOT_EXIST__", new_string="x")and confirm lookup reaches content matching instead of returningSkill '<skill>' not found.Run:
scripts/run_tests.sh tests/tools/test_skill_manager_tool.py -q python3 scripts/check-windows-footguns.py --diff origin/main uv run --extra dev ruff check .Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — focused canonical suite passes; full-suite green is not claimedDocumentation & Housekeeping
docs/, docstrings) — N/A; no public API or configuration changecli-config.yaml.exampleif I added/changed config keys — N/A; no config keys changedCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A; existing shared iterator is reusedScreenshots / Logs