From ded84d664ec6adbc008ee0830a5c103fc68e2dd6 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Mon, 7 Jul 2025 23:50:10 +0300 Subject: [PATCH 1/3] impl: add support for matching agent by name This PR adds support for matching workspace agent in the URI via the `agent_name` query param. The existing support for `agent_id` is dropped and replaced by the new param. --- CHANGELOG.md | 5 ++ README.md | 54 ++++++++++--------- .../toolbox/util/CoderProtocolHandler.kt | 21 ++++---- .../kotlin/com/coder/toolbox/util/LinkMap.kt | 4 +- 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8c11b3..2fcd70c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,16 @@ - support for basic authentication for HTTP/HTTPS proxy - support for Toolbox 2.7 release +- support for matching workspace agent in the URI via the agent name ### Changed - improved message while loading the workspace +### Removed + +- dropped support for `agent_id` as a URI parameter + ### Fixed - URI protocol handler is now able to switch to the Coder provider even if the last opened provider was something else diff --git a/README.md b/README.md index e2fb731..7e4a957 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,9 @@ You can use specially crafted JetBrains Gateway URIs to automatically: ### Example URIs ```text -jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fdev.coder.com&token=zeoX4SbSpP-j2qGpajkdwxR9jBdcekXS2&workspace=bobiverse-bob&agent=dev&ide_product_code=GO&ide_build_number=241.23774.119&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs +jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fdev.coder.com&token=zeoX4SbSpP-j2qGpajkdwxR9jBdcekXS2&workspace=bobiverse-bob&agent_name=dev&ide_product_code=GO&ide_build_number=241.23774.119&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs -jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fj5gj2r1so5nbi.pit-1.try.coder.app%2F&token=gqEirOoI1U-FfCQ6uj8iOLtybBIk99rr8&workspace=bobiverse-riker&agent=dev&ide_product_code=RR&ide_build_number=243.26053.17&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs +jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fj5gj2r1so5nbi.pit-1.try.coder.app%2F&token=gqEirOoI1U-FfCQ6uj8iOLtybBIk99rr8&workspace=bobiverse-riker&agent_name=dev&ide_product_code=RR&ide_build_number=243.26053.17&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs ``` ### URI Breakdown @@ -76,13 +76,15 @@ jetbrains://gateway/com.coder.toolbox ?url=http(s):// &token= &workspace= - &agent_id= + &agent_name= &ide_product_code= &ide_build_number= &folder= ``` -Starting from Toolbox 2.7, you can use `coder` as a shortcut in place of the full plugin ID. The URI can be simplified as: +Starting from Toolbox 2.7, you can use `coder` as a shortcut in place of the full plugin ID. The URI can be simplified +as: + ```text jetbrains://gateway/coder?url=http(s):// ``` @@ -92,16 +94,15 @@ jetbrains://gateway/coder?url=http(s):// | url | Your Coder deployment URL (encoded) | Yes | | token | Coder authentication token | Yes | | workspace | Name of the Coder workspace to connect to. | Yes | -| agent_id | ID of the agent associated with the workspace | No | +| agent_name | The name of the agent with the workspace | No | | ide_product_code | JetBrains IDE product code (e.g., GO for GoLand, RR for Rider) | No | | ide_build_number | Specific build number of the JetBrains IDE to install on the workspace | No | | folder | Absolute path to the project folder to open in the remote IDE (URL-encoded) | No | > [!NOTE] -> If only a single agent is available, specifying an agent ID is optional. However, if multiple agents exist, -> you must provide either the ID to target a specific one. Note that this version of the Coder Toolbox plugin -> does not automatically start agents if they are offline, so please ensure the selected agent is running before -> proceeding. +> If only a single agent is available, specifying an agent name. However, if multiple agents exist, you must provide the +> agent name. Note that this version of the Coder Toolbox plugin does not automatically start agents if they +> are offline, so please ensure the selected agent is running before proceeding. If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open and highlight the workspace environment page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable @@ -151,7 +152,9 @@ mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode socks5 > [!NOTE] > Coder Toolbox plugin handles only HTTP/HTTPS proxy authentication. > SOCKS5 proxy authentication is currently not supported due to limitations -> described in: https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0 +> described +> +in: https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0 ## Debugging and Reporting issues @@ -198,21 +201,21 @@ storage paths. The options can be configured from the plugin's main Workspaces p ### CLI related settings - `Binary source` specifies the source URL or relative path from which the Coder CLI should be downloaded. -If a relative path is provided, it is resolved against the deployment domain. + If a relative path is provided, it is resolved against the deployment domain. - `Enable downloads` allows automatic downloading of the CLI if the current version is missing or outdated. - `Binary directory` specifies the directory where CLI binaries are stored. If omitted, it defaults to the data -directory. + directory. - `Enable binary directory fallback` if enabled, falls back to the data directory when the specified binary -directory is not writable. + directory is not writable. - `Data directory` directory where plugin-specific data such as session tokens and binaries are stored if not -overridden by the binary directory setting. + overridden by the binary directory setting. - `Header command` command that outputs additional HTTP headers. Each line of output must be in the format key=value. -The environment variable CODER_URL will be available to the command process. + The environment variable CODER_URL will be available to the command process. ### TLS settings @@ -220,34 +223,34 @@ The following options control the secure communication behavior of the plugin wi API. - `TLS cert path` path to a client certificate file for TLS authentication with Coder deployment. -The certificate should be in X.509 PEM format. + The certificate should be in X.509 PEM format. - `TLS key path` path to the private key corresponding to the TLS certificate from above. -The certificate should be in X.509 PEM format. + The certificate should be in X.509 PEM format. - `TLS CA path` the path of a file containing certificates for an alternate certificate authority used to verify TLS -certs returned by the Coder deployment. The file should be in X.509 PEM format. This option can also be used to verify -proxy certificates. + certs returned by the Coder deployment. The file should be in X.509 PEM format. This option can also be used to verify + proxy certificates. - `TLS alternate hostname` overrides the hostname used in TLS verification. This is useful when the hostname -used to connect to the Coder deployment does not match the hostname in the TLS certificate. + used to connect to the Coder deployment does not match the hostname in the TLS certificate. ### SSH settings The following options control the SSH behavior of the Coder CLI. - `Disable autostart` adds the --disable-autostart flag to the SSH proxy command, preventing the CLI from keeping -workspaces constantly active. + workspaces constantly active. - `Enable SSH wildcard config` enables or disables wildcard entries in the SSH configuration, which allow generic -rules for matching multiple workspaces. + rules for matching multiple workspaces. - `SSH proxy log directory` directory where SSH proxy logs are written. Useful for debugging SSH connection issues. - `SSH network metrics directory` directory where network information used by the SSH proxy is stored. - `Extra SSH options` additional options appended to the SSH configuration. Can be used to customize the behavior of -SSH connections. + SSH connections. ### Saving Changes @@ -256,7 +259,7 @@ support, may trigger regeneration of SSH configurations. ### Security considerations -> [!IMPORTANT] +> [!IMPORTANT] > Token authentication is required when TLS certificates are not configured. ## Releasing @@ -269,6 +272,7 @@ support, may trigger regeneration of SSH configurations. JetBrains enabled auto-approval for the plugin, so we need to ensure we continue to meet the following requirements: - do **not** use Kotlin experimental APIs. - do **not** add any lambdas, handlers, or class handles to Java runtime hooks. - - do **not** create threads manually (including via libraries). If you must, ensure they are properly cleaned up in the plugin's `CoderRemoteProvider#close()` method. + - do **not** create threads manually (including via libraries). If you must, ensure they are properly cleaned up in + the plugin's `CoderRemoteProvider#close()` method. - do **not** bundle libraries that are already provided by Toolbox. - do **not** perform any ill-intentioned actions. diff --git a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt index 7e50c8a..23b015d 100644 --- a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt @@ -258,26 +258,25 @@ open class CoderProtocolHandler( } // If the agent is missing and the workspace has only one, use that. - val agent = - if (!parameters.agentID().isNullOrBlank()) { - agents.firstOrNull { it.id.toString() == parameters.agentID() } - } else if (agents.size == 1) { - agents.first() - } else { - null - } + val agent = if (!parameters.agentName().isNullOrBlank()) { + agents.firstOrNull { it.name == parameters.agentName() } + } else if (agents.size == 1) { + agents.first() + } else { + null + } if (agent == null) { - if (!parameters.agentID().isNullOrBlank()) { + if (!parameters.agentName().isNullOrBlank()) { context.logAndShowError( CAN_T_HANDLE_URI_TITLE, - "The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\"" + "The workspace \"${workspace.name}\" does not have an agent with name \"${parameters.agentName()}\"" ) return null } else { context.logAndShowError( CAN_T_HANDLE_URI_TITLE, - "Unable to determine which agent to connect to; \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent" + "Unable to determine which agent to connect to; \"$AGENT_NAME\" must be set because the workspace \"${workspace.name}\" has more than one agent" ) return null } diff --git a/src/main/kotlin/com/coder/toolbox/util/LinkMap.kt b/src/main/kotlin/com/coder/toolbox/util/LinkMap.kt index 0a15db8..a343e14 100644 --- a/src/main/kotlin/com/coder/toolbox/util/LinkMap.kt +++ b/src/main/kotlin/com/coder/toolbox/util/LinkMap.kt @@ -3,7 +3,7 @@ package com.coder.toolbox.util const val URL = "url" const val TOKEN = "token" const val WORKSPACE = "workspace" -const val AGENT_ID = "agent_id" +const val AGENT_NAME = "agent_name" private const val IDE_PRODUCT_CODE = "ide_product_code" private const val IDE_BUILD_NUMBER = "ide_build_number" private const val FOLDER = "folder" @@ -14,7 +14,7 @@ fun Map.token() = this[TOKEN] fun Map.workspace() = this[WORKSPACE] -fun Map.agentID() = this[AGENT_ID] +fun Map.agentName() = this[AGENT_NAME] fun Map.ideProductCode() = this[IDE_PRODUCT_CODE] From e8b0d3435610ba07c279c7a4fccf95aff7b3f8d7 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 8 Jul 2025 00:22:35 +0300 Subject: [PATCH 2/3] fix: update UTs related to agent resolution in the URI handling --- .../toolbox/util/CoderProtocolHandlerTest.kt | 85 ++++++++----------- 1 file changed, 35 insertions(+), 50 deletions(-) diff --git a/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt index 2914eae..b26acde 100644 --- a/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt @@ -18,6 +18,7 @@ import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.DisplayName import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals @@ -47,40 +48,34 @@ internal class CoderProtocolHandlerTest { private val agents = mapOf( - "agent_name_3" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24", - "agent_name_2" to "fb3daea4-da6b-424d-84c7-36b90574cfef", - "agent_name" to "9a920eee-47fb-4571-9501-e4b3120c12f2", + "agent_name_bob" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24", + "agent_name_bill" to "fb3daea4-da6b-424d-84c7-36b90574cfef", + "agent_name_riker" to "9a920eee-47fb-4571-9501-e4b3120c12f2", ) - private val oneAgent = + private val agentBob = mapOf( - "agent_name_3" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24", + "agent_name_bob" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24", ) @Test - fun tstgetMatchingAgent() { + @DisplayName("given a ws with multiple agents, expect the correct agent to be resolved if it matches the agent_name query param") + fun getMatchingAgent() { val ws = DataGen.workspace("ws", agents = agents) val tests = listOf( Pair( - mapOf("agent_id" to "9a920eee-47fb-4571-9501-e4b3120c12f2"), + mapOf("agent_name" to "agent_name_riker"), "9a920eee-47fb-4571-9501-e4b3120c12f2" ), Pair( - mapOf("agent_id" to "fb3daea4-da6b-424d-84c7-36b90574cfef"), + mapOf("agent_name" to "agent_name_bill"), "fb3daea4-da6b-424d-84c7-36b90574cfef" ), Pair( - mapOf("agent_id" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24"), + mapOf("agent_name" to "agent_name_bob"), "b0e4c54d-9ba9-4413-8512-11ca1e826a24" - ), - // Prefer agent_id. - Pair( - mapOf( - "agent_id" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24", - ), - "b0e4c54d-9ba9-4413-8512-11ca1e826a24", - ), + ) ) runBlocking { tests.forEach { @@ -90,28 +85,20 @@ internal class CoderProtocolHandlerTest { } @Test + @DisplayName("given a ws with only multiple agents expect the agent resolution to fail if none match the agent_name query param") fun failsToGetMatchingAgent() { val ws = DataGen.workspace("ws", agents = agents) val tests = listOf( Triple(emptyMap(), MissingArgumentException::class, "Unable to determine"), - Triple(mapOf("agent_id" to ""), MissingArgumentException::class, "Unable to determine"), - Triple(mapOf("agent_id" to null), MissingArgumentException::class, "Unable to determine"), - Triple(mapOf("agent_id" to "not-a-uuid"), IllegalArgumentException::class, "agent with ID"), + Triple(mapOf("agent_name" to ""), MissingArgumentException::class, "Unable to determine"), + Triple(mapOf("agent_name" to null), MissingArgumentException::class, "Unable to determine"), + Triple(mapOf("agent_name" to "not-an-agent-name"), IllegalArgumentException::class, "agent with ID"), Triple( - mapOf("agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168"), + mapOf("agent_name" to "agent_name_homer"), IllegalArgumentException::class, - "agent with ID" - ), - // Will ignore agent if agent_id is set even if agent matches. - Triple( - mapOf( - "agent" to "agent_name", - "agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168", - ), - IllegalArgumentException::class, - "agent with ID", - ), + "agent with name" + ) ) runBlocking { tests.forEach { @@ -121,15 +108,14 @@ internal class CoderProtocolHandlerTest { } @Test + @DisplayName("given a ws with only one agent, the agent is selected even when agent_name query param was not provided") fun getsFirstAgentWhenOnlyOne() { - val ws = DataGen.workspace("ws", agents = oneAgent) + val ws = DataGen.workspace("ws", agents = agentBob) val tests = listOf( emptyMap(), - mapOf("agent" to ""), - mapOf("agent_id" to ""), - mapOf("agent" to null), - mapOf("agent_id" to null), + mapOf("agent_name" to ""), + mapOf("agent_name" to null) ) runBlocking { tests.forEach { @@ -145,43 +131,42 @@ internal class CoderProtocolHandlerTest { } @Test + @DisplayName("given a ws with only one agent, the agent is NOT selected when agent_name query param was provided but does not match") fun failsToGetAgentWhenOnlyOne() { - val ws = DataGen.workspace("ws", agents = oneAgent) + val wsWithAgentBob = DataGen.workspace("ws", agents = agentBob) val tests = listOf( Triple( - mapOf("agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168"), + mapOf("agent_name" to "agent_name_garfield"), IllegalArgumentException::class, - "agent with ID" + "agent with name" ), ) runBlocking { tests.forEach { - assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id) + assertNull(protocolHandler.getMatchingAgent(it.first, wsWithAgentBob)) } } } @Test - fun failsToGetAgentWithoutAgents() { - val ws = DataGen.workspace("ws") + @DisplayName("fails to resolve any agent when the workspace has no agents") + fun failsToGetAgentWhenWorkspaceHasNoAgents() { + val wsWithoutAgents = DataGen.workspace("ws") val tests = listOf( Triple(emptyMap(), IllegalArgumentException::class, "has no agents"), - Triple(mapOf("agent" to ""), IllegalArgumentException::class, "has no agents"), - Triple(mapOf("agent_id" to ""), IllegalArgumentException::class, "has no agents"), - Triple(mapOf("agent" to null), IllegalArgumentException::class, "has no agents"), - Triple(mapOf("agent_id" to null), IllegalArgumentException::class, "has no agents"), - Triple(mapOf("agent" to "agent_name"), IllegalArgumentException::class, "has no agents"), + Triple(mapOf("agent_name" to ""), IllegalArgumentException::class, "has no agents"), + Triple(mapOf("agent_name" to null), IllegalArgumentException::class, "has no agents"), Triple( - mapOf("agent_id" to "9a920eee-47fb-4571-9501-e4b3120c12f2"), + mapOf("agent_name" to "agent_name_riker"), IllegalArgumentException::class, "has no agents" ), ) runBlocking { tests.forEach { - assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id) + assertNull(protocolHandler.getMatchingAgent(it.first, wsWithoutAgents)) } } } From d7a4fc2becf7665654556c005234cad0d78d4687 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 8 Jul 2025 10:42:33 +0300 Subject: [PATCH 3/3] chore: fix typos and update URIs --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7e4a957..41d430d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ You can use specially crafted JetBrains Gateway URIs to automatically: ```text jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fdev.coder.com&token=zeoX4SbSpP-j2qGpajkdwxR9jBdcekXS2&workspace=bobiverse-bob&agent_name=dev&ide_product_code=GO&ide_build_number=241.23774.119&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs -jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fj5gj2r1so5nbi.pit-1.try.coder.app%2F&token=gqEirOoI1U-FfCQ6uj8iOLtybBIk99rr8&workspace=bobiverse-riker&agent_name=dev&ide_product_code=RR&ide_build_number=243.26053.17&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs +jetbrains://gateway/coder?url=https%3A%2F%2Fj5gj2r1so5nbi.pit-1.try.coder.app%2F&token=gqEirOoI1U-FfCQ6uj8iOLtybBIk99rr8&workspace=bobiverse-riker&agent_name=dev&ide_product_code=RR&ide_build_number=243.26053.17&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs ``` ### URI Breakdown @@ -100,7 +100,8 @@ jetbrains://gateway/coder?url=http(s):// | folder | Absolute path to the project folder to open in the remote IDE (URL-encoded) | No | > [!NOTE] -> If only a single agent is available, specifying an agent name. However, if multiple agents exist, you must provide the +> If only a single agent is available, specifying an agent name is optional. However, if multiple agents exist, you must +> provide the > agent name. Note that this version of the Coder Toolbox plugin does not automatically start agents if they > are offline, so please ensure the selected agent is running before proceeding.