diff --git a/llm-client/src/main/kotlin/org/openprojectx/ai/plugin/llm/TemplateRequestExecutor.kt b/llm-client/src/main/kotlin/org/openprojectx/ai/plugin/llm/TemplateRequestExecutor.kt index 4dc1f88..ff05ce0 100644 --- a/llm-client/src/main/kotlin/org/openprojectx/ai/plugin/llm/TemplateRequestExecutor.kt +++ b/llm-client/src/main/kotlin/org/openprojectx/ai/plugin/llm/TemplateRequestExecutor.kt @@ -31,8 +31,9 @@ class TemplateRequestExecutor( } val safeRequestHeaders = redactHeaders(effectiveRequestHeaders) val safeRequestBody = redactSensitivePayload(renderedBody) + val curlCommand = toCurlCommand(config.method.uppercase(), renderedUrl, effectiveRequestHeaders, renderedBody) LlmRuntimeLogger.info( - "Template request start | method=${config.method.uppercase()} | url=$renderedUrl | headers=$safeRequestHeaders | body=$safeRequestBody" + "Template request start | method=${config.method.uppercase()} | url=$renderedUrl | headers=$safeRequestHeaders | body=$safeRequestBody | curl=$curlCommand" ) val response = http.request { @@ -136,6 +137,29 @@ class TemplateRequestExecutor( return "***" } + private fun toCurlCommand(method: String, url: String, headers: Map, body: String): String { + val headerArgs = headers.entries.joinToString(" ") { (name, value) -> + "-H " + shellQuote("$name: ${redactHeaderValue(name, value)}") + } + val redactedBody = redactSensitivePayload(body) + val bodyArg = if (redactedBody.isNotBlank()) " --data " + shellQuote(redactedBody) else "" + return "curl -X $method " + shellQuote(url) + + (if (headerArgs.isNotBlank()) " $headerArgs" else "") + + bodyArg + } + + private fun redactHeaderValue(name: String, value: String): String { + return if (name.contains("authorization", true) || + name.contains("token", true) || + name.contains("key", true) || + name.contains("secret", true) || + name.contains("password", true) || + name.contains("cookie", true) + ) "***" else value + } + + private fun shellQuote(value: String): String = "'" + value.replace("'", "'\"'\"'") + "'" + private companion object { const val MAX_LOG_BODY_CHARS = 4_000 } diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AiTestSettingsConfigurable.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AiTestSettingsConfigurable.kt index eea7aa2..6c5efc6 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AiTestSettingsConfigurable.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AiTestSettingsConfigurable.kt @@ -2,6 +2,7 @@ package org.openprojectx.ai.plugin import com.intellij.openapi.options.SearchableConfigurable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages @@ -336,10 +337,10 @@ class AiTestSettingsConfigurable( ApplicationManager.getApplication().executeOnPooledThread { runCatching(block) .onSuccess { value -> - ApplicationManager.getApplication().invokeLater { onSuccess(value) } + ApplicationManager.getApplication().invokeLater({ onSuccess(value) }, ModalityState.any()) } .onFailure { ex -> - ApplicationManager.getApplication().invokeLater { onFailure(ex) } + ApplicationManager.getApplication().invokeLater({ onFailure(ex) }, ModalityState.any()) } } } diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmAuthSessionService.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmAuthSessionService.kt index 731482f..04666b1 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmAuthSessionService.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmAuthSessionService.kt @@ -4,6 +4,7 @@ import com.intellij.credentialStore.CredentialAttributes import com.intellij.credentialStore.Credentials import com.intellij.ide.passwordSafe.PasswordSafe import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -77,13 +78,13 @@ class LlmAuthSessionService( ApplicationManager.getApplication().executeOnPooledThread { try { loginNow() - ApplicationManager.getApplication().invokeLater { + ApplicationManager.getApplication().invokeLater({ Messages.showInfoMessage(project, "LLM login succeeded.", "Code Quality Improver") - } + }, ModalityState.any()) } catch (e: Exception) { - ApplicationManager.getApplication().invokeLater { + ApplicationManager.getApplication().invokeLater({ Messages.showErrorDialog(project, detailedErrorMessage("LLM login failed", e), "Code Quality Improver") - } + }, ModalityState.any()) } } } @@ -159,7 +160,7 @@ class LlmAuthSessionService( if (app.isDispatchThread) { showDialog() } else { - app.invokeAndWait(showDialog) + app.invokeAndWait(showDialog, ModalityState.any()) } return credentials } diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmSettingsLoader.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmSettingsLoader.kt index bd2487c..dacc153 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmSettingsLoader.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/LlmSettingsLoader.kt @@ -1445,8 +1445,9 @@ object LlmSettingsLoader { } else { "Authorization=" } + val curlCommand = buildBitbucketCurlCommand(url, normalized, credentials) RuntimeLogStore.append( - "INFO | Bitbucket API | Request method=GET url=$url headers[$authHeaderLog] credentialSources=${credentials.joinToString(",") { it.source }.ifBlank { "" }}" + "INFO | Bitbucket API | Request method=GET url=$url headers[$authHeaderLog] credentialSources=${credentials.joinToString(",") { it.source }.ifBlank { "" }} | curl=$curlCommand" ) val code = conn.responseCode val body = (if (code in 200..299) conn.inputStream else conn.errorStream) @@ -1465,6 +1466,25 @@ object LlmSettingsLoader { return body } + + private fun buildBitbucketCurlCommand(url: String, token: String, credentials: List): String { + val authorizationHeader = when { + token.isNotBlank() && token.contains(":") -> { + val username = token.substringBefore(':') + "Authorization: Basic ${displayHeaderValue(username)}:***" + } + token.isNotBlank() -> "Authorization: Bearer ***" + credentials.isNotEmpty() -> { + val credential = credentials.first() + "Authorization: Basic ${displayHeaderValue(credential.username)}:***" + } + else -> null + } + val authPart = authorizationHeader?.let { " -H " + shellQuote(it) }.orEmpty() + return "curl -X GET " + shellQuote(url) + authPart + } + + private fun shellQuote(value: String): String = "'" + value.replace("'", "'\"'\"'") + "'" private fun describeBasicTokenHeader(token: String): String { val separatorIndex = token.indexOf(':') val username = token.substring(0, separatorIndex) diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeCoverageAction.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeCoverageAction.kt index d618e52..95a5cfc 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeCoverageAction.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeCoverageAction.kt @@ -93,7 +93,8 @@ private data class SonarQubeCoverageRequest( val password: String, val targetCoverage: Double, val maxFiles: Int, - val generateMissingTests: Boolean + val generateMissingTests: Boolean, + val skipPkixCheck: Boolean ) private class SonarQubeCoverageDialog( @@ -108,6 +109,7 @@ private class SonarQubeCoverageDialog( private val targetCoverageField = JTextField(config.targetCoverage.toString(), 8) private val maxFilesField = JTextField(config.maxFiles.toString(), 8) private val generateMissingTestsBox = JCheckBox("Generate missing tests with AI", true) + private val skipPkixCheckBox = JCheckBox("Skip PKIX/TLS certificate check", true) init { title = "SonarQube Coverage" @@ -131,6 +133,7 @@ private class SonarQubeCoverageDialog( add(JLabel("Max uncovered files to inspect")) add(maxFilesField) add(generateMissingTestsBox) + add(skipPkixCheckBox) } fun request(): SonarQubeCoverageRequest = SonarQubeCoverageRequest( @@ -141,7 +144,8 @@ private class SonarQubeCoverageDialog( password = String(passwordField.password).trim(), targetCoverage = targetCoverageField.text.trim().toDoubleOrNull() ?: 80.0, maxFiles = maxFilesField.text.trim().toIntOrNull()?.coerceIn(1, 20) ?: 5, - generateMissingTests = generateMissingTestsBox.isSelected + generateMissingTests = generateMissingTestsBox.isSelected, + skipPkixCheck = skipPkixCheckBox.isSelected ) } @@ -170,7 +174,7 @@ private class SonarQubeCoverageClient(private val request: SonarQubeCoverageRequ ) suspend fun loadCoverage(): SonarQubeCoverageReport { - val jsonClient = HttpClients.shared(timeoutSeconds = 60) + val jsonClient = HttpClients.shared(disableTlsVerification = request.skipPkixCheck, timeoutSeconds = 60) try { val baseUrl = request.serverUrl.trimEnd('/') val component = encoded(request.projectKey)