Rust CLI for tangled

Move to keyring based session storage to fix macOS auth issues #3

open opened by dunkirk.sh targeting main

This fixes session persistence issues on macOS where login credentials were not being saved to the keychain. Also standardizes the config directory location across all platforms.

Changes#

Keyring Platform Support

  • Fixed macOS keychain integration by using apple-native feature instead of Linux-only sync-secret-service
  • Added platform-specific keyring features:
    • macOS: apple-native (uses macOS Keychain)
    • Linux: sync-secret-service, vendored (uses GNOME Keyring/KWallet)
    • Windows: windows-native (uses Windows Credential Manager)

Config Path Standardization

  • Moved config directory to ~/.config/tangled on all platforms for consistency
  • Previously used platform-specific paths (e.g., ~/Library/Application Support/tangled on macOS)

Error Messages

  • Improved keychain error messages to be more descriptive
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:krxbvxvis5skq7jj6eot23ul/sh.tangled.repo.pull/3m6d3o7lwnb22
+50 -10
Diff #0
+30 -3
Cargo.lock
··· 387 387 "libc", 388 388 ] 389 389 390 + [[package]] 391 + name = "core-foundation" 392 + version = "0.10.1" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 395 + dependencies = [ 396 + "core-foundation-sys", 397 + "libc", 398 + ] 399 + 390 400 [[package]] 391 401 name = "core-foundation-sys" 392 402 version = "0.8.7" ··· 1117 1127 source = "registry+https://github.com/rust-lang/crates.io-index" 1118 1128 checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" 1119 1129 dependencies = [ 1130 + "byteorder", 1120 1131 "dbus-secret-service", 1121 1132 "log", 1122 1133 "openssl", 1134 + "security-framework 2.11.1", 1135 + "security-framework 3.5.1", 1136 + "windows-sys 0.60.2", 1123 1137 "zeroize", 1124 1138 ] 1125 1139 ··· 1316 1330 "openssl-probe", 1317 1331 "openssl-sys", 1318 1332 "schannel", 1319 - "security-framework", 1333 + "security-framework 2.11.1", 1320 1334 "security-framework-sys", 1321 1335 "tempfile", 1322 1336 ] ··· 1831 1845 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1832 1846 dependencies = [ 1833 1847 "bitflags", 1834 - "core-foundation", 1848 + "core-foundation 0.9.4", 1849 + "core-foundation-sys", 1850 + "libc", 1851 + "security-framework-sys", 1852 + ] 1853 + 1854 + [[package]] 1855 + name = "security-framework" 1856 + version = "3.5.1" 1857 + source = "registry+https://github.com/rust-lang/crates.io-index" 1858 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 1859 + dependencies = [ 1860 + "bitflags", 1861 + "core-foundation 0.10.1", 1835 1862 "core-foundation-sys", 1836 1863 "libc", 1837 1864 "security-framework-sys", ··· 2055 2082 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2056 2083 dependencies = [ 2057 2084 "bitflags", 2058 - "core-foundation", 2085 + "core-foundation 0.9.4", 2059 2086 "system-configuration-sys", 2060 2087 ] 2061 2088
+1 -1
Cargo.toml
··· 41 41 42 42 # Storage 43 43 dirs = "5.0" 44 - keyring = { version = "3.6", features = ["sync-secret-service", "vendored"] } 44 + keyring = "3.6" 45 45 46 46 # Error Handling 47 47 anyhow = "1.0"
+9 -1
crates/tangled-config/Cargo.toml
··· 8 8 [dependencies] 9 9 anyhow = { workspace = true } 10 10 dirs = { workspace = true } 11 - keyring = { workspace = true } 12 11 serde = { workspace = true, features = ["derive"] } 13 12 serde_json = { workspace = true } 14 13 toml = { workspace = true } 15 14 chrono = { workspace = true } 16 15 16 + [target.'cfg(target_os = "macos")'.dependencies] 17 + keyring = { workspace = true, features = ["apple-native"] } 18 + 19 + [target.'cfg(target_os = "linux")'.dependencies] 20 + keyring = { workspace = true, features = ["sync-secret-service", "vendored"] } 21 + 22 + [target.'cfg(target_os = "windows")'.dependencies] 23 + keyring = { workspace = true, features = ["windows-native"] } 24 +
+8 -3
crates/tangled-config/src/config.rs
··· 2 2 use std::path::{Path, PathBuf}; 3 3 4 4 use anyhow::{Context, Result}; 5 - use dirs::config_dir; 6 5 use serde::{Deserialize, Serialize}; 7 6 8 7 #[derive(Debug, Clone, Serialize, Deserialize, Default)] ··· 55 54 } 56 55 57 56 pub fn default_config_path() -> Result<PathBuf> { 58 - let base = config_dir().context("Could not determine platform config directory")?; 59 - Ok(base.join("tangled").join("config.toml")) 57 + // Use ~/.config/tangled on all platforms for consistency 58 + let home = std::env::var("HOME") 59 + .or_else(|_| std::env::var("USERPROFILE")) 60 + .context("Could not determine home directory")?; 61 + Ok(PathBuf::from(home) 62 + .join(".config") 63 + .join("tangled") 64 + .join("config.toml")) 60 65 } 61 66 62 67 pub fn load_config(path: Option<&Path>) -> Result<Option<RootConfig>> {
+2 -2
crates/tangled-config/src/keychain.rs
··· 21 21 pub fn set_password(&self, secret: &str) -> Result<()> { 22 22 self.entry()? 23 23 .set_password(secret) 24 - .map_err(|e| anyhow!("keyring error: {e}")) 24 + .map_err(|e| anyhow!("Failed to save credentials to keychain: {e}")) 25 25 } 26 26 27 27 pub fn get_password(&self) -> Result<String> { 28 28 self.entry()? 29 29 .get_password() 30 - .map_err(|e| anyhow!("keyring error: {e}")) 30 + .map_err(|e| anyhow!("Failed to load credentials from keychain: {e}")) 31 31 } 32 32 33 33 pub fn delete_password(&self) -> Result<()> {

History

1 round 3 comments
sign up or login to add to the discussion
dunkirk.sh submitted #0
1 commit
expand
f2ef3568
fix(config): use platform-specific keyring features and standardize config path
no conflicts, ready to merge
expand 3 comments

I tested this (since my macOS auth was broken without it), and it now works! However, I get a keychain prompt up for every CLI invocation to type in my password. Would be nice if it cached that, but I'm not sure what's going on with the Keychain -> Passwords app migration on macOS in recent versions.

If you select the prompt to never ask for this app, it will work in the future! It does seem to reset whenever the binary changes, so updates will retrigger it, but that seems like an acceptable compromise. The only way to not have it do that would be to sign the app with a developer cert but that seems over the top

Aha it was indeed my binary changing all the time that reset it. Signing with a developer cert for the eventual stable release binary seems like a reasonable solution to this in the longer term.