From 0e6f4cba34c7315637e5ac0d0378e1a7e99a3115 Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:48:15 +0100 Subject: [PATCH] feat(devin-desktop): pre-install remote extensions --- .../coder/modules/devin-desktop/README.md | 29 +++- .../coder/modules/devin-desktop/main.test.ts | 95 ++++++++++++- registry/coder/modules/devin-desktop/main.tf | 25 +++- .../modules/devin-desktop/main.tftest.hcl | 30 ++++ .../scripts/install-remote-server.sh.tftpl | 129 ++++++++++++++++++ 5 files changed, 300 insertions(+), 8 deletions(-) create mode 100644 registry/coder/modules/devin-desktop/main.tftest.hcl create mode 100644 registry/coder/modules/devin-desktop/scripts/install-remote-server.sh.tftpl diff --git a/registry/coder/modules/devin-desktop/README.md b/registry/coder/modules/devin-desktop/README.md index 85927e8b5..9c1c0a31b 100644 --- a/registry/coder/modules/devin-desktop/README.md +++ b/registry/coder/modules/devin-desktop/README.md @@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) module "devin-desktop" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/devin-desktop/coder" - version = "1.0.0" + version = "1.1.0" agent_id = coder_agent.main.id } ``` @@ -29,12 +29,35 @@ module "devin-desktop" { module "devin-desktop" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/devin-desktop/coder" - version = "1.0.0" + version = "1.1.0" agent_id = coder_agent.main.id folder = "/home/coder/project" } ``` +### Pre-install extensions on the workspace host + +Use `extensions` to install Devin-compatible VS Code extension IDs before the first ordinary Devin Desktop connection. The module downloads the official Devin Remote Host from the editor's stable update service, verifies the published SHA-256 checksum, and installs extensions under `~/.devin-server/extensions`. + +```tf +module "devin-desktop" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/devin-desktop/coder" + version = "1.1.0" + agent_id = coder_agent.main.id + folder = "/home/coder/project" + + extensions = [ + "ms-python.python", + "esbenp.prettier-vscode@12.4.0", + ] +} +``` + +The installation blocks ordinary workspace login for up to 30 minutes so extensions are ready before Devin Desktop connects. A download, checksum, extraction, or extension installation failure remains visible in the Coder startup logs. Later workspace starts reuse an existing executable Remote Host and do not force extension updates. + +The workspace image must provide Bash, `base64`, `tar`, either `curl` or `wget`, and either `sha256sum` or `shasum`. The workspace also needs HTTPS egress to the editor update and artifact hosts. Extension availability and compatibility depend on the configured extension marketplace; use `publisher.extension@version` to request a specific version. + ### Configure MCP servers for Devin Desktop Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.config/devin/mcp_config.json` using a `coder_script` on workspace start. @@ -45,7 +68,7 @@ The following example configures Devin Desktop to use the GitHub MCP server with module "devin-desktop" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/devin-desktop/coder" - version = "1.0.0" + version = "1.1.0" agent_id = coder_agent.main.id folder = "/home/coder/project" mcp = jsonencode({ diff --git a/registry/coder/modules/devin-desktop/main.test.ts b/registry/coder/modules/devin-desktop/main.test.ts index 24d4e57e9..799b8e31a 100644 --- a/registry/coder/modules/devin-desktop/main.test.ts +++ b/registry/coder/modules/devin-desktop/main.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "bun:test"; +import { describe, expect, it, setDefaultTimeout } from "bun:test"; import { runTerraformApply, runTerraformInit, @@ -8,8 +8,21 @@ import { removeContainer, findResourceInstance, readFileContainer, + writeFileContainer, } from "~test"; +setDefaultTimeout(30 * 1000); + +const decodeBootstrapScript = (installScript: string): string => { + const match = installScript.match(/IDE_CLI_INSTALL_SCRIPT_B64='([^']+)'/); + if (!match) { + throw new Error( + "The extension installer does not contain a bootstrap script", + ); + } + return Buffer.from(match[1], "base64").toString("utf8"); +}; + describe("devin-desktop", async () => { await runTerraformInit(import.meta.dir); @@ -39,6 +52,13 @@ describe("devin-desktop", async () => { expect(coder_app?.instances[0].attributes.display_name).toBe( "Devin Desktop", ); + + const extensionScripts = state.resources.filter( + (resource) => + resource.type === "coder_script" && + resource.name === "install_extensions", + ); + expect(extensionScripts).toHaveLength(0); }); it("adds folder", async () => { @@ -99,6 +119,77 @@ describe("devin-desktop", async () => { expect(coder_app?.instances[0].attributes.display_name).toBe("Devin"); }); + it("passes extensions and Devin Remote Host paths to the core", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + extensions: JSON.stringify([ + "ms-python.python", + "esbenp.prettier-vscode@12.4.0", + ]), + }); + const extensionInstaller = findResourceInstance( + state, + "coder_script", + "install_extensions", + ); + const bootstrapScript = decodeBootstrapScript(extensionInstaller.script); + + expect(extensionInstaller.start_blocks_login).toBe(true); + expect(bootstrapScript).toContain( + "https://windsurf-stable.codeium.com/api/update/linux-reh-$remote_arch/stable/latest", + ); + expect(bootstrapScript).toContain( + Buffer.from("$HOME/.coder-modules/coder/devin-desktop/server").toString( + "base64", + ), + ); + expect(extensionInstaller.script).toContain( + Buffer.from( + "$HOME/.coder-modules/coder/devin-desktop/server/bin/devin-server", + ).toString("base64"), + ); + expect(extensionInstaller.script).toContain( + Buffer.from("$HOME/.devin-server/extensions").toString("base64"), + ); + expect(extensionInstaller.script).toContain( + Buffer.from("esbenp.prettier-vscode@12.4.0").toString("base64"), + ); + expect(extensionInstaller.script).not.toContain("--force"); + }); + + it("does not download the Remote Host again when its CLI is executable", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + extensions: JSON.stringify(["esbenp.prettier-vscode"]), + }); + const extensionInstaller = findResourceInstance( + state, + "coder_script", + "install_extensions", + ); + const bootstrapScript = decodeBootstrapScript(extensionInstaller.script); + const id = await runContainer("node:22-bookworm-slim"); + const cliPath = + "/root/.coder-modules/coder/devin-desktop/server/bin/devin-server"; + + try { + await execContainer( + id, + ["mkdir", "-p", "/root/.coder-modules/coder/devin-desktop/server/bin"], + ["--user", "root"], + ); + await writeFileContainer(id, cliPath, "#!/bin/sh\nexit 0\n", { + user: "root", + }); + await execContainer(id, ["chmod", "755", cliPath], ["--user", "root"]); + + const result = await execContainer(id, ["bash", "-c", bootstrapScript]); + expect(result.exitCode).toBe(0); + } finally { + await removeContainer(id); + } + }); + it("writes ~/.config/devin/mcp_config.json when mcp provided", async () => { const id = await runContainer("alpine"); try { @@ -128,5 +219,5 @@ describe("devin-desktop", async () => { } finally { await removeContainer(id); } - }, 10000); + }); }); diff --git a/registry/coder/modules/devin-desktop/main.tf b/registry/coder/modules/devin-desktop/main.tf index 8a76f5436..7cbcc1158 100644 --- a/registry/coder/modules/devin-desktop/main.tf +++ b/registry/coder/modules/devin-desktop/main.tf @@ -56,11 +56,19 @@ variable "mcp" { default = "" } +variable "extensions" { + description = "Devin-compatible VS Code extension IDs to pre-install on the workspace host." + type = list(string) + default = [] +} + data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} locals { - mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : "" + module_directory = "$HOME/.coder-modules/coder/devin-desktop" + server_directory = "${local.module_directory}/server" + mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : "" } # Devin Desktop is Cognition's rebrand of the Windsurf Editor (June 2, 2026), @@ -69,7 +77,7 @@ locals { # same shared vscode-desktop-core module with Devin Desktop's branding. module "vscode-desktop-core" { source = "registry.coder.com/coder/vscode-desktop-core/coder" - version = "1.0.2" + version = "1.2.0" agent_id = var.agent_id @@ -83,7 +91,18 @@ module "vscode-desktop-core" { open_recent = var.open_recent # devin:// is registered as an external app protocol in coder/coder # (ALLOWED_EXTERNAL_APP_PROTOCOLS, coder/coder#28214). - protocol = "devin" + protocol = "devin" + config_dir = "$HOME/.config/devin" + + extensions = var.extensions + extensions_dir = "$HOME/.devin-server/extensions" + ide_cli_path = "${local.server_directory}/bin/devin-server" + ide_cli_install_script = length(var.extensions) > 0 ? templatefile( + "${path.module}/scripts/install-remote-server.sh.tftpl", + { + SERVER_DIRECTORY_B64 = base64encode(local.server_directory) + }, + ) : null } resource "coder_script" "devin_desktop_mcp" { diff --git a/registry/coder/modules/devin-desktop/main.tftest.hcl b/registry/coder/modules/devin-desktop/main.tftest.hcl new file mode 100644 index 000000000..971f7325f --- /dev/null +++ b/registry/coder/modules/devin-desktop/main.tftest.hcl @@ -0,0 +1,30 @@ +mock_provider "coder" {} + +variables { + agent_id = "test-agent" +} + +run "defaults_preserve_devin_uri" { + command = plan + + assert { + condition = startswith(output.devin_desktop_url, "devin://coder.coder-remote/open") + error_message = "The default app URL must keep the devin protocol." + } +} + +run "extensions_accept_devin_compatible_ids" { + command = plan + + variables { + extensions = [ + "ms-python.python", + "esbenp.prettier-vscode@12.4.0", + ] + } + + assert { + condition = length(var.extensions) == 2 + error_message = "The Devin Desktop wrapper must accept configured extension IDs." + } +} diff --git a/registry/coder/modules/devin-desktop/scripts/install-remote-server.sh.tftpl b/registry/coder/modules/devin-desktop/scripts/install-remote-server.sh.tftpl new file mode 100644 index 000000000..b2b8eb6ea --- /dev/null +++ b/registry/coder/modules/devin-desktop/scripts/install-remote-server.sh.tftpl @@ -0,0 +1,129 @@ +#!/usr/bin/env bash + +set -euo pipefail + +expand_home() { + case "$1" in + '$HOME') + printf '%s\n' "$HOME" + ;; + '$HOME/'*) + printf '%s/%s\n' "$HOME" "$${1#\$HOME/}" + ;; + *) + printf '%s\n' "$1" + ;; + esac +} + +download() { + local url="$1" + local destination="$2" + + if command -v curl >/dev/null 2>&1; then + curl --fail --location --silent --show-error "$url" --output "$destination" + elif command -v wget >/dev/null 2>&1; then + wget --quiet --output-document="$destination" "$url" + else + printf 'Devin Remote Host installation requires curl or wget.\n' >&2 + exit 1 + fi +} + +extract_json_string() { + local key="$1" + local source_file="$2" + local value + + value="$( + sed -n "s/.*\"$${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" "$source_file" | + head -n 1 + )" + if [ -z "$value" ]; then + printf 'Devin update response does not contain %s.\n' "$key" >&2 + exit 1 + fi + printf '%s\n' "$value" +} + +verify_sha256() { + local expected="$1" + local archive="$2" + + if command -v sha256sum >/dev/null 2>&1; then + printf '%s %s\n' "$expected" "$archive" | sha256sum --check --status + elif command -v shasum >/dev/null 2>&1; then + printf '%s %s\n' "$expected" "$archive" | shasum --algorithm 256 --check --status + else + printf 'Devin Remote Host installation requires sha256sum or shasum.\n' >&2 + exit 1 + fi +} + +SERVER_DIRECTORY="$(printf '%s' '${SERVER_DIRECTORY_B64}' | base64 -d)" +SERVER_DIRECTORY="$(expand_home "$SERVER_DIRECTORY")" +IDE_CLI_PATH="$SERVER_DIRECTORY/bin/devin-server" + +if [ -x "$IDE_CLI_PATH" ]; then + exit 0 +fi + +case "$(uname -m)" in + x86_64) + remote_arch="x64" + ;; + aarch64 | arm64) + remote_arch="arm64" + ;; + *) + printf 'Unsupported architecture for Devin Remote Host: %s\n' "$(uname -m)" >&2 + exit 1 + ;; +esac + +update_url="https://windsurf-stable.codeium.com/api/update/linux-reh-$remote_arch/stable/latest" +temporary_directory="$(mktemp -d)" +trap 'rm -rf "$temporary_directory"' EXIT + +update_response="$temporary_directory/update.json" +archive="$temporary_directory/devin-remote-host.tar.gz" +extracted_server="$temporary_directory/server" + +download "$update_url" "$update_response" + +archive_url="$(extract_json_string url "$update_response")" +archive_sha256="$(extract_json_string sha256hash "$update_response")" + +case "$archive_url" in + "https://windsurf-stable.codeiumdata.com/linux-reh-$remote_arch/stable/"*.tar.gz) ;; + *) + printf 'Devin update response returned an unexpected archive URL.\n' >&2 + exit 1 + ;; +esac + +case "$archive_sha256" in + *[!0-9a-fA-F]*) + printf 'Devin update response returned an invalid SHA-256 hash.\n' >&2 + exit 1 + ;; +esac +if [ "$${#archive_sha256}" -ne 64 ]; then + printf 'Devin update response returned an invalid SHA-256 hash.\n' >&2 + exit 1 +fi + +download "$archive_url" "$archive" +verify_sha256 "$archive_sha256" "$archive" + +mkdir -p "$extracted_server" +tar -xzf "$archive" -C "$extracted_server" + +if [ ! -x "$extracted_server/bin/devin-server" ]; then + printf 'Devin Remote Host archive does not contain the expected CLI executable.\n' >&2 + exit 1 +fi + +mkdir -p "$(dirname "$SERVER_DIRECTORY")" +rm -rf "$SERVER_DIRECTORY" +mv "$extracted_server" "$SERVER_DIRECTORY"