Skip to repository content321 lines · 9.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:53:44.342Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
zed_credentials_provider.rs
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