@@ -116,6 +116,17 @@ pub struct GitHubConfig {
116116 pub webhook_secret : Option < String > ,
117117}
118118
119+ #[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
120+ pub struct AutomationConfig {
121+ /// Outbound webhook URL for downstream automation consumers.
122+ #[ serde( default , rename = "automation_webhook_url" ) ]
123+ pub webhook_url : Option < String > ,
124+
125+ /// Optional shared secret for signing outbound automation webhooks.
126+ #[ serde( default , rename = "automation_webhook_secret" ) ]
127+ pub webhook_secret : Option < String > ,
128+ }
129+
119130#[ derive( Debug , Clone , Serialize , Deserialize ) ]
120131pub struct AgentConfig {
121132 /// Enable agent loop for iterative tool-calling review (default false).
@@ -453,6 +464,9 @@ pub struct Config {
453464 #[ serde( default , flatten) ]
454465 pub github : GitHubConfig ,
455466
467+ #[ serde( default , flatten) ]
468+ pub automation : AutomationConfig ,
469+
456470 /// When true, run separate specialized LLM passes for security, correctness,
457471 /// and style instead of a single monolithic review prompt.
458472 #[ serde( default = "default_false" ) ]
@@ -655,6 +669,7 @@ impl Default for Config {
655669 rule_priority : Vec :: new ( ) ,
656670 providers : HashMap :: new ( ) ,
657671 github : GitHubConfig :: default ( ) ,
672+ automation : AutomationConfig :: default ( ) ,
658673 multi_pass_specialized : false ,
659674 agent : AgentConfig :: default ( ) ,
660675 verification : VerificationConfig :: default ( ) ,
@@ -831,33 +846,20 @@ impl Config {
831846 . ok ( )
832847 . filter ( |s| !s. trim ( ) . is_empty ( ) ) ;
833848 }
834-
835- // Validate base_url: must be a valid http/https URL with a host
836- if let Some ( ref raw_url) = self . base_url {
837- match url:: Url :: parse ( raw_url) {
838- Ok ( parsed) => {
839- if !matches ! ( parsed. scheme( ) , "http" | "https" ) {
840- warn ! (
841- "base_url '{}' uses unsupported scheme '{}' (expected http or https), ignoring" ,
842- raw_url,
843- parsed. scheme( )
844- ) ;
845- self . base_url = None ;
846- } else if parsed. host ( ) . is_none ( ) {
847- warn ! ( "base_url '{}' has no valid host, ignoring" , raw_url) ;
848- self . base_url = None ;
849- }
850- }
851- Err ( err) => {
852- warn ! (
853- "base_url '{}' is not a valid URL ({}), ignoring" ,
854- raw_url, err
855- ) ;
856- self . base_url = None ;
857- }
858- }
849+ if self . automation . webhook_url . is_none ( ) {
850+ self . automation . webhook_url = std:: env:: var ( "DIFFSCOPE_AUTOMATION_WEBHOOK_URL" )
851+ . ok ( )
852+ . filter ( |s| !s. trim ( ) . is_empty ( ) ) ;
853+ }
854+ if self . automation . webhook_secret . is_none ( ) {
855+ self . automation . webhook_secret = std:: env:: var ( "DIFFSCOPE_AUTOMATION_WEBHOOK_SECRET" )
856+ . ok ( )
857+ . filter ( |s| !s. trim ( ) . is_empty ( ) ) ;
859858 }
860859
860+ validate_optional_http_url ( & mut self . base_url , "base_url" ) ;
861+ validate_optional_http_url ( & mut self . automation . webhook_url , "automation_webhook_url" ) ;
862+
861863 // Normalize adapter field
862864 if let Some ( ref adapter) = self . adapter {
863865 let normalized = adapter. trim ( ) . to_lowercase ( ) ;
@@ -1365,6 +1367,36 @@ impl Config {
13651367 }
13661368}
13671369
1370+ fn validate_optional_http_url ( url : & mut Option < String > , field_name : & str ) {
1371+ let Some ( raw_url) = url. clone ( ) else {
1372+ return ;
1373+ } ;
1374+
1375+ match url:: Url :: parse ( & raw_url) {
1376+ Ok ( parsed) => {
1377+ if !matches ! ( parsed. scheme( ) , "http" | "https" ) {
1378+ warn ! (
1379+ "{} '{}' uses unsupported scheme '{}' (expected http or https), ignoring" ,
1380+ field_name,
1381+ raw_url,
1382+ parsed. scheme( )
1383+ ) ;
1384+ * url = None ;
1385+ } else if parsed. host ( ) . is_none ( ) {
1386+ warn ! ( "{} '{}' has no valid host, ignoring" , field_name, raw_url) ;
1387+ * url = None ;
1388+ }
1389+ }
1390+ Err ( err) => {
1391+ warn ! (
1392+ "{} '{}' is not a valid URL ({}), ignoring" ,
1393+ field_name, raw_url, err
1394+ ) ;
1395+ * url = None ;
1396+ }
1397+ }
1398+ }
1399+
13681400fn default_model ( ) -> String {
13691401 "anthropic/claude-opus-4.5" . to_string ( )
13701402}
@@ -1746,6 +1778,39 @@ mod tests {
17461778 assert ! ( config. base_url. is_none( ) ) ;
17471779 }
17481780
1781+ #[ test]
1782+ fn normalize_accepts_automation_webhook_url_https ( ) {
1783+ let mut config = Config {
1784+ automation : AutomationConfig {
1785+ webhook_url : Some ( "https://automation.example.com/hooks/reviews" . to_string ( ) ) ,
1786+ ..AutomationConfig :: default ( )
1787+ } ,
1788+ ..Config :: default ( )
1789+ } ;
1790+
1791+ config. normalize ( ) ;
1792+
1793+ assert_eq ! (
1794+ config. automation. webhook_url. as_deref( ) ,
1795+ Some ( "https://automation.example.com/hooks/reviews" )
1796+ ) ;
1797+ }
1798+
1799+ #[ test]
1800+ fn normalize_rejects_automation_webhook_url_bad_scheme ( ) {
1801+ let mut config = Config {
1802+ automation : AutomationConfig {
1803+ webhook_url : Some ( "ftp://automation.example.com/hooks/reviews" . to_string ( ) ) ,
1804+ ..AutomationConfig :: default ( )
1805+ } ,
1806+ ..Config :: default ( )
1807+ } ;
1808+
1809+ config. normalize ( ) ;
1810+
1811+ assert ! ( config. automation. webhook_url. is_none( ) ) ;
1812+ }
1813+
17491814 #[ test]
17501815 fn normalize_clamps_max_tokens_above_limit ( ) {
17511816 let mut config = Config {
0 commit comments