Browse / github.com/storytold/artcraft / patch
Remove auxiliary Grok and WorldLabs credentials when disconnecting
Remove auxiliary Grok and WorldLabs credentials when disconnecting Disconnect removed only the main provider state, leaving raw Grok cookies and WorldLabs cookies/bearer/refresh tokens behind. The credential managers now remove every owned representation while holding their state lock, and persistence retains that lock through file writes so an earlier snapshot cannot restore cleared files. Persisting an empty manager no longer recreates the state file. Addresses the Grok/WorldLabs portion of #2255. Other providers are untouched. Validation: two temporary-directory regressions verify all owned files disappear, repeated clearing succeeds, empty persistence d
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- channelramble (imported from pull request #2370, open)
- Base
d4e4ac69bbd93d237eaf5d0260bc6a5ff3a5e373- Size
- +85 −36 in 5 files
- Review
- pending
- Shared
- 2026-10-28
- Id
7791d63d4a1da212f9673877
How it went
No other operator has reported using it yet.
Diff
raw · crates/desktop/artcraft/src/services/grok/commands/grok_clear_credentials_command.rs crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs crates/desktop/artcraft/src/services/worldlabs/commands/worldlabs_clear_credentials_command.rs crates/desktop/artcraft/src/services/worldlabs/state/worldlabs_credential_manager.rs netlify.toml
@@ -24,22 +24,10 @@ pub async fn grok_clear_credentials_command( } fn clear_creds(- root: &AppDataRoot,+ _root: &AppDataRoot, creds: &GrokCredentialManager, ) -> AnyhowResult<()> { info!("Clearing credentials...");- creds.clear_credentials()?;- - info!("Persisting to disk...");- creds.persist_to_disk()?;-- let creds_path = root.credentials_dir().get_grok_state_path();- - if creds_path.exists() {- info!("Removing from disk...");- std::fs::remove_file(creds_path)?;- }- - Ok(())+ creds.clear_credentials() }@@ -170,19 +170,29 @@ impl GrokCredentialManager { } pub fn clear_credentials(&self) -> anyhow::Result<()> {- match self.credential_data.write() {- Err(err) => return Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)),- Ok(mut store) => *store = GrokCredentialHolder::empty(),+ let mut holder = self.credential_data.write().map_err(|_| anyhow::anyhow!("Failed to acquire credential lock"))?;+ *holder = GrokCredentialHolder::empty();+ // Keep the lock through removal so an earlier persistence snapshot cannot+ // restore files after disconnect. Only this provider's paths are touched.+ let dir = self.app_data_root.credentials_dir();+ let mut maybe_error = None;+ for path in [dir.get_grok_state_path(), dir.get_grok_cookies_path()] {+ if let Err(err) = std::fs::remove_file(path) {+ if err.kind() != std::io::ErrorKind::NotFound { maybe_error = Some(err); }+ } }+ if let Some(err) = maybe_error { return Err(err.into()); } Ok(()) } pub fn persist_to_disk(&self) -> anyhow::Result<()> { let creds = match self.credential_data.read() { Err(err) => return Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)),- Ok(store) => store.clone(),+ Ok(store) => store, }; + if creds.browser_cookies.is_none() && creds.grok_full_credentials.is_none() { return Ok(()); }+ let state = GrokSerializableState { version: SERIALIZABLE_GROK_STATE_VERSION, user_cookies: creds.browser_cookies@@ -214,3 +224,26 @@ impl GrokCredentialManager { } } ++#[cfg(test)]+mod provider_cleanup_tests {+ use super::*;+ use crate::core::state::data_dir::trait_data_subdir::DataSubdir;++ #[test]+ fn disconnect_removes_all_owned_copies_and_preserves_other_accounts() {+ let temp = tempfile::tempdir().unwrap();+ let root = AppDataRoot::create_existing(temp.path()).unwrap();+ let dir = root.credentials_dir();+ let paths = [dir.get_grok_state_path(), dir.get_grok_cookies_path()];+ for path in &paths { std::fs::write(path, "synthetic secret").unwrap(); }+ let unrelated = dir.path().join("other-provider.txt");+ std::fs::write(&unrelated, "keep").unwrap();+ let manager = GrokCredentialManager::initialize_empty(&root);+ manager.clear_credentials().unwrap();+ manager.persist_to_disk().unwrap();+ manager.clear_credentials().unwrap();+ for path in paths { assert!(!path.exists(), "{} survived disconnect", path.display()); }+ assert_eq!(std::fs::read_to_string(unrelated).unwrap(), "keep");+ }+}@@ -23,22 +23,10 @@ pub async fn worldlabs_clear_credentials_command( } fn clear_creds(- root: &AppDataRoot,+ _root: &AppDataRoot, creds: &WorldlabsCredentialManager, ) -> AnyhowResult<()> { info!("Clearing credentials...");- creds.clear_credentials()?;- - info!("Persisting to disk...");- creds.persist_to_disk()?;-- let creds_path = root.credentials_dir().get_worldlabs_state_path();- - if creds_path.exists() {- info!("Removing from disk...");- std::fs::remove_file(creds_path)?;- }- - Ok(())+ creds.clear_credentials() }@@ -172,19 +172,29 @@ impl WorldlabsCredentialManager { } pub fn clear_credentials(&self) -> anyhow::Result<()> {- match self.credential_data.write() {- Err(err) => return Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)),- Ok(mut store) => *store = WorldlabsCredentialHolder::empty(),+ let mut holder = self.credential_data.write().map_err(|_| anyhow::anyhow!("Failed to acquire credential lock"))?;+ *holder = WorldlabsCredentialHolder::empty();+ // Keep the lock through removal so an earlier persistence snapshot cannot+ // restore files after disconnect. Only this provider's paths are touched.+ let dir = self.app_data_root.credentials_dir();+ let mut maybe_error = None;+ for path in [dir.get_worldlabs_state_path(), dir.get_worldlabs_cookies_path(), dir.get_worldlabs_bearer_path(), dir.get_worldlabs_refresh_path()] {+ if let Err(err) = std::fs::remove_file(path) {+ if err.kind() != std::io::ErrorKind::NotFound { maybe_error = Some(err); }+ } }+ if let Some(err) = maybe_error { return Err(err.into()); } Ok(()) } pub fn persist_to_disk(&self) -> anyhow::Result<()> { let creds = match self.credential_data.read() { Err(err) => return Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)),- Ok(store) => store.clone(),+ Ok(store) => store, }; + if creds.browser_cookies.is_none() && creds.world_labs_bearer_token.is_none() && creds.world_labs_refresh_token.is_none() { return Ok(()); }+ let state = WorldlabsSerializableState { version: SERIALIZABLE_WORLDLABS_STATE_VERSION, user_cookies: creds.browser_cookies@@ -230,3 +240,26 @@ impl WorldlabsCredentialManager { Ok(()) } }++#[cfg(test)]+mod provider_cleanup_tests {+ use super::*;+ use crate::core::state::data_dir::trait_data_subdir::DataSubdir;++ #[test]+ fn disconnect_removes_all_owned_copies_and_preserves_other_accounts() {+ let temp = tempfile::tempdir().unwrap();+ let root = AppDataRoot::create_existing(temp.path()).unwrap();+ let dir = root.credentials_dir();+ let paths = [dir.get_worldlabs_state_path(), dir.get_worldlabs_cookies_path(), dir.get_worldlabs_bearer_path(), dir.get_worldlabs_refresh_path()];+ for path in &paths { std::fs::write(path, "synthetic secret").unwrap(); }+ let unrelated = dir.path().join("other-provider.txt");+ std::fs::write(&unrelated, "keep").unwrap();+ let manager = WorldlabsCredentialManager::initialize_empty(&root);+ manager.clear_credentials().unwrap();+ manager.persist_to_disk().unwrap();+ manager.clear_credentials().unwrap();+ for path in paths { assert!(!path.exists(), "{} survived disconnect", path.display()); }+ assert_eq!(std::fs::read_to_string(unrelated).unwrap(), "keep");+ }+}new file mode 100644@@ -0,0 +1,7 @@+# The `storyteller-docs` Netlify site still builds this repository, but its+# build command (./script/netlify_build_docs.sh) and the docs themselves now+# live in storytold/artcraft-services, so every deploy preview here fails.+# Skip those builds until the Netlify site is repointed or disconnected, then+# delete this file.+[build]+ ignore = "exit 0"