Add CredentialsProvider to silence keychain prompts in development (#25266)

This PR adds a new `CredentialsProvider` trait that abstracts over
interacting with the system keychain.

We had previously introduced a version of this scoped just to Zed auth
in https://github.com/zed-industries/zed/pull/11505.

However, after landing https://github.com/zed-industries/zed/pull/25123,
we now have a similar issue with the credentials for language model
providers that are also stored in the keychain (and thus also produce a
spam of popups when running a development build of Zed).

This PR takes the existing approach and makes it more generic, such that
we can use it everywhere that we need to read/store credentials in the
keychain.

There are still two credential provider implementations:

- `KeychainCredentialsProvider` will interact with the system keychain
(using the existing GPUI APIs)
- `DevelopmentCredentialsProvider` will use a local file on the file
system

We only use the `DevelopmentCredentialsProvider` when:

1. We are running a development build of Zed
2. The `ZED_DEVELOPMENT_AUTH` environment variable is set
- I am considering removing the need for this and making it the default,
but that will be explored in a follow-up PR.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-02-20 12:58:50 -05:00 committed by GitHub
parent 31aad858f8
commit 21bb7242ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 401 additions and 226 deletions

View file

@ -16,6 +16,7 @@ anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
client.workspace = true
collections.workspace = true
credentials_provider.workspace = true
copilot = { workspace = true, features = ["schemars"] }
deepseek = { workspace = true, features = ["schemars"] }
editor.workspace = true

View file

@ -2,6 +2,7 @@ use crate::AllLanguageModelSettings;
use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent};
use anyhow::{anyhow, Context as _, Result};
use collections::{BTreeMap, HashMap};
use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
use futures::Stream;
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryStreamExt as _};
@ -70,10 +71,16 @@ pub struct State {
impl State {
fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let delete_credentials =
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).anthropic.api_url);
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.anthropic
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
delete_credentials.await.ok();
credentials_provider
.delete_credentials(&api_url, &cx)
.await
.ok();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
@ -83,16 +90,16 @@ impl State {
}
fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
let write_credentials = cx.write_credentials(
AllLanguageModelSettings::get_global(cx)
.anthropic
.api_url
.as_str(),
"Bearer",
api_key.as_bytes(),
);
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.anthropic
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
write_credentials.await?;
credentials_provider
.write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
.await
.ok();
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
@ -110,6 +117,7 @@ impl State {
return Task::ready(Ok(()));
}
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.anthropic
.api_url
@ -119,8 +127,8 @@ impl State {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR) {
(api_key, true)
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
let (_, api_key) = credentials_provider
.read_credentials(&api_url, &cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(

View file

@ -1,5 +1,6 @@
use anyhow::{anyhow, Context as _, Result};
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{
@ -57,10 +58,16 @@ impl State {
}
fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).deepseek;
let delete_credentials = cx.delete_credentials(&settings.api_url);
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.deepseek
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
delete_credentials.await.log_err();
credentials_provider
.delete_credentials(&api_url, &cx)
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
@ -70,12 +77,15 @@ impl State {
}
fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).deepseek;
let write_credentials =
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.deepseek
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
write_credentials.await?;
credentials_provider
.write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
.await?;
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
cx.notify();
@ -88,17 +98,17 @@ impl State {
return Task::ready(Ok(()));
}
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.deepseek
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(DEEPSEEK_API_KEY_VAR) {
(api_key, true)
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
let (_, api_key) = credentials_provider
.read_credentials(&api_url, &cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(

View file

@ -1,5 +1,6 @@
use anyhow::{anyhow, Context as _, Result};
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, FutureExt, StreamExt};
use google_ai::stream_generate_content;
@ -59,10 +60,16 @@ impl State {
}
fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let delete_credentials =
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).google.api_url);
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.google
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
delete_credentials.await.ok();
credentials_provider
.delete_credentials(&api_url, &cx)
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
@ -72,12 +79,15 @@ impl State {
}
fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).google;
let write_credentials =
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.google
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
write_credentials.await?;
credentials_provider
.write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
.await?;
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
cx.notify();
@ -90,6 +100,7 @@ impl State {
return Task::ready(Ok(()));
}
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.google
.api_url
@ -99,8 +110,8 @@ impl State {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(GOOGLE_AI_API_KEY_VAR) {
(api_key, true)
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
let (_, api_key) = credentials_provider
.read_credentials(&api_url, &cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
@ -208,16 +219,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
let state = self.state.clone();
let delete_credentials =
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).google.api_url);
cx.spawn(|mut cx| async move {
delete_credentials.await.log_err();
state.update(&mut cx, |this, cx| {
this.api_key = None;
cx.notify();
})
})
self.state.update(cx, |state, cx| state.reset_api_key(cx))
}
}

View file

@ -1,5 +1,6 @@
use anyhow::{anyhow, Context as _, Result};
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::{
@ -62,10 +63,16 @@ impl State {
}
fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).mistral;
let delete_credentials = cx.delete_credentials(&settings.api_url);
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.mistral
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
delete_credentials.await.log_err();
credentials_provider
.delete_credentials(&api_url, &cx)
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
@ -75,12 +82,15 @@ impl State {
}
fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).mistral;
let write_credentials =
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.mistral
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
write_credentials.await?;
credentials_provider
.write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
.await?;
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
cx.notify();
@ -93,6 +103,7 @@ impl State {
return Task::ready(Ok(()));
}
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.mistral
.api_url
@ -101,8 +112,8 @@ impl State {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(MISTRAL_API_KEY_VAR) {
(api_key, true)
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
let (_, api_key) = credentials_provider
.read_credentials(&api_url, &cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(

View file

@ -1,5 +1,6 @@
use anyhow::{anyhow, Context as _, Result};
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::{
@ -63,10 +64,16 @@ impl State {
}
fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).openai;
let delete_credentials = cx.delete_credentials(&settings.api_url);
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.openai
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
delete_credentials.await.log_err();
credentials_provider
.delete_credentials(&api_url, &cx)
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
@ -76,12 +83,16 @@ impl State {
}
fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).openai;
let write_credentials =
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.openai
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
write_credentials.await?;
credentials_provider
.write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
cx.notify();
@ -94,6 +105,7 @@ impl State {
return Task::ready(Ok(()));
}
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.openai
.api_url
@ -102,8 +114,8 @@ impl State {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(OPENAI_API_KEY_VAR) {
(api_key, true)
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
let (_, api_key) = credentials_provider
.read_credentials(&api_url, &cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(