11import json
2+ import tempfile
3+ from logging import getLogger
24from pathlib import Path
35
46from pydantic import BaseModel , Field
1214)
1315from crewai .cli .shared .token_manager import TokenManager
1416
17+ logger = getLogger (__name__ )
18+
1519DEFAULT_CONFIG_PATH = Path .home () / ".config" / "crewai" / "settings.json"
1620
21+
22+ def get_writable_config_path () -> Path | None :
23+ """
24+ Find a writable location for the config file with fallback options.
25+
26+ Tries in order:
27+ 1. Default: ~/.config/crewai/settings.json
28+ 2. Temp directory: /tmp/crewai_settings.json (or OS equivalent)
29+ 3. Current directory: ./crewai_settings.json
30+ 4. In-memory only (returns None)
31+
32+ Returns:
33+ Path object for writable config location, or None if no writable location found
34+ """
35+ fallback_paths = [
36+ DEFAULT_CONFIG_PATH , # Default location
37+ Path (tempfile .gettempdir ()) / "crewai_settings.json" , # Temporary directory
38+ Path .cwd () / "crewai_settings.json" , # Current working directory
39+ ]
40+
41+ for config_path in fallback_paths :
42+ try :
43+ config_path .parent .mkdir (parents = True , exist_ok = True )
44+ test_file = config_path .parent / ".crewai_write_test"
45+ try :
46+ test_file .write_text ("test" )
47+ test_file .unlink () # Clean up test file
48+ logger .info (f"Using config path: { config_path } " )
49+ return config_path
50+ except Exception : # noqa: S112
51+ continue
52+
53+ except Exception : # noqa: S112
54+ continue
55+
56+ return None
57+
58+
1759# Settings that are related to the user's account
1860USER_SETTINGS_KEYS = [
1961 "tool_repository_username" ,
@@ -93,16 +135,32 @@ class Settings(BaseModel):
93135 default = DEFAULT_CLI_SETTINGS ["oauth2_domain" ],
94136 )
95137
96- def __init__ (self , config_path : Path = DEFAULT_CONFIG_PATH , ** data ):
97- """Load Settings from config path"""
98- config_path .parent .mkdir (parents = True , exist_ok = True )
138+ def __init__ (self , config_path : Path | None = None , ** data ):
139+ """Load Settings from config path with fallback support"""
140+ if config_path is None :
141+ config_path = get_writable_config_path ()
142+
143+ # If config_path is None, we're in memory-only mode
144+ if config_path is None :
145+ merged_data = {** data }
146+ # Dummy path for memory-only mode
147+ super ().__init__ (config_path = Path ("/dev/null" ), ** merged_data )
148+ return
149+
150+ try :
151+ config_path .parent .mkdir (parents = True , exist_ok = True )
152+ except Exception :
153+ merged_data = {** data }
154+ # Dummy path for memory-only mode
155+ super ().__init__ (config_path = Path ("/dev/null" ), ** merged_data )
156+ return
99157
100158 file_data = {}
101159 if config_path .is_file ():
102160 try :
103161 with config_path .open ("r" ) as f :
104162 file_data = json .load (f )
105- except json . JSONDecodeError :
163+ except Exception :
106164 file_data = {}
107165
108166 merged_data = {** file_data , ** data }
@@ -122,15 +180,22 @@ def reset(self) -> None:
122180
123181 def dump (self ) -> None :
124182 """Save current settings to settings.json"""
125- if self .config_path .is_file ():
126- with self .config_path .open ("r" ) as f :
127- existing_data = json .load (f )
128- else :
129- existing_data = {}
130-
131- updated_data = {** existing_data , ** self .model_dump (exclude_unset = True )}
132- with self .config_path .open ("w" ) as f :
133- json .dump (updated_data , f , indent = 4 )
183+ if str (self .config_path ) == "/dev/null" :
184+ return
185+
186+ try :
187+ if self .config_path .is_file ():
188+ with self .config_path .open ("r" ) as f :
189+ existing_data = json .load (f )
190+ else :
191+ existing_data = {}
192+
193+ updated_data = {** existing_data , ** self .model_dump (exclude_unset = True )}
194+ with self .config_path .open ("w" ) as f :
195+ json .dump (updated_data , f , indent = 4 )
196+
197+ except Exception : # noqa: S110
198+ pass
134199
135200 def _reset_user_settings (self ) -> None :
136201 """Reset all user settings to default values"""
0 commit comments