Skip to content

Conversation

@sheikhlimon
Copy link
Contributor

Summary

Problem: Each call to get_secret() hits keychain separately, causing multiple prompts when loading provider config with multiple secret parameters.
Solution: Add in-memory cache to all_secrets() so keychain is accessed once per config load.

  • Before: N keychain prompts for N secret parameters when loading provider config
  • After: 1 keychain prompt per config load, cached for subsequent reads in same session

Type of Change

  • Feature
  • Bug fix
  • Refactor / Code quality
  • Performance improvement
  • Documentation
  • Tests
  • Security fix
  • Build / Release
  • Other (specify below)

AI Assistance

  • This PR was created or reviewed with AI assistance

Related Issues

Fixes #6595

Copy link
Collaborator

@michaelneale michaelneale left a comment

Choose a reason for hiding this comment

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

I think this would be very nice @codefromthecrypt looks ok to you (once it is a clean build)

Signed-off-by: sheikhlimon <sheikhlimon404@gmail.com>
Signed-off-by: sheikhlimon <sheikhlimon404@gmail.com>
Signed-off-by: sheikhlimon <sheikhlimon404@gmail.com>
Copy link
Collaborator

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

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

TL;DR; Thanks for doing this. Only main thing to pay attention to is a lazy loading race, and let me know if I'm off on it. Otherwise, looks good.


I think if we wanted to do a super clean job it would be refactoring to a real loading cache like moka, and/or concurrent tests like loom, but that's probably over solving and I like your stepping in cautiously.

For this code.. I think there's a potential lazy loading race (e.g., multiple threads could check None outside the lock and all try to load, causing redundant hits). Something like this might fix it (didn't try it, but should serialize the load):

pub fn all_secrets(&self) -> Result<HashMap<String, Value>, ConfigError> {
    let mut cache = self.secrets_cache.lock().unwrap();
    let values = if let Some(ref cached) = *cache {
        cached.clone()
    } else {
        let loaded = match &self.secrets {
            SecretStorage::Keyring { service } => { /* ... load ... */ },
            SecretStorage::File { path } => self.read_secrets_from_file(path)?,
        };
        *cache = Some(loaded.clone());
        loaded
    };
    Ok(values)
}

Also, if you wanted to test this you could add instrumentation like a test-only SecretStorage.load_count: Arc<Mutex<u32>> (increment on load), then in a unit test simulate multiple get_secret calls (maybe even in threads) and assert only 1 load happened. Again, that's not full parallel testing, but low hanging fruit.

Signed-off-by: sheikhlimon <sheikhlimon404@gmail.com>
Signed-off-by: sheikhlimon <sheikhlimon404@gmail.com>
Signed-off-by: sheikhlimon <sheikhlimon404@gmail.com>
@sheikhlimon
Copy link
Contributor Author

For this code.. I think there's a potential lazy loading race (e.g., multiple threads could check None outside the lock and all try to load, causing redundant hits).

Thanks for reviewing this! I’ve implemented your suggested fix by holding the lock across the entire cache check and load operation.
This prevents multiple threads from observing None and loading simultaneously. The first thread to acquire the lock performs the load, and subsequent callers wait and then use the cached result.

@codefromthecrypt codefromthecrypt merged commit 698382c into block:main Jan 23, 2026
18 checks passed
@codefromthecrypt
Copy link
Collaborator

thanks again for the help!

fbalicchia pushed a commit to fbalicchia/goose that referenced this pull request Jan 23, 2026
Signed-off-by: sheikhlimon <sheikhlimon404@gmail.com>
Signed-off-by: fbalicchia <fbalicchia@cuebiq.com>
@sheikhlimon sheikhlimon deleted the fix/keychain-cache branch January 23, 2026 13:49
tlongwell-block added a commit that referenced this pull request Jan 23, 2026
* origin/main:
  Fix GCP Vertex AI global endpoint support for Gemini 3 models (#6187)
  fix: macOS keychain infinite prompt loop    (#6620)
  chore: reduce duplicate or unused cargo deps (#6630)
  feat: codex subscription support (#6600)
  smoke test allow pass for flaky providers (#6638)
  feat: Add built-in skill for goose documentation reference (#6534)
  Native images (#6619)
  docs: ml-based prompt injection detection (#6627)
  Strip the audience for compacting (#6646)
  chore(release): release version 1.21.0 (minor) (#6634)
  add collapsable chat nav (#6649)
  fix: capitalize Rust in CONTRIBUTING.md (#6640)
  chore(deps): bump lodash from 4.17.21 to 4.17.23 in /ui/desktop (#6623)
  Vibe mcp apps (#6569)
  Add session forking capability (#5882)
  chore(deps): bump lodash from 4.17.21 to 4.17.23 in /documentation (#6624)
  fix(docs): use named import for globby v13 (#6639)
  PR Code Review (#6043)
  fix(docs): use dynamic import for globby ESM module (#6636)

# Conflicts:
#	Cargo.lock
#	crates/goose-server/src/routes/session.rs
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.

[Bug] macOS Keychain infinite prompt loop - 'Allow' vs 'Always Allow' causes corrupted state

3 participants