feat: 3-tier provider credential resolution (env var → .env → stored config)#3211
feat: 3-tier provider credential resolution (env var → .env → stored config)#3211TheArchitectit wants to merge 3 commits into
Conversation
…config) Before this change, the setup wizard (PR ultraworkers#3017) saved provider config to ~/.claw/settings.json, but no provider ever read it back. The wizard was dead code: users who ran it saw no effect. This commit implements the 3-tier credential resolution chain that makes stored provider config functional: 1. Environment variable (highest priority, immediate override) - ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, DASHSCOPE_API_KEY - ANTHROPIC_BASE_URL, OPENAI_BASE_URL, etc. 2. .env file in the current working directory (via existing dotenv_value) 3. Stored provider config in ~/.claw/settings.json (lowest priority) - Reads provider.kind, provider.apiKey, provider.baseUrl - Only returned when stored kind matches the provider being resolved Changes: - RuntimeProviderConfig: new struct with kind/api_key/base_url fields parsed from the 'provider' JSON object in settings.json - RuntimeFeatureConfig: added provider field - RuntimeConfig::provider(): accessor for stored provider config - parse_optional_provider_config(): extracts provider section from merged settings - read_env_or_config() in mod.rs: core 3-tier resolution function - read_base_url_from_config() in mod.rs: base URL resolution from stored config - AuthSource::from_env_or_saved() in anthropic.rs: now actually reads from stored config (tier 3) when env vars are absent - resolve_startup_auth_source(): same 3-tier treatment - has_auth_from_env_or_saved(): also checks stored config - read_base_url() in anthropic.rs: 3-tier base URL resolution - OpenAiCompatClient::from_env_or_saved(): new method with 3-tier resolution for OpenAI/xAI/DashScope providers - read_base_url() in openai_compat.rs: 3-tier base URL resolution - 3 new tests for RuntimeProviderConfig parsing The setup wizard saves settings that providers now actually consume. Users without env vars can configure providers entirely through the wizard or by editing ~/.claw/settings.json manually.
|
This makes the setup wizard actually functional — good catch that #3017 saved config but nothing read it. The 3-tier priority (env var → .env → stored) is well thought out, and covering all four providers (Anthropic, OpenAI, xAI, DashScope) with both API key and base URL resolution is thorough.\n\nMinor concern: the rom_env_or_saved naming across multiple provider files could be consolidated into a shared trait or helper to reduce duplication. Also, cargo fmt is failing — a quick cargo fmt --all before merge will fix that. |
Addresses reviewer feedback on ultraworkers#3211 requesting consolidation of duplicated credential-resolution logic across provider files. Changes: 1. Extract read_env_non_empty() into mod.rs - Previously duplicated identically in anthropic.rs (line 773) and openai_compat.rs (line 1608). - Both copies read a process env var, fall back to .env via dotenv_value(), and return Result<Option<String>, ApiError>. - Now defined once as pub(crate) in mod.rs; both provider files call super::read_env_non_empty() instead. 2. Extract resolve_base_url() into mod.rs - Both anthropic::read_base_url() and openai_compat::read_base_url() implemented the same 3-tier base URL resolution: Tier 1: process env var (e.g. ANTHROPIC_BASE_URL) Tier 2: .env file via dotenv_value() Tier 3: stored config via read_base_url_from_config() Default: provider-specific fallback string - New shared helper: resolve_base_url(env_var, provider_kind, default) - anthropic::read_base_url() now delegates to super::resolve_base_url("ANTHROPIC_BASE_URL", "anthropic", DEFAULT_BASE_URL) - openai_compat::read_base_url(config) now delegates to super::resolve_base_url(config.base_url_env, provider_kind, config.default_base_url) 3. Preserve from_env_or_saved() naming - The "_or_saved" suffix distinguishes the 3-tier credential path (env + .env + stored config) from the 2-tier from_env() path (env + .env only). Both methods exist on the same AuthSource type and serve different callers. Renaming would lose this semantic distinction. CI: cargo fmt --all run to fix the formatting check failure.
…lpers Resolved merge conflicts between worktree-provider-config-fallback and upstream/main while preserving: 1. Shared credential helpers in rust/crates/api/src/providers/mod.rs: - read_env_non_empty() — extracted from anthropic.rs + openai_compat.rs - resolve_base_url() — 3-tier URL resolution shared across providers 2. RuntimeProviderConfig in rust/crates/runtime/src/config.rs: - struct with kind, api_key, base_url fields - impl block with from_provider_object(), kind(), api_key(), base_url(), is_set() - save_user_provider_settings() and clear_user_provider_settings() helpers - All provider_config_* tests Also merged upstream additions: - RulesImportConfig enum + impl with should_import() - RuntimeHookCommand struct + RuntimeHookConfig hooks updates - All rules_import_* tests - ConfigFileStatus/ConfigFileReport/ConfigInspection structured reporting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Update: resolved reviewer feedback + merged upstream/main Ask 1 — Consolidate
|
Problem
PR #3017 merged
setup_wizard.rswhich saves provider config (kind, apiKey, baseUrl, model) to~/.claw/settings.json. However, no provider ever reads that stored config back. The credential resolution path is purely env-var-based:AuthSource::from_env_or_saved()— Despite its name, never reads from stored configresolve_startup_auth_source()— Discards its OAuth config callback (let _ = load_oauth_config;) and reads only env varsOpenAiCompatClient::from_env()— Reads only from env varsread_base_url()(both providers) — Reads only from env varsResult: Users who run the setup wizard see no effect. The wizard is dead code.
Solution
Implement a 3-tier credential resolution chain:
.envfile in current working directory~/.claw/settings.jsonHow it works
The stored provider kind must match the provider being resolved for — if settings.json has
kind: "xai"and we are resolving credentials for the Anthropic provider, tier 3 returnsNone. This prevents cross-provider credential leakage.Changes
New types
RuntimeProviderConfig— struct withkind,api_key,base_urlfields parsed from the"provider"JSON object in settings.jsonRuntimeConfig::provider()— accessor for stored provider configparse_optional_provider_config()— extracts provider section from merged settingsUpdated functions
AuthSource::from_env_or_saved()— Now actually reads from stored config (tier 3) when env vars are absent. Previously the "or_saved" name was aspirational — the function was identical tofrom_env().resolve_startup_auth_source()— Same 3-tier treatment. No longer discards the OAuth config callback (kept for API compatibility).has_auth_from_env_or_saved()— Also checks stored configread_base_url()(anthropic.rs) — 3-tier base URL resolutionread_base_url()(openai_compat.rs) — 3-tier base URL resolutionNew functions
read_env_or_config()(mod.rs) — Core 3-tier credential resolutionread_base_url_from_config()(mod.rs) — Base URL resolution from stored configOpenAiCompatClient::from_env_or_saved()— 3-tier resolution for OpenAI/xAI/DashScope providers (analogous to the Anthropic path)Tests
3 new tests for
RuntimeProviderConfigparsing:provider_config_default_is_empty_when_unset— empty settings → all fields Noneprovider_config_parses_kind_api_key_and_base_url— full provider section parsed correctlyprovider_config_handles_partial_provider_object— only kind+apiKey, no baseUrlDiff verification
All additions, no deletions. No upstream commits are reverted. The diff is feature-only.
Settings.json structure
{ "provider": { "kind": "anthropic", "apiKey": "sk-ant-...", "baseUrl": "https://api.anthropic.com" }, "model": "sonnet" }This is exactly the format that
save_user_provider_settings()(already on main from PR #3017) writes.Testing