Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions registry/coder/modules/devin-desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```
Expand All @@ -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.
Expand All @@ -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({
Expand Down
95 changes: 93 additions & 2 deletions registry/coder/modules/devin-desktop/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "bun:test";
import { describe, expect, it, setDefaultTimeout } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
Expand All @@ -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);

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -128,5 +219,5 @@ describe("devin-desktop", async () => {
} finally {
await removeContainer(id);
}
}, 10000);
});
});
25 changes: 22 additions & 3 deletions registry/coder/modules/devin-desktop/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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

Expand All @@ -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" {
Expand Down
30 changes: 30 additions & 0 deletions registry/coder/modules/devin-desktop/main.tftest.hcl
Original file line number Diff line number Diff line change
@@ -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."
}
}
Original file line number Diff line number Diff line change
@@ -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"