fix: use skill directory name from URL instead of cache-internal "tree" - #2626
Conversation
PR Summary by QodoFix URL skill name resolution by symlinking cache tree to URL basename Description
Diagram
High-Level Assessment
Files changed (2)
|
Site previewPreview: https://26cb7791-site.fullsend-ai.workers.dev Commit: |
|
🤖 Finished Review · ✅ Success · Started 6:42 PM UTC · Completed 6:53 PM UTC |
Code Review by Qodo
1. Stale /tree lock tests
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
ReviewVerdict: Approve The prior review's code-duplication finding is resolved. The latest commit extracts the symlink creation logic into a shared helper The stale-fallback paths in Test coverage is thorough: the helper has direct unit tests (creation, idempotency, reserved-name fallback, concurrent EEXIST tolerance), and integration-level tests cover both cache-hit and fresh-fetch paths in FindingsLow
Prior review (49e7a47): 1 low finding (code-duplication across 3 call sites) → resolved (extracted into shared Previous runReviewVerdict: Approve All prior review findings are resolved. The latest commit addresses the medium-severity cache-hit path inconsistency by applying the same symlink logic to The security sub-agent raised a concern about null-byte truncation bypassing the FindingsLow
Prior review (8ff231c): 1 medium finding (inconsistency — cache-hit path bypassed symlink logic) → resolved. 2 low findings (test-adequacy, code-duplication) → test-adequacy resolved (new test Previous runReviewVerdict: Approve All prior review findings are resolved. The latest commit addresses the medium-severity cache-hit path inconsistency by applying the same symlink logic to The security sub-agent raised a concern about null-byte truncation bypassing the FindingsLow
Prior review (8ff231c): 1 medium finding (inconsistency — cache-hit path bypassed symlink logic) → resolved. 2 low findings (test-adequacy, code-duplication) → test-adequacy resolved (new test Previous run (2)ReviewVerdict: Comment The bug fix correctly addresses URL-resolved skills appearing as However, the fix is incomplete in FindingsMedium
Low
Prior review (735a7ce): 2 low findings (race-condition, error-handling-gap) → both resolved in this revision. Previous runReviewVerdict: Approve FindingsLow
Prior review (c17bf61): 1 low finding (error-handling-gap) → carried (unchanged code). 1 low finding (race-condition) added after challenger downgrade from medium. Previous runReviewVerdict: Approve FindingsLow
Prior review (71dc217): 1 medium finding (path-traversal via missing Previous runReviewFindingsMedium
Labels: Bug fix in skill URL resolution within the harness component. Previous run (3)ReviewVerdict: Comment The bug fix correctly addresses URL-resolved skills appearing as However, the fix is incomplete in FindingsMedium
Low
Prior review (735a7ce): 2 low findings (race-condition, error-handling-gap) → both resolved in this revision. Previous run (4)ReviewVerdict: Approve FindingsLow
Prior review (c17bf61): 1 low finding (error-handling-gap) → carried (unchanged code). 1 low finding (race-condition) added after challenger downgrade from medium. Previous runReviewVerdict: Approve FindingsLow
Prior review (71dc217): 1 medium finding (path-traversal via missing Previous runReviewFindingsMedium
Labels: Bug fix in skill URL resolution within the harness component. Previous run (5)ReviewVerdict: Approve Clean bug fix that resolves URL-resolved skills incorrectly appearing as Test coverage is thorough: fresh fetch, cache hit, FindingsLow
Prior review (c17bf61): 1 low finding (error-handling-gap) → carried (unchanged code). 1 low finding (race-condition) added after challenger downgrade from medium. Previous run (6)ReviewVerdict: Approve FindingsLow
Prior review (71dc217): 1 medium finding (path-traversal via missing Previous runReviewFindingsMedium
Labels: Bug fix in skill URL resolution within the harness component. Previous run (7)ReviewVerdict: Approve Clean bug fix that addresses URL-resolved skills incorrectly appearing as Test coverage is good: fresh fetch, cache hit, FindingsLow
Prior review (71dc217): 1 medium finding (path-traversal via missing Previous run (8)ReviewFindingsMedium
Labels: Bug fix in skill URL resolution within the harness component. |
When resolving URL-based skills, the cache stores content under a "tree/" subdirectory. The resolved path was returned as-is, so filepath.Base() returned "tree" instead of the actual skill name (e.g., "architecture"). This caused the skill to be uploaded to the sandbox as "tree" and logged with the wrong name. With multiple URL skills, they'd collide on the same "tree" directory name. Fix by creating a symlink named after the skill directory (from the URL path) alongside the "tree" directory in the cache, and returning that path instead. This is idempotent across cache hits. Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
…rsal The skillName guard already rejected "" and "." but not "..". If forgeInfo.Path ended with "..", filepath.Base would return ".." and the symlink would escape the cache directory. Fall back to "tree" in that case, same as the other special names. Signed-off-by: Ralph Bean <rbean@redhat.com> Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
71dc217 to
c17bf61
Compare
|
🤖 Finished Review · ✅ Success · Started 8:26 PM UTC · Completed 8:36 PM UTC |
waynesun09
left a comment
There was a problem hiding this comment.
Review squad findings (medium+ only; see full report for low-severity items and previously-addressed concerns).
[HIGH] Symlink naming fix not applied to base-composed harness skills — internal/harness/compose.go's fetchBaseSkillDir (line 957) independently resolves a URL-referenced skill directory and returns the raw treePath (ending in /tree), assigned directly to base.Skills[i] at line 727, with no renaming logic. This file isn't part of this PR's diff so I couldn't leave an inline comment on the exact line — flagging here instead. Any harness using base: composition with a URL-resolved skill will still show up as "tree" in the sandbox/logs, the same bug this PR fixes via a different code path.
The MEDIUM finding (reserved metadata.json filename collision) is posted inline on internal/resolve/resolve.go.
Add "metadata.json" to the reserved-name fallback list so that a skill URL ending in that segment does not collide with the cache-internal metadata.json file. Signed-off-by: Ralph Bean <rbean@redhat.com> Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 6:15 PM UTC · Completed 6:25 PM UTC |
|
Following up on the HIGH finding from the earlier squad review ( Suggested fix, if you'd like to fold it into this PR: 1. Extract a shared helper, e.g. in // NamedSkillPath returns treePath renamed to a symlink using rawName as the
// basename (falling back to treePath unchanged for reserved/unsafe names),
// creating the symlink idempotently if it doesn't already exist.
func NamedSkillPath(treePath, rawName string) (string, error) {
skillName := filepath.Base(rawName)
switch skillName {
case "", ".", "..", "tree", "metadata.json":
return treePath, nil
}
namedPath := filepath.Join(filepath.Dir(treePath), skillName)
if _, err := os.Lstat(namedPath); os.IsNotExist(err) {
if err := os.Symlink("tree", namedPath); err != nil && !os.IsExist(err) {
return "", fmt.Errorf("creating named symlink: %w", err)
}
} else if err != nil {
return "", fmt.Errorf("checking named symlink: %w", err)
}
return namedPath, nil
}(this also folds in the two LOW findings from the review — tolerating 2. Call it from treePath, err = fetch.NamedSkillPath(treePath, forgeInfo.Path)
if err != nil {
return Dependency{}, "", fmt.Errorf("%s: %w", field, err)
}3. Call it from
Both already have the URL path in scope ( Happy to see this land in this PR since it's the same root cause, or as a fast-follow if you'd rather keep this one scoped — your call given you own the tradeoff on merge velocity vs. completeness here. |
Address review feedback on PR #2626: - Handle TOCTOU race in Lstat-then-Symlink by treating os.IsExist as success when a concurrent process already created the symlink. - Return errors from Lstat when the failure is not IsNotExist, rather than silently proceeding with a potentially invalid path. - Apply the same skill directory naming symlink logic to fetchBaseSkillDir in compose.go, which was returning raw /tree paths for base-composed harness skills. Signed-off-by: Ralph Bean <rbean@redhat.com> Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
|
🤖 Review · ❌ Terminated · Started 7:14 PM UTC · Ended 7:28 PM UTC |
|
Nice catch on the compose path, @waynesun09. Applied the same symlink naming in fetchBaseSkillDir in 8ff231c — also incorporated the TOCTOU and Lstat error handling improvements from the bot feedback while I was at it. |
|
🤖 Finished Review · ❌ Failure · Started 7:14 PM UTC · Completed 7:28 PM UTC |
waynesun09
left a comment
There was a problem hiding this comment.
Confirmed the resolve.go fixes (path-traversal, TOCTOU, Lstat error handling) are solid in this revision. One remaining gap found — see inline comment. Not approving until the cache-hit path in fetchBaseSkill gets the same treatment, since it's the code path that actually runs on every re-resolution after the first fetch.
The cache-hit branch in fetchBaseSkill returned the raw treePath from CacheGetDir (ending in /tree) without applying the symlink renaming that fetchBaseSkillDir applies on cache miss. This meant any base:-composed harness with a URL-resolved skill that was already cached would still surface as "tree" in sandbox/logs. Apply the same Lstat/Symlink block to the cache-hit path so both code paths return consistent skill directory names. Signed-off-by: Ralph Bean <rbean@redhat.com> Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 11:42 AM UTC · Completed 11:54 AM UTC |
waynesun09
left a comment
There was a problem hiding this comment.
Approving — the fix is well-scoped (no scope creep) and the commit history shows good iterative hardening (path traversal, metadata.json collision). Left a few non-blocking findings inline plus one general note:
- The core fix's premise — that
openshell sandbox uploadfollows directory symlinks when uploading — isn't verified anywhere in this diff. If it doesn't resolve symlinks, the original bug may not actually be fixed by the sandbox-upload path. Similarly,resolveSkillDisplayNameininternal/runtime/claude.goonly falls back tofilepath.BasewhenSKILL.mdfrontmatter is absent, so the "logging sees the correct name" claim in the commit message is only exercised in that fallback case, and it isn't covered by a new test. Worth a quick manual/integration check that the symlink is actually followed end-to-end, even if it lands as a fast-follow rather than blocking this PR.
None of this blocks merging — flagging for awareness and possible fast-follow.
The reserved-name guard and Lstat/Symlink block was duplicated verbatim in three places (resolve.go, compose.go cache-hit, compose.go fresh-fetch). Extract to fetch.CacheNamedSymlink so future guard additions only need one change. Also adds a missing filepath.Base assertion to TestFetchBaseSkill_FullDirectory. Signed-off-by: Ralph Bean <rbean@redhat.com> Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 5:06 PM UTC · Completed 5:15 PM UTC |
|
🤖 Finished Retro · ✅ Success · Started 6:42 PM UTC · Completed 6:51 PM UTC |
|
PR #2626 fixed a bug where URL-resolved skills were named 'tree' instead of their actual name. The fix required 6 commits over 19 days and 7 review runs due to findings surfacing incrementally. The review bot caught path-traversal in the first review but missed three critical cross-file gaps caught only by human reviewer waynesun09: metadata.json collision, compose.go cache-miss bypass, and cache-hit bypass. One review run failed with a 422 post-review error. The human also drove the code duplication extraction. These patterns strengthen existing tracked issues: #1525 (cross-file impact analysis), #1582 (first-pass completeness), and #2569 (post-review 422 regression). Evidence notes (not filed as issues)
|
Summary
tree/subdirectory andfilepath.Base()picked that uptreein the cache, so all downstream consumers (sandbox upload, logging) see the correct nameDiscovered via https://github.com/konflux-ci/refinement/actions/runs/28116641395/job/83257718265 where the
architectureskill showed up asSkill "tree": uploaded to sandbox.Test plan
TestResolveHarness_SkillDirFetchAndCache(fresh fetch)TestResolveHarness_SkillDirCacheHit(cache hit path)TestResolveHarness_MultipleSkills(two URL skills get distinct names)internal/runtimeandinternal/fetchsvctests pass🤖 Generated with Claude Code