fix: resolve relative plugin paths from URL-sourced harnesses - #5978
Conversation
PR Summary by QodoFix plugin resolution for URL-sourced harness bases via cached plugin directories
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
|
🤖 Review · |
|
The agents repo harness ( |
Code Review by Qodo
1.
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
waynesun09
left a comment
There was a problem hiding this comment.
Review sweep findings (2 new items; 1 additional candidate finding was a duplicate of an existing bot review comment on the same lines and was skipped).
|
🤖 Review · |
|
🤖 Review · |
|
🤖 Finished Review · ✅ Success · Started 1:44 PM UTC · Completed 1:59 PM UTC |
waynesun09
left a comment
There was a problem hiding this comment.
Review sweep findings (2 new items, both MEDIUM). Checked against existing review comments on this PR (bot chmod findings, prior sweep's test-coverage and chmod-duplication findings) — no overlap found.
ReviewFindingsLow
Previous runReviewFindingsLow
Previous run (2)ReviewFindingsMedium
Low
Previous run (3)ReviewFindingsMedium
Low
Previous run (4)ReviewFindingsLow
Previous run (5)ReviewFindingsLow
Labels: PR modifies harness composition internals (compose.go, harness.go) and is a bug fix for plugin path resolution |
|
🤖 Finished Review · ✅ Success · Started 2:20 PM UTC · Completed 2:37 PM UTC |
|
🤖 Review · ❌ Terminated · Started 2:46 PM UTC · Ended 3:03 PM UTC |
Local end-to-end verificationBuilt the PR branch locally and ran the Setup
Baseline repro (main, pre-fix) — bug confirmedRelative plugin path resolved against the local Fix run (PR branch) — Checkpoint A PASS
Checkpoint B (full agent run)Blocked by pre-script: issue #5954 already has an open PR (#5956), so the pre-script correctly skipped the agent run. This is a legitimate guard, not a fix failure. ConclusionThe fix conclusively resolves #5977. |
waynesun09
left a comment
There was a problem hiding this comment.
Review sweep finding (1 new item, HIGH). Checked against all 14 existing review comments and 8 prior reviews on this PR (bot chmod/coverage/duplication findings, and this account's two prior sweep passes) — no overlap; this traces a new interaction between the PR's new base-plugin URL format and the existing (unmodified) lock-file resolution path.
|
🤖 Finished Review · ✅ Success · Started 2:46 PM UTC · Completed 3:03 PM UTC |
|
🤖 Finished Review · ✅ Success · Started 3:09 PM UTC · Completed 3:30 PM UTC |
waynesun09
left a comment
There was a problem hiding this comment.
Review sweep findings (2 new items: 1 HIGH, 1 MEDIUM). Checked against all 17 existing review comments and 11 reviews on this PR — no overlap found (existing comments cover the plugins[] fix, chmod issues, and doc-comment nits, but not the skills[] lock-resolution gap or the resolveBasePlugins variable shadowing).
waynesun09
left a comment
There was a problem hiding this comment.
Approved after multi-agent review (3 agents: 2 Claude, 1 Grok) and local end-to-end verification.
Local verification:
- Bug reproduced on main:
validating files: plugins[0]: stat .../plugins/gopls-lsp: no such file or directory - Fix verified on PR branch: plugin fetched from URL source → absolute cache path, no file validation error, harness loaded successfully
- Lock resolution fix (df9c9a7) verified with reproducing test:
lockTreeDirNamecorrectly strips marker files from raw-content URLs for both plugins and skills
Review squad result: 0 critical, 0 high, 0 medium. 3 low findings (filepath.Base/path.Base mixing in lockTreeDirName, stale-fallback guard asymmetry, ChmodPluginDir godoc missing cache-mutation warning) — all non-blocking. Structural duplication deferred to #5982.
All prior HIGH/MEDIUM findings from 5 review rounds resolved in subsequent commits.
|
🤖 Finished Review · ✅ Success · Started 6:12 PM UTC · Completed 6:25 PM UTC |
resolveBaseResources in compose.go fetches agent, policy, and skills from the base URL when a harness is loaded from a remote source, but it did not handle plugins. Relative plugin paths (e.g. plugins/gopls-lsp) passed through to ResolveRelativeTo, which resolved them against the target repo's .fullsend directory where they do not exist, causing ValidateFilesExist to fail. Add resolveBasePlugins (with fetchBasePlugin/fetchBasePluginDir) that fetches plugin directories from the base URL using plugin.json as the marker file, matching the pattern used for skills with SKILL.md. Call it from all three resolution sites in LoadWithBase: the no-base SourceURL path, the post-merge SourceURL path, and the URL base path in loadBaseChain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hector Martinez <hemartin@redhat.com>
Propagate the filepath.Walk error from the plugin chmod step instead of discarding it, matching chmodPluginDir in resolve.go. Add plugins to the resource type list in the SourceURL resolution comment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hector Martinez <hemartin@redhat.com>
Add ChmodPluginDir calls to the three cache-hit return paths in fetchBasePlugin (fresh hit, offline stale fallback, transient-error stale fallback) so cached plugin files are executable after resolution. Extract the chmod-walk logic into an exported ChmodPluginDir helper in harness.go and have both compose.go and resolve.go delegate to it, removing duplication. Add fetchBasePluginDir success-path tests using fakeTreeFetcher: full directory fetch with chmod verification, missing plugin.json rejection, and fetch error propagation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hector Martinez <hemartin@redhat.com>
Add tests for fetchBasePlugin covering: allowlist rejection, fresh fetch (no cache), full cache hit, stale cache invalidation with re-fetch, stale cache served in offline mode, transient error fallback to stale cache, non-transient error propagation, offline with no cache, partial index hit triggering re-fetch via TreeFetcher, and invalid base URL in resolveBasePlugins. Fix stale comment at line 194 to list all resolved resource types (was missing profiles and providers). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hector Martinez <hemartin@redhat.com>
Remove unreachable offline-stale-fallback path in fetchBasePlugin (staleFallback is only set when Offline=false, so the Offline=true guard can never see it non-nil). Add tests for: resolveBasePlugins skip/reject logic, fetchBasePluginDir allowlist and token error paths, ChmodPluginDir symlink error, LoadWithBase plugin resolution through SourceURL and chained URL bases, and post-merge plugin path validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hector Martinez <hemartin@redhat.com>
Add ValidPluginBasename check after path validation so names with spaces or special characters are rejected early. Clean up two doc comments for accuracy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hector Martinez <hemartin@redhat.com>
Base-composed plugins store Dependency.URL as a raw.githubusercontent.com path ending in /plugin.json. resolveFromLock's plugin branch called ParseForgeURL (which only accepts github.com/gitlab.com hosts), failed, left dirName as "plugin.json", and ValidPluginBasename rejected the dot. This silently bypassed the lock file's pinned-content guarantee for every base-composed plugin entry. Fall back to ParseRawContentURL when ParseForgeURL fails, strip the marker file via path.Dir, and take filepath.Base of the parent directory to get the plugin name (e.g. "gopls-lsp"). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hector Martinez <hemartin@redhat.com>
Base-composed skills record their lock Dependency.URL as the raw.githubusercontent.com URL of the SKILL.md marker file (see fetchBaseSkill), but resolveFromLock derived the cached directory name with path.Base(URL), yielding a directory literally named "SKILL.md". That basename becomes the sandbox upload directory name in the Claude runtime, so a base-composed skill resolved from the lock file was uploaded under "SKILL.md" instead of its real slug, and two such skills collided on the duplicate-name check. Extract the plugins[ marker-stripping logic added in d61055a into a shared lockTreeDirName helper and apply it to skills[ and plugins[ entries. Only SKILL.md and plugin.json are treated as marker files; any other raw-content URL keeps its last segment as the directory name, so the helper cannot mis-derive a parent directory name if a directory-shaped URL is ever recorded. Forge-scoped base skills (forge.<platform>.skills[N], same producer and URL shape) previously bypassed the naming logic entirely and fell into the mutation switch's default case, appending a duplicate skill under the cache's internal "tree" name — reproducing the same sandbox-name collision one field shape over. Route them through the shared helper via isTreeLockField and add an explicit no-op mutation case, since ResolveForge already merged the correctly named path into h.Skills during LoadWithBase. Validate() now also rejects repo-root skill URLs, matching the existing plugins[ check. Without it, the helper's repo-root error was only reachable at lock-resolution time, where run falls back to network resolution and silently bypasses the lock's pinned-content guarantee for a URL shape that fullsend lock happily accepted. Also rename a local variable in resolveBasePlugins that shadowed the base *Harness parameter. Assisted-by: Claude (review, fix), Grok (review) Signed-off-by: Wayne Sun <gsun@redhat.com>
fbc5536 to
78f54ed
Compare
|
🤖 Finished Review · ✅ Success · Started 6:14 AM UTC · Completed 6:27 AM UTC |
|
🤖 Finished Retro · ✅ Success · Started 6:53 AM UTC · Completed 7:18 AM UTC |
Retro: PR #5978 — fix: resolve relative plugin paths from URL-sourced harnessesTimelinerh-hemartin opened issue #5977 and PR #5978 simultaneously (Aug 6 13:11 UTC), fixing a bug where Key review sequence:
What went well
Gaps identified
Proposals filed |
Summary
resolveBaseResourcesincompose.gofetchesagent,policy, andskillsfrom the base URL when a harness is loaded from a remote source, but skipspluginsplugins/gopls-lsp) pass through toResolveRelativeTo, which resolves them against the target repo's.fullsenddirectory where they do not exist, causingValidateFilesExistto failresolveBasePlugins(withfetchBasePlugin/fetchBasePluginDir) usingplugin.jsonas the marker file, and call it from all three resolution sites inLoadWithBaseFailing runs on v0.34.0
Test plan
TestLoadWithBase_URLBase_PluginFetchedAsDir-- forge URL parse path is hitTestLoadWithBase_URLBase_PluginOfflineCacheHit-- plugins resolve from cache in offline modeTestLoadWithBase_SourceURL_Plugins-- SourceURL no-base path resolves pluginsTestLoadWithBase_SourceURL_PluginPassesValidateFilesExist-- regression test:ResolveRelativeTo+ValidateFilesExistpasses when plugin is resolved to cache path-raceCloses #5977