From 1460293de4deb63a5125260fe7cc66d06d864161 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:16:38 +0100 Subject: [PATCH 1/5] feat(coder/mux): add restart retries for mux exits (#800) ## Summary - add optional mux auto-restarts with delay, lock cleanup, and restart-attempt caps - restart mux after any exit when enabled, including intentional exits and signals - require `max_restart_attempts` to be a non-negative whole number and update docs/tests for the new restart semantics ## Validation - `bash -n registry/coder/modules/mux/run.sh` - `cd registry/coder/modules/mux && terraform validate` - `cd registry/coder/modules/mux && terraform test -verbose` - `cd registry/coder/modules/mux && bun test main.test.ts` Generated with OpenAI using Mux --- registry/coder/modules/mux/README.md | 40 +++++-- registry/coder/modules/mux/main.test.ts | 137 ++++++++++++++++++++++ registry/coder/modules/mux/main.tf | 31 +++++ registry/coder/modules/mux/mux.tftest.hcl | 105 +++++++++++++++++ registry/coder/modules/mux/run.sh | 91 ++++++++++++-- 5 files changed, 384 insertions(+), 20 deletions(-) diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 46bf295b..fb26d381 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -8,13 +8,13 @@ tags: [ai, agents, development, multiplexer] # Mux -Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module auto-detects an available package manager (`npm`, `pnpm`, or `bun`) to install `mux@next` (with a fallback to downloading the npm tarball if none is found). You can also force a specific package manager via `package_manager` and point to a custom registry with `registry_url`. The launcher now keeps watching the mux process after startup and appends signal/exit-code diagnostics to the mux log when the server is killed outside the Node runtime. Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces. +Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module auto-detects an available package manager (`npm`, `pnpm`, or `bun`) to install `mux@next` (with a fallback to downloading the npm tarball if none is found). You can also force a specific package manager via `package_manager` and point to a custom registry with `registry_url`. The launcher keeps watching the mux process after startup, appends signal/exit-code diagnostics to the mux log when the server is killed outside the Node runtime, and can optionally wait a few seconds, remove the stale server lock, and restart Mux after any exit until an optional restart-attempt cap is reached. Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces. ```tf module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id } ``` @@ -37,7 +37,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id } ``` @@ -48,7 +48,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id # Default is "latest"; set to a specific version to pin install_version = "0.4.0" @@ -63,7 +63,7 @@ Start Mux with `mux server --add-project /path/to/project`: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id add_project = "/path/to/project" } @@ -78,19 +78,35 @@ The module parses quoted values, so grouped arguments remain intact. module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'" } ``` +### Restart After Mux Exits + +Enable automatic restarts after Mux exits, including clean exits and intentional shutdown signals such as `SIGTERM`. The launcher waits for `restart_delay_seconds`, removes `~/.mux/server.lock`, and starts Mux again. Set `max_restart_attempts` to a whole number to stop retrying after a fixed number of restarts, or leave it at `0` for unlimited retries. + +```tf +module "mux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/mux/coder" + version = "1.4.3" + agent_id = coder_agent.main.id + restart_on_kill = true + restart_delay_seconds = 3 + max_restart_attempts = 5 +} +``` + ### Custom Port ```tf module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id port = 8080 } @@ -104,7 +120,7 @@ Force a specific package manager instead of auto-detection: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id package_manager = "pnpm" # or "npm", "bun" } @@ -118,7 +134,7 @@ Use a private or mirrored npm registry: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id registry_url = "https://npm.pkg.github.com" } @@ -132,7 +148,7 @@ Run an existing copy of Mux if found, otherwise install from npm: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id use_cached = true } @@ -146,7 +162,7 @@ Run without installing from the network (requires Mux to be pre-installed): module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.4.0" + version = "1.4.3" agent_id = coder_agent.main.id install = false } @@ -164,3 +180,5 @@ module "mux" { - Installs `mux@next` from the npm registry by default; set `registry_url` to use a private or mirrored registry - Falls back to a direct tarball download when no package manager is found - Appends best-effort signal and external-kill diagnostics to `log_path` if the mux process dies after startup +- Set `restart_on_kill = true` to wait `restart_delay_seconds`, remove `~/.mux/server.lock`, and restart Mux after it exits +- Set `max_restart_attempts` to a whole-number cap on restart attempts, or leave it at `0` for unlimited retries diff --git a/registry/coder/modules/mux/main.test.ts b/registry/coder/modules/mux/main.test.ts index 9537e9de..a8944dee 100644 --- a/registry/coder/modules/mux/main.test.ts +++ b/registry/coder/modules/mux/main.test.ts @@ -145,6 +145,143 @@ chmod +x /tmp/mux/mux`, } }, 60000); + it("restarts after a clean exit when enabled", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + install: false, + log_path: "/tmp/mux.log", + restart_on_kill: true, + restart_delay_seconds: 1, + max_restart_attempts: 1, + }); + + 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 +run_count_file="/tmp/mux-run-count" +run_count=0 +if [ -f "$run_count_file" ]; then + run_count=$(cat "$run_count_file") +fi +run_count=$((run_count + 1)) +printf '%s' "$run_count" > "$run_count_file" +echo "run=$run_count" +if [ "$run_count" -eq 1 ]; then + mkdir -p "$HOME/.mux" + touch "$HOME/.mux/server.lock" + exit 0 +fi +if [ -f "$HOME/.mux/server.lock" ]; then + echo "lock=present" +else + echo "lock=cleaned" +fi +exit 0 +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 4"]); + const log = await readFileContainer(id, "/tmp/mux.log"); + const runCount = await readFileContainer(id, "/tmp/mux-run-count"); + expect(log).toContain("run=1"); + expect(log).toContain("mux server exited cleanly."); + expect(log).toContain( + "Waiting 1 seconds before restarting mux after it exited.", + ); + expect(log).toContain( + "Removing /root/.mux/server.lock before restarting mux.", + ); + expect(log).toContain("run=2"); + expect(log).toContain("lock=cleaned"); + expect(log).toContain( + "Reached the max restart attempts limit (1); not restarting mux again.", + ); + expect(runCount.trim()).toBe("2"); + } finally { + await removeContainer(id); + } + }, 60000); + + it("restarts after SIGTERM when enabled", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + install: false, + log_path: "/tmp/mux.log", + restart_on_kill: true, + restart_delay_seconds: 1, + max_restart_attempts: 1, + }); + + 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 +run_count_file="/tmp/mux-run-count" +run_count=0 +if [ -f "$run_count_file" ]; then + run_count=$(cat "$run_count_file") +fi +run_count=$((run_count + 1)) +printf '%s' "$run_count" > "$run_count_file" +echo "run=$run_count" +if [ "$run_count" -eq 1 ]; then + kill -TERM $$ +fi +exit 0 +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 4"]); + const log = await readFileContainer(id, "/tmp/mux.log"); + const runCount = await readFileContainer(id, "/tmp/mux-run-count"); + expect(log).toContain("run=1"); + expect(log).toContain("signal TERM (15); shell exit code 143."); + expect(log).toContain( + "Waiting 1 seconds before restarting mux after it exited.", + ); + expect(log).toContain("run=2"); + expect(log).toContain( + "Reached the max restart attempts limit (1); not restarting mux again.", + ); + expect(runCount.trim()).toBe("2"); + } finally { + await removeContainer(id); + } + }, 60000); + it("runs with npm present", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index ba475b0c..f80b8b3f 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -49,6 +49,34 @@ variable "log_path" { default = "/tmp/mux.log" } +variable "restart_on_kill" { + type = bool + description = "Restart Mux after it exits by waiting briefly, removing the server lock, and launching it again." + default = false +} + +variable "restart_delay_seconds" { + type = number + description = "How long to wait before restarting Mux after it exits when restart_on_kill is enabled." + default = 5 + + validation { + condition = var.restart_delay_seconds >= 0 + error_message = "The 'restart_delay_seconds' variable must be greater than or equal to 0." + } +} + +variable "max_restart_attempts" { + type = number + description = "Maximum whole-number restart attempts before giving up. Set to 0 for unlimited restarts when restart_on_kill is enabled." + default = 0 + + validation { + condition = var.max_restart_attempts >= 0 && floor(var.max_restart_attempts) == var.max_restart_attempts + error_message = "The 'max_restart_attempts' variable must be a whole number greater than or equal to 0." + } +} + variable "add_project" { type = string description = "Optional path to add/open as a project in Mux on startup." @@ -171,6 +199,9 @@ resource "coder_script" "mux" { OFFLINE : !var.install, USE_CACHED : var.use_cached, AUTH_TOKEN : local.mux_auth_token, + RESTART_ON_KILL : var.restart_on_kill, + RESTART_DELAY_SECONDS : var.restart_delay_seconds, + MAX_RESTART_ATTEMPTS : var.max_restart_attempts, PACKAGE_MANAGER : var.package_manager, REGISTRY_URL : local.registry_url, }) diff --git a/registry/coder/modules/mux/mux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl index e7816de8..af4cbfe2 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -111,6 +111,111 @@ run "launcher_logs_external_kills" { } } +run "restart_on_kill_enabled" { + command = plan + + variables { + agent_id = "foo" + restart_on_kill = true + restart_delay_seconds = 7 + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "restart_on_kill_value=\"true\"") + error_message = "mux launcher must receive the restart_on_kill setting" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "restart_delay_seconds_value=\"7\"") + error_message = "mux launcher must receive the configured restart delay" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "Waiting $${RESTART_DELAY_SECONDS_VALUE} seconds before restarting mux after it exited.") + error_message = "mux launcher must log the restart delay before relaunching" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "Removing $HOME/.mux/server.lock before restarting mux.") + error_message = "mux launcher must clean up the server lock before relaunching" + } + + assert { + condition = !strcontains(resource.coder_script.mux.script, "\"$exit_code\" -le 128") + error_message = "mux launcher must no longer exclude non-signal exits from restart handling" + } + + assert { + condition = !strcontains(resource.coder_script.mux.script, "1|2|15)") + error_message = "mux launcher must no longer exclude intentional signals from restart handling" + } +} + +run "restart_on_kill_with_restart_cap" { + command = plan + + variables { + agent_id = "foo" + restart_on_kill = true + restart_delay_seconds = 7 + max_restart_attempts = 2 + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "max_restart_attempts_value=\"2\"") + error_message = "mux launcher must receive the configured restart cap" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "Mux will stop restarting after $${max_restart_attempts_value} restart attempts.") + error_message = "mux launcher must describe the configured restart cap" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "Reached the max restart attempts limit ($MAX_RESTART_ATTEMPTS_VALUE); not restarting mux again.") + error_message = "mux launcher must log when it hits the restart cap" + } +} + +run "invalid_max_restart_attempts" { + command = plan + + variables { + agent_id = "foo" + max_restart_attempts = -1 + } + + expect_failures = [ + var.max_restart_attempts + ] +} + +run "fractional_max_restart_attempts" { + command = plan + + variables { + agent_id = "foo" + max_restart_attempts = 0.5 + } + + expect_failures = [ + var.max_restart_attempts + ] +} + +run "invalid_restart_delay_seconds" { + command = plan + + variables { + agent_id = "foo" + restart_delay_seconds = -1 + } + + expect_failures = [ + var.restart_delay_seconds + ] +} + run "custom_version" { command = plan diff --git a/registry/coder/modules/mux/run.sh b/registry/coder/modules/mux/run.sh index fb583480..bd2bb811 100644 --- a/registry/coder/modules/mux/run.sh +++ b/registry/coder/modules/mux/run.sh @@ -5,17 +5,30 @@ RESET='\033[0m' MUX_BINARY="${INSTALL_PREFIX}/mux" function run_mux() { - # Remove stale server lock if present - rm -f "$HOME/.mux/server.lock" - local port_value local auth_token_value + local restart_on_kill_value + local restart_delay_seconds_value + local max_restart_attempts_value + port_value="${PORT}" auth_token_value="${AUTH_TOKEN}" + restart_on_kill_value="${RESTART_ON_KILL}" + restart_delay_seconds_value="${RESTART_DELAY_SECONDS}" + max_restart_attempts_value="${MAX_RESTART_ATTEMPTS}" + if [ -z "$port_value" ]; then port_value="4000" fi + if [ -z "$restart_delay_seconds_value" ]; then + restart_delay_seconds_value="5" + fi + + if [ -z "$max_restart_attempts_value" ]; then + max_restart_attempts_value="0" + fi + mkdir -p "$(dirname "${LOG_PATH}")" # Build args for mux (POSIX-compatible, avoid bash arrays) @@ -41,13 +54,24 @@ EOF_ARGS echo "🚀 Starting mux server on port $port_value..." echo "Check logs at ${LOG_PATH}!" - echo "ℹ️ Unexpected exits will be appended to ${LOG_PATH} by the launcher." + echo "ℹ️ Mux exit details will be appended to ${LOG_PATH} by the launcher." + if [ "$restart_on_kill_value" = true ]; then + echo "ℹ️ Auto-restart after mux exits is enabled with a $${restart_delay_seconds_value}-second delay." + if [ "$max_restart_attempts_value" = "0" ]; then + echo "ℹ️ Automatic restarts are unlimited for every mux exit." + else + echo "ℹ️ Mux will stop restarting after $${max_restart_attempts_value} restart attempts." + fi + fi nohup env \ LOG_PATH="${LOG_PATH}" \ MUX_BINARY="$MUX_BINARY" \ AUTH_TOKEN="$auth_token_value" \ PORT_VALUE="$port_value" \ + RESTART_ON_KILL_VALUE="$restart_on_kill_value" \ + RESTART_DELAY_SECONDS_VALUE="$restart_delay_seconds_value" \ + MAX_RESTART_ATTEMPTS_VALUE="$max_restart_attempts_value" \ bash -s -- "$@" > /dev/null 2>&1 << 'EOF_LAUNCHER' & signal_name() { local signal_number="$1" @@ -82,6 +106,14 @@ append_kernel_kill_context() { fi } +cleanup_mux_lock() { + rm -f "$HOME/.mux/server.lock" +} + +should_restart_mux() { + [ "$RESTART_ON_KILL_VALUE" = "true" ] +} + log_mux_exit() { local mux_pid="$1" local exit_code="$2" @@ -114,11 +146,52 @@ log_mux_exit() { echo "[$timestamp] Check the earlier mux log lines for any in-process crash breadcrumbs from mux itself." } -MUX_SERVER_AUTH_TOKEN="$AUTH_TOKEN" PORT="$PORT_VALUE" "$MUX_BINARY" "$@" >> "$LOG_PATH" 2>&1 & -mux_pid=$! -wait "$mux_pid" -exit_code=$? -log_mux_exit "$mux_pid" "$exit_code" >> "$LOG_PATH" 2>&1 +log_mux_restart_wait() { + local timestamp + + timestamp="$(date -Iseconds 2> /dev/null || date)" + echo "[$timestamp] Waiting $${RESTART_DELAY_SECONDS_VALUE} seconds before restarting mux after it exited." +} + +log_mux_restart_cleanup() { + local timestamp + + timestamp="$(date -Iseconds 2> /dev/null || date)" + echo "[$timestamp] Removing $HOME/.mux/server.lock before restarting mux." +} + +log_mux_restart_cap_reached() { + local timestamp + + timestamp="$(date -Iseconds 2> /dev/null || date)" + echo "[$timestamp] Reached the max restart attempts limit ($MAX_RESTART_ATTEMPTS_VALUE); not restarting mux again." +} + +restart_attempt_count=0 +while true; do + cleanup_mux_lock + MUX_SERVER_AUTH_TOKEN="$AUTH_TOKEN" PORT="$PORT_VALUE" "$MUX_BINARY" "$@" >> "$LOG_PATH" 2>&1 & + mux_pid=$! + wait "$mux_pid" + exit_code=$? + log_mux_exit "$mux_pid" "$exit_code" >> "$LOG_PATH" 2>&1 + + if should_restart_mux; then + if [ "$MAX_RESTART_ATTEMPTS_VALUE" -gt 0 ] && [ "$restart_attempt_count" -ge "$MAX_RESTART_ATTEMPTS_VALUE" ]; then + log_mux_restart_cap_reached >> "$LOG_PATH" 2>&1 + break + fi + + restart_attempt_count=$((restart_attempt_count + 1)) + log_mux_restart_wait >> "$LOG_PATH" 2>&1 + sleep "$RESTART_DELAY_SECONDS_VALUE" + cleanup_mux_lock + log_mux_restart_cleanup >> "$LOG_PATH" 2>&1 + continue + fi + + break +done EOF_LAUNCHER } # Check if mux is already installed for offline mode From 4fdcf0d71257c741e2d1be6d0f50ec738061341e Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:13:31 +0530 Subject: [PATCH 2/5] fix(coder/modules/claude-code): update claude session workdir normalization (#803) ## Description - This lead to a bug where if the folder name is in the form `a.b.c`: - we check for: `-home-coder-ai.coder.com/cd32e253-ca16-4fd3-9825-d837e74ae3c2.jsonl` - But the actual file path for claude-session is: `-home-coder-ai-coder-com/cd32e253-ca16-4fd3-9825-d837e74ae3c2.jsonl` - The above bug might also occur in the case of `a_b_c` - update workdir normalization to handle dot in path ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.8.1` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --- registry/coder/modules/claude-code/README.md | 18 +++++++++--------- .../coder/modules/claude-code/scripts/start.sh | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 3d875046..6c25c8cc 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -60,7 +60,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -81,7 +81,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -110,7 +110,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" ai_prompt = data.coder_task.me.prompt @@ -133,7 +133,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -189,7 +189,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -211,7 +211,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -284,7 +284,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -341,7 +341,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 2df8fce1..5ccbc8fa 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -88,7 +88,7 @@ TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" get_project_dir() { local workdir_normalized - workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/._' '-') echo "$HOME/.claude/projects/${workdir_normalized}" } From 85c51816f9bc7b8c36cd789bc7250b618235f155 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:54:51 +0500 Subject: [PATCH 3/5] chore(deps): bump the github-actions group with 3 updates (#804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 3 updates: [dorny/paths-filter](https://github.com/dorny/paths-filter), [coder/coder](https://github.com/coder/coder) and [oven-sh/setup-bun](https://github.com/oven-sh/setup-bun). Updates `dorny/paths-filter` from 3.0.2 to 4.0.1
Release notes

Sourced from dorny/paths-filter's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/dorny/paths-filter/compare/v3.0.3...v4.0.0

v3.0.3

What's Changed

New Contributors

Full Changelog: https://github.com/dorny/paths-filter/compare/v3...v3.0.3

Changelog

Sourced from dorny/paths-filter's changelog.

Changelog

v4.0.0

v3.0.3

v3.0.2

v3.0.1

v3.0.0

v2.11.1

v2.11.0

v2.10.2

v2.10.1

v2.10.0

v2.9.3

v2.9.2

v2.9.1

v2.9.0

... (truncated)

Commits

Updates `coder/coder` from 2.31.3 to 2.31.5
Release notes

Sourced from coder/coder's releases.

v2.31.5

Changelog

[!NOTE] This is a mainline Coder release. We advise enterprise customers without a staging environment to install our latest stable release while we refine this version. Learn more about our Release Schedule.

Bug fixes

Compare: v2.31.4...v2.31.5

Container image

Install/upgrade

Refer to our docs to install or upgrade Coder, or use a release asset below.

v2.31.4

Changelog

[!NOTE] This is a mainline Coder release. We advise enterprise customers without a staging environment to install our latest stable release while we refine this version. Learn more about our Release Schedule.

Features

Bug fixes

Compare: v2.31.3...v2.31.4

Container image

Install/upgrade

Refer to our docs to install or upgrade Coder, or use a release asset below.

Commits

Updates `oven-sh/setup-bun` from 2.1.3 to 2.2.0
Release notes

Sourced from oven-sh/setup-bun's releases.

v2.2.0

oven-sh/setup-bun is the github action for setting up Bun.

What's Changed

New Contributors

Full Changelog: https://github.com/oven-sh/setup-bun/compare/v2...v2.2.0

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/version-bump.yaml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6a8c79d2..cf1e01a2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ jobs: - name: Check out code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Detect changed files - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: filter with: list-files: shell @@ -37,9 +37,9 @@ jobs: all: - '**' - name: Set up Terraform - uses: coder/coder/.github/actions/setup-tf@deaacff8437e3f4ee84bc51c4e5162f6dd7d190e # v2.31.3 + uses: coder/coder/.github/actions/setup-tf@1a774ab7ce99063a2e01beb94de3fcbccaf84dbe # v2.31.5 - name: Set up Bun - uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: # We're using the latest version of Bun for now, but it might be worth # reconsidering. They've pushed breaking changes in patch releases @@ -82,12 +82,12 @@ jobs: - name: Check out code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Bun - uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: latest # Need Terraform for its formatter - name: Install Terraform - uses: coder/coder/.github/actions/setup-tf@deaacff8437e3f4ee84bc51c4e5162f6dd7d190e # v2.31.3 + uses: coder/coder/.github/actions/setup-tf@1a774ab7ce99063a2e01beb94de3fcbccaf84dbe # v2.31.5 - name: Install dependencies run: bun install - name: Validate formatting diff --git a/.github/workflows/version-bump.yaml b/.github/workflows/version-bump.yaml index c5dbc1b8..8a8d4d40 100644 --- a/.github/workflows/version-bump.yaml +++ b/.github/workflows/version-bump.yaml @@ -26,12 +26,12 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Bun - uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: latest - name: Set up Terraform - uses: coder/coder/.github/actions/setup-tf@deaacff8437e3f4ee84bc51c4e5162f6dd7d190e # v2.31.3 + uses: coder/coder/.github/actions/setup-tf@1a774ab7ce99063a2e01beb94de3fcbccaf84dbe # v2.31.5 - name: Install dependencies run: bun install From 69407746285d9985880a73a1fb04ca23def586c6 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 17 Mar 2026 10:07:35 +0100 Subject: [PATCH 4/5] feat: add the portabledesktop module (#805) ## Description Add a module to install https://github.com/coder/portabledesktop in a workspace. This will be required for the virtual desktop feature in Coder Agents. ## Type of Change - [x] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/portabledesktop` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --- .../coder/modules/portabledesktop/README.md | 46 ++++ .../modules/portabledesktop/main.test.ts | 242 ++++++++++++++++++ .../coder/modules/portabledesktop/main.tf | 65 +++++ .../portabledesktop.tftest.hcl | 36 +++ registry/coder/modules/portabledesktop/run.sh | 132 ++++++++++ 5 files changed, 521 insertions(+) create mode 100644 registry/coder/modules/portabledesktop/README.md create mode 100644 registry/coder/modules/portabledesktop/main.test.ts create mode 100644 registry/coder/modules/portabledesktop/main.tf create mode 100644 registry/coder/modules/portabledesktop/portabledesktop.tftest.hcl create mode 100644 registry/coder/modules/portabledesktop/run.sh diff --git a/registry/coder/modules/portabledesktop/README.md b/registry/coder/modules/portabledesktop/README.md new file mode 100644 index 00000000..a5afac77 --- /dev/null +++ b/registry/coder/modules/portabledesktop/README.md @@ -0,0 +1,46 @@ +--- +display_name: Portable Desktop +description: Install the portabledesktop binary for lightweight Linux desktop sessions. +icon: ../../../../.icons/desktop.svg +verified: true +tags: [desktop, vnc, ai] +--- + +# Portable Desktop + +Install [portabledesktop](https://github.com/coder/portabledesktop) for lightweight Linux desktop sessions over VNC. The binary is stored in the agent's script data directory and is automatically available on PATH via `CODER_SCRIPT_BIN_DIR`. + +```tf +module "portabledesktop" { + source = "registry.coder.com/coder/portabledesktop/coder" + version = "0.1.0" + agent_id = coder_agent.example.id +} +``` + +## Examples + +### Custom download URL with checksum verification + +```tf +module "portabledesktop" { + source = "registry.coder.com/coder/portabledesktop/coder" + version = "0.1.0" + agent_id = coder_agent.example.id + url = "https://example.com/portabledesktop-linux-x64" + sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} +``` + +### Additionally copy to a system path + +Use `install_dir` to copy the binary to a system-wide directory in addition to the default script data directory: + +```tf +module "portabledesktop" { + source = "registry.coder.com/coder/portabledesktop/coder" + version = "0.1.0" + agent_id = coder_agent.example.id + install_dir = "/usr/local/bin" +} +``` diff --git a/registry/coder/modules/portabledesktop/main.test.ts b/registry/coder/modules/portabledesktop/main.test.ts new file mode 100644 index 00000000..dd73f4e7 --- /dev/null +++ b/registry/coder/modules/portabledesktop/main.test.ts @@ -0,0 +1,242 @@ +import { describe, expect, it } from "bun:test"; +import { + execContainer, + findResourceInstance, + removeContainer, + runContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, + type TerraformState, +} from "~test"; + +interface TestFixture { + state: TerraformState; + server: ReturnType; + [Symbol.asyncDispose](): Promise; +} + +interface ContainerHandle { + id: string; + [Symbol.asyncDispose](): Promise; +} + +async function setupContainer(image: string): Promise { + const id = await runContainer(image); + return { + id, + [Symbol.asyncDispose]: async () => { + await removeContainer(id); + }, + }; +} + +const ENV_PREFIX = + 'export CODER_SCRIPT_DATA_DIR=/tmp/coder-script-data && export CODER_SCRIPT_BIN_DIR=/tmp/coder-script-data/bin && mkdir -p "$CODER_SCRIPT_DATA_DIR" "$CODER_SCRIPT_BIN_DIR" && '; + +async function setupFakeBinaryServer( + dir: string, + extraVars?: Record, +): Promise { + const fakeBinary = "#!/bin/sh\necho portabledesktop"; + const server = Bun.serve({ + port: 0, + fetch() { + return new Response(fakeBinary); + }, + }); + + const state = await runTerraformApply(dir, { + agent_id: "foo", + url: `http://localhost:${server.port}/portabledesktop`, + ...extraVars, + }); + + return { + state, + server, + [Symbol.asyncDispose]: async () => { + server.stop(true); + }, + }; +} + +describe("portabledesktop", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("installs portabledesktop successfully", async () => { + await using fixture = await setupFakeBinaryServer(import.meta.dir); + await using container = await setupContainer("alpine/curl"); + + const script = findResourceInstance(fixture.state, "coder_script").script; + const resp = await execContainer(container.id, [ + "sh", + "-c", + ENV_PREFIX + script, + ]); + + expect(resp.exitCode).toBe(0); + expect(resp.stdout).toContain("portabledesktop installed successfully"); + + // Check binary exists at CODER_SCRIPT_DATA_DIR. + const checkBinary = await execContainer(container.id, [ + "test", + "-x", + "/tmp/coder-script-data/portabledesktop", + ]); + expect(checkBinary.exitCode).toBe(0); + + // Check symlink exists at CODER_SCRIPT_BIN_DIR. + const checkSymlink = await execContainer(container.id, [ + "test", + "-L", + "/tmp/coder-script-data/bin/portabledesktop", + ]); + expect(checkSymlink.exitCode).toBe(0); + }, 30000); + + it("verifies checksum when sha256 is provided", async () => { + const fakeBinary = "#!/bin/sh\necho portabledesktop"; + const hasher = new Bun.CryptoHasher("sha256"); + hasher.update(fakeBinary); + const sha256 = hasher.digest("hex"); + + await using fixture = await setupFakeBinaryServer(import.meta.dir, { + sha256, + }); + await using container = await setupContainer("alpine/curl"); + + const script = findResourceInstance(fixture.state, "coder_script").script; + const resp = await execContainer(container.id, [ + "sh", + "-c", + ENV_PREFIX + script, + ]); + + expect(resp.exitCode).toBe(0); + expect(resp.stdout).toContain("Checksum verified successfully"); + expect(resp.stdout).toContain("portabledesktop installed successfully"); + }, 30000); + + it("fails when sha256 does not match", async () => { + const wrongSha256 = + "0000000000000000000000000000000000000000000000000000000000000000"; + + await using fixture = await setupFakeBinaryServer(import.meta.dir, { + sha256: wrongSha256, + }); + await using container = await setupContainer("alpine/curl"); + + const script = findResourceInstance(fixture.state, "coder_script").script; + const resp = await execContainer(container.id, [ + "sh", + "-c", + ENV_PREFIX + script, + ]); + + expect(resp.exitCode).toBe(1); + expect(resp.stdout).toContain("Checksum mismatch"); + }, 30000); + + it("skips checksum verification when sha256 is not set", async () => { + await using fixture = await setupFakeBinaryServer(import.meta.dir); + await using container = await setupContainer("alpine/curl"); + + const script = findResourceInstance(fixture.state, "coder_script").script; + const resp = await execContainer(container.id, [ + "sh", + "-c", + ENV_PREFIX + script, + ]); + + expect(resp.exitCode).toBe(0); + expect(resp.stdout).not.toContain("Checksum verified"); + expect(resp.stdout).toContain("portabledesktop installed successfully"); + }, 30000); + + it("falls back to sudo when install_dir is not writable", async () => { + await using fixture = await setupFakeBinaryServer(import.meta.dir, { + install_dir: "/usr/local/bin", + }); + await using container = await setupContainer("alpine/curl"); + + await execContainer(container.id, [ + "sh", + "-c", + "apk add sudo && " + + "adduser -D testuser && " + + "echo 'testuser ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers && " + + "mkdir -p /usr/local/bin", + ]); + + const script = findResourceInstance(fixture.state, "coder_script").script; + const resp = await execContainer( + container.id, + ["sh", "-c", ENV_PREFIX + script], + ["--user", "testuser"], + ); + + expect(resp.exitCode).toBe(0); + expect(resp.stdout).toContain("via sudo"); + expect(resp.stdout).toContain("portabledesktop installed successfully"); + + // Verify the binary was copied to the install_dir. + const check = await execContainer(container.id, [ + "test", + "-x", + "/usr/local/bin/portabledesktop", + ]); + expect(check.exitCode).toBe(0); + }, 30000); + + it("creates install_dir if it does not exist", async () => { + await using fixture = await setupFakeBinaryServer(import.meta.dir, { + install_dir: "/opt/custom/bin", + }); + await using container = await setupContainer("alpine/curl"); + + const script = findResourceInstance(fixture.state, "coder_script").script; + const resp = await execContainer(container.id, [ + "sh", + "-c", + ENV_PREFIX + script, + ]); + + expect(resp.exitCode).toBe(0); + expect(resp.stdout).toContain("portabledesktop installed successfully"); + + const check = await execContainer(container.id, [ + "test", + "-x", + "/opt/custom/bin/portabledesktop", + ]); + expect(check.exitCode).toBe(0); + }, 30000); + + it("falls back to wget when curl is not available", async () => { + await using fixture = await setupFakeBinaryServer(import.meta.dir); + await using container = await setupContainer("alpine"); + + // Install wget but ensure curl is not present. + await execContainer(container.id, [ + "sh", + "-c", + "apk add wget && ! command -v curl", + ]); + + const script = findResourceInstance(fixture.state, "coder_script").script; + const resp = await execContainer(container.id, [ + "sh", + "-c", + ENV_PREFIX + script, + ]); + + expect(resp.exitCode).toBe(0); + expect(resp.stdout).toContain("via wget"); + expect(resp.stdout).toContain("portabledesktop installed successfully"); + }, 30000); +}); diff --git a/registry/coder/modules/portabledesktop/main.tf b/registry/coder/modules/portabledesktop/main.tf new file mode 100644 index 00000000..68303c17 --- /dev/null +++ b/registry/coder/modules/portabledesktop/main.tf @@ -0,0 +1,65 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "install_dir" { + type = string + description = "Optional directory to copy the binary into (e.g. /usr/local/bin). The binary is always stored in the agent's script data directory and available on PATH via CODER_SCRIPT_BIN_DIR." + default = null +} + +variable "url" { + type = string + description = "Custom download URL. Overrides the default GitHub latest release URL when set." + default = null +} + +variable "sha256" { + type = string + description = "SHA256 checksum. When set, the downloaded binary is verified against it." + default = null +} + +locals { + default_amd64_url = "https://github.com/coder/portabledesktop/releases/latest/download/portabledesktop-linux-x64" + default_arm64_url = "https://github.com/coder/portabledesktop/releases/latest/download/portabledesktop-linux-arm64" + + using_custom_url = var.url != null + + amd64_url = local.using_custom_url ? var.url : local.default_amd64_url + arm64_url = local.using_custom_url ? var.url : local.default_arm64_url + + # Empty string signals "skip verification" to the shell script. + sha256 = var.sha256 != null ? var.sha256 : "" + install_dir = var.install_dir != null ? var.install_dir : "" +} + +resource "coder_script" "portabledesktop" { + agent_id = var.agent_id + display_name = "Portable Desktop" + icon = "/icon/desktop.svg" + script = <<-EOT + #!/bin/sh + set -eu + echo -n '${base64encode(file("${path.module}/run.sh"))}' | base64 -d > /tmp/portabledesktop-install.sh + chmod +x /tmp/portabledesktop-install.sh + ARG_AMD64_URL="$(echo -n '${base64encode(local.amd64_url)}' | base64 -d)" \ + ARG_ARM64_URL="$(echo -n '${base64encode(local.arm64_url)}' | base64 -d)" \ + ARG_SHA256="$(echo -n '${base64encode(local.sha256)}' | base64 -d)" \ + ARG_INSTALL_DIR="$(echo -n '${base64encode(local.install_dir)}' | base64 -d)" \ + /tmp/portabledesktop-install.sh + EOT + run_on_start = true +} diff --git a/registry/coder/modules/portabledesktop/portabledesktop.tftest.hcl b/registry/coder/modules/portabledesktop/portabledesktop.tftest.hcl new file mode 100644 index 00000000..20a23170 --- /dev/null +++ b/registry/coder/modules/portabledesktop/portabledesktop.tftest.hcl @@ -0,0 +1,36 @@ +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + } +} + +run "plan_with_custom_install_dir" { + command = plan + + variables { + agent_id = "example-agent-id" + install_dir = "/opt/bin" + } + + assert { + condition = resource.coder_script.portabledesktop.display_name == "Portable Desktop" + error_message = "Expected coder_script resource to have correct display name" + } +} + +run "plan_with_custom_url" { + command = plan + + variables { + agent_id = "example-agent-id" + url = "https://example.com/custom-portabledesktop" + sha256 = "abc123" + } + + assert { + condition = resource.coder_script.portabledesktop.run_on_start == true + error_message = "Expected coder_script to run on start" + } +} diff --git a/registry/coder/modules/portabledesktop/run.sh b/registry/coder/modules/portabledesktop/run.sh new file mode 100644 index 00000000..f7e62eff --- /dev/null +++ b/registry/coder/modules/portabledesktop/run.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2292 +# SC2292: We use [ ] instead of [[ ]] for POSIX sh compatibility. +set -eu + +error() { + printf "ERROR: %s\n" "$@" + exit 1 +} + +# Check if portabledesktop is already in PATH. +if command -v portabledesktop > /dev/null 2>&1; then + printf "portabledesktop is already installed and in PATH.\n" + exit 0 +fi + +# Determine the storage path. +STORAGE_DIR="${CODER_SCRIPT_DATA_DIR}" +BINARY_PATH="${STORAGE_DIR}/portabledesktop" +mkdir -p "${STORAGE_DIR}" + +# If the binary already exists and is executable, skip download. +if [ -x "${BINARY_PATH}" ]; then + printf "portabledesktop is already installed at %s, skipping download.\n" "${BINARY_PATH}" +else + # Detect architecture and select the appropriate download URL. + ARCH=$(uname -m) + case "${ARCH}" in + x86_64) + URL="${ARG_AMD64_URL}" + ;; + aarch64) + URL="${ARG_ARM64_URL}" + ;; + *) + error "Unsupported architecture: ${ARCH}" + ;; + esac + + # Select download tool. + if command -v curl > /dev/null 2>&1; then + DOWNLOAD_CMD="curl" + elif command -v wget > /dev/null 2>&1; then + DOWNLOAD_CMD="wget" + else + error "No download tool available (curl or wget required)." + fi + + # Download with retry loop (3 attempts, 1s sleep between). + TMPFILE=$(mktemp) + MAX_ATTEMPTS=3 + DOWNLOAD_SUCCESS=false + ATTEMPT=1 + + while [ "${ATTEMPT}" -le "${MAX_ATTEMPTS}" ]; do + printf "Downloading portabledesktop (attempt %s/%s) via %s...\n" "${ATTEMPT}" "${MAX_ATTEMPTS}" "${DOWNLOAD_CMD}" + + DOWNLOAD_OK=false + if [ "${DOWNLOAD_CMD}" = "curl" ]; then + curl -fsSL "${URL}" -o "${TMPFILE}" && DOWNLOAD_OK=true + else + wget -qO "${TMPFILE}" "${URL}" && DOWNLOAD_OK=true + fi + + if [ "${DOWNLOAD_OK}" = "true" ]; then + # Verify checksum when ARG_SHA256 is non-empty. + if [ -n "${ARG_SHA256}" ]; then + CHECKSUM_MATCH=false + if command -v sha256sum > /dev/null 2>&1; then + echo "${ARG_SHA256} ${TMPFILE}" | sha256sum -c - > /dev/null 2>&1 && CHECKSUM_MATCH=true + elif command -v shasum > /dev/null 2>&1; then + echo "${ARG_SHA256} ${TMPFILE}" | shasum -a 256 -c - > /dev/null 2>&1 && CHECKSUM_MATCH=true + else + rm -f "${TMPFILE}" + error "No SHA256 tool available (sha256sum or shasum required)." + fi + + if [ "${CHECKSUM_MATCH}" != "true" ]; then + printf "WARNING: Checksum mismatch (attempt %s/%s): expected %s\n" \ + "${ATTEMPT}" "${MAX_ATTEMPTS}" "${ARG_SHA256}" + rm -f "${TMPFILE}" + if [ "${ATTEMPT}" -lt "${MAX_ATTEMPTS}" ]; then + sleep 1 + fi + ATTEMPT=$((ATTEMPT + 1)) + continue + fi + printf "Checksum verified successfully.\n" + fi + + DOWNLOAD_SUCCESS=true + break + else + printf "WARNING: Download failed (attempt %s/%s).\n" "${ATTEMPT}" "${MAX_ATTEMPTS}" + if [ "${ATTEMPT}" -lt "${MAX_ATTEMPTS}" ]; then + sleep 1 + fi + fi + + ATTEMPT=$((ATTEMPT + 1)) + done + + if [ "${DOWNLOAD_SUCCESS}" != "true" ]; then + rm -f "${TMPFILE}" + error "Failed to download portabledesktop after ${MAX_ATTEMPTS} attempts." + fi + + # Make the binary executable and move to storage path. + chmod 755 "${TMPFILE}" + mv "${TMPFILE}" "${BINARY_PATH}" +fi + +# Symlink into CODER_SCRIPT_BIN_DIR for PATH access. +if [ -n "${CODER_SCRIPT_BIN_DIR}" ] && [ ! -e "${CODER_SCRIPT_BIN_DIR}/portabledesktop" ]; then + ln -s "${CODER_SCRIPT_DATA_DIR}/portabledesktop" "${CODER_SCRIPT_BIN_DIR}/portabledesktop" +fi + +# If ARG_INSTALL_DIR is set, copy the binary there with sudo fallback. +if [ -n "${ARG_INSTALL_DIR}" ]; then + if [ ! -d "${ARG_INSTALL_DIR}" ]; then + mkdir -p "${ARG_INSTALL_DIR}" 2> /dev/null || sudo mkdir -p "${ARG_INSTALL_DIR}" 2> /dev/null || true + fi + if cp "${CODER_SCRIPT_DATA_DIR}/portabledesktop" "${ARG_INSTALL_DIR}/portabledesktop" 2> /dev/null; then + printf "Copied portabledesktop to %s.\n" "${ARG_INSTALL_DIR}/portabledesktop" + elif sudo cp "${CODER_SCRIPT_DATA_DIR}/portabledesktop" "${ARG_INSTALL_DIR}/portabledesktop" 2> /dev/null; then + printf "Copied portabledesktop to %s (via sudo).\n" "${ARG_INSTALL_DIR}/portabledesktop" + else + error "Failed to copy portabledesktop to ${ARG_INSTALL_DIR}/portabledesktop." + fi +fi + +printf "portabledesktop installed successfully.\n" From ce50e52fc5e07576c0457cc646d8f4b3d1ee98ec Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:39:59 +0530 Subject: [PATCH 5/5] feat(coder-labs/modules/codex): update default configuration to use model providers instead of profiles (#806) ## Description - update default configuration to use model providers instead of profiles ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder-labs/modules/codex` **New version:** `v4.3.1` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --- registry/coder-labs/modules/codex/README.md | 21 +++++++------------ .../coder-labs/modules/codex/main.test.ts | 5 +---- registry/coder-labs/modules/codex/main.tf | 15 ++++++------- .../modules/codex/scripts/install.sh | 14 ++++++++++--- .../coder-labs/modules/codex/scripts/start.sh | 2 +- 5 files changed, 27 insertions(+), 30 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 5e8ac272..16786d02 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.0" + version = "4.3.1" agent_id = coder_agent.example.id openai_api_key = var.openai_api_key workdir = "/home/coder/project" @@ -32,7 +32,7 @@ module "codex" { module "codex" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.0" + version = "4.3.1" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" @@ -51,7 +51,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.0" + version = "4.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" enable_aibridge = true @@ -60,21 +60,16 @@ module "codex" { When `enable_aibridge = true`, the module: -- Configures Codex to use the AI Bridge profile with `base_url` pointing to `${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1` and `env_key` pointing to the workspace owner's session token +- Configures Codex to use the aibridge model_provider with `base_url` pointing to `${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1` and `env_key` pointing to the workspace owner's session token ```toml -profile = "aibridge" # sets the default profile to aibridge +model_provider = "aibridge" [model_providers.aibridge] name = "AI Bridge" base_url = "https://example.coder.com/api/v2/aibridge/openai/v1" env_key = "CODER_AIBRIDGE_SESSION_TOKEN" wire_api = "responses" - -[profiles.aibridge] -model_provider = "aibridge" -model = "" # as configured in the module input -model_reasoning_effort = "" # as configured in the module input ``` This allows Codex to route API requests through Coder's AI Bridge instead of directly to OpenAI's API. @@ -94,7 +89,7 @@ data "coder_task" "me" {} module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.0" + version = "4.3.1" agent_id = coder_agent.example.id openai_api_key = "..." ai_prompt = data.coder_task.me.prompt @@ -114,7 +109,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.0" + version = "4.3.1" agent_id = coder_agent.main.id openai_api_key = var.openai_api_key workdir = "/home/coder/project" @@ -132,7 +127,7 @@ This example shows additional configuration options for custom models, MCP serve ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.0" + version = "4.3.1" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 3eff3eca..13055867 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -468,10 +468,7 @@ describe("codex", async () => { id, "/home/coder/.codex/config.toml", ); - expect(configToml).toContain( - "[profiles.aibridge]\n" + 'model_provider = "aibridge"', - ); - expect(configToml).toContain('profile = "aibridge"'); + expect(configToml).toContain('model_provider = "aibridge"'); }); test("boundary-enabled", async () => { diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 4d9c5ae1..b5f71cb3 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -84,10 +84,10 @@ variable "enable_aibridge" { variable "model_reasoning_effort" { type = string - description = "The reasoning effort for the AI Bridge model. One of: none, low, medium, high. https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort" - default = "medium" + description = "The reasoning effort for the model. One of: none, low, medium, high. https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort" + default = "" validation { - condition = contains(["none", "low", "medium", "high"], var.model_reasoning_effort) + condition = contains(["", "none", "minimal", "low", "medium", "high", "xhigh"], var.model_reasoning_effort) error_message = "model_reasoning_effort must be one of: none, low, medium, high." } } @@ -137,7 +137,7 @@ variable "agentapi_version" { variable "codex_model" { type = string description = "The model for Codex to use. Defaults to gpt-5.3-codex." - default = "gpt-5.3-codex" + default = "gpt-5.4" } variable "pre_install_script" { @@ -225,7 +225,7 @@ locals { install_script = file("${path.module}/scripts/install.sh") start_script = file("${path.module}/scripts/start.sh") module_dir_name = ".codex-module" - latest_codex_model = "gpt-5.3-codex" + latest_codex_model = "gpt-5.4" aibridge_config = <<-EOF [model_providers.aibridge] name = "AI Bridge" @@ -233,10 +233,6 @@ locals { env_key = "CODER_AIBRIDGE_SESSION_TOKEN" wire_api = "responses" - [profiles.aibridge] - model_provider = "aibridge" - model = "${var.codex_model}" - model_reasoning_effort = "${var.model_reasoning_effort}" EOF } @@ -302,6 +298,7 @@ module "agentapi" { ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \ ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \ ARG_CODEX_START_DIRECTORY='${local.workdir}' \ + ARG_MODEL_REASONING_EFFORT='${var.model_reasoning_effort}' \ ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \ /tmp/install.sh EOT diff --git a/registry/coder-labs/modules/codex/scripts/install.sh b/registry/coder-labs/modules/codex/scripts/install.sh index 4742c413..9a191a02 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh +++ b/registry/coder-labs/modules/codex/scripts/install.sh @@ -93,10 +93,14 @@ function install_codex() { write_minimal_default_config() { local config_path="$1" - ARG_DEFAULT_PROFILE="" + ARG_OPTIONAL_TOP_LEVEL_CONFIG="" if [[ "${ARG_ENABLE_AIBRIDGE}" = "true" ]]; then - ARG_DEFAULT_PROFILE='profile = "aibridge"' + ARG_OPTIONAL_TOP_LEVEL_CONFIG='model_provider = "aibridge"' + fi + + if [[ "${ARG_MODEL_REASONING_EFFORT}" != "" ]]; then + ARG_OPTIONAL_TOP_LEVEL_CONFIG+=$'\n'"model_reasoning_effort = \"${ARG_MODEL_REASONING_EFFORT}\"" fi cat << EOF > "$config_path" @@ -104,13 +108,17 @@ write_minimal_default_config() { sandbox_mode = "workspace-write" approval_policy = "never" preferred_auth_method = "apikey" -${ARG_DEFAULT_PROFILE} +${ARG_OPTIONAL_TOP_LEVEL_CONFIG} [sandbox_workspace_write] network_access = true [notice.model_migrations] "${ARG_CODEX_MODEL}" = "${ARG_LATEST_CODEX_MODEL}" + +[projects."${ARG_CODEX_START_DIRECTORY}"] +trust_level = "trusted" + EOF } diff --git a/registry/coder-labs/modules/codex/scripts/start.sh b/registry/coder-labs/modules/codex/scripts/start.sh index 0dbf5a60..bac0cb45 100644 --- a/registry/coder-labs/modules/codex/scripts/start.sh +++ b/registry/coder-labs/modules/codex/scripts/start.sh @@ -155,7 +155,7 @@ setup_workdir() { build_codex_args() { CODEX_ARGS=() - if [[ -n "${ARG_CODEX_MODEL}" ]] && [[ "${ARG_ENABLE_AIBRIDGE}" != "true" ]]; then + if [[ -n "${ARG_CODEX_MODEL}" ]]; then CODEX_ARGS+=("--model" "${ARG_CODEX_MODEL}") fi