From 967c966e69ed79dc04329a90dbe7ac7966714ad7 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 18 Aug 2026 23:03:41 +0300 Subject: [PATCH 1/2] fix: pass remote OS to Toolbox agent deployment Toolbox reads deployment settings from the SSH environment contents view, not from the parent remote environment. The API documentation did not make that ownership clear, so identifying the issue required tracing the Toolbox bytecode through its connection and agent deployment flow. Toolbox's default OS detection also masked the bug during testing: connections could still work even though our override was being ignored. Expose the setting on EnvironmentView, cover every OS mapping, and log the selected OS from BeforeConnectionHook before deployment starts. --- CHANGELOG.md | 16 +++++++---- .../coder/toolbox/CoderRemoteEnvironment.kt | 28 ++++++------------- .../coder/toolbox/views/EnvironmentView.kt | 18 ++++++++++++ .../toolbox/views/EnvironmentViewTest.kt | 26 +++++++++++++++++ 4 files changed, 63 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b20fcc7..ac7f458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - support for configuring the SSH config path, letting Toolbox manage a separate SSH config file instead of always writing to `~/.ssh/config` +### Fixed + +- pass the workspace agent's operating system to Toolbox before it deploys the remote agent + ## 0.9.3 - 2026-08-11 ### Changed @@ -20,8 +24,8 @@ - keep workspace polling alive when SSH config updates fail - improved validation and handling of workspace connection data -- expand `~` and `$HOME` in the SSH log directory and network info directory settings, consistent with how the data - and binary directories are already resolved +- expand `~` and `$HOME` in the SSH log directory and network info directory settings, consistent with how the data and + binary directories are already resolved ### Changed @@ -57,8 +61,8 @@ - skip the Coder TLS alternate hostname when fetching IDE metadata from JetBrains - notifications are now persistent popups instead of snackbars, so they survive a hidden window and no longer get dropped -- workspace lists now default to `My workspaces`, so users initially see only workspaces they own. Users can - switch to `All workspaces`, and that selection is persisted per Coder deployment hostname. +- workspace lists now default to `My workspaces`, so users initially see only workspaces they own. Users can switch to + `All workspaces`, and that selection is persisted per Coder deployment hostname. ## 0.9.0 - 2026-05-14 @@ -281,8 +285,8 @@ ### Changed -- the plugin will now remember the SSH connection state for each workspace, and it will try to automatically - establish it after an expired token was refreshed. +- the plugin will now remember the SSH connection state for each workspace, and it will try to automatically establish + it after an expired token was refreshed. ### Fixed diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 2810535..e43ff0b 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -22,10 +22,7 @@ import com.jetbrains.toolbox.api.remoteDev.AfterDisconnectHook import com.jetbrains.toolbox.api.remoteDev.BeforeConnectionHook import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment -import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentSettings -import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentTarget import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView -import com.jetbrains.toolbox.api.remoteDev.environments.WithDeploymentSettings import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState import com.jetbrains.toolbox.api.ui.actions.ActionDescription @@ -52,11 +49,11 @@ private val POLL_INTERVAL = 5.seconds private fun environmentId(workspace: Workspace, agent: WorkspaceAgent?): String = agent?.let { "${workspace.name}.${it.name}" } ?: workspace.name -private fun OS?.toDeploymentTarget(): DeploymentTarget = when (this) { - OS.LINUX -> DeploymentTarget.LINUX - OS.MAC -> DeploymentTarget.MACOS - OS.WINDOWS -> DeploymentTarget.WINDOWS - else -> DeploymentTarget.AUTO +private fun OS?.displayName(): String = when (this) { + OS.LINUX -> "Linux" + OS.WINDOWS -> "Windows" + OS.MAC -> "macOS" + null -> "unknown-OS" } /** @@ -71,8 +68,7 @@ class CoderRemoteEnvironment( private val workspaceRefreshTrigger: Channel, private var workspace: Workspace, private var agent: WorkspaceAgent?, -) : RemoteProviderEnvironment(environmentId(workspace, agent)), BeforeConnectionHook, AfterDisconnectHook, - WithDeploymentSettings { +) : RemoteProviderEnvironment(environmentId(workspace, agent)), BeforeConnectionHook, AfterDisconnectHook { private var environmentStatus = WorkspaceAndAgentStatus.from(workspace, agent) override var name: String = environmentId(workspace, agent) @@ -86,13 +82,6 @@ class CoderRemoteEnvironment( override val additionalEnvironmentInformation: MutableMap = mutableMapOf() override val actionsList: MutableStateFlow> = MutableStateFlow(emptyList()) - /** - * Derives the deployment target from the agent's reported OS, falling back to - * AUTO (automatic remote OS detection) when the agent or its OS is unknown. - */ - override val deploymentSettings: DeploymentSettings - get() = DeploymentSettings.Default.copy(deploymentTarget = agent?.operatingSystem.toDeploymentTarget()) - private val networkMetricsMarshaller = Moshi.Builder().build().adapter(NetworkMetrics::class.java) private val proxyCommandHandle = SshCommandProcessHandle(context) private var pollJob: Job? = null @@ -236,7 +225,9 @@ class CoderRemoteEnvironment( if (agent == null) { return } - context.logger.info("Connecting to $id...") + context.logger.info( + "Launching SSH connection to $id on a ${agent?.operatingSystem.displayName()} machine" + ) isConnected.update { true } context.settingsStore.updateAutoConnect(this.id, true) pollJob = pollNetworkMetrics() @@ -378,7 +369,6 @@ class CoderRemoteEnvironment( connectionRequest.update { true } - context.logger.info("Workspace status is ready and there is no existing connection, resuming SSH connection to $id") } } diff --git a/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt b/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt index 4978db0..0e0d3e5 100644 --- a/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt +++ b/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt @@ -5,11 +5,21 @@ import com.coder.toolbox.cli.CoderCLIManager import com.coder.toolbox.cli.WorkspaceAddress import com.coder.toolbox.sdk.v2.models.Workspace import com.coder.toolbox.sdk.v2.models.WorkspaceAgent +import com.coder.toolbox.util.OS +import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentSettings +import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentTarget import com.jetbrains.toolbox.api.remoteDev.environments.SshEnvironmentContentsView import com.jetbrains.toolbox.api.remoteDev.ssh.SshConnectionInfo import java.net.URL import kotlin.time.Duration.Companion.seconds +private fun OS?.toDeploymentTarget(): DeploymentTarget = when (this) { + OS.LINUX -> DeploymentTarget.LINUX + OS.MAC -> DeploymentTarget.MACOS + OS.WINDOWS -> DeploymentTarget.WINDOWS + else -> DeploymentTarget.AUTO +} + /** * A view for a single environment. It displays the projects and IDEs. * @@ -25,6 +35,14 @@ class EnvironmentView( private val workspace: Workspace, private val agent: WorkspaceAgent, ) : SshEnvironmentContentsView { + + /** + * Derives the deployment target from the agent's reported OS, falling back to + * AUTO (automatic remote OS detection) when its OS is unknown. + */ + override val deploymentSettings: DeploymentSettings + get() = DeploymentSettings.Default.copy(deploymentTarget = agent.operatingSystem.toDeploymentTarget()) + override suspend fun getConnectionInfo(): SshConnectionInfo = WorkspaceSshConnectionInfo(context, url, cli, workspace, agent) } diff --git a/src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt b/src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt index 397a9c7..9ead00b 100644 --- a/src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt +++ b/src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt @@ -6,6 +6,8 @@ import com.coder.toolbox.cli.WorkspaceAddress import com.coder.toolbox.sdk.v2.models.Workspace import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.store.CoderSettingsStore +import com.coder.toolbox.util.OS +import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentTarget import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.runBlocking @@ -14,6 +16,30 @@ import kotlin.test.Test import kotlin.test.assertEquals class EnvironmentViewTest { + @Test + fun `deployment target follows the workspace agent operating system`() { + val context = mockk(relaxed = true) + val cli = mockk() + val workspace = mockk() + val url = URL("https://coder.example.com") + val cases = listOf( + OS.LINUX to DeploymentTarget.LINUX, + OS.MAC to DeploymentTarget.MACOS, + OS.WINDOWS to DeploymentTarget.WINDOWS, + null to DeploymentTarget.AUTO, + ) + + cases.forEach { (operatingSystem, expectedTarget) -> + val agent = mockk { + every { this@mockk.operatingSystem } returns operatingSystem + } + + val deploymentSettings = EnvironmentView(context, url, cli, workspace, agent).deploymentSettings + + assertEquals(expectedTarget, deploymentSettings.deploymentTarget) + } + } + @Test fun `connection info passes the configured SSH config path to Toolbox`() = runBlocking { val context = mockk(relaxed = true) From 8bc6d422c0b1b0528c51b3d13500737bb56bf46e Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Mon, 24 Aug 2026 16:35:48 +0300 Subject: [PATCH 2/2] chore: next version is 0.9.4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 32d4626..98f923f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.9.3 +version=0.9.4 group=com.coder.toolbox name=coder-toolbox \ No newline at end of file