1use std::path::{Path, PathBuf};
17use std::{hash::Hash, hash::Hasher};
18
19use rusqlite::{Connection, OpenFlags};
20
21use crate::display::sanitize_untrusted_path;
22use crate::error::{AppError, Result};
23
24const TOKEN_KEY: &str = "cursorAuth/accessToken";
25const SCOPED_PROFILE_KEY: &str = "cursorAuth/cachedScopedProfile";
28
29pub fn default_db_path() -> Result<PathBuf> {
37 let base = directories::BaseDirs::new().ok_or_else(|| {
38 AppError::Other("could not resolve the platform config directory (no HOME?)".into())
39 })?;
40 Ok(base
41 .config_dir()
42 .join("Cursor")
43 .join("User")
44 .join("globalStorage")
45 .join("state.vscdb"))
46}
47
48pub fn read_access_token(path: &Path) -> Result<String> {
54 if !path.exists() {
55 return Err(AppError::Credentials(format!(
56 "Cursor database not found at {}. Open the Cursor IDE (or run `cursor-agent`) and \
57 sign in at least once, then try again.",
58 sanitize_untrusted_path(path)
59 )));
60 }
61 let conn =
65 Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY).map_err(|e| {
66 AppError::Credentials(format!(
67 "could not open Cursor database at {}: {e}",
68 sanitize_untrusted_path(path)
69 ))
70 })?;
71 let token: String = conn
72 .query_row(
73 "SELECT value FROM ItemTable WHERE key = ?1",
74 [TOKEN_KEY],
75 |row| row.get(0),
76 )
77 .map_err(|_| {
78 AppError::Credentials(format!(
79 "no Cursor session found in {}. Sign in to the Cursor IDE, then try again.",
80 sanitize_untrusted_path(path)
81 ))
82 })?;
83 if token.trim().is_empty() {
84 return Err(AppError::Credentials(
85 "Cursor session token is empty. Sign in to the Cursor IDE again.".into(),
86 ));
87 }
88 Ok(token)
89}
90
91pub fn default_agent_auth_path() -> Result<PathBuf> {
105 let base = directories::BaseDirs::new().ok_or_else(|| {
106 AppError::Other("could not resolve the platform config directory (no HOME?)".into())
107 })?;
108 Ok(if cfg!(target_os = "macos") {
109 base.home_dir().join(".cursor").join("auth.json")
110 } else if cfg!(windows) {
111 base.config_dir().join("Cursor").join("auth.json")
112 } else {
113 base.config_dir().join("cursor").join("auth.json")
114 })
115}
116
117pub fn read_agent_access_token(path: &Path) -> Result<String> {
122 if !path.exists() {
123 return Err(AppError::Credentials(format!(
124 "cursor-agent auth file not found at {}. Run `cursor-agent` and sign in at least \
125 once, then try again.",
126 sanitize_untrusted_path(path)
127 )));
128 }
129 let bytes = std::fs::read(path).map_err(|e| AppError::io_at(path, e))?;
130 let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| {
131 AppError::Credentials(format!(
132 "could not parse {}: {e}",
133 sanitize_untrusted_path(path)
134 ))
135 })?;
136 let token = value
137 .get("accessToken")
138 .and_then(serde_json::Value::as_str)
139 .filter(|s| !s.trim().is_empty())
140 .ok_or_else(|| {
141 AppError::Credentials(format!(
142 "no accessToken in {}. Sign in with `cursor-agent` again.",
143 sanitize_untrusted_path(path)
144 ))
145 })?;
146 Ok(token.to_string())
147}
148
149#[cfg(target_os = "macos")]
155const AGENT_KEYCHAIN_SERVICE: &str = "cursor-access-token";
156#[cfg(target_os = "macos")]
157const AGENT_KEYCHAIN_ACCOUNT: &str = "cursor-user";
158
159#[cfg(target_os = "macos")]
166pub fn read_agent_keychain_token() -> Option<String> {
167 let out = agent_keychain_command().output().ok()?;
168 if !out.status.success() {
169 return None;
170 }
171 let token = String::from_utf8(out.stdout).ok()?;
172 let token = token.trim();
173 (!token.is_empty()).then(|| token.to_string())
174}
175
176#[cfg(target_os = "macos")]
179fn agent_keychain_command() -> std::process::Command {
180 let mut command = std::process::Command::new("/usr/bin/security");
181 command
182 .args([
183 "find-generic-password",
184 "-s",
185 AGENT_KEYCHAIN_SERVICE,
186 "-a",
187 AGENT_KEYCHAIN_ACCOUNT,
188 "-w",
189 ])
190 .stdin(std::process::Stdio::null())
191 .stderr(std::process::Stdio::null());
192 for var in crate::vendor::vendor_secret_env_vars_to_remove(&[]) {
194 command.env_remove(var);
195 }
196 command
197}
198
199#[cfg(not(target_os = "macos"))]
200pub fn read_agent_keychain_token() -> Option<String> {
201 None
202}
203
204pub fn resolve_access_token(db_path: &Path, agent_auth_path: &Path) -> Result<String> {
219 Ok(resolve_access_tokens(db_path, agent_auth_path)?.primary)
220}
221
222pub struct AccessTokens {
226 pub primary: String,
227 pub agent_fallback: Option<String>,
228}
229
230pub fn resolve_access_tokens(db_path: &Path, agent_auth_path: &Path) -> Result<AccessTokens> {
231 let default_agent = default_agent_auth_path().ok();
232 let ambient = default_agent.as_deref() == Some(agent_auth_path);
233 match read_access_token(db_path) {
234 Ok(primary) => {
235 let agent_fallback = read_agent_access_token(agent_auth_path)
236 .ok()
237 .or_else(|| ambient.then(read_agent_keychain_token).flatten())
238 .filter(|token| token != &primary);
239 Ok(AccessTokens {
240 primary,
241 agent_fallback,
242 })
243 }
244 Err(ide_err) if !db_path.exists() => {
245 let primary = if agent_auth_path.exists() {
246 read_agent_access_token(agent_auth_path).or_else(|agent_err| {
247 ambient
248 .then(read_agent_keychain_token)
249 .flatten()
250 .ok_or(agent_err)
251 })?
252 } else {
253 ambient
254 .then(read_agent_keychain_token)
255 .flatten()
256 .ok_or(ide_err)?
257 };
258 Ok(AccessTokens {
259 primary,
260 agent_fallback: None,
261 })
262 }
263 Err(ide_err) => Err(ide_err),
264 }
265}
266
267pub fn resolve_access_token_with(
269 db_path: &Path,
270 agent_auth_path: &Path,
271 keychain: impl FnOnce() -> Option<String>,
272) -> Result<String> {
273 match read_access_token(db_path) {
274 Ok(token) => Ok(token),
275 Err(ide_err) if !db_path.exists() => {
276 if agent_auth_path.exists() {
277 match read_agent_access_token(agent_auth_path) {
278 Ok(token) => return Ok(token),
279 Err(agent_err) => return keychain().ok_or(agent_err),
280 }
281 }
282 keychain().ok_or(ide_err)
283 }
284 Err(ide_err) => Err(ide_err),
285 }
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct SessionAuth {
294 pub user_id: String,
295 pub account_key: String,
299 pub cookie_value: String,
300}
301
302pub fn session_auth(token: &str) -> Result<SessionAuth> {
307 let claims = crate::jwt::claims(token).ok_or_else(opaque_token_error)?;
308 let sub = claims
309 .get("sub")
310 .and_then(serde_json::Value::as_str)
311 .ok_or_else(|| AppError::Credentials("Cursor session token has no `sub` claim.".into()))?;
312 auth_from_sub(token, sub)
313}
314
315pub fn session_auth_for(db_path: &Path, token: &str) -> Result<SessionAuth> {
325 match session_auth(token) {
326 Ok(auth) => Ok(auth),
327 Err(jwt_err) => match read_cached_auth_id(db_path) {
328 Some(auth_id) => auth_from_sub(token, &auth_id),
329 None => Err(jwt_err),
330 },
331 }
332}
333
334fn read_cached_auth_id(path: &Path) -> Option<String> {
338 let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY).ok()?;
339 let profile: String = conn
340 .query_row(
341 "SELECT value FROM ItemTable WHERE key = ?1",
342 [SCOPED_PROFILE_KEY],
343 |row| row.get(0),
344 )
345 .ok()?;
346 let value: serde_json::Value = serde_json::from_str(&profile).ok()?;
347 value
348 .get("authId")
349 .and_then(serde_json::Value::as_str)
350 .map(str::to_owned)
351}
352
353fn opaque_token_error() -> AppError {
354 AppError::Credentials(
355 "Cursor session token could not be decoded. Sign in to the Cursor IDE again.".into(),
356 )
357}
358
359fn auth_from_sub(token: &str, sub: &str) -> Result<SessionAuth> {
362 let user_id = sub
363 .split('|')
364 .nth(1)
365 .filter(|s| !s.is_empty())
366 .ok_or_else(|| {
367 AppError::Credentials(format!(
368 "Cursor session token `sub` claim has an unexpected shape: {sub:?}"
369 ))
370 })?
371 .to_string();
372 let mut hasher = std::collections::hash_map::DefaultHasher::new();
373 user_id.hash(&mut hasher);
374 let account_key = format!("{:016x}", hasher.finish());
375 let cookie_value = format!("{user_id}%3A%3A{token}");
376 Ok(SessionAuth {
377 user_id,
378 account_key,
379 cookie_value,
380 })
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use base64::Engine as _;
387 use tempfile::TempDir;
388
389 fn fake_jwt(claims: serde_json::Value) -> String {
392 let header = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(br#"{"alg":"none"}"#);
393 let payload =
394 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims.to_string().as_bytes());
395 format!("{header}.{payload}.sig")
396 }
397
398 fn seed_db(path: &Path, token: Option<&str>) {
399 let conn = Connection::open(path).unwrap();
400 conn.execute("CREATE TABLE ItemTable (key TEXT, value TEXT)", [])
401 .unwrap();
402 if let Some(t) = token {
403 conn.execute(
404 "INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
405 rusqlite::params![TOKEN_KEY, t],
406 )
407 .unwrap();
408 }
409 }
410
411 fn seed_scoped_profile(path: &Path, profile: &str) {
413 let conn = Connection::open(path).unwrap();
414 conn.execute(
415 "INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
416 rusqlite::params![SCOPED_PROFILE_KEY, profile],
417 )
418 .unwrap();
419 }
420
421 #[test]
425 fn an_opaque_token_uses_the_cached_profile_identity() {
426 let td = TempDir::new().unwrap();
427 let path = td.path().join("state.vscdb");
428 let opaque = format!("curs_{}", "a".repeat(400));
429 seed_db(&path, None);
430 seed_scoped_profile(
431 &path,
432 r#"{"authId":"grok|user_01M4473VZ67Q3X6HF8WZFE3SMW","displayName":"Someone"}"#,
433 );
434 let auth = session_auth_for(&path, &opaque).unwrap();
435 assert_eq!(auth.user_id, "user_01M4473VZ67Q3X6HF8WZFE3SMW");
436 assert_eq!(
437 auth.cookie_value,
438 format!("user_01M4473VZ67Q3X6HF8WZFE3SMW%3A%3A{opaque}")
439 );
440 }
441
442 #[test]
443 fn an_opaque_token_without_a_profile_keeps_the_decode_error() {
444 let td = TempDir::new().unwrap();
445 let path = td.path().join("state.vscdb");
446 seed_db(&path, None);
447 let err = session_auth_for(&path, "curs_opaque_without_profile").unwrap_err();
448 assert!(err.to_string().contains("could not be decoded"), "{err}");
449 }
450
451 #[test]
452 fn a_jwt_still_wins_over_the_cached_profile() {
453 let td = TempDir::new().unwrap();
454 let path = td.path().join("state.vscdb");
455 seed_db(&path, None);
456 seed_scoped_profile(&path, r#"{"authId":"grok|user_from_profile"}"#);
457 let jwt = fake_jwt(serde_json::json!({"sub": "auth0|user_from_jwt"}));
458 let auth = session_auth_for(&path, &jwt).unwrap();
459 assert_eq!(auth.user_id, "user_from_jwt");
460 }
461
462 #[test]
463 fn a_profile_without_a_usable_auth_id_is_not_an_identity() {
464 let td = TempDir::new().unwrap();
465 let path = td.path().join("state.vscdb");
466 seed_db(&path, None);
467 seed_scoped_profile(&path, r#"{"authId":"no-pipe-here"}"#);
468 assert!(session_auth_for(&path, "curs_opaque").is_err());
469 seed_scoped_profile(&path, r#"{"displayName":"no auth id"}"#);
470 assert!(session_auth_for(&path, "curs_opaque").is_err());
471 }
472
473 #[test]
474 fn a_missing_profile_row_falls_back_to_the_jwt_path() {
475 let td = TempDir::new().unwrap();
476 let path = td.path().join("state.vscdb");
477 seed_db(&path, None);
478 let jwt = fake_jwt(serde_json::json!({"sub": "auth0|only_jwt"}));
479 assert_eq!(session_auth_for(&path, &jwt).unwrap().user_id, "only_jwt");
480 }
481
482 #[test]
483 fn default_db_path_ends_with_the_cursor_state_file() {
484 let p = default_db_path().unwrap();
485 assert!(
486 p.ends_with(
487 std::path::Path::new("Cursor")
488 .join("User")
489 .join("globalStorage")
490 .join("state.vscdb")
491 )
492 );
493 }
494
495 #[test]
496 fn missing_file_is_a_credentials_error_naming_the_path() {
497 let dir = TempDir::new().unwrap();
498 let path = dir.path().join("state.vscdb");
499 let err = read_access_token(&path).unwrap_err();
500 match err {
501 AppError::Credentials(m) => assert!(m.contains(&path.display().to_string())),
502 other => panic!("expected Credentials error, got {other:?}"),
503 }
504 }
505
506 #[test]
507 fn reads_the_token_back_out_of_the_item_table() {
508 let dir = TempDir::new().unwrap();
509 let path = dir.path().join("state.vscdb");
510 seed_db(&path, Some("fake-token-value"));
511 assert_eq!(read_access_token(&path).unwrap(), "fake-token-value");
512 }
513
514 #[test]
515 fn missing_row_is_a_credentials_error() {
516 let dir = TempDir::new().unwrap();
517 let path = dir.path().join("state.vscdb");
518 seed_db(&path, None);
519 let err = read_access_token(&path).unwrap_err();
520 assert!(matches!(err, AppError::Credentials(_)));
521 }
522
523 #[test]
524 fn empty_token_is_a_credentials_error() {
525 let dir = TempDir::new().unwrap();
526 let path = dir.path().join("state.vscdb");
527 seed_db(&path, Some(""));
528 let err = read_access_token(&path).unwrap_err();
529 assert!(matches!(err, AppError::Credentials(_)));
530 }
531
532 #[test]
533 fn session_auth_extracts_user_id_and_builds_the_cookie_value() {
534 let token = fake_jwt(serde_json::json!({"sub": "auth0|user_abc123"}));
535 let auth = session_auth(&token).unwrap();
536 assert_eq!(auth.user_id, "user_abc123");
537 assert_eq!(auth.account_key.len(), 16);
538 assert!(!auth.account_key.contains("user_abc123"));
539 assert_eq!(auth.cookie_value, format!("user_abc123%3A%3A{token}"));
540 }
541
542 #[test]
543 fn session_auth_account_key_is_stable_and_account_specific() {
544 let one = session_auth(&fake_jwt(serde_json::json!({"sub": "auth0|one"}))).unwrap();
545 let one_again = session_auth(&fake_jwt(serde_json::json!({"sub": "auth0|one"}))).unwrap();
546 let two = session_auth(&fake_jwt(serde_json::json!({"sub": "auth0|two"}))).unwrap();
547 assert_eq!(one.account_key, one_again.account_key);
548 assert_ne!(one.account_key, two.account_key);
549 }
550
551 #[test]
552 fn session_auth_rejects_a_non_jwt_token() {
553 let err = session_auth("not-a-jwt").unwrap_err();
554 assert!(matches!(err, AppError::Credentials(_)));
555 }
556
557 #[test]
558 fn session_auth_rejects_missing_sub_claim() {
559 let token = fake_jwt(serde_json::json!({"other": "value"}));
560 let err = session_auth(&token).unwrap_err();
561 match err {
562 AppError::Credentials(m) => assert!(m.contains("sub")),
563 other => panic!("expected Credentials error, got {other:?}"),
564 }
565 }
566
567 #[test]
568 fn session_auth_rejects_sub_without_a_pipe_separated_user_id() {
569 let token = fake_jwt(serde_json::json!({"sub": "no-pipe-here"}));
570 let err = session_auth(&token).unwrap_err();
571 assert!(matches!(err, AppError::Credentials(_)));
572 }
573
574 #[test]
575 fn default_agent_auth_path_follows_the_cli_own_convention_per_os() {
576 let p = default_agent_auth_path().unwrap();
577 let expected_tail = if cfg!(target_os = "macos") {
578 std::path::Path::new(".cursor").join("auth.json")
579 } else if cfg!(windows) {
580 std::path::Path::new("Cursor").join("auth.json")
581 } else {
582 std::path::Path::new("cursor").join("auth.json")
583 };
584 assert!(p.ends_with(expected_tail), "{}", p.display());
585 assert!(!cfg!(target_os = "macos") || !p.to_string_lossy().contains("Application Support"));
588 }
589
590 #[test]
591 fn agent_auth_missing_file_is_a_credentials_error_naming_the_path() {
592 let dir = TempDir::new().unwrap();
593 let path = dir.path().join("auth.json");
594 let err = read_agent_access_token(&path).unwrap_err();
595 match err {
596 AppError::Credentials(m) => assert!(m.contains(&path.display().to_string())),
597 other => panic!("expected Credentials error, got {other:?}"),
598 }
599 }
600
601 #[test]
602 fn agent_auth_reads_access_token_out_of_the_json_file() {
603 let dir = TempDir::new().unwrap();
604 let path = dir.path().join("auth.json");
605 std::fs::write(
606 &path,
607 serde_json::json!({"accessToken": "agent-token-value", "refreshToken": "r"})
608 .to_string(),
609 )
610 .unwrap();
611 assert_eq!(read_agent_access_token(&path).unwrap(), "agent-token-value");
612 }
613
614 #[test]
615 fn agent_auth_missing_field_is_a_credentials_error() {
616 let dir = TempDir::new().unwrap();
617 let path = dir.path().join("auth.json");
618 std::fs::write(&path, serde_json::json!({"refreshToken": "r"}).to_string()).unwrap();
619 let err = read_agent_access_token(&path).unwrap_err();
620 assert!(matches!(err, AppError::Credentials(_)));
621 }
622
623 #[test]
624 fn agent_auth_empty_token_is_a_credentials_error() {
625 let dir = TempDir::new().unwrap();
626 let path = dir.path().join("auth.json");
627 std::fs::write(&path, serde_json::json!({"accessToken": ""}).to_string()).unwrap();
628 let err = read_agent_access_token(&path).unwrap_err();
629 assert!(matches!(err, AppError::Credentials(_)));
630 }
631
632 #[test]
633 fn agent_auth_malformed_json_is_a_credentials_error() {
634 let dir = TempDir::new().unwrap();
635 let path = dir.path().join("auth.json");
636 std::fs::write(&path, "not json").unwrap();
637 let err = read_agent_access_token(&path).unwrap_err();
638 assert!(matches!(err, AppError::Credentials(_)));
639 }
640
641 #[test]
642 fn resolve_prefers_the_ide_db_when_both_are_present() {
643 let dir = TempDir::new().unwrap();
644 let db_path = dir.path().join("state.vscdb");
645 seed_db(&db_path, Some("ide-token"));
646 let agent_path = dir.path().join("auth.json");
647 std::fs::write(
648 &agent_path,
649 serde_json::json!({"accessToken": "agent-token"}).to_string(),
650 )
651 .unwrap();
652 assert_eq!(
653 resolve_access_token_with(&db_path, &agent_path, || None).unwrap(),
654 "ide-token"
655 );
656 }
657
658 #[test]
659 fn resolve_falls_back_to_the_agent_file_when_the_ide_db_is_missing() {
660 let dir = TempDir::new().unwrap();
661 let db_path = dir.path().join("state.vscdb");
662 let agent_path = dir.path().join("auth.json");
663 std::fs::write(
664 &agent_path,
665 serde_json::json!({"accessToken": "agent-token"}).to_string(),
666 )
667 .unwrap();
668 assert_eq!(
669 resolve_access_token_with(&db_path, &agent_path, || None).unwrap(),
670 "agent-token"
671 );
672 }
673
674 #[test]
675 fn resolve_does_not_hide_an_existing_broken_ide_db_with_the_agent_file() {
676 let dir = TempDir::new().unwrap();
677 let db_path = dir.path().join("state.vscdb");
678 seed_db(&db_path, None);
679 let agent_path = dir.path().join("auth.json");
680 std::fs::write(
681 &agent_path,
682 serde_json::json!({"accessToken": "agent-token"}).to_string(),
683 )
684 .unwrap();
685
686 let err = resolve_access_token_with(&db_path, &agent_path, || None).unwrap_err();
687 match err {
688 AppError::Credentials(m) => {
689 assert!(m.contains(&db_path.display().to_string()));
690 assert!(!m.contains(&agent_path.display().to_string()));
691 }
692 other => panic!("expected Credentials error, got {other:?}"),
693 }
694 }
695
696 #[test]
697 fn resolve_surfaces_the_ide_error_when_both_sources_are_missing() {
698 let dir = TempDir::new().unwrap();
699 let db_path = dir.path().join("state.vscdb");
700 let agent_path = dir.path().join("auth.json");
701 let err = resolve_access_token_with(&db_path, &agent_path, || None).unwrap_err();
702 match err {
703 AppError::Credentials(m) => assert!(m.contains(&db_path.display().to_string())),
704 other => panic!("expected Credentials error, got {other:?}"),
705 }
706 }
707
708 #[test]
709 fn resolve_surfaces_the_agent_error_when_its_file_exists_but_is_malformed() {
710 let dir = TempDir::new().unwrap();
711 let db_path = dir.path().join("state.vscdb");
712 let agent_path = dir.path().join("auth.json");
713 std::fs::write(&agent_path, "not json").unwrap();
714
715 let err = resolve_access_token_with(&db_path, &agent_path, || None).unwrap_err();
716 match err {
717 AppError::Credentials(m) => {
718 assert!(m.contains(&agent_path.display().to_string()));
719 assert!(m.contains("could not parse"));
720 }
721 other => panic!("expected Credentials error, got {other:?}"),
722 }
723 }
724
725 #[test]
726 fn resolve_falls_back_to_the_keychain_when_the_ide_db_and_agent_file_are_missing() {
727 let dir = TempDir::new().unwrap();
728 let db_path = dir.path().join("state.vscdb");
729 let agent_path = dir.path().join("auth.json");
730 assert_eq!(
731 resolve_access_token_with(&db_path, &agent_path, || Some("keychain-token".into()))
732 .unwrap(),
733 "keychain-token"
734 );
735 }
736
737 #[test]
738 fn resolve_tries_the_keychain_when_the_agent_file_is_unusable() {
739 let dir = TempDir::new().unwrap();
740 let db_path = dir.path().join("state.vscdb");
741 let agent_path = dir.path().join("auth.json");
742 std::fs::write(
743 &agent_path,
744 serde_json::json!({"refreshToken": "r"}).to_string(),
745 )
746 .unwrap();
747 assert_eq!(
748 resolve_access_token_with(&db_path, &agent_path, || Some("keychain-token".into()))
749 .unwrap(),
750 "keychain-token"
751 );
752 }
753
754 #[test]
755 fn resolve_never_consults_the_keychain_over_a_present_ide_db() {
756 let dir = TempDir::new().unwrap();
757 let db_path = dir.path().join("state.vscdb");
758 seed_db(&db_path, None);
759 let agent_path = dir.path().join("auth.json");
760 let err = resolve_access_token_with(&db_path, &agent_path, || {
761 panic!("an existing IDE db must not be widened to the keychain")
762 })
763 .unwrap_err();
764 assert!(matches!(err, AppError::Credentials(_)));
765 }
766
767 #[test]
768 fn resolve_leaves_the_keychain_alone_for_a_non_default_agent_path() {
769 let dir = TempDir::new().unwrap();
772 let err = resolve_access_token(
773 &dir.path().join("state.vscdb"),
774 &dir.path().join("auth.json"),
775 )
776 .unwrap_err();
777 assert!(matches!(err, AppError::Credentials(_)));
778 }
779
780 #[cfg(target_os = "macos")]
783 #[test]
784 fn agent_keychain_lookup_scrubs_vendor_secret_env_vars() {
785 let command = agent_keychain_command();
786 assert_eq!(command.get_program(), "/usr/bin/security");
787 let removed: Vec<String> = command
788 .get_envs()
789 .filter(|(_, value)| value.is_none())
790 .map(|(key, _)| key.to_string_lossy().into_owned())
791 .collect();
792 for var in crate::vendor::vendor_secret_env_vars_to_remove(&[]) {
793 assert!(
794 removed.iter().any(|key| key == var),
795 "expected {var} to be scrubbed from the cursor-agent keychain lookup"
796 );
797 }
798 }
799}