@@ -65,6 +65,38 @@ pub struct RuntimeFeatureConfig {
6565 sandbox : SandboxConfig ,
6666 provider_fallbacks : ProviderFallbackConfig ,
6767 trusted_roots : Vec < String > ,
68+ provider : RuntimeProviderConfig ,
69+ }
70+
71+ /// Stored provider configuration from the setup wizard.
72+ #[ derive( Debug , Clone , PartialEq , Eq , Default ) ]
73+ pub struct RuntimeProviderConfig {
74+ kind : Option < String > ,
75+ api_key : Option < String > ,
76+ base_url : Option < String > ,
77+ model : Option < String > ,
78+ }
79+
80+ impl RuntimeProviderConfig {
81+ #[ must_use]
82+ pub fn kind ( & self ) -> Option < & str > {
83+ self . kind . as_deref ( )
84+ }
85+
86+ #[ must_use]
87+ pub fn api_key ( & self ) -> Option < & str > {
88+ self . api_key . as_deref ( )
89+ }
90+
91+ #[ must_use]
92+ pub fn base_url ( & self ) -> Option < & str > {
93+ self . base_url . as_deref ( )
94+ }
95+
96+ #[ must_use]
97+ pub fn model ( & self ) -> Option < & str > {
98+ self . model . as_deref ( )
99+ }
68100}
69101
70102/// Ordered chain of fallback model identifiers used when the primary
@@ -315,6 +347,7 @@ impl ConfigLoader {
315347 sandbox : parse_optional_sandbox_config ( & merged_value) ?,
316348 provider_fallbacks : parse_optional_provider_fallbacks ( & merged_value) ?,
317349 trusted_roots : parse_optional_trusted_roots ( & merged_value) ?,
350+ provider : parse_optional_provider_config ( & merged_value) ?,
318351 } ;
319352
320353 Ok ( RuntimeConfig {
@@ -414,6 +447,11 @@ impl RuntimeConfig {
414447 pub fn trusted_roots ( & self ) -> & [ String ] {
415448 & self . feature_config . trusted_roots
416449 }
450+
451+ #[ must_use]
452+ pub fn provider ( & self ) -> & RuntimeProviderConfig {
453+ & self . feature_config . provider
454+ }
417455}
418456
419457impl RuntimeFeatureConfig {
@@ -483,6 +521,11 @@ impl RuntimeFeatureConfig {
483521 pub fn trusted_roots ( & self ) -> & [ String ] {
484522 & self . trusted_roots
485523 }
524+
525+ #[ must_use]
526+ pub fn provider ( & self ) -> & RuntimeProviderConfig {
527+ & self . provider
528+ }
486529}
487530
488531impl ProviderFallbackConfig {
@@ -564,6 +607,91 @@ pub fn default_config_home() -> PathBuf {
564607 . unwrap_or_else ( || PathBuf :: from ( ".claw" ) )
565608}
566609
610+ /// Save provider settings to the user-level `~/.claw/settings.json`.
611+ /// Creates the file and directory if they don't exist. Sets file permissions
612+ /// to `0o600` (owner read/write only) to protect stored API keys.
613+ pub fn save_user_provider_settings (
614+ kind : & str ,
615+ api_key : & str ,
616+ base_url : Option < & str > ,
617+ model : Option < & str > ,
618+ ) -> Result < ( ) , ConfigError > {
619+ let config_home = default_config_home ( ) ;
620+ fs:: create_dir_all ( & config_home) . map_err ( ConfigError :: Io ) ?;
621+ let settings_path = config_home. join ( "settings.json" ) ;
622+
623+ let mut root = read_settings_root ( & settings_path) ;
624+
625+ let mut provider = serde_json:: Map :: new ( ) ;
626+ provider. insert ( "kind" . to_string ( ) , serde_json:: Value :: String ( kind. to_string ( ) ) ) ;
627+ provider. insert ( "apiKey" . to_string ( ) , serde_json:: Value :: String ( api_key. to_string ( ) ) ) ;
628+ if let Some ( base_url) = base_url {
629+ provider. insert ( "baseUrl" . to_string ( ) , serde_json:: Value :: String ( base_url. to_string ( ) ) ) ;
630+ } else {
631+ provider. remove ( "baseUrl" ) ;
632+ }
633+ if let Some ( model) = model {
634+ provider. insert ( "model" . to_string ( ) , serde_json:: Value :: String ( model. to_string ( ) ) ) ;
635+ } else {
636+ provider. remove ( "model" ) ;
637+ }
638+ root. insert ( "provider" . to_string ( ) , serde_json:: Value :: Object ( provider) ) ;
639+
640+ write_settings_root ( & settings_path, & root) ?;
641+
642+ #[ cfg( unix) ]
643+ {
644+ use std:: os:: unix:: fs:: PermissionsExt ;
645+ let perms = std:: fs:: Permissions :: from_mode ( 0o600 ) ;
646+ fs:: set_permissions ( & settings_path, perms) . map_err ( ConfigError :: Io ) ?;
647+ }
648+
649+ Ok ( ( ) )
650+ }
651+
652+ /// Remove the `provider` section from the user-level `~/.claw/settings.json`.
653+ pub fn clear_user_provider_settings ( ) -> Result < ( ) , ConfigError > {
654+ let config_home = default_config_home ( ) ;
655+ let settings_path = config_home. join ( "settings.json" ) ;
656+
657+ if !settings_path. exists ( ) {
658+ return Ok ( ( ) ) ;
659+ }
660+
661+ let mut root = read_settings_root ( & settings_path) ;
662+ if root. remove ( "provider" ) . is_none ( ) {
663+ return Ok ( ( ) ) ;
664+ }
665+
666+ write_settings_root ( & settings_path, & root) ?;
667+
668+ Ok ( ( ) )
669+ }
670+
671+ fn read_settings_root ( path : & Path ) -> serde_json:: Map < String , serde_json:: Value > {
672+ match fs:: read_to_string ( path) {
673+ Ok ( contents) if !contents. trim ( ) . is_empty ( ) => {
674+ serde_json:: from_str :: < serde_json:: Value > ( & contents)
675+ . ok ( )
676+ . and_then ( |v| v. as_object ( ) . cloned ( ) )
677+ . unwrap_or_default ( )
678+ }
679+ _ => serde_json:: Map :: new ( ) ,
680+ }
681+ }
682+
683+ fn write_settings_root (
684+ path : & Path ,
685+ root : & serde_json:: Map < String , serde_json:: Value > ,
686+ ) -> Result < ( ) , ConfigError > {
687+ if let Some ( parent) = path. parent ( ) {
688+ fs:: create_dir_all ( parent) . map_err ( ConfigError :: Io ) ?;
689+ }
690+ let rendered = serde_json:: to_string_pretty ( & serde_json:: Value :: Object ( root. clone ( ) ) )
691+ . map_err ( |e| ConfigError :: Parse ( e. to_string ( ) ) ) ?;
692+ fs:: write ( path, format ! ( "{rendered}\n " ) ) . map_err ( ConfigError :: Io )
693+ }
694+
567695impl RuntimeHookConfig {
568696 #[ must_use]
569697 pub fn new (
@@ -950,6 +1078,25 @@ fn parse_optional_oauth_config(
9501078 } ) )
9511079}
9521080
1081+ fn parse_optional_provider_config ( root : & JsonValue ) -> Result < RuntimeProviderConfig , ConfigError > {
1082+ let Some ( provider_value) = root. as_object ( ) . and_then ( |object| object. get ( "provider" ) ) else {
1083+ return Ok ( RuntimeProviderConfig :: default ( ) ) ;
1084+ } ;
1085+ let Some ( object) = provider_value. as_object ( ) else {
1086+ return Ok ( RuntimeProviderConfig :: default ( ) ) ;
1087+ } ;
1088+ let kind = optional_string ( object, "kind" , "provider" ) ?. map ( str:: to_string) ;
1089+ let api_key = optional_string ( object, "apiKey" , "provider" ) ?. map ( str:: to_string) ;
1090+ let base_url = optional_string ( object, "baseUrl" , "provider" ) ?. map ( str:: to_string) ;
1091+ let model = optional_string ( object, "model" , "provider" ) ?. map ( str:: to_string) ;
1092+ Ok ( RuntimeProviderConfig {
1093+ kind,
1094+ api_key,
1095+ base_url,
1096+ model,
1097+ } )
1098+ }
1099+
9531100fn parse_mcp_server_config (
9541101 server_name : & str ,
9551102 value : & JsonValue ,
0 commit comments