Skip to content

Commit abdf6d8

Browse files
authored
impl: add support for matching agent by name (#146)
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.
1 parent 1a2212b commit abdf6d8

File tree

5 files changed

+82
-88
lines changed

5 files changed

+82
-88
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66

77
- support for basic authentication for HTTP/HTTPS proxy
88
- support for Toolbox 2.7 release
9+
- support for matching workspace agent in the URI via the agent name
910

1011
### Changed
1112

1213
- improved message while loading the workspace
1314

15+
### Removed
16+
17+
- dropped support for `agent_id` as a URI parameter
18+
1419
### Fixed
1520

1621
- URI protocol handler is now able to switch to the Coder provider even if the last opened provider was something else

README.md

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ You can use specially crafted JetBrains Gateway URIs to automatically:
6464
### Example URIs
6565

6666
```text
67-
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
67+
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
6868
69-
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
69+
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
7070
```
7171

7272
### URI Breakdown
@@ -76,13 +76,15 @@ jetbrains://gateway/com.coder.toolbox
7676
?url=http(s)://<your-coder-deployment>
7777
&token=<auth-token>
7878
&workspace=<workspace-name>
79-
&agent_id=<agent--id>
79+
&agent_name=<agent-name>
8080
&ide_product_code=<IDE-code>
8181
&ide_build_number=<IDE-build>
8282
&folder=<absolute-path-to-a-project-folder>
8383
```
8484

85-
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:
85+
Starting from Toolbox 2.7, you can use `coder` as a shortcut in place of the full plugin ID. The URI can be simplified
86+
as:
87+
8688
```text
8789
jetbrains://gateway/coder?url=http(s)://<your-coder-deployment>
8890
```
@@ -92,16 +94,16 @@ jetbrains://gateway/coder?url=http(s)://<your-coder-deployment>
9294
| url | Your Coder deployment URL (encoded) | Yes |
9395
| token | Coder authentication token | Yes |
9496
| workspace | Name of the Coder workspace to connect to. | Yes |
95-
| agent_id | ID of the agent associated with the workspace | No |
97+
| agent_name | The name of the agent with the workspace | No |
9698
| ide_product_code | JetBrains IDE product code (e.g., GO for GoLand, RR for Rider) | No |
9799
| ide_build_number | Specific build number of the JetBrains IDE to install on the workspace | No |
98100
| folder | Absolute path to the project folder to open in the remote IDE (URL-encoded) | No |
99101

100102
> [!NOTE]
101-
> If only a single agent is available, specifying an agent ID is optional. However, if multiple agents exist,
102-
> you must provide either the ID to target a specific one. Note that this version of the Coder Toolbox plugin
103-
> does not automatically start agents if they are offline, so please ensure the selected agent is running before
104-
> proceeding.
103+
> If only a single agent is available, specifying an agent name is optional. However, if multiple agents exist, you must
104+
> provide the
105+
> agent name. Note that this version of the Coder Toolbox plugin does not automatically start agents if they
106+
> are offline, so please ensure the selected agent is running before proceeding.
105107
106108
If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open and highlight the workspace environment
107109
page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable
@@ -151,7 +153,9 @@ mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode socks5
151153
> [!NOTE]
152154
> Coder Toolbox plugin handles only HTTP/HTTPS proxy authentication.
153155
> SOCKS5 proxy authentication is currently not supported due to limitations
154-
> described in: https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0
156+
> described
157+
>
158+
in: https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0
155159

156160
## Debugging and Reporting issues
157161

@@ -198,56 +202,56 @@ storage paths. The options can be configured from the plugin's main Workspaces p
198202
### CLI related settings
199203

200204
- `Binary source` specifies the source URL or relative path from which the Coder CLI should be downloaded.
201-
If a relative path is provided, it is resolved against the deployment domain.
205+
If a relative path is provided, it is resolved against the deployment domain.
202206

203207
- `Enable downloads` allows automatic downloading of the CLI if the current version is missing or outdated.
204208

205209
- `Binary directory` specifies the directory where CLI binaries are stored. If omitted, it defaults to the data
206-
directory.
210+
directory.
207211

208212
- `Enable binary directory fallback` if enabled, falls back to the data directory when the specified binary
209-
directory is not writable.
213+
directory is not writable.
210214

211215
- `Data directory` directory where plugin-specific data such as session tokens and binaries are stored if not
212-
overridden by the binary directory setting.
216+
overridden by the binary directory setting.
213217

214218
- `Header command` command that outputs additional HTTP headers. Each line of output must be in the format key=value.
215-
The environment variable CODER_URL will be available to the command process.
219+
The environment variable CODER_URL will be available to the command process.
216220

217221
### TLS settings
218222

219223
The following options control the secure communication behavior of the plugin with Coder deployment and its available
220224
API.
221225

222226
- `TLS cert path` path to a client certificate file for TLS authentication with Coder deployment.
223-
The certificate should be in X.509 PEM format.
227+
The certificate should be in X.509 PEM format.
224228

225229
- `TLS key path` path to the private key corresponding to the TLS certificate from above.
226-
The certificate should be in X.509 PEM format.
230+
The certificate should be in X.509 PEM format.
227231

228232
- `TLS CA path` the path of a file containing certificates for an alternate certificate authority used to verify TLS
229-
certs returned by the Coder deployment. The file should be in X.509 PEM format. This option can also be used to verify
230-
proxy certificates.
233+
certs returned by the Coder deployment. The file should be in X.509 PEM format. This option can also be used to verify
234+
proxy certificates.
231235

232236
- `TLS alternate hostname` overrides the hostname used in TLS verification. This is useful when the hostname
233-
used to connect to the Coder deployment does not match the hostname in the TLS certificate.
237+
used to connect to the Coder deployment does not match the hostname in the TLS certificate.
234238

235239
### SSH settings
236240

237241
The following options control the SSH behavior of the Coder CLI.
238242

239243
- `Disable autostart` adds the --disable-autostart flag to the SSH proxy command, preventing the CLI from keeping
240-
workspaces constantly active.
244+
workspaces constantly active.
241245

242246
- `Enable SSH wildcard config` enables or disables wildcard entries in the SSH configuration, which allow generic
243-
rules for matching multiple workspaces.
247+
rules for matching multiple workspaces.
244248

245249
- `SSH proxy log directory` directory where SSH proxy logs are written. Useful for debugging SSH connection issues.
246250

247251
- `SSH network metrics directory` directory where network information used by the SSH proxy is stored.
248252

249253
- `Extra SSH options` additional options appended to the SSH configuration. Can be used to customize the behavior of
250-
SSH connections.
254+
SSH connections.
251255

252256
### Saving Changes
253257

@@ -256,7 +260,7 @@ support, may trigger regeneration of SSH configurations.
256260

257261
### Security considerations
258262

259-
> [!IMPORTANT]
263+
> [!IMPORTANT]
260264
> Token authentication is required when TLS certificates are not configured.
261265
262266
## Releasing
@@ -269,6 +273,7 @@ support, may trigger regeneration of SSH configurations.
269273
JetBrains enabled auto-approval for the plugin, so we need to ensure we continue to meet the following requirements:
270274
- do **not** use Kotlin experimental APIs.
271275
- do **not** add any lambdas, handlers, or class handles to Java runtime hooks.
272-
- do **not** create threads manually (including via libraries). If you must, ensure they are properly cleaned up in the plugin's `CoderRemoteProvider#close()` method.
276+
- do **not** create threads manually (including via libraries). If you must, ensure they are properly cleaned up in
277+
the plugin's `CoderRemoteProvider#close()` method.
273278
- do **not** bundle libraries that are already provided by Toolbox.
274279
- do **not** perform any ill-intentioned actions.

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -258,26 +258,25 @@ open class CoderProtocolHandler(
258258
}
259259

260260
// If the agent is missing and the workspace has only one, use that.
261-
val agent =
262-
if (!parameters.agentID().isNullOrBlank()) {
263-
agents.firstOrNull { it.id.toString() == parameters.agentID() }
264-
} else if (agents.size == 1) {
265-
agents.first()
266-
} else {
267-
null
268-
}
261+
val agent = if (!parameters.agentName().isNullOrBlank()) {
262+
agents.firstOrNull { it.name == parameters.agentName() }
263+
} else if (agents.size == 1) {
264+
agents.first()
265+
} else {
266+
null
267+
}
269268

270269
if (agent == null) {
271-
if (!parameters.agentID().isNullOrBlank()) {
270+
if (!parameters.agentName().isNullOrBlank()) {
272271
context.logAndShowError(
273272
CAN_T_HANDLE_URI_TITLE,
274-
"The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\""
273+
"The workspace \"${workspace.name}\" does not have an agent with name \"${parameters.agentName()}\""
275274
)
276275
return null
277276
} else {
278277
context.logAndShowError(
279278
CAN_T_HANDLE_URI_TITLE,
280-
"Unable to determine which agent to connect to; \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent"
279+
"Unable to determine which agent to connect to; \"$AGENT_NAME\" must be set because the workspace \"${workspace.name}\" has more than one agent"
281280
)
282281
return null
283282
}

src/main/kotlin/com/coder/toolbox/util/LinkMap.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package com.coder.toolbox.util
33
const val URL = "url"
44
const val TOKEN = "token"
55
const val WORKSPACE = "workspace"
6-
const val AGENT_ID = "agent_id"
6+
const val AGENT_NAME = "agent_name"
77
private const val IDE_PRODUCT_CODE = "ide_product_code"
88
private const val IDE_BUILD_NUMBER = "ide_build_number"
99
private const val FOLDER = "folder"
@@ -14,7 +14,7 @@ fun Map<String, String>.token() = this[TOKEN]
1414

1515
fun Map<String, String>.workspace() = this[WORKSPACE]
1616

17-
fun Map<String, String?>.agentID() = this[AGENT_ID]
17+
fun Map<String, String?>.agentName() = this[AGENT_NAME]
1818

1919
fun Map<String, String>.ideProductCode() = this[IDE_PRODUCT_CODE]
2020

src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt

Lines changed: 35 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.mockk.mockk
1818
import kotlinx.coroutines.CoroutineScope
1919
import kotlinx.coroutines.flow.MutableStateFlow
2020
import kotlinx.coroutines.runBlocking
21+
import org.junit.jupiter.api.DisplayName
2122
import java.util.UUID
2223
import kotlin.test.Test
2324
import kotlin.test.assertEquals
@@ -47,40 +48,34 @@ internal class CoderProtocolHandlerTest {
4748

4849
private val agents =
4950
mapOf(
50-
"agent_name_3" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24",
51-
"agent_name_2" to "fb3daea4-da6b-424d-84c7-36b90574cfef",
52-
"agent_name" to "9a920eee-47fb-4571-9501-e4b3120c12f2",
51+
"agent_name_bob" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24",
52+
"agent_name_bill" to "fb3daea4-da6b-424d-84c7-36b90574cfef",
53+
"agent_name_riker" to "9a920eee-47fb-4571-9501-e4b3120c12f2",
5354
)
54-
private val oneAgent =
55+
private val agentBob =
5556
mapOf(
56-
"agent_name_3" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24",
57+
"agent_name_bob" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24",
5758
)
5859

5960
@Test
60-
fun tstgetMatchingAgent() {
61+
@DisplayName("given a ws with multiple agents, expect the correct agent to be resolved if it matches the agent_name query param")
62+
fun getMatchingAgent() {
6163
val ws = DataGen.workspace("ws", agents = agents)
6264

6365
val tests =
6466
listOf(
6567
Pair(
66-
mapOf("agent_id" to "9a920eee-47fb-4571-9501-e4b3120c12f2"),
68+
mapOf("agent_name" to "agent_name_riker"),
6769
"9a920eee-47fb-4571-9501-e4b3120c12f2"
6870
),
6971
Pair(
70-
mapOf("agent_id" to "fb3daea4-da6b-424d-84c7-36b90574cfef"),
72+
mapOf("agent_name" to "agent_name_bill"),
7173
"fb3daea4-da6b-424d-84c7-36b90574cfef"
7274
),
7375
Pair(
74-
mapOf("agent_id" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24"),
76+
mapOf("agent_name" to "agent_name_bob"),
7577
"b0e4c54d-9ba9-4413-8512-11ca1e826a24"
76-
),
77-
// Prefer agent_id.
78-
Pair(
79-
mapOf(
80-
"agent_id" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24",
81-
),
82-
"b0e4c54d-9ba9-4413-8512-11ca1e826a24",
83-
),
78+
)
8479
)
8580
runBlocking {
8681
tests.forEach {
@@ -90,28 +85,20 @@ internal class CoderProtocolHandlerTest {
9085
}
9186

9287
@Test
88+
@DisplayName("given a ws with only multiple agents expect the agent resolution to fail if none match the agent_name query param")
9389
fun failsToGetMatchingAgent() {
9490
val ws = DataGen.workspace("ws", agents = agents)
9591
val tests =
9692
listOf(
9793
Triple(emptyMap(), MissingArgumentException::class, "Unable to determine"),
98-
Triple(mapOf("agent_id" to ""), MissingArgumentException::class, "Unable to determine"),
99-
Triple(mapOf("agent_id" to null), MissingArgumentException::class, "Unable to determine"),
100-
Triple(mapOf("agent_id" to "not-a-uuid"), IllegalArgumentException::class, "agent with ID"),
94+
Triple(mapOf("agent_name" to ""), MissingArgumentException::class, "Unable to determine"),
95+
Triple(mapOf("agent_name" to null), MissingArgumentException::class, "Unable to determine"),
96+
Triple(mapOf("agent_name" to "not-an-agent-name"), IllegalArgumentException::class, "agent with ID"),
10197
Triple(
102-
mapOf("agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168"),
98+
mapOf("agent_name" to "agent_name_homer"),
10399
IllegalArgumentException::class,
104-
"agent with ID"
105-
),
106-
// Will ignore agent if agent_id is set even if agent matches.
107-
Triple(
108-
mapOf(
109-
"agent" to "agent_name",
110-
"agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168",
111-
),
112-
IllegalArgumentException::class,
113-
"agent with ID",
114-
),
100+
"agent with name"
101+
)
115102
)
116103
runBlocking {
117104
tests.forEach {
@@ -121,15 +108,14 @@ internal class CoderProtocolHandlerTest {
121108
}
122109

123110
@Test
111+
@DisplayName("given a ws with only one agent, the agent is selected even when agent_name query param was not provided")
124112
fun getsFirstAgentWhenOnlyOne() {
125-
val ws = DataGen.workspace("ws", agents = oneAgent)
113+
val ws = DataGen.workspace("ws", agents = agentBob)
126114
val tests =
127115
listOf(
128116
emptyMap(),
129-
mapOf("agent" to ""),
130-
mapOf("agent_id" to ""),
131-
mapOf("agent" to null),
132-
mapOf("agent_id" to null),
117+
mapOf("agent_name" to ""),
118+
mapOf("agent_name" to null)
133119
)
134120
runBlocking {
135121
tests.forEach {
@@ -145,43 +131,42 @@ internal class CoderProtocolHandlerTest {
145131
}
146132

147133
@Test
134+
@DisplayName("given a ws with only one agent, the agent is NOT selected when agent_name query param was provided but does not match")
148135
fun failsToGetAgentWhenOnlyOne() {
149-
val ws = DataGen.workspace("ws", agents = oneAgent)
136+
val wsWithAgentBob = DataGen.workspace("ws", agents = agentBob)
150137
val tests =
151138
listOf(
152139
Triple(
153-
mapOf("agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168"),
140+
mapOf("agent_name" to "agent_name_garfield"),
154141
IllegalArgumentException::class,
155-
"agent with ID"
142+
"agent with name"
156143
),
157144
)
158145
runBlocking {
159146
tests.forEach {
160-
assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id)
147+
assertNull(protocolHandler.getMatchingAgent(it.first, wsWithAgentBob))
161148
}
162149
}
163150
}
164151

165152
@Test
166-
fun failsToGetAgentWithoutAgents() {
167-
val ws = DataGen.workspace("ws")
153+
@DisplayName("fails to resolve any agent when the workspace has no agents")
154+
fun failsToGetAgentWhenWorkspaceHasNoAgents() {
155+
val wsWithoutAgents = DataGen.workspace("ws")
168156
val tests =
169157
listOf(
170158
Triple(emptyMap(), IllegalArgumentException::class, "has no agents"),
171-
Triple(mapOf("agent" to ""), IllegalArgumentException::class, "has no agents"),
172-
Triple(mapOf("agent_id" to ""), IllegalArgumentException::class, "has no agents"),
173-
Triple(mapOf("agent" to null), IllegalArgumentException::class, "has no agents"),
174-
Triple(mapOf("agent_id" to null), IllegalArgumentException::class, "has no agents"),
175-
Triple(mapOf("agent" to "agent_name"), IllegalArgumentException::class, "has no agents"),
159+
Triple(mapOf("agent_name" to ""), IllegalArgumentException::class, "has no agents"),
160+
Triple(mapOf("agent_name" to null), IllegalArgumentException::class, "has no agents"),
176161
Triple(
177-
mapOf("agent_id" to "9a920eee-47fb-4571-9501-e4b3120c12f2"),
162+
mapOf("agent_name" to "agent_name_riker"),
178163
IllegalArgumentException::class,
179164
"has no agents"
180165
),
181166
)
182167
runBlocking {
183168
tests.forEach {
184-
assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id)
169+
assertNull(protocolHandler.getMatchingAgent(it.first, wsWithoutAgents))
185170
}
186171
}
187172
}

0 commit comments

Comments
 (0)