diff --git a/internal/acp/execution.go b/internal/acp/execution.go index 25be338..b15c335 100644 --- a/internal/acp/execution.go +++ b/internal/acp/execution.go @@ -148,7 +148,7 @@ func (s *Server) runSingleAgentViaExternalProvider( params map[string]any, notify func(map[string]any), ) (map[string]any, error) { - endpoint := strings.TrimSpace(provider.Endpoint) + endpoint := resolveSingleAgentForwardEndpoint(provider) if endpoint == "" { return nil, fmt.Errorf("external provider endpoint is missing") } @@ -163,6 +163,21 @@ func (s *Server) runSingleAgentViaExternalProvider( ) } +func resolveSingleAgentForwardEndpoint(provider syncedProvider) string { + endpoint := strings.TrimSpace(provider.Endpoint) + if endpoint == "" { + return "" + } + if !strings.Contains(strings.ToLower(endpoint), "xworkmate-bridge.svc.plus") { + return endpoint + } + providerID := strings.TrimSpace(strings.ToLower(provider.ProviderID)) + if providerID == "" { + return endpoint + } + return fmt.Sprintf("https://acp-server.svc.plus/%s/acp/rpc", providerID) +} + func sanitizeExternalACPParams(method string, params map[string]any) map[string]any { if len(params) == 0 { return map[string]any{} @@ -259,8 +274,8 @@ func requestExternalACPHTTP( } req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Accept", "application/json") - if strings.TrimSpace(authorization) != "" { - req.Header.Set("Authorization", strings.TrimSpace(authorization)) + if normalized := normalizeAuthorizationHeader(authorization); normalized != "" { + req.Header.Set("Authorization", normalized) } response, err := (&http.Client{Timeout: 2 * time.Minute}).Do(req) if err != nil { @@ -280,6 +295,17 @@ func requestExternalACPHTTP( return decoded, nil } +func normalizeAuthorizationHeader(raw string) string { + normalized := strings.TrimSpace(raw) + if normalized == "" { + return "" + } + if strings.Contains(normalized, " ") { + return normalized + } + return "Bearer " + normalized +} + func requestExternalACPWebSocket( ctx context.Context, endpoint *urlSpec, diff --git a/internal/acp/execution_test.go b/internal/acp/execution_test.go new file mode 100644 index 0000000..5b4ace9 --- /dev/null +++ b/internal/acp/execution_test.go @@ -0,0 +1,68 @@ +package acp + +import "testing" + +func TestResolveSingleAgentForwardEndpoint(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + provider syncedProvider + want string + }{ + { + name: "preserves upstream endpoint", + provider: syncedProvider{ + ProviderID: "opencode", + Endpoint: "https://acp-server.svc.plus/opencode/acp/rpc", + }, + want: "https://acp-server.svc.plus/opencode/acp/rpc", + }, + { + name: "rewrites bridge discovery endpoint to codex upstream", + provider: syncedProvider{ + ProviderID: "codex", + Endpoint: "https://xworkmate-bridge.svc.plus", + }, + want: "https://acp-server.svc.plus/codex/acp/rpc", + }, + { + name: "rewrites bridge discovery endpoint to gemini upstream", + provider: syncedProvider{ + ProviderID: "gemini", + Endpoint: "https://xworkmate-bridge.svc.plus", + }, + want: "https://acp-server.svc.plus/gemini/acp/rpc", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := resolveSingleAgentForwardEndpoint(tc.provider); got != tc.want { + t.Fatalf("resolveSingleAgentForwardEndpoint() = %q, want %q", got, tc.want) + } + }) + } +} + +func TestNormalizeAuthorizationHeader(t *testing.T) { + t.Parallel() + + cases := map[string]string{ + "": "", + "Bearer bridge": "Bearer bridge", + "bridge-token": "Bearer bridge-token", + " bridge-token ": "Bearer bridge-token", + } + for raw, want := range cases { + raw, want := raw, want + t.Run(raw, func(t *testing.T) { + t.Parallel() + if got := normalizeAuthorizationHeader(raw); got != want { + t.Fatalf("normalizeAuthorizationHeader(%q) = %q, want %q", raw, got, want) + } + }) + } +}