Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:53:44.342Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

zed_credentials_provider.rs

321 lines · 9.8 KB · rust
1use std::collections::HashMap;
2use std::future::Future;
3use std::path::PathBuf;
4use std::pin::Pin;
5use std::sync::{Arc, LazyLock};
6
7use anyhow::Result;
8use credentials_provider::CredentialsProvider;
9use futures::FutureExt as _;
10use gpui::{App, AsyncApp, Global};
11use release_channel::ReleaseChannel;
12
13/// An environment variable whose presence indicates that the system keychain
14/// should be used in development.
15///
16/// By default, running Zed in development uses the development credentials
17/// provider. Setting this environment variable allows you to interact with the
18/// system keychain (for instance, if you need to test something).
19///
20/// Only works in development. Setting this environment variable in other
21/// release channels is a no-op.
22static ZED_DEVELOPMENT_USE_KEYCHAIN: LazyLock<bool> = LazyLock::new(|| {
23    std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").is_ok_and(|value| !value.is_empty())
24});
25
26pub struct ZedCredentialsProvider(pub Arc<dyn CredentialsProvider>);
27
28impl Global for ZedCredentialsProvider {}
29
30/// Returns the global [`CredentialsProvider`].
31pub fn init_global(cx: &mut App) {
32    // The `CredentialsProvider` trait has `Send + Sync` bounds on it, so it
33    // seems like this is a false positive from Clippy.
34    #[allow(clippy::arc_with_non_send_sync)]
35    let provider = new(cx);
36    cx.set_global(ZedCredentialsProvider(provider));
37}
38
39pub fn global(cx: &App) -> Arc<dyn CredentialsProvider> {
40    cx.try_global::<ZedCredentialsProvider>()
41        .map(|provider| provider.0.clone())
42        .unwrap_or_else(|| new(cx))
43}
44
45/// Returns a channel-namespaced system keychain provider.
46///
47/// This entry point never selects the plaintext development provider. It is
48/// not the sovereign identity custody API: the generic provider copies secret
49/// bytes into ordinary vectors. Identity custody must use the typed, direct
50/// keyring boundary introduced with the signer.
51pub fn system_keychain(cx: &App) -> Arc<dyn CredentialsProvider> {
52    new_with_sensitivity(cx, CredentialSensitivity::Sovereign)
53}
54
55fn new(cx: &App) -> Arc<dyn CredentialsProvider> {
56    new_with_sensitivity(cx, CredentialSensitivity::Normal)
57}
58
59fn new_with_sensitivity(
60    cx: &App,
61    sensitivity: CredentialSensitivity,
62) -> Arc<dyn CredentialsProvider> {
63    let release_channel =
64        ReleaseChannel::try_global(cx).unwrap_or(*release_channel::RELEASE_CHANNEL);
65    let backend = backend_for(release_channel, sensitivity, *ZED_DEVELOPMENT_USE_KEYCHAIN);
66
67    let inner: Arc<dyn CredentialsProvider> = match backend {
68        CredentialBackend::DevelopmentFile => Arc::new(DevelopmentCredentialsProvider::new()),
69        CredentialBackend::SystemKeychain => Arc::new(KeychainCredentialsProvider),
70    };
71
72    Arc::new(NamespacedCredentialsProvider {
73        namespace: release_channel.credential_namespace(),
74        inner,
75    })
76}
77
78#[derive(Debug, Copy, Clone, PartialEq, Eq)]
79enum CredentialSensitivity {
80    Normal,
81    Sovereign,
82}
83
84#[derive(Debug, Copy, Clone, PartialEq, Eq)]
85enum CredentialBackend {
86    DevelopmentFile,
87    SystemKeychain,
88}
89
90fn backend_for(
91    release_channel: ReleaseChannel,
92    sensitivity: CredentialSensitivity,
93    development_use_keychain: bool,
94) -> CredentialBackend {
95    if release_channel == ReleaseChannel::Dev
96        && sensitivity == CredentialSensitivity::Normal
97        && !development_use_keychain
98    {
99        CredentialBackend::DevelopmentFile
100    } else {
101        CredentialBackend::SystemKeychain
102    }
103}
104
105fn namespaced_credential_key(namespace: &str, url: &str) -> String {
106    format!("{namespace}:{url}")
107}
108
109struct NamespacedCredentialsProvider {
110    namespace: &'static str,
111    inner: Arc<dyn CredentialsProvider>,
112}
113
114impl CredentialsProvider for NamespacedCredentialsProvider {
115    fn read_credentials<'a>(
116        &'a self,
117        url: &'a str,
118        cx: &'a AsyncApp,
119    ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
120        async move {
121            let key = namespaced_credential_key(self.namespace, url);
122            self.inner.read_credentials(&key, cx).await
123        }
124        .boxed_local()
125    }
126
127    fn write_credentials<'a>(
128        &'a self,
129        url: &'a str,
130        username: &'a str,
131        password: &'a [u8],
132        cx: &'a AsyncApp,
133    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
134        async move {
135            let key = namespaced_credential_key(self.namespace, url);
136            self.inner
137                .write_credentials(&key, username, password, cx)
138                .await
139        }
140        .boxed_local()
141    }
142
143    fn delete_credentials<'a>(
144        &'a self,
145        url: &'a str,
146        cx: &'a AsyncApp,
147    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
148        async move {
149            let key = namespaced_credential_key(self.namespace, url);
150            self.inner.delete_credentials(&key, cx).await
151        }
152        .boxed_local()
153    }
154}
155
156/// A credentials provider that stores credentials in the system keychain.
157struct KeychainCredentialsProvider;
158
159impl CredentialsProvider for KeychainCredentialsProvider {
160    fn read_credentials<'a>(
161        &'a self,
162        url: &'a str,
163        cx: &'a AsyncApp,
164    ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
165        async move { cx.update(|cx| cx.read_credentials(url)).await }.boxed_local()
166    }
167
168    fn write_credentials<'a>(
169        &'a self,
170        url: &'a str,
171        username: &'a str,
172        password: &'a [u8],
173        cx: &'a AsyncApp,
174    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
175        async move {
176            cx.update(move |cx| cx.write_credentials(url, username, password))
177                .await
178        }
179        .boxed_local()
180    }
181
182    fn delete_credentials<'a>(
183        &'a self,
184        url: &'a str,
185        cx: &'a AsyncApp,
186    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
187        async move { cx.update(move |cx| cx.delete_credentials(url)).await }.boxed_local()
188    }
189}
190
191/// A credentials provider that stores credentials in a local file.
192///
193/// This MUST only be used in development, as this is not a secure way of storing
194/// credentials on user machines.
195///
196/// Its existence is purely to work around the annoyance of having to constantly
197/// re-allow access to the system keychain when developing Zed.
198struct DevelopmentCredentialsProvider {
199    path: PathBuf,
200}
201
202impl DevelopmentCredentialsProvider {
203    fn new() -> Self {
204        let path = paths::config_dir().join("development_credentials");
205
206        Self { path }
207    }
208
209    fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
210        let json = std::fs::read(&self.path)?;
211        let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
212
213        Ok(credentials)
214    }
215
216    fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
217        let json = serde_json::to_string(credentials)?;
218        std::fs::write(&self.path, json)?;
219
220        Ok(())
221    }
222}
223
224impl CredentialsProvider for DevelopmentCredentialsProvider {
225    fn read_credentials<'a>(
226        &'a self,
227        url: &'a str,
228        _cx: &'a AsyncApp,
229    ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
230        async move {
231            Ok(self
232                .load_credentials()
233                .unwrap_or_default()
234                .get(url)
235                .cloned())
236        }
237        .boxed_local()
238    }
239
240    fn write_credentials<'a>(
241        &'a self,
242        url: &'a str,
243        username: &'a str,
244        password: &'a [u8],
245        _cx: &'a AsyncApp,
246    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
247        async move {
248            let mut credentials = self.load_credentials().unwrap_or_default();
249            credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
250
251            self.save_credentials(&credentials)
252        }
253        .boxed_local()
254    }
255
256    fn delete_credentials<'a>(
257        &'a self,
258        url: &'a str,
259        _cx: &'a AsyncApp,
260    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
261        async move {
262            let mut credentials = self.load_credentials()?;
263            credentials.remove(url);
264
265            self.save_credentials(&credentials)
266        }
267        .boxed_local()
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use std::collections::HashSet;
274
275    use super::*;
276
277    #[test]
278    fn sovereign_credentials_always_use_the_system_keychain() {
279        for release_channel in ReleaseChannel::ALL {
280            for development_use_keychain in [false, true] {
281                assert_eq!(
282                    backend_for(
283                        release_channel,
284                        CredentialSensitivity::Sovereign,
285                        development_use_keychain,
286                    ),
287                    CredentialBackend::SystemKeychain
288                );
289            }
290        }
291    }
292
293    #[test]
294    fn normal_development_credentials_retain_the_development_provider() {
295        assert_eq!(
296            backend_for(ReleaseChannel::Dev, CredentialSensitivity::Normal, false),
297            CredentialBackend::DevelopmentFile
298        );
299        assert_eq!(
300            backend_for(ReleaseChannel::Dev, CredentialSensitivity::Normal, true),
301            CredentialBackend::SystemKeychain
302        );
303    }
304
305    #[test]
306    fn credential_keys_are_isolated_by_release_channel() {
307        let keys = ReleaseChannel::ALL.map(|release_channel| {
308            namespaced_credential_key(
309                release_channel.credential_namespace(),
310                "https://example.com",
311            )
312        });
313
314        assert_eq!(keys.iter().collect::<HashSet<_>>().len(), 4);
315        assert!(
316            keys.iter()
317                .all(|key| key.starts_with("com.openagents.omega"))
318        );
319    }
320}
321
Served at tenant.openagents/omega Member data and write actions are omitted.