feat(compose): resolve local profiles and providers in base harness (#5240)#5461
feat(compose): resolve local profiles and providers in base harness (#5240)#5461maruiz93 wants to merge 7 commits into
Conversation
PR Summary by QodoResolve local profiles/providers when composing and running base harnesses
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
|
🤖 Review · |
84d4a79 to
1daf9b1
Compare
|
🤖 Finished Review · ✅ Success · Started 1:43 PM UTC · Completed 2:01 PM UTC |
Site previewPreview: https://ab98e092-site.fullsend-ai.workers.dev Commit: |
Code Review by Qodo
1.
|
ReviewFindingsMedium
Low
Previous runReviewFindingsHigh
Medium
Low
Labels: PR modifies harness composition and resolve logic (internal/harness/, internal/resolve/, internal/cli/) |
|
Didn't read anything yet: make sure you allow for overriding by the same name. We detected that problem with skills, in which if you wanted to override an existing skill you got an error. |
Already handled — child entries override base entries by name/ID using last-wins semantics:
Tests covering the compose override scenario: |
|
🤖 Finished Review · ❌ Failure · Started 2:45 PM UTC · Completed 3:26 PM UTC |
Extend base composition to resolve local profile and provider paths relative to the base URL, matching existing behavior for agent, policy, skills, scripts, and host_files. - Add resolveBaseProfiles() to fetch and cache profiles from URL bases - Add resolveBaseProviders() to fetch and cache providers from URL bases - Wire both functions into loadBaseChain and LoadWithBase SourceURL path - Update validation to allow local profile paths (not just URLs) - Update provider validation to skip path validation for file paths - Fix HasRemoteResources to check profile URLs individually - Add comprehensive tests for profile and provider resolution Issue #5240 Signed-off-by: Marta Anon <manon@redhat.com>
Add profiles and providers to relative path resolution. Bare provider names (without "/" or .yaml/.yml suffix) are left unchanged. Issue #5240 Signed-off-by: Marta Anon <manon@redhat.com>
Update ResolveHarness to resolve local profile paths (reading content and extracting ID) and absolute-path providers (parsing ProviderDef). Bare provider names are kept for LoadProviderDefs. Issue #5240. Signed-off-by: Marta Anon <manon@redhat.com>
Fix two issues in harness resolution: 1. Local-only profiles were silently dropped when ResolveHarness wasn't called. Added a post-URL-references block in run.go to call ResolveHarness for local profiles/providers even when no URL references exist. Added hasLocalProviders helper to detect absolute path providers. 2. resolveBaseProviders in compose.go lacked bare-name heuristic, which could cause 404s when URL-fetched bases had bare provider names like "fullsend-github". Added skip logic matching the heuristic in ResolveRelativeTo (no "/" and no .yaml/.yml extension). Added TestLoadWithBase_URLBase_BareProviderNameSkipped to verify bare provider names are preserved while relative paths are fetched. Signed-off-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Marta Anon <manon@redhat.com>
The local-only ResolveHarness fallback was gated on len(result.Profiles)==0, skipping it when the lock-file already produced profiles, leaving absolute-path providers unresolved. - Remove the profile-count guard; run whenever local profiles or providers exist, merge all result fields - Strip absolute-path provider entries in resolveFromLock - Preserve local-path profiles in resolveFromLock instead of unconditionally niling them Signed-off-by: Marta Anon <manon@redhat.com>
Signed-off-by: Marta Anon <manon@redhat.com>
waynesun09
left a comment
There was a problem hiding this comment.
Multi-agent review — MEDIUM+ findings
Parallel review (Claude + Gemini agents). Posting only MEDIUM-and-above findings that are not already covered by existing comments. The already-flagged local providers dropped in the lock path issue (qodo bot at run.go:481) is still unresolved — note that the mechanism it describes (gated by len(result.Profiles)==0) no longer matches the current code, but the underlying bug persists in the reworked gate; see the inline notes below for the current form.
Inline comments cover: duplicate base-composed profiles (High), the Accepted-ADR rewrite policy violation (High), the untested merge design (Medium), and the hasLocalProviders absolute-path assumption (Medium).
[MEDIUM] test-coverage — No internal/cli test for the lock ↔ second-pass merge seam
The added tests exercise ResolveHarness, IsProviderPath, parseProviderDef, and ValidateFilesExist in isolation, but the diff touches no internal/cli/run_test.go or internal/cli/lock_test.go. The correctness bugs in the inline comments all live specifically in the resolveFromLock → hasLocalProviders → merge composition, which has zero coverage. Please add internal/cli integration tests over the matrix {lock, no-lock} × {base-composed, child-local} × {profile, provider}, asserting exactly one resolved entry per resource and no dropped local providers — these would fail today and pin the fixes.
Signed-off-by: Marta Anon <manon@redhat.com>
25258f1 to
893ec29
Compare
|
🤖 Finished Review · ✅ Success · Started 3:52 PM UTC · Completed 4:12 PM UTC |
Superseded by updated review
| h.OpenShell.Profiles = nil | ||
| remaining := h.OpenShell.Profiles[:0] | ||
| for _, p := range h.OpenShell.Profiles { | ||
| if !harness.IsURL(p) && !filepath.IsAbs(p) { |
There was a problem hiding this comment.
[medium] logic-error
resolveFromLock strips ALL absolute-path profiles via !filepath.IsAbs(p) and all provider paths via !harness.IsProviderPath(p), but local-path profiles/providers resolved by ResolveRelativeTo are also absolute. When a harness combines URL references (triggering the lock-file path) with local-path profiles/providers, the local entries are stripped without appearing in the lock file. They cannot be recovered by the second ResolveHarness call because the guards find no remaining entries. A harness with a URL-referenced skill and a local profile path would silently lose the local profile when running with a lock file.
Suggested fix: Track which entries correspond to lock-file dependencies and only strip those. Alternatively, preserve remaining absolute-path entries that have no corresponding lock dep so the second ResolveHarness pass can process them.
| // directly; bare provider names are kept in h.Providers for LoadProviderDefs. | ||
| var resolvedProviders []ResolvedProvider | ||
| remaining := h.Providers[:0] | ||
| for i, p := range h.Providers { |
There was a problem hiding this comment.
[low] arbitrary-file-read
In ResolveHarness, when a provider entry is an absolute path, os.ReadFile(p) is called without a containment check against WorkspaceRoot. Upstream guards prevent untrusted paths from reaching here, but the defense-in-depth is absent at this layer.
Suggested fix: Consider adding a containment check verifying the path starts with WorkspaceRoot or the cache directory before calling os.ReadFile.
|
|
||
| // IsProviderPath returns true if s looks like a file path rather than a bare | ||
| // provider name. A provider string is a path if it contains a directory | ||
| // separator or ends with a YAML extension. |
There was a problem hiding this comment.
[low] function-comment-consistency
IsProviderPath comment uses 'looks like' while other predicate functions in url.go (IsURL, IsAbsPath, IsRelPath) use direct 'returns true if s is' phrasing.
waynesun09
left a comment
There was a problem hiding this comment.
Re-review at 893ec298 — the round of fixes doesn't close the blockers
Verified the pushed fixes against the current code (multi-agent, Claude + Gemini). One new top-level finding below; I've also replied in-thread to the ADR, profile-strip, and second-pass comments where the fix is incomplete or the premise is contested. Out of scope but worth noting: dedupResolvedProfiles/Providers (pre-existing, not in this diff) silently collapse two distinct entries sharing an id/name with no warning — unlike mergeProviderDefs, which surfaces a shadowed list. Consider a warning on content mismatch in a follow-up.
| remainingProviders := h.Providers[:0] | ||
| for _, p := range h.Providers { | ||
| if !harness.IsURL(p) { | ||
| if !harness.IsURL(p) && !harness.IsProviderPath(p) { |
There was a problem hiding this comment.
[HIGH] correctness — H1 is not actually fixed: local providers are still dropped in the lock path
The hasLocalProviders change (now !IsURL && IsProviderPath) can't take effect here, because this strip runs first and removes exactly the entries the gate looks for.
Trace: runAgent calls ResolveRelativeTo unconditionally at run.go:386, so a locally-declared providers/custom.yaml is already absolute by the time resolveFromLock runs. This loop then drops it (IsProviderPath is true for /abs/.../custom.yaml). Genuinely-local providers are never fetched, so ResolveHarness records no dependency for them — they're absent from the lock file and are not reconstructed. Back at run.go:486, hasLocalProviders(h) inspects the now-stripped h.Providers, returns false, and the compensating second pass never runs. Net: a harness with any URL reference (so a lock entry exists) plus a local provider drops that provider silently — no error, no warning. The same root cause hits the profile strip below (see my reply on that thread).
The strip is also asymmetric: providers strip on IsProviderPath (relative + absolute), profiles on filepath.IsAbs (absolute only). Post-ResolveRelativeTo both only ever hold absolute paths so they converge today, but the discriminator should be whether the entry was consumed by a lock dep, not its path shape.
Suggestion: In resolveFromLock, strip only entries backed by a reconstructed lock dep (URL-resolved); leave genuinely-local paths for the second pass to resolve — or evaluate the run.go:486 gate on a snapshot of h.Providers/h.OpenShell.Profiles taken before stripping. Please add a lock-path test: harness with a URL skill + providers/custom.yaml, locked, asserting the provider survives into result.Providers (no lock-path test currently covers a local path-form provider).
Summary
resolveBaseProfilesandresolveBaseProvidersto harness composition so local paths in base harnesses are fetched and cached, matching existing behavior for skills, agent, policy, and scriptsIsProviderPathhelper to distinguish bare provider names (e.g.fullsend-github) from file paths, consolidating a heuristic previously duplicated in 3 locationsparseProviderDefhelper to deduplicate provider YAML validation between local-path and URL branchesWarningsfield toResolveResultso credential warnings from local providers surface to the userrun.gowith result merging to preserve lock-file deps/providersValidateFilesExistchecks for profiles and provider pathsCloses #5240
Test plan
TestResolveBaseProfiles— 5 cases: URL, relative, absolute, mixed, emptyTestResolveBaseProviders— 6 cases: URL, bare name, relative path, absolute, mixed, emptyTestIsProviderPath— 9 cases covering bare names, slashes, YAML extensionsTestParseProviderDef— 7 cases: valid, missing name/type, invalid chars, credential warningTestResolveHarness_LocalProviderWarnings— end-to-end warning propagationTestValidateFilesExist_Missing{Profile,ProviderPath}and bare-name skipgo test ./internal/harness/... ./internal/resolve/...)🤖 Generated with Claude Code