Skip to content

feat(providers): derive discovery from provider profiles #1460

@johntmyers

Description

@johntmyers

Problem Statement

Provider discovery should be driven by provider profiles wherever possible instead of requiring a hard-coded Rust ProviderPlugin and ProviderDiscoverySpec per provider. PR #1313 (docker-agent) shows the current failure mode: adding one partner/provider requires a new YAML profile plus provider-specific Rust discovery, registry registration, alias handling, command detection, and tests. Medium term, partners such as Docker should be able to publish provider profiles from their own repositories and have OpenShell discover credentials, binaries, and command intent generically.

Technical Context

The current v1 discovery path is mostly a thin wrapper around environment-variable lookup. Most provider plugins define a ProviderDiscoverySpec { id, credential_env_vars } and call discover_with_spec, while v2 provider profiles already model the same credential env vars and policy binaries. That means the profile model has enough information for static env-var discovery today, but the CLI and TUI still use the hard-coded registry for provider type lists, credential env vars, --from-existing, and auto-provider command inference.

PR #1313 adds Docker Agent by extending both systems: a v2 profile in providers/docker-agent.yaml, plus v1-style Rust provider discovery and hard-coded command detection for docker agent. The useful behavior is generic: discover credential env vars from the profile, optionally detect local binaries, and infer provider type from command metadata.

Affected Components

Component Key Files Role
Provider discovery crate crates/openshell-providers/src/lib.rs, crates/openshell-providers/src/discovery.rs, crates/openshell-providers/src/profiles.rs Owns provider plugins, discovery specs, built-in profiles, profile DTOs, and normalization helpers.
CLI provider creation/autocreation crates/openshell-cli/src/run.rs Uses provider discovery for provider create --from-existing, provider update --from-existing, sandbox auto-provider creation, and command inference.
TUI provider creation crates/openshell-tui/src/app.rs Lists provider types and performs autodetection from the hard-coded registry.
Gateway policy composition crates/openshell-server/src/grpc/policy.rs Already composes policy from built-in or custom provider profiles once provider records exist.
Partner/provider profile data providers/*.yaml Defines credentials, endpoints, binaries, and categories for profile-backed provider v2 behavior.

Technical Investigation

Architecture Overview

Provider v1 discovery is local and registry-driven. ProviderRegistry::new() registers hard-coded plugins; callers ask the registry to discover credentials for a provider type. discover_with_spec then scans a static env-var list and returns a DiscoveredProvider with credential keys matching env-var names.

Provider v2 profiles are data-driven. Built-in YAML profiles are loaded through profiles.rs, custom profiles can be imported into the gateway store, and policy composition uses provider type/profile IDs to construct provider policy layers. The profile DTO already exposes credential_env_vars() by flattening credentials[*].env_vars, and profile binaries are preserved through proto conversion.

The missing bridge is discovery. CLI/TUI code does not ask profiles how to discover a provider. It asks ProviderRegistry, so every new discoverable provider still needs Rust registration even if the profile already contains the credential env vars and policy binaries.

Code References

Location Description
crates/openshell-providers/src/lib.rs:46 ProviderDiscoverySpec only stores an id and env-var names. This duplicates profile credential env vars.
crates/openshell-providers/src/lib.rs:81 ProviderRegistry::new() hard-registers every supported discovery plugin. Partners cannot extend this without a core PR.
crates/openshell-providers/src/lib.rs:111 ProviderRegistry::discover_existing() fails with UnsupportedProvider for types that are not registered as plugins.
crates/openshell-providers/src/lib.rs:136 known_types() returns plugin keys, not built-in or imported provider profiles.
crates/openshell-providers/src/lib.rs:143 normalize_provider_type() is a hard-coded alias map. Custom provider profile IDs are not normalized here.
crates/openshell-providers/src/lib.rs:162 detect_provider_from_command() only inspects the first command basename, so shapes like docker agent need provider-specific Rust special-casing.
crates/openshell-providers/src/discovery.rs:6 discover_with_spec() is generic env-var scanning over ProviderDiscoverySpec. This can be profile-backed.
crates/openshell-providers/src/profiles.rs:283 ProviderTypeProfile::credential_env_vars() already derives the env-var scan list from profile credentials.
crates/openshell-cli/src/run.rs:3419 Sandbox command inference calls the hard-coded detect_provider_from_command() helper.
crates/openshell-cli/src/run.rs:3549 auto_create_provider() uses ProviderRegistry::discover_existing() and cannot discover custom profile-backed providers.
crates/openshell-cli/src/run.rs:4058 provider_create() can resolve custom profiles, but --from-existing still calls the hard-coded registry.
crates/openshell-cli/src/run.rs:4862 provider update --from-existing also uses hard-coded registry discovery.
crates/openshell-tui/src/app.rs:1769 TUI provider creation lists ProviderRegistry::known_types(), not provider profiles.
crates/openshell-tui/src/app.rs:1810 TUI credential prompts come from registry env vars instead of profile credentials.
crates/openshell-tui/src/app.rs:1849 TUI autodetect calls registry discovery.
crates/openshell-server/src/grpc/policy.rs:544 Gateway policy composition already resolves built-in or custom profiles from provider type/profile ID.

Current Behavior

A simple provider currently requires multiple core-code touch points:

  1. Add profile YAML if it should participate in providers v2 policy composition.
  2. Add a Rust provider module with a ProviderDiscoverySpec and ProviderPlugin implementation for env scanning.
  3. Register the plugin in ProviderRegistry::new().
  4. Update normalize_provider_type() for aliases.
  5. Update detect_provider_from_command() if command inference is not a one-token executable basename.
  6. Update CLI/TUI tests for the new hard-coded provider.

This is what PR #1313 does for Docker Agent. The actual desired discovery behavior is profile-shaped: DOCKER_ACCESS_TOKEN is an optional credential env var, and Docker-related binaries/commands indicate the provider may be useful even without a static token.

What Would Need to Change

Provider discovery should gain a generic profile-backed path:

  • Add a discover_from_profile(profile, context) helper that reads profile.credentials[*].env_vars and stores discovered values under the actual env-var key so existing injection behavior remains compatible.
  • Extend DiscoveryContext with binary/command existence checks if binary presence should be a discovery signal. PR feat(providers): add Docker Agent provider #1313's path_exists() is directionally useful, but the API should be shaped around profile discovery semantics rather than one provider.
  • Decide whether profile.binaries should double as host discovery paths. These paths are currently policy enforcement paths inside the sandbox; using them for host discovery is convenient but semantically leaky. A small explicit discovery block in the profile may be cleaner for command matching and host-side aliases.
  • Add generic command inference from profile metadata so profiles can express command shapes such as docker agent, not only executable basenames.
  • Change CLI provider create --from-existing, provider update --from-existing, and sandbox auto-provider creation to resolve the provider profile first and then run generic profile discovery. Keep plugin discovery as a fallback for advanced legacy cases.
  • Change TUI provider creation to list available profiles and derive credential rows/autodetect behavior from profile metadata instead of ProviderRegistry::known_types().
  • Keep provider-specific plugins only for discovery that cannot be expressed in profile data, such as reading provider-specific config files or future local OAuth cache importers.

Alternative Approaches Considered

  • Keep adding provider plugins: Lowest immediate implementation cost per provider, but it keeps every partner profile blocked on a core OpenShell PR and duplicates v2 profile metadata.
  • Use existing binaries directly for discovery: Minimal schema change, but conflates sandbox policy binaries with host discovery paths and does not model multi-token command shapes like docker agent cleanly.
  • Add explicit profile discovery metadata: More schema work, but it separates policy enforcement from local discovery and lets profiles express env vars, binary/path checks, command prefixes, and tokenless optional-credential discovery in a portable way.
  • Hybrid approach: Use profile credentials for generic env-var discovery immediately, keep plugin fallback for non-generic discovery, and add explicit discovery metadata for command/binary inference. This is the recommended direction.

Patterns to Follow

  • The providers v2 profile DTO pattern in profiles.rs should remain the source of truth for profile-defined fields and proto conversion.
  • Existing credential injection stores provider credentials by env-var key, so generic discovery should preserve keys like GITHUB_TOKEN, ANTHROPIC_API_KEY, or DOCKER_ACCESS_TOKEN rather than storing logical credential names.
  • Gateway policy composition already supports custom profile IDs; CLI/TUI changes should align with that existing server behavior instead of introducing a second notion of custom provider type.
  • Legacy provider aliases should continue to work for backwards compatibility, but aliases should not be the only way to discover or create providers.

Proposed Approach

Move common discovery to profile-backed helpers and treat hard-coded plugins as legacy or advanced-provider fallbacks. Profiles should be sufficient for the common partner use case: declare credentials, optional credential behavior, binaries/endpoints for policy, and optional command discovery metadata. CLI and TUI should resolve provider profiles from built-ins and the gateway store, then use the generic profile discovery path for --from-existing, provider auto-create, and credential prompts.

This would let a partner ship a provider profile without adding a Rust plugin for basic env-var discovery and command/binary matching. PRs like #1313 could shrink to profile data plus tests, or move entirely to partner-owned profile repositories once external profile distribution exists.

Scope Assessment

  • Complexity: Medium
  • Confidence: High for env-var discovery; medium for command/binary discovery schema because it needs a product decision
  • Estimated files to change: 6-10 core files plus tests
  • Issue type: feat

Decisions

  • Add an explicit profile discovery section for credential discovery instead of dual-purposing policy binaries.
  • Use credential-name references in discovery.credentials rather than duplicating env vars. credentials[*].env_vars remains the source of truth for which local env vars are scanned.
  • Scope providers v2 discovery to explicit credential discovery only. Do not add command inference or auto-attach semantics for providers v2.
  • Preserve current --from-existing behavior: fail when no credential or config material is discovered, even when profile credentials are optional. Empty providers should be created through explicit non-discovery flows.
  • CLI/TUI may fetch profiles from the gateway when needed. For CLI, this is acceptable for provider create --type <profile> --from-existing and related explicit provider operations.
  • When providers v2 is enabled, profile-backed discovery should be the discovery path. Do not fall back to v1 provider-plugin discovery for v2 profiles.
  • Do not introduce aliases for partner/custom profiles. Use exact profile IDs.
  • Keep provider attachment explicit in providers v2. Profile-backed discovery must not infer or attach providers from sandbox commands.
  • TUI support can land after the CLI/gateway implementation if needed.

Risks & Open Questions

  • Confirm whether the final proto field names should mirror the YAML shape exactly or use more explicit generated names. The intended YAML shape is credential-name based:

    credentials:
      - name: docker_access_token
        env_vars:
          - DOCKER_ACCESS_TOKEN
        required: false
    
    discovery:
      credentials:
        - docker_access_token

    discovery.credentials references credentials[*].name; OpenShell then scans the referenced credential's env_vars. Do not duplicate env vars directly in discovery.

  • Should any legacy v1 command inference remain available only for legacy provider paths, or should enabling providers v2 disable it entirely for provider attachment?

  • How should custom provider profile fetch failures be surfaced in explicit CLI flows: unsupported profile, gateway unavailable, or profile missing?

Test Considerations

  • Unit test discover_from_profile() for env-var discovery, duplicate env vars, required vs optional credentials, empty env values, and tokenless optional-credential discovery.
  • Unit test binary/command discovery once the discovery metadata shape is chosen, including docker agent style command prefixes.
  • CLI integration coverage for provider create --from-existing using an imported custom profile with profile-defined env vars.
  • CLI integration coverage for provider update --from-existing using profile-derived discovery.
  • Sandbox auto-provider coverage proving command inference can create/attach a profile-backed provider without a hard-coded registry plugin.
  • TUI tests or focused unit coverage proving provider type selection and credential prompts are derived from profiles.
  • Regression tests for existing built-in providers and aliases such as gh, glab, and claude.

Created by spike investigation. Use build-from-issue to plan and implement.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions