@@ -127,6 +127,19 @@ pub struct AutomationConfig {
127127 pub webhook_secret : Option < String > ,
128128}
129129
130+ pub ( crate ) const DEFAULT_SERVER_RATE_LIMIT_PER_MINUTE : u32 = 60 ;
131+
132+ #[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
133+ pub struct ServerSecurityConfig {
134+ /// Shared API key required for protected server mutations when configured.
135+ #[ serde( default , rename = "server_api_key" ) ]
136+ pub api_key : Option < String > ,
137+
138+ /// Maximum protected API mutations allowed per minute when auth is enabled.
139+ #[ serde( default , rename = "server_rate_limit_per_minute" ) ]
140+ pub rate_limit_per_minute : Option < u32 > ,
141+ }
142+
130143#[ derive( Debug , Clone , Serialize , Deserialize ) ]
131144pub struct AgentConfig {
132145 /// Enable agent loop for iterative tool-calling review (default false).
@@ -467,6 +480,9 @@ pub struct Config {
467480 #[ serde( default , flatten) ]
468481 pub automation : AutomationConfig ,
469482
483+ #[ serde( default , flatten) ]
484+ pub server_security : ServerSecurityConfig ,
485+
470486 /// When true, run separate specialized LLM passes for security, correctness,
471487 /// and style instead of a single monolithic review prompt.
472488 #[ serde( default = "default_false" ) ]
@@ -670,6 +686,7 @@ impl Default for Config {
670686 providers : HashMap :: new ( ) ,
671687 github : GitHubConfig :: default ( ) ,
672688 automation : AutomationConfig :: default ( ) ,
689+ server_security : ServerSecurityConfig :: default ( ) ,
673690 multi_pass_specialized : false ,
674691 agent : AgentConfig :: default ( ) ,
675692 verification : VerificationConfig :: default ( ) ,
@@ -856,6 +873,23 @@ impl Config {
856873 . ok ( )
857874 . filter ( |s| !s. trim ( ) . is_empty ( ) ) ;
858875 }
876+ if self . server_security . api_key . is_none ( ) {
877+ self . server_security . api_key = std:: env:: var ( "DIFFSCOPE_SERVER_API_KEY" )
878+ . ok ( )
879+ . filter ( |s| !s. trim ( ) . is_empty ( ) ) ;
880+ }
881+ if self . server_security . rate_limit_per_minute . is_none ( ) {
882+ self . server_security . rate_limit_per_minute =
883+ std:: env:: var ( "DIFFSCOPE_SERVER_RATE_LIMIT_PER_MINUTE" )
884+ . ok ( )
885+ . and_then ( |raw| raw. trim ( ) . parse :: < u32 > ( ) . ok ( ) )
886+ . filter ( |value| * value > 0 ) ;
887+ }
888+ if self . server_security . api_key . is_some ( )
889+ && self . server_security . rate_limit_per_minute . unwrap_or ( 0 ) == 0
890+ {
891+ self . server_security . rate_limit_per_minute = Some ( DEFAULT_SERVER_RATE_LIMIT_PER_MINUTE ) ;
892+ }
859893
860894 validate_optional_http_url ( & mut self . base_url , "base_url" ) ;
861895 validate_optional_http_url ( & mut self . automation . webhook_url , "automation_webhook_url" ) ;
@@ -1811,6 +1845,39 @@ mod tests {
18111845 assert ! ( config. automation. webhook_url. is_none( ) ) ;
18121846 }
18131847
1848+ #[ test]
1849+ fn normalize_defaults_server_rate_limit_when_api_key_present ( ) {
1850+ let mut config = Config {
1851+ server_security : ServerSecurityConfig {
1852+ api_key : Some ( "shared-key" . to_string ( ) ) ,
1853+ rate_limit_per_minute : None ,
1854+ } ,
1855+ ..Config :: default ( )
1856+ } ;
1857+
1858+ config. normalize ( ) ;
1859+
1860+ assert_eq ! (
1861+ config. server_security. rate_limit_per_minute,
1862+ Some ( DEFAULT_SERVER_RATE_LIMIT_PER_MINUTE )
1863+ ) ;
1864+ }
1865+
1866+ #[ test]
1867+ fn normalize_preserves_explicit_server_rate_limit ( ) {
1868+ let mut config = Config {
1869+ server_security : ServerSecurityConfig {
1870+ api_key : Some ( "shared-key" . to_string ( ) ) ,
1871+ rate_limit_per_minute : Some ( 120 ) ,
1872+ } ,
1873+ ..Config :: default ( )
1874+ } ;
1875+
1876+ config. normalize ( ) ;
1877+
1878+ assert_eq ! ( config. server_security. rate_limit_per_minute, Some ( 120 ) ) ;
1879+ }
1880+
18141881 #[ test]
18151882 fn normalize_clamps_max_tokens_above_limit ( ) {
18161883 let mut config = Config {
0 commit comments