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:
parent
31aad858f8
commit
21bb7242ea
15 changed files with 401 additions and 226 deletions
|
@ -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
|
||||
|
|
|
@ -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)?;
|
||||
(
|
||||
|
|
|
@ -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)?;
|
||||
(
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)?;
|
||||
(
|
||||
|
|
|
@ -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)?;
|
||||
(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue