@@ -20,6 +20,15 @@ var firewallLogLog = logger.New("cli:firewall_log")
2020// Pre-compiled regexes for firewall log parsing (performance optimization)
2121var (
2222 firewallLogFieldSplitter = regexp .MustCompile (`(?:[^\s"]+|"[^"]*")+` )
23+
24+ // agentLogAllowDomainsPattern matches Codex CLI firewall warning lines that suggest
25+ // adding a blocked domain to the allow-list.
26+ // Captures the full token after --allow-domains (up to whitespace) to support
27+ // hostnames, protocol prefixes, wildcard patterns, and ports.
28+ // Example: "add --allow-domains chatgpt.com to your command"
29+ // Example: "add --allow-domains chatgpt.com,other.com to your command"
30+ // Example: "add --allow-domains https://api.example.com:443 to your command"
31+ agentLogAllowDomainsPattern = regexp .MustCompile (`--allow-domains\s+([^\s]+)` )
2332)
2433
2534// Firewall Log Parser
@@ -127,7 +136,44 @@ func (f *FirewallAnalysis) AddMetrics(other LogAnalysis) {
127136 f .AllowedRequests += otherFirewall .AllowedRequests
128137 f .BlockedRequests += otherFirewall .BlockedRequests
129138
139+ // Merge blocked domain lists
140+ if len (otherFirewall .BlockedDomains ) > 0 {
141+ domainSet := make (map [string ]bool , len (f .BlockedDomains )+ len (otherFirewall .BlockedDomains ))
142+ for _ , d := range f .BlockedDomains {
143+ domainSet [d ] = true
144+ }
145+ for _ , d := range otherFirewall .BlockedDomains {
146+ domainSet [d ] = true
147+ }
148+ merged := make ([]string , 0 , len (domainSet ))
149+ for d := range domainSet {
150+ merged = append (merged , d )
151+ }
152+ sort .Strings (merged )
153+ f .SetBlockedDomains (merged )
154+ }
155+
156+ // Merge allowed domain lists
157+ if len (otherFirewall .AllowedDomains ) > 0 {
158+ domainSet := make (map [string ]bool , len (f .AllowedDomains )+ len (otherFirewall .AllowedDomains ))
159+ for _ , d := range f .AllowedDomains {
160+ domainSet [d ] = true
161+ }
162+ for _ , d := range otherFirewall .AllowedDomains {
163+ domainSet [d ] = true
164+ }
165+ merged := make ([]string , 0 , len (domainSet ))
166+ for d := range domainSet {
167+ merged = append (merged , d )
168+ }
169+ sort .Strings (merged )
170+ f .SetAllowedDomains (merged )
171+ }
172+
130173 // Merge request stats by domain
174+ if f .RequestsByDomain == nil {
175+ f .RequestsByDomain = make (map [string ]DomainRequestStats )
176+ }
131177 for domain , stats := range otherFirewall .RequestsByDomain {
132178 existing := f .RequestsByDomain [domain ]
133179 existing .Allowed += stats .Allowed
@@ -399,3 +445,67 @@ func analyzeMultipleFirewallLogs(logsDir string, verbose bool) (*FirewallAnalysi
399445 },
400446 )
401447}
448+
449+ // extractFirewallFromAgentLog scans agent-stdio.log for firewall-blocked domain warnings.
450+ // This supplements dedicated proxy firewall logs (e.g., Squid access logs) by extracting
451+ // network-block information from the agent's own output when proxy logs are unavailable.
452+ //
453+ // The Codex CLI emits lines containing "--allow-domains <domain>" when a domain is blocked
454+ // by the sandbox firewall. For example:
455+ //
456+ // "[WARN] chatgpt.com is not in the allowed domains. To allow access, add --allow-domains chatgpt.com to your command."
457+ //
458+ // Returns nil when no agent-stdio.log exists or no blocked domains are found.
459+ func extractFirewallFromAgentLog (logsPath string , verbose bool ) * FirewallAnalysis {
460+ agentStdioPath := filepath .Clean (filepath .Join (logsPath , "agent-stdio.log" ))
461+ content , err := os .ReadFile (agentStdioPath ) // #nosec G304 -- path is cleaned via filepath.Clean and logsPath is a trusted run output directory
462+ if err != nil {
463+ // File not present is normal (agent didn't run, or run used a different log path)
464+ firewallLogLog .Printf ("No agent-stdio.log found at %s: %v" , agentStdioPath , err )
465+ return nil
466+ }
467+
468+ blockedDomainsSet := make (map [string ]bool )
469+ for line := range strings .SplitSeq (string (content ), "\n " ) {
470+ if matches := agentLogAllowDomainsPattern .FindStringSubmatch (line ); len (matches ) > 1 {
471+ // Domains can be comma-separated in the suggestion
472+ for domain := range strings .SplitSeq (matches [1 ], "," ) {
473+ if d := strings .TrimSpace (domain ); d != "" {
474+ blockedDomainsSet [d ] = true
475+ }
476+ }
477+ }
478+ }
479+
480+ if len (blockedDomainsSet ) == 0 {
481+ firewallLogLog .Printf ("No blocked domains found in agent-stdio.log at %s" , agentStdioPath )
482+ return nil
483+ }
484+
485+ blockedDomains := make ([]string , 0 , len (blockedDomainsSet ))
486+ for d := range blockedDomainsSet {
487+ blockedDomains = append (blockedDomains , d )
488+ }
489+ sort .Strings (blockedDomains )
490+
491+ analysis := & FirewallAnalysis {
492+ TotalRequests : len (blockedDomains ),
493+ AllowedRequests : 0 ,
494+ BlockedRequests : len (blockedDomains ),
495+ RequestsByDomain : make (map [string ]DomainRequestStats ),
496+ }
497+ analysis .SetBlockedDomains (blockedDomains )
498+ for _ , d := range blockedDomains {
499+ analysis .RequestsByDomain [d ] = DomainRequestStats {Blocked : 1 }
500+ }
501+
502+ firewallLogLog .Printf ("Extracted %d firewall-blocked domain(s) from agent-stdio.log: %s" , len (blockedDomains ), strings .Join (blockedDomains , ", " ))
503+ if verbose {
504+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf (
505+ "Found %d firewall-blocked domain(s) in agent log: %s" ,
506+ len (blockedDomains ), strings .Join (blockedDomains , ", " ),
507+ )))
508+ }
509+
510+ return analysis
511+ }
0 commit comments