Skip to content

feat: add .env file support for provider credentials#26

Merged
dvaJi merged 3 commits intomainfrom
feat/dotenv-support
Mar 24, 2026
Merged

feat: add .env file support for provider credentials#26
dvaJi merged 3 commits intomainfrom
feat/dotenv-support

Conversation

@dvaJi
Copy link
Copy Markdown
Owner

@dvaJi dvaJi commented Mar 23, 2026

Summary

  • Automatically loads .env files from current directory and up to 3 parent directories (monorepo-friendly)
  • Adds --no-env global flag to skip .env loading and use credentials manager only
  • Credentials priority (lowest to highest): .envcredentials.toml → keychain

Supported environment variables

Provider Environment Variable
OpenRouter OPENROUTER_API_KEY
Fal.ai FALAI_API_KEY
Replicate REPLICATE_API_TOKEN
WaveSpeed WAVESPEED_API_KEY

Usage

# Default: loads .env from current/parent directories
infs provider list

# Skip .env, use only credentials manager
infs --no-env provider list

Tests

  • Added unit tests for .env loading (current dir, parent dirs, no file)
  • Added unit tests for env var credential extraction

Summary by CodeRabbit

  • New Features
    • Added a global --no-env flag to disable automatic .env discovery/loading.
    • Implemented automatic .env discovery/loading (current dir → parents) with opt-out.
    • Support for sourcing provider credentials from environment variables and merging into configuration; CLI commands now respect the env-loading flag.
  • Tests
    • Added unit tests for environment credential extraction, merge precedence, and .env discovery scenarios.

- Add dotenvy dependency for .env file parsing
- Search .env from current dir up to 3 parent directories (monorepo-friendly)
- Add --no-env global flag to skip .env loading
- Credentials priority: .env < credentials.toml < keychain
- Support env vars: OPENROUTER_API_KEY, FALAI_API_KEY, REPLICATE_API_TOKEN, WAVESPEED_API_KEY
Copilot AI review requested due to automatic review settings March 23, 2026 15:05
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Adds optional .env discovery/loading and extraction of provider credentials from environment variables, a global --no-env CLI flag to disable this behavior, a new load_config_with_env(load_env: bool) path, and merging semantics where credentials.toml overrides env-derived values.

Changes

Cohort / File(s) Summary
Dependency
Cargo.toml
Added dotenvy = "0.15" dependency for parsing .env files.
CLI Flag
src/cli/mod.rs
Added global --no-env (pub no_env: bool) to control dotenv loading.
Main Integration
src/main.rs
Computed load_env = !cli.no_env, call config::load_dotenv() when enabled, log resolved .env path, and thread load_env into run(...) and downstream handlers.
Config: dotenv discovery & env creds
src/config/mod.rs
Added MAX_ENV_PARENT_DEPTH, PROVIDER_ENV_PATTERNS, load_dotenv(), credentials_from_env(), and load_config_with_env(load_env: bool); refactored load_config() to delegate; added merge helpers with file credentials overwriting env/keychain; included unit tests for .env discovery and env credential merging.
CLI Command Wiring
src/cli/app.rs, src/cli/provider.rs, src/cli/doctor.rs
Updated handle(...) signatures to accept load_env: bool and replaced config::load_config() calls with config::load_config_with_env(load_env) across app, provider, and doctor handlers.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI Parser
    participant Main as main()
    participant Dotenv as load_dotenv()
    participant Config as config::load_config_with_env()
    participant Env as Environment
    participant Keychain as Keychain
    participant File as credentials.toml

    CLI->>Main: Parse args (including --no-env)
    Main->>Dotenv: call load_dotenv() if load_env == true
    Dotenv->>Env: search cwd & parents for .env
    Dotenv-->>Main: return .env path or None
    Main->>Config: call load_config_with_env(load_env)
    Config->>Env: credentials_from_env()
    Env-->>Config: provider credential map
    Config->>Keychain: load keychain credentials
    Keychain-->>Config: keychain credential map
    Config->>File: load credentials.toml
    File-->>Config: file credential map
    Config->>Config: merge maps (file overrides env/keychain)
    Config-->>Main: return AppConfig
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I sniff the dotenv breeze with care,
pull secrets from the hidden air,
a flag to hush the scented trail,
files may win where breezes fail,
config hops home with a happy stare.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding .env file support for provider credentials, which aligns perfectly with the primary objective of the pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dotenv-support

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds .env-based credential loading to the CLI so provider credentials can be supplied via environment variables (optionally discovered from .env files in the current directory or up to 3 parent directories), alongside existing credentials.toml/keychain support.

Changes:

  • Load a .env file from CWD / parent dirs on startup (unless --no-env is set).
  • Merge provider credentials from environment variables into AppConfig during config load.
  • Add dotenvy dependency and unit tests for env extraction + .env discovery.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/main.rs Loads .env at startup, gated by new --no-env flag.
src/config/mod.rs Adds .env search/loading + env-var credential extraction, and introduces load_config_with_env.
src/cli/mod.rs Adds global --no-env CLI flag.
Cargo.toml Adds dotenvy dependency.
Cargo.lock Locks dotenvy and updates dependency graph.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +533 to +571
#[test]
fn test_credentials_from_env_empty() {
std::env::remove_var("OPENROUTER_API_KEY");
std::env::remove_var("FALAI_API_KEY");
std::env::remove_var("REPLICATE_API_TOKEN");
std::env::remove_var("WAVESPEED_API_KEY");

let creds = credentials_from_env();
assert!(creds.is_empty());
}

#[test]
fn test_credentials_from_env_with_values() {
std::env::set_var("OPENROUTER_API_KEY", "test-openrouter-key");
std::env::set_var("FALAI_API_KEY", "test-falai-key");

let creds = credentials_from_env();
assert_eq!(
creds.get("openrouter").and_then(|c| c.get("api_key")),
Some(&"test-openrouter-key".to_string())
);
assert_eq!(
creds.get("falai").and_then(|c| c.get("api_key")),
Some(&"test-falai-key".to_string())
);

std::env::remove_var("OPENROUTER_API_KEY");
std::env::remove_var("FALAI_API_KEY");
}

#[test]
fn test_credentials_from_env_ignores_empty() {
std::env::set_var("OPENROUTER_API_KEY", "");

let creds = credentials_from_env();
assert!(!creds.contains_key("openrouter"));

std::env::remove_var("OPENROUTER_API_KEY");
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

These tests mutate process-global environment variables via std::env::set_var/remove_var without restoring prior values. Because Rust tests run in parallel by default, this can lead to flaky failures and can clobber state for other tests in the same process. Capture previous values and restore them (RAII guard), and/or serialize env-mutating tests behind a global mutex.

Copilot uses AI. Check for mistakes.
Comment on lines +573 to +635
#[test]
fn test_load_dotenv_finds_file_in_current_dir() {
use std::io::Write;
let temp_dir = tempfile::tempdir().unwrap();
let env_path = temp_dir.path().join(".env");
let mut file = std::fs::File::create(&env_path).unwrap();
writeln!(file, "TEST_VAR=test_value").unwrap();

let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();

std::env::remove_var("TEST_VAR");
let loaded_path = load_dotenv();

assert!(loaded_path.is_some());
assert_eq!(
std::env::var("TEST_VAR").ok(),
Some("test_value".to_string())
);

std::env::set_current_dir(original_cwd).unwrap();
std::env::remove_var("TEST_VAR");
}

#[test]
fn test_load_dotenv_searches_parent_dirs() {
use std::io::Write;
let temp_dir = tempfile::tempdir().unwrap();
let env_path = temp_dir.path().join(".env");
let mut file = std::fs::File::create(&env_path).unwrap();
writeln!(file, "PARENT_TEST_VAR=parent_value").unwrap();

let child_dir = temp_dir.path().join("child");
std::fs::create_dir(&child_dir).unwrap();

let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(&child_dir).unwrap();

std::env::remove_var("PARENT_TEST_VAR");
let loaded_path = load_dotenv();

assert!(loaded_path.is_some());
assert_eq!(
std::env::var("PARENT_TEST_VAR").ok(),
Some("parent_value".to_string())
);

std::env::set_current_dir(original_cwd).unwrap();
std::env::remove_var("PARENT_TEST_VAR");
}

#[test]
fn test_load_dotenv_returns_none_when_no_file() {
let temp_dir = tempfile::tempdir().unwrap();

let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();

let loaded_path = load_dotenv();
assert!(loaded_path.is_none());

std::env::set_current_dir(original_cwd).unwrap();
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The load_dotenv tests change the process working directory via set_current_dir, which is also process-global and not safe with parallel test execution. This can make unrelated tests flaky if they assume a stable CWD. Consider serializing the CWD-mutating tests behind a global mutex and restoring the original directory in a drop guard so it’s reset even if the test panics.

Copilot uses AI. Check for mistakes.
src/main.rs Outdated
Comment on lines +26 to +30
if !cli.no_env {
if let Some(env_path) = config::load_dotenv() {
tracing::info!("Loaded .env from: {:?}", env_path);
}
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

--no-env currently only skips load_dotenv() in main, but config loading still reads provider credentials from environment variables because downstream code calls config::load_config() (which hard-codes load_config_with_env(true)). This makes --no-env ineffective for the documented behavior (“use credentials manager only”). Consider threading the flag through so CLI handlers load config via load_config_with_env(!cli.no_env) (or load config once in run and pass it down).

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +224
// Load credentials from environment variables first (lowest priority).
if load_env {
let env_creds = credentials_from_env();
for (provider_id, creds) in env_creds {
let provider_config = config.providers.entry(provider_id).or_default();
for (key, value) in creds {
provider_config.credentials.entry(key).or_insert(value);
}
}
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Env credentials are inserted with or_insert here, but later in load_config_with_env the credentials.toml merge also uses or_insert (see further down in this function). That combination makes env credentials higher priority than credentials.toml, which contradicts the documented precedence .env (lowest) → credentials.toml → keychain. Adjust the merge logic so values from credentials.toml can override env-provided values while still preserving keychain as the highest priority.

Copilot uses AI. Check for mistakes.
Comment on lines 199 to 206
pub fn load_config() -> Result<AppConfig, InfsError> {
load_config_with_env(true)
}

pub fn load_config_with_env(load_env: bool) -> Result<AppConfig, InfsError> {
let config_path = get_config_path()?;

let mut config = if config_path.exists() {
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

load_config() now always calls load_config_with_env(true), so there’s no way for the CLI’s --no-env flag to prevent env-var credential extraction unless callers are updated to use load_config_with_env(false). If --no-env is meant to disable all env-based credentials (not just .env file parsing), consider making load_config() accept an option or ensuring all CLI call sites switch to load_config_with_env(!no_env).

Copilot uses AI. Check for mistakes.
@gemini-code-assist
Copy link
Copy Markdown

Warning

Gemini encountered an error creating the summary. You can try again by commenting /gemini summary.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config/mod.rs`:
- Around line 215-223: The code currently loads .env creds first and then uses
or_insert when applying credentials.toml, preventing file values from overriding
env. To make credentials.toml higher priority than .env, change the insertion
used when processing the credentials.toml block (the code that currently calls
provider_config.credentials.entry(key).or_insert(value) around the
credentials.toml loading) to perform an unconditional insert/overwrite (e.g.,
provider_config.credentials.insert(key, value)) or load the file before env;
keep the env-loading block (credentials_from_env()) using or_insert so env only
supplies defaults.
- Around line 533-635: The tests mutate global process env and CWD causing
races; update the tests (test_credentials_from_env_empty,
test_credentials_from_env_with_values, test_credentials_from_env_ignores_empty,
test_load_dotenv_finds_file_in_current_dir,
test_load_dotenv_searches_parent_dirs,
test_load_dotenv_returns_none_when_no_file) to run serially and to always
restore state: add a global synchronization (e.g., a static Mutex via
lazy_static or once_cell) and acquire it at the start of each test, and wrap
env/CWD changes in a guard that restores the original current_dir and original
env vars in a Drop implementation (or use the serial_test crate with #[serial]
plus explicit restore) so load_dotenv() and credentials_from_env() calls no
longer race with parallel tests.

In `@src/main.rs`:
- Around line 26-30: The --no-env flag currently only skips
config::load_dotenv() but downstream still calls load_config_with_env(true), so
environment variables are still used; propagate the CLI flag by computing a
load_env boolean (e.g., let load_env = !cli.no_env) and pass that into
load_config_with_env(load_env) wherever config loading occurs (replace hardcoded
true), and also use load_env to conditionally call config::load_dotenv()
(instead of checking !cli.no_env in two places) so all env ingestion paths
(load_dotenv() and load_config_with_env) honor the same flag.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2b72372-ccdf-4bcb-9cee-8c6f669854be

📥 Commits

Reviewing files that changed from the base of the PR and between 930cd7a and 8fdc998.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • Cargo.toml
  • src/cli/mod.rs
  • src/config/mod.rs
  • src/main.rs

src/main.rs Outdated
Comment on lines +26 to +30
if !cli.no_env {
if let Some(env_path) = config::load_dotenv() {
tracing::info!("Loaded .env from: {:?}", env_path);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--no-env currently disables .env file loading only, not env credential ingestion.

At Line 26, you gate load_dotenv(), but downstream config loading still defaults to load_config_with_env(true) (see src/config/mod.rs Line 199). So --no-env does not enforce “credentials manager only” if process env vars are set.

[suggested fix] Thread !cli.no_env into config loading paths and use load_config_with_env(load_env) consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 26 - 30, The --no-env flag currently only skips
config::load_dotenv() but downstream still calls load_config_with_env(true), so
environment variables are still used; propagate the CLI flag by computing a
load_env boolean (e.g., let load_env = !cli.no_env) and pass that into
load_config_with_env(load_env) wherever config loading occurs (replace hardcoded
true), and also use load_env to conditionally call config::load_dotenv()
(instead of checking !cli.no_env in two places) so all env ingestion paths
(load_dotenv() and load_config_with_env) honor the same flag.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/config/mod.rs (2)

241-260: ⚠️ Potential issue | 🟠 Major

Keychain still loses to credentials.toml.

This block hydrates keychain first, then merge_file_credentials() overwrites any matching key. That makes credentials.toml the highest-priority source, not the documented .envcredentials.toml → keychain order. A regression test for file-vs-keychain precedence would be worth adding with the fix.

💡 Suggested fix
-    // Load credentials: keychain next (for keys recorded in keychain_credentials),
-    // then fall back to credentials.toml for anything not yet migrated.
-    for (provider_id, provider_config) in config.providers.iter_mut() {
-        for cred_key in &provider_config.keychain_credentials {
-            if let Some(value) = keyring_get(provider_id, cred_key)? {
-                provider_config.credentials.insert(cred_key.clone(), value);
-            }
-        }
-    }
-
     // Merge credentials from separate file (even if config.toml was absent or for
     // providers whose keys are not yet in keychain_credentials).
     let creds_path = get_credentials_path()?;
     if creds_path.exists() {
         let creds_content = std::fs::read_to_string(&creds_path)
@@
         let creds: HashMap<String, ProviderConfig> = toml::from_str(&creds_content)
             .map_err(|e| InfsError::ConfigError(format!("Failed to parse credentials: {}", e)))?;
         merge_file_credentials(&mut config, creds);
     }
+
+    // Load keychain last (highest priority).
+    for (provider_id, provider_config) in config.providers.iter_mut() {
+        for cred_key in &provider_config.keychain_credentials {
+            if let Some(value) = keyring_get(provider_id, cred_key)? {
+                provider_config.credentials.insert(cred_key.clone(), value);
+            }
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/mod.rs` around lines 241 - 260, The current code hydrates keychain
into provider_config.credentials but then calls merge_file_credentials which
overwrites those values, making credentials.toml take precedence; fix by
ensuring keychain wins: either call merge_file_credentials before the keyring
loop or modify merge_file_credentials to only insert keys that do not already
exist in config.providers[*].credentials (i.e., preserve existing entries filled
from keychain), referencing merge_file_credentials, keychain_get,
provider_config.keychain_credentials, provider_config.credentials and
get_credentials_path; add a regression test asserting that a key present in the
keychain is not replaced by credentials.toml.

111-120: ⚠️ Potential issue | 🟠 Major

Env-backed providers never become "connected".

merge_env_credentials() fills a fresh ProviderConfig but leaves connected at false. Downstream status checks still key off connected / get_api_key(), so providers discovered only via .env keep showing up as disconnected. Prefer deriving status from the effective config instead of persisted flags.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/mod.rs` around lines 111 - 120, merge_env_credentials currently
populates a new ProviderConfig but leaves its connected flag false so env-only
providers appear disconnected; update merge_env_credentials (and/or
ProviderConfig initialization logic) so after inserting creds into
provider_config.credentials you derive and set provider_config.connected based
on the effective config (e.g., set connected = true if
provider_config.get_api_key().is_some() or other required fields are present)
instead of relying on a persisted flag — ensure you reference
provider_config.credentials and provider_config.connected (or the get_api_key
method) when computing the status so env-discovered providers report as
connected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config/mod.rs`:
- Around line 220-222: The current load_config() unconditionally calls
load_config_with_env(true), which causes callers (including provider
connect/disconnect paths that write persistent storage) to ingest and then
persist env-only secrets; change load_config() to call
load_config_with_env(false) so environment sources are not loaded by default,
and ensure only explicit callers that should accept env secrets call
load_config_with_env(true) (e.g., interactive or import flows), leaving provider
connect/disconnect to use the non-env default to avoid persisting env-only
secrets.

---

Duplicate comments:
In `@src/config/mod.rs`:
- Around line 241-260: The current code hydrates keychain into
provider_config.credentials but then calls merge_file_credentials which
overwrites those values, making credentials.toml take precedence; fix by
ensuring keychain wins: either call merge_file_credentials before the keyring
loop or modify merge_file_credentials to only insert keys that do not already
exist in config.providers[*].credentials (i.e., preserve existing entries filled
from keychain), referencing merge_file_credentials, keychain_get,
provider_config.keychain_credentials, provider_config.credentials and
get_credentials_path; add a regression test asserting that a key present in the
keychain is not replaced by credentials.toml.
- Around line 111-120: merge_env_credentials currently populates a new
ProviderConfig but leaves its connected flag false so env-only providers appear
disconnected; update merge_env_credentials (and/or ProviderConfig initialization
logic) so after inserting creds into provider_config.credentials you derive and
set provider_config.connected based on the effective config (e.g., set connected
= true if provider_config.get_api_key().is_some() or other required fields are
present) instead of relying on a persisted flag — ensure you reference
provider_config.credentials and provider_config.connected (or the get_api_key
method) when computing the status so env-discovered providers report as
connected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0410ef22-2b6a-40b6-9c5e-90f66893910b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fdc998 and ef0249e.

📒 Files selected for processing (5)
  • src/cli/app.rs
  • src/cli/doctor.rs
  • src/cli/provider.rs
  • src/config/mod.rs
  • src/main.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main.rs

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/config/mod.rs (1)

241-261: ⚠️ Potential issue | 🟠 Major

Credential precedence is still inverted: credentials.toml overrides keychain.

The PR objective states precedence as: .envcredentials.toml → keychain (lowest → highest). However, the current code loads keychain credentials (Lines 243-249) before file credentials (Lines 253-260), and merge_file_credentials uses unconditional insert(). This means credentials.toml values overwrite keychain values, making keychain the lower priority source.

To match the stated precedence, swap the order so keychain loading happens last:

Proposed fix to correct precedence
     // Load credentials from environment variables first (lowest priority).
     if load_env {
         merge_env_credentials(&mut config, credentials_from_env());
     }

-    // Load credentials: keychain next (for keys recorded in keychain_credentials),
-    // then fall back to credentials.toml for anything not yet migrated.
-    for (provider_id, provider_config) in config.providers.iter_mut() {
-        for cred_key in &provider_config.keychain_credentials {
-            if let Some(value) = keyring_get(provider_id, cred_key)? {
-                provider_config.credentials.insert(cred_key.clone(), value);
-            }
-        }
-    }
-
-    // Merge credentials from separate file (even if config.toml was absent or for
-    // providers whose keys are not yet in keychain_credentials).
+    // Merge credentials from separate file next (overrides env).
     let creds_path = get_credentials_path()?;
     if creds_path.exists() {
         let creds_content = std::fs::read_to_string(&creds_path)
             .map_err(|e| InfsError::ConfigError(format!("Failed to read credentials: {}", e)))?;

         let creds: HashMap<String, ProviderConfig> = toml::from_str(&creds_content)
             .map_err(|e| InfsError::ConfigError(format!("Failed to parse credentials: {}", e)))?;
         merge_file_credentials(&mut config, creds);
     }
+
+    // Load credentials from keychain last (highest priority).
+    for (provider_id, provider_config) in config.providers.iter_mut() {
+        for cred_key in &provider_config.keychain_credentials {
+            if let Some(value) = keyring_get(provider_id, cred_key)? {
+                provider_config.credentials.insert(cred_key.clone(), value);
+            }
+        }
+    }

     Ok(config)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/mod.rs` around lines 241 - 261, The current load order makes
credentials.toml override keychain; to fix, load file credentials
(get_credentials_path, reading/parsing and calling merge_file_credentials)
before applying keychain entries, then apply keychain values last so they take
precedence; update merge_file_credentials (or its usage) to not overwrite
existing keys if you keep file-then-keychain approach, or simply ensure the loop
that uses keyring_get and provider_config.credentials.insert runs after
merge_file_credentials so keyring values replace any file values (refer to
provider_config.keychain_credentials, provider_config.credentials.insert,
keyring_get, get_credentials_path, merge_file_credentials).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/config/mod.rs`:
- Around line 241-261: The current load order makes credentials.toml override
keychain; to fix, load file credentials (get_credentials_path, reading/parsing
and calling merge_file_credentials) before applying keychain entries, then apply
keychain values last so they take precedence; update merge_file_credentials (or
its usage) to not overwrite existing keys if you keep file-then-keychain
approach, or simply ensure the loop that uses keyring_get and
provider_config.credentials.insert runs after merge_file_credentials so keyring
values replace any file values (refer to provider_config.keychain_credentials,
provider_config.credentials.insert, keyring_get, get_credentials_path,
merge_file_credentials).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad9baaf0-ec96-44c5-ac86-d29dd5d71543

📥 Commits

Reviewing files that changed from the base of the PR and between ef0249e and b838f78.

📒 Files selected for processing (1)
  • src/config/mod.rs

@dvaJi dvaJi merged commit 8c02842 into main Mar 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants