Skip to content

feat(resolve): portable provider and profile definitions via URL references#3062

Open
maruiz93 wants to merge 9 commits into
fullsend-ai:mainfrom
maruiz93:2672-portable-providers
Open

feat(resolve): portable provider and profile definitions via URL references#3062
maruiz93 wants to merge 9 commits into
fullsend-ai:mainfrom
maruiz93:2672-portable-providers

Conversation

@maruiz93

@maruiz93 maruiz93 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #2672

Implements portable provider and profile definitions that can be referenced by URL with SHA-256 integrity hashing, extending the existing resource resolution system (ADRs 0038, 0045) to cover the full harness surface.

  • Add profiles field (URL-only) and extend providers to accept mixed local/URL entries in harness schema
  • Resolve URL-referenced profiles and providers during fullsend run, with ParseProfileID validation and WarnLiteralCredentials for credential hygiene
  • Import resolved profiles via sandbox.ImportProfile and create/update providers via sandbox.EnsureProvider with reserved credential key validation
  • Merge profiles and providers during base harness composition (base + child, child wins)
  • Extend lock file resolution to reconstruct profiles and providers from cache
  • Validate referential integrity between provider types and declared profile IDs
  • Add ADR 0066 documenting the design decisions

Test plan

  • go test ./internal/resolve/ — profile/provider resolution, ParseProfileID, WarnLiteralCredentials
  • go test ./internal/harness/ — profiles field validation, HasURLReferences, base composition merge
  • go test ./internal/sandbox/ImportProfile, reserved credential key rejection
  • go test ./internal/cli/dedupResolvedProfiles, mergeProviderDefs, checkProviderProfileIntegrity, lock file reconstruction
  • make lint passes
  • 5 rounds of code review (42+ findings triaged across rounds)

🤖 Generated with Claude Code

@maruiz93 maruiz93 requested a review from a team as a code owner July 6, 2026 10:38
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 10:39 AM UTC · Ended 10:40 AM UTC
Commit: 0a95cac · View workflow run →

@maruiz93 maruiz93 force-pushed the 2672-portable-providers branch from 653f391 to dcf40a7 Compare July 6, 2026 10:40
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Portable provider/profile definitions via URL references (sha256-pinned)

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add URL-resolved profiles and mixed local/URL providers to the harness schema.
• Resolve, validate, and cache remote profile/provider YAML during fullsend run and lock
 resolution.
• Import profiles and ensure providers on the gateway with credential hygiene checks and integrity
 validation.
Diagram

graph TD
  H[/"Harness YAML"/] --> C["compose.go merge"] --> R["resolve.ResolveHarness"] --> Cache[(".fullsend-cache")]
  R --> Run["cli/run.go"] --> OS{{"openshell gateway"}}
  Lock[/"lock.yaml"/] --> LockRes["lock.go resolveFromLock"] --> Run
  LockRes --> Cache

  subgraph Legend
    direction LR
    _doc[/"Document"/] ~~~ _proc["Process"] ~~~ _db[("Cache/DB")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fetch provider/profile directories as a git tree (skill-style)
  • ➕ Single fetch per repo subtree; potentially fewer network round-trips
  • ➕ Keeps provider/profile files colocated in a repo without per-file URLs
  • ➖ More complex semantics: directory layout conventions, name/id discovery, and dedup rules
  • ➖ Harder integrity pinning at the individual definition level; tree hashes are coarser-grained
2. Allow local-path profiles/providers relative to harness repo
  • ➕ Simpler authoring for local development and monorepos
  • ➕ Avoids remote fetching for common cases
  • ➖ Undermines portability guarantees when composing remote base harnesses
  • ➖ Increases ambiguity around search paths and precedence vs .fullsend/* directories
3. Introduce a unified `resources:` map with typed entries
  • ➕ More extensible long-term (future resource types)
  • ➕ Can encode richer metadata per entry (type, url, hash, name overrides)
  • ➖ Schema churn and migration cost vs incremental extension
  • ➖ Heavier authoring burden for a targeted portability gap

Recommendation: The PR’s approach (explicit profiles URL list + mixed providers list, both sha256-pinned) is the best tradeoff for portability and security: it preserves existing local-provider workflows, keeps integrity pinning per definition, and cleanly integrates with existing resolver/cache/lock mechanisms. The main alternatives either add significant complexity (tree fetch + discovery) or weaken the portability/security goals (local profiles).

Files changed (13) +1530 / -145

Enhancement (6) +417 / -42
lock.goReconstruct resolved profiles/providers from lock cache +70/-13

Reconstruct resolved profiles/providers from lock cache

• Updates lock-time harness resolution to return a 'ResolveResult' (deps + resolved profiles/providers). Rehydrates profile IDs and provider defs from cached YAML content, attaches credential-hygiene warnings to dependency entries, and strips URL providers from 'h.Providers' to mirror normal resolution behavior.

internal/cli/lock.go

run.goImport resolved profiles and merge URL/local provider defs at runtime +114/-12

Import resolved profiles and merge URL/local provider defs at runtime

• Threads 'ResolveResult' through the run flow (including lock-file path), prints non-fatal resolution warnings, imports resolved profiles via 'sandbox.ImportProfile', merges local provider defs with URL-resolved defs deterministically, and validates provider->profile referential integrity before ensuring providers.

internal/cli/run.go

compose.goMerge 'profiles' during base harness composition +6/-0

Merge 'profiles' during base harness composition

• Extends 'mergeBaseIntoChild' to concatenate base and child profile URL lists (base first), matching existing merge patterns for providers/skills.

internal/harness/compose.go

harness.goAdd 'Profiles' field and URL/hash validation for profiles/providers +27/-3

Add 'Profiles' field and URL/hash validation for profiles/providers

• Adds 'Profiles []string' to the harness schema, enforces that profiles are URL-only with required '#sha256=' integrity hashes, and tightens provider validation so URL providers must include integrity hashes. Extends 'HasURLReferences' to include profiles and URL providers.

internal/harness/harness.go

resolve.goResolve URL profiles/providers and return structured 'ResolveResult' +144/-9

Resolve URL profiles/providers and return structured 'ResolveResult'

• Introduces 'ResolveResult', 'ResolvedProfile', and 'ResolvedProvider' outputs; adds profile ID parsing/validation and provider YAML parsing/validation for URL entries. Adds 'WarnLiteralCredentials' to flag non-${VAR} credential values, strips URL providers from 'h.Providers', and updates error handling to return the new result type.

internal/resolve/resolve.go

sandbox.goAdd profile import and harden provider creation against reserved keys +56/-5

Add profile import and harden provider creation against reserved keys

• Adds 'ImportProfile' to invoke 'openshell provider profile import' with idempotent handling for already-existing profiles. Updates 'EnsureProvider'/'updateProvider' to accept context and reject reserved credential keys that could influence process/shell/proxy behavior before executing openshell.

internal/sandbox/sandbox.go

Tests (6) +881 / -103
lock_test.goExpand lock resolution tests for profiles/providers reconstruction +213/-37

Expand lock resolution tests for profiles/providers reconstruction

• Refactors existing tests to use 'ResolveResult' and adds coverage for profile/provider reconstruction from cache, including missing id/name/type errors and literal-credential warnings.

internal/cli/lock_test.go

run_test.goAdd unit tests for profile dedup, provider merge, and integrity checks +188/-0

Add unit tests for profile dedup, provider merge, and integrity checks

• Adds focused tests for 'dedupResolvedProfiles', 'mergeProviderDefs' precedence/ordering rules, and 'checkProviderProfileIntegrity' warning/error behavior.

internal/cli/run_test.go

compose_test.goTest base+child profile list concatenation +66/-0

Test base+child profile list concatenation

• Adds tests covering profile inheritance from base harnesses and child-only behavior when base has or lacks profiles.

internal/harness/compose_test.go

harness_test.goTest profiles/providers URL detection and validation rules +82/-0

Test profiles/providers URL detection and validation rules

• Expands 'HasURLReferences' coverage for profiles and provider URL/local cases and adds 'ValidateResourceTypes' tests for profile URL-only + hash requirements and provider URL hash enforcement.

internal/harness/harness_test.go

resolve_test.goAdd resolver tests for profile/provider URL resolution and warnings +285/-63

Add resolver tests for profile/provider URL resolution and warnings

• Refactors existing tests to use 'ResolveResult' and adds new coverage for profile fetching/id validation, provider fetching/validation, provider URL stripping behavior, and credential warning detection/sorting.

internal/resolve/resolve_test.go

sandbox_test.goTest profile import failure and reserved credential key rejection +47/-3

Test profile import failure and reserved credential key rejection

• Adds tests for 'ImportProfile' error behavior when openshell is unavailable and for 'EnsureProvider' rejecting reserved credential env-var keys (case-insensitive), while leaving allowed keys unaffected.

internal/sandbox/sandbox_test.go

Documentation (1) +232 / -0
0066-portable-provider-profile-resolution.mdAdd ADR 0066 for portable profile/provider URL resolution +232/-0

Add ADR 0066 for portable profile/provider URL resolution

• Introduces an accepted ADR describing new harness fields ('profiles', extended 'providers'), composition/merge semantics, resolution and runtime flows, validation rules, and security posture (hash pinning, SSRF hardening, credential hygiene warnings).

docs/ADRs/0066-portable-provider-profile-resolution.md

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:41 AM UTC · Completed 10:56 AM UTC
Commit: dcf40a7 · View workflow run →

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Site preview

Preview: https://98ff1f10-site.fullsend-ai.workers.dev

Commit: f839bdf96d2f660a8f856868d4f5abdfd7a4f9fb

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Qodo Logo

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review

The branch was rebased since the prior review (SHA 8def383); all production code is unchanged. The prior HIGH finding (Validate() rejecting URL-based providers) remains fixed — the IsURL(p) guard correctly skips URL entries. The resolveFromLock deny-by-default enforcement is correct (AllowedRemoteResources checked unconditionally, confirmed by tests). The reservedCredentialKeys blocklist is comprehensive (27 entries) and case-insensitive. ImportProfile handles idempotency correctly. Profile/provider lock-file reconstruction is well-tested. Base composition merge for profiles follows the child-wins pattern. dedupResolvedProviders now has comprehensive test coverage. sandboxProviderNames deduplicates correctly via a seen map. The checkProviderProfileIntegrity function validates referential integrity with proper mismatch reporting. mergeProviderDefs correctly handles local-shadows-URL semantics with deterministic ordering.

The EnsureProvider/updateProvider context propagation fix is correct — both now use the passed-in ctx as the timeout parent instead of context.Background(). CreateWithRetry now receives allProviderNames (harness-declared + URL-resolved) instead of h.Providers, correctly including URL-resolved providers in the sandbox scope.

Two medium-severity defense-in-depth gaps persist unchanged from prior reviews. Neither blocks the feature.

Findings

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs, URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name with special characters (spaces, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in both resolve.go and lock.go.

  • [env-var-exfiltration-via-config] internal/sandbox/sandbox.goProviderDef.Config map values undergo os.ExpandEnv() (in buildProviderArgs) and are passed as --config KEY=VALUE arguments. WarnLiteralCredentials only inspects credential values, not config values. A URL-fetched provider could include config values containing ${SECRET_VAR} references to exfiltrate host environment variables into openshell config flags, where they may be logged or sent to a remote endpoint.
    Remediation: Apply equivalent ${VAR} pattern warnings to config values in URL-fetched providers, or restrict os.ExpandEnv on config values to local providers only.

Low

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Attack surface is narrow since exploitation requires control over a SHA256-pinned provider YAML.

  • [behavior-change] internal/cli/run.go — The "Provider declared but no definition found" warning was removed. If a user declares providers: [misspelled-name] and no matching YAML exists in providers/, the name still appears in sandboxProviderNames and is passed to CreateWithRetry, which may fail at the openshell level. The loss of the explicit warning makes debugging harder.

  • [stale-doc] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations. The return type changed to (ResolveResult, error) in this PR. These are historical plan documents, not user-facing API docs.

  • [doc-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The ADR describes referential integrity validation at step 4 (after loading local defs and merging), but the implementation checks it at step 2b (before loading local defs). The implementation's fail-fast ordering is better, but diverges from the documented flow.

Previous run

Review

The only change since the prior review (SHA 215edb0) is commit 8def383 which adds unit tests for dedupResolvedProviders and other helpers — resolving the prior [test-adequacy] finding. All production code is unchanged.

The prior HIGH finding (Validate() rejecting URL-based providers) remains fixed — the IsURL(p) guard correctly skips URL entries. The resolveFromLock deny-by-default enforcement is correct (AllowedRemoteResources checked unconditionally, confirmed by tests). The reservedCredentialKeys blocklist is comprehensive (27 entries) and case-insensitive. ImportProfile handles idempotency correctly. Profile/provider lock-file reconstruction is well-tested. Base composition merge for profiles follows the child-wins pattern. dedupResolvedProviders now has comprehensive test coverage.

Two medium-severity defense-in-depth gaps persist unchanged from the prior review. Neither blocks the feature.

Findings

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs, URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name with special characters (spaces, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in both resolve.go and lock.go.

  • [env-var-exfiltration-via-config] internal/sandbox/sandbox.goProviderDef.Config map values undergo os.ExpandEnv() (in buildProviderArgs) and are passed as --config KEY=VALUE arguments. WarnLiteralCredentials only inspects credential values, not config values. A URL-fetched provider could include config values containing ${SECRET_VAR} references to exfiltrate host environment variables into openshell config flags, where they may be logged or sent to a remote endpoint.
    Remediation: Apply equivalent ${VAR} pattern warnings to config values in URL-fetched providers, or restrict os.ExpandEnv on config values to local providers only.

Low

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Attack surface is narrow since exploitation requires control over a SHA256-pinned provider YAML.

  • [behavior-change] internal/cli/run.go — The "Provider declared but no definition found" warning was removed. If a user declares providers: [misspelled-name] and no matching YAML exists in providers/, the name still appears in sandboxProviderNames and is passed to CreateWithRetry, which may fail at the openshell level. The loss of the explicit warning makes debugging harder.

  • [stale-doc] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations. The return type changed to (ResolveResult, error) in this PR. These are historical plan documents, not user-facing API docs.

  • [doc-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The ADR describes referential integrity validation at step 4 (after loading local defs and merging), but the implementation checks it at step 2b (before loading local defs). The implementation's fail-fast ordering is better, but diverges from the documented flow.

Previous run

Review

The only change since the prior review (SHA 215edb0) is commit 8def383 which adds unit tests for dedupResolvedProviders and other helpers — resolving the prior [test-adequacy] finding. All production code is unchanged.

The prior HIGH finding (Validate() rejecting URL-based providers) remains fixed — the IsURL(p) guard correctly skips URL entries. The resolveFromLock deny-by-default enforcement is correct (AllowedRemoteResources checked unconditionally, confirmed by tests). The reservedCredentialKeys blocklist is comprehensive (27 entries) and case-insensitive. ImportProfile handles idempotency correctly. Profile/provider lock-file reconstruction is well-tested. Base composition merge for profiles follows the child-wins pattern. dedupResolvedProviders now has comprehensive test coverage.

Two medium-severity defense-in-depth gaps persist unchanged from the prior review. Neither blocks the feature.

Findings

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs, URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name with special characters (spaces, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in both resolve.go and lock.go.

  • [env-var-exfiltration-via-config] internal/sandbox/sandbox.goProviderDef.Config map values undergo os.ExpandEnv() (in buildProviderArgs) and are passed as --config KEY=VALUE arguments. WarnLiteralCredentials only inspects credential values, not config values. A URL-fetched provider could include config values containing ${SECRET_VAR} references to exfiltrate host environment variables into openshell config flags, where they may be logged or sent to a remote endpoint.
    Remediation: Apply equivalent ${VAR} pattern warnings to config values in URL-fetched providers, or restrict os.ExpandEnv on config values to local providers only.

Low

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Attack surface is narrow since exploitation requires control over a SHA256-pinned provider YAML.

  • [behavior-change] internal/cli/run.go — The "Provider declared but no definition found" warning was removed. If a user declares providers: [misspelled-name] and no matching YAML exists in providers/, the name still appears in sandboxProviderNames and is passed to CreateWithRetry, which may fail at the openshell level. The loss of the explicit warning makes debugging harder.

  • [stale-doc] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations. The return type changed to (ResolveResult, error) in this PR. These are historical plan documents, not user-facing API docs.

  • [doc-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The ADR describes referential integrity validation at step 4 (after loading local defs and merging), but the implementation checks it at step 2b (before loading local defs). The implementation's fail-fast ordering is better, but diverges from the documented flow.

Previous run (2)

Review

The prior HIGH finding (Validate() rejecting URL-based providers) remains fixed — the IsURL(p) guard correctly skips URL entries. Prior false positives (comment-code-mismatch on dedupResolvedProviders, behavior-change about LoadProviderDefs filter removal, ADR heading inconsistency) have been verified as incorrect and removed. The dedupResolvedProviders doc comment is correct; LoadProviderDefs is still called with the declared filter; and the ADR heading correctly reads "68" throughout.

The persistent medium-severity defense-in-depth gap from prior reviews (missing validProviderName regex on URL-fetched providers) remains unchanged. One new medium finding was identified regarding os.ExpandEnv on config values in URL-fetched providers.

Findings

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs, URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name with special characters (spaces, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in both resolve.go and lock.go.

  • [env-var-exfiltration-via-config] internal/sandbox/sandbox.goProviderDef.Config map values undergo os.ExpandEnv() (in buildProviderArgs) and are passed on --config KEY=VALUE arguments. WarnLiteralCredentials only inspects credential values, not config values. A URL-fetched provider could include config values containing ${SECRET_VAR} references to exfiltrate host environment variables into openshell config flags, where they may be logged or sent to a remote endpoint.
    Remediation: Apply equivalent ${VAR} pattern warnings to config values in URL-fetched providers, or restrict os.ExpandEnv on config values to local providers only.

Low

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Attack surface is narrow since exploitation requires control over a SHA256-pinned provider YAML.

  • [behavior-change] internal/cli/run.go — The "Provider declared but no definition found" warning was removed. If a user declares providers: [misspelled-name] and no matching YAML exists in providers/, the name still appears in sandboxProviderNames and is passed to CreateWithRetry, which may fail at the openshell level. The loss of the explicit warning makes debugging harder.

  • [test-adequacy] internal/cli/run_test.go — No direct test for dedupResolvedProviders. The sibling function dedupResolvedProfiles has comprehensive test coverage via TestDedupResolvedProfiles.

  • [stale-doc] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations. The return type changed to (ResolveResult, error) in this PR. These are historical plan documents, not user-facing API docs.

  • [doc-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The ADR describes referential integrity validation at step 4 (after loading local defs and merging), but the implementation checks it at step 2b (before loading local defs). The implementation's fail-fast ordering is better, but diverges from the documented flow.

Previous run (3)

Review

The prior HIGH finding (Validate() rejecting URL-based providers) remains fixed — the IsURL(p) guard correctly skips URL entries. Prior false positives (comment-code-mismatch on dedupResolvedProviders, behavior-change about LoadProviderDefs filter removal, ADR heading inconsistency) have been verified as incorrect and removed. The dedupResolvedProviders doc comment is correct; LoadProviderDefs is still called with the declared filter; and the ADR heading correctly reads "68" throughout.

The persistent medium-severity defense-in-depth gap from prior reviews (missing validProviderName regex on URL-fetched providers) remains unchanged. One new medium finding was identified regarding os.ExpandEnv on config values in URL-fetched providers.

Findings

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs, URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name with special characters (spaces, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in both resolve.go and lock.go.

  • [env-var-exfiltration-via-config] internal/sandbox/sandbox.goProviderDef.Config map values undergo os.ExpandEnv() (in buildProviderArgs) and are passed on --config KEY=VALUE arguments. WarnLiteralCredentials only inspects credential values, not config values. A URL-fetched provider could include config values containing ${SECRET_VAR} references to exfiltrate host environment variables into openshell config flags, where they may be logged or sent to a remote endpoint.
    Remediation: Apply equivalent ${VAR} pattern warnings to config values in URL-fetched providers, or restrict os.ExpandEnv on config values to local providers only.

Low

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Attack surface is narrow since exploitation requires control over a SHA256-pinned provider YAML.

  • [behavior-change] internal/cli/run.go — The "Provider declared but no definition found" warning was removed. If a user declares providers: [misspelled-name] and no matching YAML exists in providers/, the name still appears in sandboxProviderNames and is passed to CreateWithRetry, which may fail at the openshell level. The loss of the explicit warning makes debugging harder.

  • [test-adequacy] internal/cli/run_test.go — No direct test for dedupResolvedProviders. The sibling function dedupResolvedProfiles has comprehensive test coverage via TestDedupResolvedProfiles.

  • [stale-doc] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations. The return type changed to (ResolveResult, error) in this PR. These are historical plan documents, not user-facing API docs.

  • [doc-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The ADR describes referential integrity validation at step 4 (after loading local defs and merging), but the implementation checks it at step 2b (before loading local defs). The implementation's fail-fast ordering is better, but diverges from the documented flow.

Previous run (4)

Review

The two medium-severity defense-in-depth gaps from the prior review persist unchanged. No new blocking findings. The HIGH finding from earlier reviews (Validate() rejecting URL-based providers) remains fixed. The IsURL(p) guard at harness.go skips URL entries correctly. resolveFromLock deny-by-default enforcement is correct — AllowedRemoteResources is checked unconditionally, confirmed by TestResolveFromLock_EmptyAllowlistDeniesURLs and TestResolveFromLock_RejectsDisallowedURL. reservedCredentialKeys is comprehensive and case-insensitive. ImportProfile handles idempotency correctly. sandboxProviderNames now deduplicates via a seen map. EnsureProvider/updateProvider use the passed-in context as the timeout parent. Profile/provider lock-file reconstruction is well-tested. Base composition merge for profiles follows the established child-wins pattern. Docs correctly show nested openshell.profiles.

One new low-severity finding: the dedupResolvedProviders function has a copy-paste comment error describing itself as dedupResolvedProfiles.

Findings

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs (harness.go:86-93), URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name or type with special characters (spaces, slashes, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in the URL-fetched provider resolution code in both resolve.go and lock.go, matching LoadProviderDefs validation.

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject git hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Since the openshell child process runs git operations, a URL-fetched provider could set these as credential keys, potentially enabling command execution or credential capture within the sandbox.
    Remediation: Add GIT_SSH_COMMAND, GIT_TEMPLATE_DIR, and GIT_ASKPASS to reservedCredentialKeys.

Low

  • [comment-code-mismatch] internal/cli/run.go — The dedupResolvedProviders function has a doc comment that reads "dedupResolvedProfiles removes duplicate profiles by ID" — a copy-paste error from the profiles version directly below. The function operates on []resolve.ResolvedProvider and deduplicates by Def.Name, not by profile ID.

  • [behavior-change] internal/cli/run.goLoadProviderDefs is now called without the declared filter. Previously, only harness-declared providers were loaded and created on the gateway. Now ALL providers from the providers/ directory are loaded and created, even undeclared ones. While undeclared providers are correctly excluded from sandbox attachment via sandboxProviderNames, this widens gateway-side provider creation scope.

  • [internal-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The frontmatter correctly says title: "68. Portable provider and profile resolution" but the markdown heading reads # 66. Portable provider and profile resolution. The file was renumbered from 0066 to 0068 to avoid collision, but the heading was not updated.

  • [doc-staleness] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations (at least 5 occurrences across phase1, phase2, and the main plan doc). The return type changed to (ResolveResult, error) in this PR. While plan documents may be treated as historical, the stale signature references could mislead future readers.

  • [test-adequacy] internal/cli/run_test.go — No direct test for dedupResolvedProviders, despite it having a copy-paste comment error. The sibling function dedupResolvedProfiles has comprehensive test coverage. A similar test table for providers would increase confidence.

Previous run (5)

Review

The blocking HIGH finding from prior reviews — Validate() rejecting URL-based providers before ResolveHarness could process them — has been fixed. The IsURL(p) guard at harness.go line 413 now skips URL entries, matching the existing pattern for agents at line 374. TestLoad_ProviderURLPassesValidation confirms the fix. Three LOW findings were also resolved: sandboxProviderNames now deduplicates via a seen map, EnsureProvider/updateProvider now use the passed-in context as the timeout parent (matching ImportProfile), and the missing test gap was filled.

The deny-by-default enforcement in resolveFromLock remains correct — AllowedRemoteResources is checked unconditionally, confirmed by TestResolveFromLock_EmptyAllowlistDeniesURLs and TestResolveFromLock_RejectsDisallowedURL. The reservedCredentialKeys blocklist is comprehensive and case-insensitive (strings.ToUpper). ImportProfile handles idempotency correctly. Profile/provider lock-file reconstruction is well-tested. The ResolveResult struct is a clean evolution of the return type. Base composition merge for profiles follows the established child-wins pattern. Docs now correctly show nested openshell.profiles.

Two medium-severity defense-in-depth gaps remain from the prior review. Neither blocks the feature, but both should be tracked for follow-up.

Findings

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs (harness.go:86-93), URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name or type with special characters (spaces, slashes, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in the URL-fetched provider resolution code in both resolve.go and lock.go, matching LoadProviderDefs validation.

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject git hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Since the openshell child process runs git operations, a URL-fetched provider could set these as credential keys, potentially enabling command execution or credential capture within the sandbox.
    Remediation: Add GIT_SSH_COMMAND, GIT_TEMPLATE_DIR, and GIT_ASKPASS to reservedCredentialKeys.

Low

  • [behavior-change] internal/cli/run.goLoadProviderDefs is now called without the declared filter. Previously, only harness-declared providers were loaded and created on the gateway. Now ALL providers from the providers/ directory are loaded and created, even undeclared ones. While undeclared providers are correctly excluded from sandbox attachment via sandboxProviderNames, this widens gateway-side provider creation scope.

  • [internal-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The frontmatter correctly says title: "68. Portable provider and profile resolution" but the markdown heading reads # 66. Portable provider and profile resolution. The file was renumbered from 0066 to 0068 to avoid collision, but the heading was not updated.

  • [doc-staleness] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations (at least 5 occurrences across phase1, phase2, and the main plan doc). The return type changed to (ResolveResult, error) in this PR. While plan documents may be treated as historical, the stale signature references could mislead future readers.

Previous run (6)

Review

The blocking HIGH finding from prior reviews — Validate() rejecting URL-based providers before ResolveHarness could process them — has been fixed. The IsURL(p) guard at harness.go line 413 now skips URL entries, matching the existing pattern for agents at line 374. TestLoad_ProviderURLPassesValidation confirms the fix. Three LOW findings were also resolved: sandboxProviderNames now deduplicates via a seen map, EnsureProvider/updateProvider now use the passed-in context as the timeout parent (matching ImportProfile), and the missing test gap was filled.

The deny-by-default enforcement in resolveFromLock remains correct — AllowedRemoteResources is checked unconditionally, confirmed by TestResolveFromLock_EmptyAllowlistDeniesURLs and TestResolveFromLock_RejectsDisallowedURL. The reservedCredentialKeys blocklist is comprehensive and case-insensitive (strings.ToUpper). ImportProfile handles idempotency correctly. Profile/provider lock-file reconstruction is well-tested. The ResolveResult struct is a clean evolution of the return type. Base composition merge for profiles follows the established child-wins pattern. Docs now correctly show nested openshell.profiles.

Two medium-severity defense-in-depth gaps remain from the prior review. Neither blocks the feature, but both should be tracked for follow-up.

Findings

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs (harness.go:86-93), URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name or type with special characters (spaces, slashes, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in the URL-fetched provider resolution code in both resolve.go and lock.go, matching LoadProviderDefs validation.

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject git hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Since the openshell child process runs git operations, a URL-fetched provider could set these as credential keys, potentially enabling command execution or credential capture within the sandbox.
    Remediation: Add GIT_SSH_COMMAND, GIT_TEMPLATE_DIR, and GIT_ASKPASS to reservedCredentialKeys.

Low

  • [behavior-change] internal/cli/run.goLoadProviderDefs is now called without the declared filter. Previously, only harness-declared providers were loaded and created on the gateway. Now ALL providers from the providers/ directory are loaded and created, even undeclared ones. While undeclared providers are correctly excluded from sandbox attachment via sandboxProviderNames, this widens gateway-side provider creation scope.

  • [internal-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The frontmatter correctly says title: "68. Portable provider and profile resolution" but the markdown heading reads # 66. Portable provider and profile resolution. The file was renumbered from 0066 to 0068 to avoid collision, but the heading was not updated.

  • [doc-staleness] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations (at least 5 occurrences across phase1, phase2, and the main plan doc). The return type changed to (ResolveResult, error) in this PR. While plan documents may be treated as historical, the stale signature references could mislead future readers.

Previous run (7)

Review

The deny-by-default enforcement in resolveFromLock remains correct — AllowedRemoteResources is checked unconditionally, confirmed by TestResolveFromLock_EmptyAllowlistDeniesURLs and TestResolveFromLock_RejectsDisallowedURL. The expanded reservedCredentialKeys blocklist is comprehensive and case-insensitive (strings.ToUpper). ImportProfile handles idempotency correctly. sandboxProviderNames() correctly limits sandbox scope to harness-declared + URL-resolved provider names. Profile/provider lock-file reconstruction is well-tested. The ResolveResult struct is a clean evolution of the return type. Base composition merge for profiles follows the established child-wins pattern. The doc-code-mismatch from prior reviews was fixed (docs now show nested openshell.profiles), and the ADR was renumbered from 0066 to 0068 to avoid collision. ADR cross-reference annotations to ADRs 0024 and 0038 are minor and acceptable under immutability rules.

However, the blocking validation bug that prevents URL-based providers from working end-to-end persists, URL-fetched provider definitions are still not validated against the validProviderName regex, and the reservedCredentialKeys blocklist is missing several git-related env vars that could allow command execution.

Findings

High

  • [logic-error] internal/harness/harness.go:401Validate() rejects URL-based providers before ResolveHarness can process them. The provider validation loop at line 401 checks every entry in h.Providers against validProviderName (regex ^[a-zA-Z0-9_-]+$) with no IsURL guard. URL entries like https://github.com/org/repo/tree/main/providers/my.yaml#sha256=... fail this check. Validate() is called from LoadWithBase (compose.go line 181) BEFORE ResolveHarness() strips URL entries. This makes the URL provider feature non-functional — any harness declaring a URL-based provider will fail to load with an "invalid characters" error. Note: the agent field already has an IsURL skip at line 374, but providers do not. ValidateResourceTypes() (called later within Validate at line 426) correctly handles URL providers, but execution never reaches it.
    Remediation: Add if IsURL(p) { continue } before the validProviderName check at line 402, matching the pattern used for agents at line 374.

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs (harness.go:86-93), URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name or type with special characters (spaces, slashes, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in the URL-fetched provider resolution code in both resolve.go and lock.go, matching LoadProviderDefs validation.

  • [incomplete-blocklist] internal/sandbox/sandbox.goreservedCredentialKeys includes GIT_CONFIG_GLOBAL and GIT_EXEC_PATH but omits GIT_SSH_COMMAND (executes arbitrary command when git uses SSH), GIT_TEMPLATE_DIR (can inject git hooks via template directories), and GIT_ASKPASS (runs a program to capture credentials). Since the openshell child process runs git operations, a URL-fetched provider could set these as credential keys, potentially enabling command execution or credential capture within the sandbox.
    Remediation: Add GIT_SSH_COMMAND, GIT_TEMPLATE_DIR, and GIT_ASKPASS to reservedCredentialKeys.

Low

  • [edge-case] internal/cli/run.go (sandboxProviderNames) — Does not deduplicate when a URL-resolved provider has the same name as a harness-declared local provider. mergeProviderDefs correctly shadows the URL-resolved ProviderDef with the local one for gateway registration, but sandboxProviderNames independently concatenates both lists, potentially producing duplicate --provider flags to CreateWithRetry.

  • [logic-error] internal/sandbox/sandbox.go (EnsureProvider, updateProvider) — Both functions accept a ctx context.Context parameter but immediately shadow it with ctx, cancel := context.WithTimeout(context.Background(), providerTimeout). The passed-in context (from run.go's CLI context) is never used as the parent for the timeout, so parent context cancellation does not propagate to the openshell subprocess. ImportProfile correctly uses the passed context.

  • [behavior-change] internal/cli/run.goLoadProviderDefs is now called without the declared filter. Previously, only harness-declared providers were loaded and created on the gateway. Now ALL providers from the providers/ directory are loaded and created, even undeclared ones. While undeclared providers are correctly excluded from sandbox attachment via sandboxProviderNames, this widens gateway-side provider creation scope.

  • [missing-test] internal/harness/harness_test.go — No test verifies that Validate() rejects a harness with URL-based providers. Such a test would demonstrate the HIGH finding and serve as a regression test once the IsURL guard is added. (ValidateResourceTypes tests were added but do not cover the Validate() code path.)

  • [internal-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The frontmatter correctly says title: "68. Portable provider and profile resolution" but the markdown heading reads # 66. Portable provider and profile resolution. The file was renumbered from 0066 to 0068 to avoid collision, but the heading was not updated.

  • [doc-staleness] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations (at least 5 occurrences across phase1, phase2, and the main plan doc). The return type changed to (ResolveResult, error) in this PR. While plan documents may be treated as historical, the stale signature references could mislead future readers.

Previous run (8)

Review

The deny-by-default enforcement in resolveFromLock remains correct — AllowedRemoteResources is checked unconditionally, confirmed by TestResolveFromLock_EmptyAllowlistDeniesURLs and TestResolveFromLock_RejectsDisallowedURL. The expanded reservedCredentialKeys blocklist is comprehensive and case-insensitive (strings.ToUpper). ImportProfile handles idempotency correctly. sandboxProviderNames() correctly limits sandbox scope to harness-declared + URL-resolved provider names. Profile/provider lock-file reconstruction is well-tested. The ResolveResult struct is a clean evolution of the return type. Base composition merge for profiles follows the established child-wins pattern. The doc-code-mismatch from the prior review was fixed (docs now show nested openshell.profiles), and the ADR was renumbered from 0066 to 0068 to avoid collision.

However, the blocking validation bug that prevents URL-based providers from working end-to-end persists, and URL-fetched provider definitions are still not validated against the validProviderName regex.

Findings

High

  • [logic-error] internal/harness/harness.go:401Validate() rejects URL-based providers before ResolveHarness can process them. The provider validation loop at line 401 checks every entry in h.Providers against validProviderName (regex ^[a-zA-Z0-9_-]+$) with no IsURL guard. URL entries like https://github.com/org/repo/tree/main/providers/my.yaml#sha256=... fail this check. Validate() is called from LoadWithBase (compose.go line 181) BEFORE ResolveHarness() strips URL entries. This makes the URL provider feature non-functional — any harness declaring a URL-based provider will fail to load with an "invalid characters" error. Note: the agent field already has an IsURL skip at line 374, but providers do not. ValidateResourceTypes() (called later within Validate at line 426) correctly handles URL providers, but execution never reaches it.
    Remediation: Add if IsURL(p) { continue } before the validProviderName check at line 402, matching the pattern used for agents at line 374.

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs (harness.go:86-93), URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name or type with special characters (spaces, slashes, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in the URL-fetched provider resolution code in both resolve.go and lock.go, matching LoadProviderDefs validation.

Low

  • [inconsistent-blocklist] internal/sandbox/sandbox.go / internal/cli/run.goreservedCredentialKeys (sandbox.go, 30 entries) and reservedSandboxKeys (run.go, 13 entries) serve related purposes but remain unsynchronized. The credential blocklist is broader (includes proxy, language runtime injection, TLS, hostname, and git config variables absent from the sandbox blocklist). Additionally, reservedSandboxKeys uses case-sensitive lookup while reservedCredentialKeys uses strings.ToUpper(k) for case-insensitive matching. The different trust levels (URL-fetched untrusted YAML vs org-authored harness YAML) justify some divergence, but the case-sensitivity gap is a real inconsistency.

  • [edge-case] internal/cli/run.go (sandboxProviderNames) — Does not deduplicate when a URL-resolved provider has the same name as a harness-declared local provider. mergeProviderDefs correctly shadows the URL-resolved ProviderDef with the local one for gateway registration, but sandboxProviderNames independently concatenates both lists, potentially producing duplicate --provider flags to CreateWithRetry.

  • [logic-error] internal/sandbox/sandbox.go (EnsureProvider, updateProvider) — Both functions accept a ctx context.Context parameter but immediately shadow it with ctx, cancel := context.WithTimeout(context.Background(), providerTimeout). The passed-in context (from run.go's CLI context) is never used as the parent for the timeout, so parent context cancellation does not propagate to the openshell subprocess. ImportProfile correctly uses the passed context.

  • [behavior-change] internal/cli/run.goLoadProviderDefs is now called without the declared filter. Previously, only harness-declared providers were loaded and created on the gateway. Now ALL providers from the providers/ directory are loaded and created, even undeclared ones. While undeclared providers are correctly excluded from sandbox attachment via sandboxProviderNames, this widens gateway-side provider creation scope.

  • [missing-test] internal/harness/harness_test.go — No test verifies that Validate() rejects a harness with URL-based providers. Such a test would demonstrate the HIGH finding and serve as a regression test once the IsURL guard is added. (ValidateResourceTypes tests were added but do not cover the Validate() code path.)

  • [internal-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The frontmatter correctly says title: "68. Portable provider and profile resolution" but the markdown heading reads # 66. Portable provider and profile resolution. The file was renumbered from 0066 to 0068 to avoid collision, but the heading was not updated.

Previous run (9)

Review

The deny-by-default enforcement in resolveFromLock remains correct — AllowedRemoteResources is checked unconditionally, confirmed by TestResolveFromLock_EmptyAllowlistDeniesURLs and TestResolveFromLock_RejectsDisallowedURL. The expanded reservedCredentialKeys blocklist is comprehensive and case-insensitive (strings.ToUpper). ImportProfile handles idempotency correctly. sandboxProviderNames() correctly limits sandbox scope to harness-declared + URL-resolved provider names. Profile/provider lock-file reconstruction is well-tested. The ResolveResult struct is a clean evolution of the return type. Base composition merge for profiles follows the established child-wins pattern. The doc-code-mismatch from the prior review was fixed (docs now show nested openshell.profiles), and the ADR was renumbered from 0066 to 0068 to avoid collision.

However, the blocking validation bug that prevents URL-based providers from working end-to-end persists, and URL-fetched provider definitions are still not validated against the validProviderName regex.

Findings

High

  • [logic-error] internal/harness/harness.go:401Validate() rejects URL-based providers before ResolveHarness can process them. The provider validation loop at line 401 checks every entry in h.Providers against validProviderName (regex ^[a-zA-Z0-9_-]+$) with no IsURL guard. URL entries like https://github.com/org/repo/tree/main/providers/my.yaml#sha256=... fail this check. Validate() is called from LoadWithBase (compose.go line 181) BEFORE ResolveHarness() strips URL entries. This makes the URL provider feature non-functional — any harness declaring a URL-based provider will fail to load with an "invalid characters" error. Note: the agent field already has an IsURL skip at line 374, but providers do not. ValidateResourceTypes() (called later within Validate at line 426) correctly handles URL providers, but execution never reaches it.
    Remediation: Add if IsURL(p) { continue } before the validProviderName check at line 402, matching the pattern used for agents at line 374.

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs (harness.go:86-93), URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name or type with special characters (spaces, slashes, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in the URL-fetched provider resolution code in both resolve.go and lock.go, matching LoadProviderDefs validation.

Low

  • [inconsistent-blocklist] internal/sandbox/sandbox.go / internal/cli/run.goreservedCredentialKeys (sandbox.go, 30 entries) and reservedSandboxKeys (run.go, 13 entries) serve related purposes but remain unsynchronized. The credential blocklist is broader (includes proxy, language runtime injection, TLS, hostname, and git config variables absent from the sandbox blocklist). Additionally, reservedSandboxKeys uses case-sensitive lookup while reservedCredentialKeys uses strings.ToUpper(k) for case-insensitive matching. The different trust levels (URL-fetched untrusted YAML vs org-authored harness YAML) justify some divergence, but the case-sensitivity gap is a real inconsistency.

  • [edge-case] internal/cli/run.go (sandboxProviderNames) — Does not deduplicate when a URL-resolved provider has the same name as a harness-declared local provider. mergeProviderDefs correctly shadows the URL-resolved ProviderDef with the local one for gateway registration, but sandboxProviderNames independently concatenates both lists, potentially producing duplicate --provider flags to CreateWithRetry.

  • [logic-error] internal/sandbox/sandbox.go (EnsureProvider, updateProvider) — Both functions accept a ctx context.Context parameter but immediately shadow it with ctx, cancel := context.WithTimeout(context.Background(), providerTimeout). The passed-in context (from run.go's CLI context) is never used as the parent for the timeout, so parent context cancellation does not propagate to the openshell subprocess. ImportProfile correctly uses the passed context.

  • [behavior-change] internal/cli/run.goLoadProviderDefs is now called without the declared filter. Previously, only harness-declared providers were loaded and created on the gateway. Now ALL providers from the providers/ directory are loaded and created, even undeclared ones. While undeclared providers are correctly excluded from sandbox attachment via sandboxProviderNames, this widens gateway-side provider creation scope.

  • [missing-test] internal/harness/harness_test.go — No test verifies that Validate() rejects a harness with URL-based providers. Such a test would demonstrate the HIGH finding and serve as a regression test once the IsURL guard is added. (ValidateResourceTypes tests were added but do not cover the Validate() code path.)

  • [internal-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The frontmatter correctly says title: "68. Portable provider and profile resolution" but the markdown heading reads # 66. Portable provider and profile resolution. The file was renumbered from 0066 to 0068 to avoid collision, but the heading was not updated.

Previous run (10)

Review

The doc-code-mismatch finding from the prior review has been fixed — user documentation now correctly shows the nested openshell:\n profiles: form matching the Go struct. The resolveFromLock deny-by-default enforcement remains correct (unconditional AllowedRemoteResources check), confirmed by TestResolveFromLock_EmptyAllowlistDeniesURLs and TestResolveFromLock_RejectsDisallowedURL. The expanded reservedCredentialKeys blocklist is comprehensive and case-insensitive (strings.ToUpper). ImportProfile handles idempotency correctly. sandboxProviderNames() correctly limits sandbox scope to harness-declared + URL-resolved provider names. Profile/provider lock-file reconstruction is well-tested. The ResolveResult struct is a clean evolution of the return type. Base composition merge for profiles follows the established child-wins pattern.

However, the blocking validation bug that prevents URL-based providers from working end-to-end persists, URL-fetched provider definitions are still not validated against the validProviderName regex, and the reservedSandboxKeys blocklist is significantly narrower than the new reservedCredentialKeys blocklist.

Findings

High

  • [logic-error] internal/harness/harness.go:401Validate() rejects URL-based providers before ResolveHarness can process them. The provider validation loop at line 401 checks every entry in h.Providers against validProviderName (regex ^[a-zA-Z0-9_-]+$) with no IsURL guard. URL entries like https://github.com/org/repo/tree/main/providers/my.yaml#sha256=... fail this check. Validate() is called from LoadWithBase (compose.go line 181) BEFORE ResolveHarness() strips URL entries. This makes the URL provider feature non-functional — any harness declaring a URL-based provider will fail to load with an "invalid characters" error. Note: the agent field already has an IsURL skip at line 374, but providers do not. ValidateResourceTypes() (called later within Validate at line 426) correctly handles URL providers, but execution never reaches it.
    Remediation: Add if IsURL(p) { continue } before the validProviderName check at line 402, matching the pattern used for agents at line 374.

Medium

  • [missing-input-validation] internal/resolve/resolve.go — URL-fetched providers are validated only for non-empty name and type fields. Unlike local providers loaded via LoadProviderDefs (harness.go:86-93), URL-fetched providers are NOT validated against the validProviderName regex (^[a-zA-Z0-9_-]+$). A URL-fetched provider YAML could contain a name or type with special characters (spaces, slashes, leading hyphens like --flag) that pass resolution but cause issues when passed to openshell CLI commands via buildProviderArgs. The same gap exists in resolveFromLock (lock.go).
    Remediation: Add validProviderName regex checks for def.Name and def.Type in the URL-fetched provider resolution code in both resolve.go and lock.go, matching LoadProviderDefs validation.

  • [inconsistent-blocklist] internal/sandbox/sandbox.go / internal/cli/run.go:1547reservedCredentialKeys (sandbox.go, 30 entries) and reservedSandboxKeys (run.go, 13 entries) serve related purposes — both prevent env var overrides in the openshell child process — but remain unsynchronized. The credential blocklist includes HTTP_PROXY, HTTPS_PROXY, NODE_OPTIONS, PYTHONPATH, PYTHONSTARTUP, IFS, CDPATH, JAVA_TOOL_OPTIONS, RUBYOPT, PERL5OPT, LD_AUDIT, DYLD_INSERT_LIBRARIES, SSL_CERT_FILE, SSL_CERT_DIR, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS, HOSTALIASES, GIT_CONFIG_GLOBAL, GIT_EXEC_PATH, PROMPT_COMMAND, NO_PROXY, ALL_PROXY — all absent from reservedSandboxKeys. A harness author could inject these via env.sandbox in the harness YAML, bypassing the credential blocklist. Additionally, reservedSandboxKeys uses case-sensitive lookup (reservedSandboxKeys[k]) while reservedCredentialKeys correctly uses strings.ToUpper(k) for case-insensitive matching.
    Remediation: Unify or cross-reference the two blocklists. Extract a shared set of security-sensitive env var names, or add the missing entries to reservedSandboxKeys. Apply case-insensitive comparison to reservedSandboxKeys to match the credential blocklist pattern.

Low

  • [edge-case] internal/cli/run.go (sandboxProviderNames) — Does not deduplicate when a URL-resolved provider has the same name as a harness-declared local provider. mergeProviderDefs correctly shadows the URL-resolved ProviderDef with the local one for gateway registration, but sandboxProviderNames independently concatenates both lists, potentially producing duplicate --provider flags to CreateWithRetry.

  • [missing-test] internal/cli/run_test.go — No integration-level test verifying that URL-resolved provider names flow through to CreateWithRetry. TestSandboxProviderNames and TestCheckProviderProfileIntegrity test helpers in isolation but do not exercise the full runAgent path from URL-resolved providers through to sandbox creation.

  • [logic-error] internal/sandbox/sandbox.go (EnsureProvider, updateProvider) — Both functions accept a ctx context.Context parameter but immediately shadow it with ctx, cancel := context.WithTimeout(context.Background(), providerTimeout). The passed-in context (from run.go's CLI context) is never used as the parent for the timeout, so parent context cancellation does not propagate to the openshell subprocess. ImportProfile correctly uses the passed context.

  • [behavior-change] internal/cli/run.goLoadProviderDefs is now called without the declared filter. Previously, only harness-declared providers were loaded and created on the gateway. Now ALL providers from the providers/ directory are loaded and created, even undeclared ones. While undeclared providers are correctly excluded from sandbox attachment via sandboxProviderNames, this widens gateway-side provider creation scope. The "provider declared but no definition found" warning was also removed.

  • [missing-test] internal/harness/harness_test.go — No test verifies that Validate() rejects a harness with URL-based providers. Such a test would demonstrate the HIGH finding and serve as a regression test once the IsURL guard is added. (ValidateResourceTypes tests were added but do not cover the Validate() code path.)

  • [internal-inconsistency] docs/ADRs/0068-portable-provider-profile-resolution.md — The frontmatter correctly says title: "68. Portable provider and profile resolution" but the markdown heading reads # 66. Portable provider and profile resolution. The file was renumbered from 0066 to 0068 to avoid collision, but the heading was not updated.

  • [doc-staleness] docs/plans/universal-harness-access*.md — Plan documents reference the old ResolveHarness return type ([]Dependency, error) in multiple locations. The return type changed to (ResolveResult, error) in this PR. While plan documents may be treated as historical, the stale signature references could mislead future readers.

@maruiz93

maruiz93 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Keeping warn-only as designed for WarnLiteralCredentials. A hard error would produce false positives for compound ${VAR} expressions (e.g., ${HOST}:${PORT}) and brace-less $VAR syntax — both are legitimate credential value patterns. The current regex requires the entire value to be a single ${VAR} reference, which is intentionally strict as a warning but would be too aggressive as a hard stop.

Regarding checkProviderProfileIntegrity warn vs error: this is intentional per ADR 0066 §Referential Integrity. Providers can exist without profiles (e.g., local providers whose types are validated by the gateway at creation time). The warning covers the case where URL-resolved providers exist but no URL-resolved profiles are declared — integrity can't be verified ahead of time, but this isn't necessarily an error.

All other findings addressed in 5bcc974:

  • Renamed profilesopenshell-profiles (YAML field, errors, tests, ADR 0066) to disambiguate the overloaded term
  • Extended reservedCredentialKeys with IFS, CDPATH, DYLD_INSERT_LIBRARIES, JAVA_TOOL_OPTIONS, RUBYOPT, PERL5OPT
  • Added TestResolveHarness_ProviderURLMissingType
  • Fixed ImportProfile duplicate check to use "profile already exists" instead of broad "duplicate"
  • Added ADR cross-references from 0024 and 0038 to 0066
  • Clarified "policy" as "openshell policy" in ADR 0066
  • Documented openshell-profiles and URL-based providers in user guide
  • Added ImportProfile step to sandbox lifecycle in cli-internals.md

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure · Started 3:45 PM UTC · Completed 4:00 PM UTC
Commit: 5bcc974 · View workflow run →

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Squad Report — #3062

Agents dispatched: 5 (claude-coder ×2, claude-researcher, gemini-code-review, cursor-code-review)
Models used: Claude, Gemini, Codex
Total findings: 14 (after dedup + verification) — 6 posted inline (MEDIUM+)
False positives removed: 5

Summary

Solid, well-structured PR that cleanly extends the existing resolution system. The strongest finding is the incomplete reserved credential key blocklist (missing LD_AUDIT and TLS trust chain variables), which 4/5 agents flagged independently. The h.Profiles not being cleared after resolution is a real asymmetry with h.Providers that should be fixed for consistency. Remaining findings are hardening opportunities around error message clarity and warning-vs-error enforcement for literal credentials.

Assisted-by: Claude (review), Gemini (review), Codex (review)

Comment thread internal/sandbox/sandbox.go
Comment thread internal/resolve/resolve.go
Comment thread internal/cli/run.go
Comment thread internal/resolve/resolve.go
Comment thread internal/cli/lock.go
Comment thread docs/ADRs/0068-portable-provider-profile-resolution.md
@maruiz93

maruiz93 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Addressed findings from this review in 401384a:

  • [logic-error] critical — URL-resolved provider names now passed to CreateWithRetry via collectProviderNames(allDefs). Added TestCollectProviderNames covering local+URL, URL-only, local-only, and empty cases.
  • [inconsistent-blocklist] — Noted; reservedSandboxKeys synchronization deferred (separate concern from this PR's scope).
  • [missing-doc] architecture.md — Will add ADR 0066 reference.

Remaining low findings are either already addressed (ImportProfile duplicate check, ADR cross-refs) or intentional per ADR 0066 (WarnLiteralCredentials warn-only, checkProviderProfileIntegrity warn for no-profiles case, lock deps[len-1] pattern).

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 9:50 AM UTC · Ended 10:06 AM UTC
Commit: e8381e3 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:06 AM UTC · Completed 10:20 AM UTC
Commit: 5a56f20 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment component/harness Agent harness, config, and skills loading component/sandbox OpenShell sandbox environment and removed requires-manual-review Review requires human judgment labels Jul 7, 2026
@maruiz93 maruiz93 requested a review from ifireball July 7, 2026 10:44
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:14 AM UTC · Completed 7:24 AM UTC
Commit: 6fab326 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed requires-manual-review Review requires human judgment labels Jul 10, 2026
@maruiz93 maruiz93 force-pushed the 2672-portable-providers branch from 6fab326 to 215edb0 Compare July 10, 2026 07:32
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:32 AM UTC · Completed 7:46 AM UTC
Commit: 215edb0 · View workflow run →

@maruiz93 maruiz93 requested a review from waynesun09 July 10, 2026 07:34
@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed requires-manual-review Review requires human judgment labels Jul 10, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:34 AM UTC · Completed 9:48 AM UTC
Commit: 8def383 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed requires-manual-review Review requires human judgment labels Jul 10, 2026
maruiz93 and others added 9 commits July 10, 2026 12:13
Add Profiles []string field to Harness struct for URL-referenced
openshell profile definitions. Extend ValidateResourceTypes to
require integrity hashes on profile and provider URLs. Local
provider names pass through unchanged.

Signed-off-by: Marta Anon <manon@redhat.com>
Add profiles to mergeBaseIntoChild using the same concatenation
pattern as skills and providers (base + child).

Signed-off-by: Marta Anon <manon@redhat.com>
Add profile and provider URL resolution to ResolveHarness.
Profiles are fetched, cached, and validated for a non-empty id.
Provider URLs are fetched, cached, parsed as ProviderDef, and
removed from h.Providers (leaving only local names). Credential
values that don't look like ${VAR} references produce a warning.

Changes:
- Add ResolveResult struct containing Deps, Profiles, Providers
- Add ResolvedProfile and ResolvedProvider types
- Change ResolveHarness return type from ([]Dependency, error) to (ResolveResult, error)
- Add profile resolution loop that validates id field
- Add provider resolution loop that validates name/type and checks credentials
- Update all callers in internal/cli/run.go and internal/cli/lock.go
- Update all tests to use new return type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Marta Anon <manon@redhat.com>
Add ImportProfile function that imports an openshell provider profile
from a YAML file path. Treats 'already exists' as success for
idempotent imports.

Signed-off-by: Marta Anon <manon@redhat.com>
Import URL-resolved profiles to the gateway before provider
creation. Merge URL-resolved providers with local definitions
(local wins on name collision). Validate referential integrity:
every provider type must match a declared profile id.

Signed-off-by: Marta Anon <manon@redhat.com>
Extends ADR 0038 (URL harness access) and ADR 0065 (provider
composition) with URL-resolvable profiles and providers. Adds
profiles field, extends providers to accept URLs, defines
referential integrity validation and base merge semantics.

Signed-off-by: Marta Anon <manon@redhat.com>
- Rename profiles → openshell-profiles (YAML field, error messages,
  tests, ADR 0066) to disambiguate from overloaded "profile" term
- Extend reservedCredentialKeys with IFS, CDPATH, DYLD_INSERT_LIBRARIES,
  JAVA_TOOL_OPTIONS, RUBYOPT, PERL5OPT
- Add TestResolveHarness_ProviderURLMissingType for untested validation
- Fix ImportProfile duplicate check: "profile already exists" instead
  of overly broad "duplicate" substring
- Add ADR cross-references from 0024 and 0038 to 0066
- Clarify "policy" as "openshell policy" in ADR 0066
- Document openshell-profiles and URL-based providers in user guide
- Add ImportProfile step to sandbox lifecycle in cli-internals.md

Signed-off-by: Marta Anon <marta@fullsend.ai>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Marta Anon <manon@redhat.com>
- Pass URL-resolved provider names to CreateWithRetry so sandboxes
  can actually use them (critical bug fix)
- Extend reservedCredentialKeys with LD_AUDIT, TLS trust chain vars,
  HOSTALIASES, PYTHONSTARTUP, GIT_CONFIG_GLOBAL, GIT_EXEC_PATH
- Clear h.Profiles after resolution for consistency with h.Providers
- Report all provider-profile integrity mismatches (not just first)
  with improved error message for gateway-resident profiles
- Lock resolution now validates URLs against allowed_remote_resources

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Marta Anon <manon@redhat.com>
Cover three gaps found during test coverage review:
- dedupResolvedProviders: empty, single, no-dups,
  last-wins (mirrors dedupResolvedProfiles)
- ImportProfile: success, idempotent "already exists",
  other-error propagation
- ParseProfileID: valid, missing id, invalid YAML,
  empty input

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Marta Anon <manon@redhat.com>
@maruiz93 maruiz93 force-pushed the 2672-portable-providers branch from 8def383 to f839bdf Compare July 10, 2026 10:13
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:14 AM UTC · Completed 10:29 AM UTC
Commit: f839bdf · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed requires-manual-review Review requires human judgment labels Jul 10, 2026
waynesun09

This comment was marked as outdated.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-agent review at HEAD f839bdf9. Findings were cross-checked against existing review threads; only unique MEDIUM+ items are posted inline below.

Comment thread internal/cli/run.go
for _, rp := range result.Profiles {
profileStart := time.Now()
printer.StepStart("Importing profile: " + rp.ID)
if err := sandbox.ImportProfile(ctx, rp.LocalPath); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] URL-resolved profiles are imported here (step 2c) before EnableProvidersV2() runs in step 2d. The pre-existing directory-profile import (ImportProfiles) deliberately runs after v2 is enabled inside the guarded block, and profiles are a providers-v2 concept (ADR 0065). Additionally, because the step 2d block is gated on len(h.Providers) > 0 || len(result.Providers) > 0, a harness that declares only openshell.profiles and no providers never enables providers-v2 at all — yet its profiles were already imported here.

Suggestion: Move EnableProvidersV2() ahead of this profile-import loop and extend the gate with || len(result.Profiles) > 0 so profiles-only harnesses still enable v2. A runAgent-level test for the profiles-only path would catch this ordering.

// openshell exits non-zero when the profile already exists;
// treat this as success (idempotent import).
outStr := strings.ToLower(string(out))
if strings.Contains(outStr, "profile already exists") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] ImportProfile treats "profile already exists" as success without updating. Since the cache is content-addressed by SHA-256, a changed profile with the same id is fetched to a new local path, but the gateway silently keeps the stale definition (old credential schema/endpoints). The directory flow (ImportProfiles) deletes by id and re-imports so content changes propagate; the URL flow does not. Low impact on fresh CI gateways, but a hard-to-diagnose staleness bug on persistent/local gateways.

Suggestion: Mirror the directory flow (delete by id, then re-import), or document that URL profile content changes require a gateway reset. Note also the idempotency substring here ("profile already exists") is narrower than the "already exists" match used by ImportProfiles.

if reservedCredentialKeys[strings.ToUpper(k)] {
return fmt.Errorf("provider %q: credential key %q is a reserved environment variable name", name, k)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] This reserved-credential-key rejection applies to all providers, not just URL-fetched ones — local providers/*.yaml definitions also flow through EnsureProvider (via mergeProviderDefs in run.go), and the check is case-insensitive. An existing local provider using a key like ENV, PATH, or HTTP_PROXY as a credential name now hard-fails where it previously worked, which contradicts ADR 0068's "Fully backwards-compatible" claim (the doc comment above frames the threat model as URL-fetched definitions).

Suggestion: Either scope the check to URL-resolved providers only, or keep the broader hardening and call it out in ADR 0068 / the changelog as an intentional behavior change with the list of blocked key names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/harness Agent harness, config, and skills loading component/sandbox OpenShell sandbox environment requires-manual-review Review requires human judgment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provider and profile definitions cannot be resolved from URL-referenced base harnesses

3 participants