Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv

This commit is contained in:
DevCats 2026-02-25 12:24:35 -06:00 committed by GitHub
commit f14d174afc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 291 additions and 86 deletions

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

@ -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

@ -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.0"
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.0"
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.0"
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.0"
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.0"
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.0"
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

@ -84,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"
@ -104,13 +110,15 @@ data "coder_parameter" "dotfiles_uri" {
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"
@ -127,7 +135,8 @@ resource "coder_app" "dotfiles" {
group = var.group group = var.group
command = templatefile("${path.module}/run.sh", { command = 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
}) })
} }

View File

@ -43,3 +43,14 @@ if [ -n "$${DOTFILES_URI// }" ]; then
sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log" sudo -u "$DOTFILES_USER" "$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

@ -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.2.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.2.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.2.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,19 +63,34 @@ 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.2.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
add-project = "/path/to/project" add-project = "/path/to/project"
} }
``` ```
### Pass Arbitrary `mux server` Arguments
Use `additional_arguments` to append additional arguments to `mux server`.
The module parses quoted values, so grouped arguments remain intact.
```tf
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.2.0"
agent_id = coder_agent.main.id
additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
}
```
### Custom Port ### Custom Port
```tf ```tf
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.2.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
port = 8080 port = 8080
} }
@ -89,7 +104,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.2.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
use_cached = true use_cached = true
} }
@ -103,7 +118,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.2.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
install = false install = false
} }

View File

@ -1,6 +1,11 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { import {
executeScriptInContainer, executeScriptInContainer,
execContainer,
findResourceInstance,
readFileContainer,
removeContainer,
runContainer,
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
@ -40,6 +45,57 @@ describe("mux", async () => {
} }
}, 60000); }, 60000);
it("parses custom additional_arguments", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
install: false,
log_path: "/tmp/mux.log",
additional_arguments:
"--open-mode pinned --add-project '/workspaces/my repo'",
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer("alpine/curl");
try {
const setup = await execContainer(id, [
"sh",
"-c",
`apk add --no-cache bash >/dev/null
mkdir -p /tmp/mux
cat <<'EOF' > /tmp/mux/mux
#!/usr/bin/env sh
i=1
for arg in "$@"; do
echo "arg$i=$arg"
i=$((i + 1))
done
EOF
chmod +x /tmp/mux/mux`,
]);
expect(setup.exitCode).toBe(0);
const output = await execContainer(id, ["sh", "-c", instance.script]);
if (output.exitCode !== 0) {
console.log("STDOUT:\n" + output.stdout);
console.log("STDERR:\n" + output.stderr);
}
expect(output.exitCode).toBe(0);
await execContainer(id, ["sh", "-c", "sleep 1"]);
const log = await readFileContainer(id, "/tmp/mux.log");
expect(log).toContain("arg1=server");
expect(log).toContain("arg2=--port");
expect(log).toContain("arg3=4000");
expect(log).toContain("arg4=--open-mode");
expect(log).toContain("arg5=pinned");
expect(log).toContain("arg6=--add-project");
expect(log).toContain("arg7=/workspaces/my repo");
} finally {
await removeContainer(id);
}
}, 60000);
it("runs with npm present", async () => { it("runs with npm present", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",

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"
}
} }
} }
@ -51,6 +55,12 @@ variable "add-project" {
default = null default = null
} }
variable "additional_arguments" {
type = string
description = "Additional command-line arguments to pass to `mux server` (for example: `--add-project /path --open-mode pinned`)."
default = ""
}
variable "install_version" { variable "install_version" {
type = string type = string
description = "The version or dist-tag of Mux to install." description = "The version or dist-tag of Mux to install."
@ -113,6 +123,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
@ -122,9 +148,11 @@ resource "coder_script" "mux" {
PORT : var.port, PORT : var.port,
LOG_PATH : var.log_path, LOG_PATH : var.log_path,
ADD_PROJECT : var.add-project == null ? "" : var.add-project, ADD_PROJECT : var.add-project == null ? "" : var.add-project,
ADDITIONAL_ARGUMENTS : var.additional_arguments,
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 +168,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 +182,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,65 @@ 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"
}
}
run "custom_additional_arguments" {
command = plan
variables {
agent_id = "foo"
additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "--open-mode pinned --add-project '/workspaces/my repo'")
error_message = "mux launch script must include the configured additional arguments"
} }
} }
@ -62,5 +121,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
@ -18,9 +20,25 @@ function run_mux() {
if [ -n "${ADD_PROJECT}" ]; then if [ -n "${ADD_PROJECT}" ]; then
set -- "$@" --add-project "${ADD_PROJECT}" set -- "$@" --add-project "${ADD_PROJECT}"
fi fi
# Parse additional user-supplied server arguments while preserving quoted groups.
if [ -n "${ADDITIONAL_ARGUMENTS}" ]; then
local parsed_additional_arguments
if ! parsed_additional_arguments="$(printf "%s\n" "${ADDITIONAL_ARGUMENTS}" | xargs -n1 printf "%s\n" 2> /dev/null)"; then
echo "❌ Failed to parse additional_arguments. Ensure quotes are balanced."
exit 1
fi
while IFS= read -r parsed_arg; do
[ -n "$parsed_arg" ] || continue
set -- "$@" "$parsed_arg"
done << EOF
$${parsed_additional_arguments}
EOF
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