From 875d244b668d63d0bb512205e5b71f545a09f955 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 14 Jan 2026 10:28:30 +0000 Subject: [PATCH] fix(ai): allow OpenCode UI to be embedded in iframe The OpenCode reverse proxy now properly modifies response headers to allow iframe embedding within Pulse's AI panel: - ai_handler.go: Add ModifyResponse to strip X-Frame-Options and modify CSP frame-ancestors from OpenCode's responses - security.go: Skip frame-related security headers for /opencode/ paths since the proxy manages its own headers This fixes the "refused to connect" error when opening the AI sidebar. --- internal/api/ai_handler.go | 24 ++++++++++++++++++++++++ internal/api/security.go | 18 +++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/internal/api/ai_handler.go b/internal/api/ai_handler.go index 8d0202439..bfc5b7bcb 100644 --- a/internal/api/ai_handler.go +++ b/internal/api/ai_handler.go @@ -421,6 +421,30 @@ func (h *AIHandler) HandleOpenCodeUI(w http.ResponseWriter, r *http.Request) { req.Host = target.Host } + // Modify response to allow embedding in iframe + // OpenCode sets X-Frame-Options: DENY and CSP frame-ancestors 'none' + // which prevents embedding - we need to remove these for the Pulse panel + proxy.ModifyResponse = func(resp *http.Response) error { + log.Debug().Msg("ModifyResponse called") + // Remove X-Frame-Options to allow iframe embedding + resp.Header.Del("X-Frame-Options") + + // Handle multiple CSP headers - get all values, modify, and set back + cspHeaders := resp.Header.Values("Content-Security-Policy") + if len(cspHeaders) > 0 { + // Delete all existing CSP headers + resp.Header.Del("Content-Security-Policy") + // Add back modified versions + for _, csp := range cspHeaders { + // Replace frame-ancestors 'none' with 'self' to allow embedding + modified := strings.ReplaceAll(csp, "frame-ancestors 'none'", "frame-ancestors 'self'") + resp.Header.Add("Content-Security-Policy", modified) + } + } + + return nil + } + // Handle WebSocket upgrades if r.Header.Get("Upgrade") == "websocket" { proxy.ServeHTTP(w, r) diff --git a/internal/api/security.go b/internal/api/security.go index f9cebf90a..4704222b6 100644 --- a/internal/api/security.go +++ b/internal/api/security.go @@ -409,8 +409,14 @@ func ResetLockout(identifier string) { // SecurityHeadersWithConfig applies security headers with embedding configuration func SecurityHeadersWithConfig(next http.Handler, allowEmbedding bool, allowedOrigins string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Skip frame-related headers for /opencode/ paths - these are managed by the proxy + // The OpenCode proxy modifies headers to allow embedding within Pulse's AI panel + isOpenCodePath := strings.HasPrefix(r.URL.Path, "/opencode") + // Configure clickjacking protection based on embedding settings - if allowEmbedding { + if isOpenCodePath { + // OpenCode proxy manages its own iframe headers + } else if allowEmbedding { // When embedding is allowed, don't set X-Frame-Options header // This allows embedding from any origin // Security note: User explicitly enabled this for iframe embedding @@ -436,7 +442,10 @@ func SecurityHeadersWithConfig(next http.Handler, allowEmbedding bool, allowedOr } // Add frame-ancestors based on embedding settings - if allowEmbedding { + // Skip for /opencode/ paths - the proxy manages its own CSP + if isOpenCodePath { + // OpenCode proxy manages its own CSP headers + } else if allowEmbedding { if allowedOrigins != "" { // Parse comma-separated origins and add them to frame-ancestors origins := strings.Split(allowedOrigins, ",") @@ -457,7 +466,10 @@ func SecurityHeadersWithConfig(next http.Handler, allowEmbedding bool, allowedOr cspDirectives = append(cspDirectives, "frame-ancestors 'none'") } - w.Header().Set("Content-Security-Policy", strings.Join(cspDirectives, "; ")) + // Only set CSP for non-OpenCode paths (OpenCode proxy manages its own headers) + if !isOpenCodePath { + w.Header().Set("Content-Security-Policy", strings.Join(cspDirectives, "; ")) + } // Referrer Policy w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")