From 6b57d707fa40b4c0363f5d18b400bf0a5891155d Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:23:16 +0000 Subject: [PATCH 1/5] fix(mux): install @coder/xum instead of the mux compat shim The npm mux package is only a compatibility wrapper whose single dependency is an exact-pinned @coder/xum of the same version. The two packages are published independently and mux usually lands before its own @coder/xum dependency, so a workspace starting inside that window fails with: No version matching "" found for specifier "@coder/xum". Install @coder/xum directly (it ships the mux bin) through the package manager and tarball paths, and fix the README pin example that claimed the default install_version is latest (main.tf defaults to next). --- registry/coder/modules/mux/README.md | 12 ++++---- registry/coder/modules/mux/main.test.ts | 2 +- registry/coder/modules/mux/main.tf | 4 +-- registry/coder/modules/mux/mux.tftest.hcl | 35 +++++++++++++++++++++-- registry/coder/modules/mux/run.sh | 16 +++++++---- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 2f4155e29..192d878da 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -8,7 +8,7 @@ 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 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. +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 `@coder/xum@next`, the npm package that ships the `mux` CLI (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" { @@ -50,8 +50,9 @@ module "mux" { source = "registry.coder.com/coder/mux/coder" version = "1.5.0" agent_id = coder_agent.main.id - # Default is "latest"; set to a specific version to pin - install_version = "0.4.0" + # Default is "next"; set to a specific version to pin. + # Only versions published as @coder/xum (0.28.3 or newer) are available. + install_version = "0.28.4" } ``` @@ -128,7 +129,7 @@ module "mux" { ### Custom Registry -Use a private or mirrored npm registry: +Use a private or mirrored npm registry. The registry must serve the scoped `@coder/xum` package: ```tf module "mux" { @@ -178,7 +179,8 @@ module "mux" { - Requires internet connectivity for agent operations (unless `install` is set to false) - Auto-detects `npm`, `pnpm`, or `bun` by default; set `package_manager` to force a specific one - Requires a Node.js runtime; if `node` is not on the workspace `PATH`, the module bootstraps a pinned Node.js runtime into `~/.local/share/coder-mux` (override the version with the `MUX_NODE_VERSION` environment variable) -- Installs `mux@next` from the npm registry by default; set `registry_url` to use a private or mirrored registry +- Installs `@coder/xum@next` from the npm registry by default (this package ships the `mux` binary); set `registry_url` to use a private or mirrored registry +- `install_version` must be a version or dist-tag published as `@coder/xum` (0.28.3 or newer); older releases were only published under the legacy `mux` package name - 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 diff --git a/registry/coder/modules/mux/main.test.ts b/registry/coder/modules/mux/main.test.ts index a8944deec..e8559d43e 100644 --- a/registry/coder/modules/mux/main.test.ts +++ b/registry/coder/modules/mux/main.test.ts @@ -296,7 +296,7 @@ chmod +x /tmp/mux/mux`, expect(output.exitCode).toBe(0); const expectedLines = [ - "📦 Installing mux via npm into /tmp/mux...", + "📦 Installing @coder/xum via npm into /tmp/mux...", "⏭️ Skipping lifecycle scripts with --ignore-scripts", "🥳 mux has been installed in /tmp/mux", "🚀 Starting mux server on port 4000...", diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index f80b8b3ff..4413ebc87 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -91,7 +91,7 @@ variable "additional_arguments" { variable "install_version" { type = string - description = "The version or dist-tag of Mux to install." + description = "The version or dist-tag of the @coder/xum npm package to install. Only versions published as @coder/xum (0.28.3 or newer) are available." default = "next" } @@ -107,7 +107,7 @@ variable "package_manager" { variable "registry_url" { type = string - description = "The npm-compatible registry URL to install Mux from. Override this for private registries or mirrors." + description = "The npm-compatible registry URL to install @coder/xum from. Override this for private registries or mirrors." default = "https://registry.npmjs.org" } diff --git a/registry/coder/modules/mux/mux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl index af4cbfe23..40995b943 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -221,7 +221,7 @@ run "custom_version" { variables { agent_id = "foo" - install_version = "0.3.0" + install_version = "0.28.4" } } @@ -245,6 +245,37 @@ run "use_cached_only_success" { } } +# The installed npm package must be @coder/xum (it ships the mux bin); +# the legacy mux package is only a compat shim with an exact-pinned +# @coder/xum dependency that is published separately. +run "installs_coder_xum_package" { + command = plan + + variables { + agent_id = "foo" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "PKG=\"@coder/xum\"") + error_message = "mux script must install the @coder/xum npm package" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "https://registry.npmjs.org/@coder%2Fxum/") + error_message = "tarball fallback must fetch @coder/xum metadata with the URL-encoded scoped name" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "https://registry.npmjs.org/@coder/xum/-/xum-") + error_message = "tarball fallback must construct the scoped @coder/xum tarball URL" + } + + assert { + condition = !strcontains(resource.coder_script.mux.script, "PKG=\"mux\"") + error_message = "mux script must not install the legacy mux compat package" + } +} + # Custom package_manager should appear in generated script run "custom_package_manager_npm" { command = plan @@ -332,7 +363,7 @@ run "registry_url_trailing_slash" { } assert { - condition = strcontains(resource.coder_script.mux.script, "https://npm.example.com/mux/") + condition = strcontains(resource.coder_script.mux.script, "https://npm.example.com/@coder%2Fxum/") error_message = "registry URL trailing slash must be stripped to avoid double slashes" } } diff --git a/registry/coder/modules/mux/run.sh b/registry/coder/modules/mux/run.sh index 04431e02f..a40f63ab0 100644 --- a/registry/coder/modules/mux/run.sh +++ b/registry/coder/modules/mux/run.sh @@ -272,8 +272,11 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then fi fi + # @coder/xum is the package that ships the mux CLI (bins: mux, xum). + PKG="@coder/xum" + if [ -n "$PM_CMD" ]; then - echo "📦 Installing mux via $PM_CMD into ${INSTALL_PREFIX}..." + echo "📦 Installing $PKG via $PM_CMD into ${INSTALL_PREFIX}..." NPM_WORKDIR="${INSTALL_PREFIX}/npm" mkdir -p "$NPM_WORKDIR" cd "$NPM_WORKDIR" || exit 1 @@ -281,7 +284,6 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then echo '{}' > package.json fi echo "⏭️ Skipping lifecycle scripts with --ignore-scripts" - PKG="mux" if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then PKG_SPEC="$PKG@latest" else @@ -306,7 +308,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then ;; esac if [ "$INSTALL_OK" != true ]; then - echo "❌ Failed to install mux via $PM_CMD" + echo "❌ Failed to install $PKG via $PM_CMD" exit 1 fi # Determine the installed binary path @@ -324,7 +326,8 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then if [ -z "$VERSION_TO_USE" ]; then VERSION_TO_USE="next" fi - META_URL="${REGISTRY_URL}/mux/$VERSION_TO_USE" + # Scoped package names are URL-encoded in npm registry metadata paths. + META_URL="${REGISTRY_URL}/@coder%2Fxum/$VERSION_TO_USE" META_JSON="$(curl -fsSL "$META_URL" || true)" if [ -z "$META_JSON" ]; then echo "❌ Failed to fetch npm metadata: $META_URL" @@ -360,10 +363,11 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then VERSION_TO_USE="$RESOLVED_VERSION" fi if [ -z "$VERSION_TO_USE" ]; then - echo "❌ Could not determine version for mux" + echo "❌ Could not determine version for $PKG" exit 1 fi - TARBALL_URL="${REGISTRY_URL}/mux/-/mux-$VERSION_TO_USE.tgz" + # Registry tarball layout for scoped packages: /@scope/name/-/name-.tgz + TARBALL_URL="${REGISTRY_URL}/@coder/xum/-/xum-$VERSION_TO_USE.tgz" fi TMP_DIR="$(mktemp -d)" TAR_PATH="$TMP_DIR/mux.tgz" From e6bd2672de8bcf61d564b12f38134fff0839ff4a Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:23:28 +0000 Subject: [PATCH 2/5] chore(mux): bump module version to 2.0.0 (major) --- registry/coder/modules/mux/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 192d878da..a490d7d77 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -14,7 +14,7 @@ Automatically install and run [Mux](https://github.com/coder/mux) in a Coder wor module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.5.0" + version = "2.0.0" 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.5.0" + version = "2.0.0" 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.5.0" + version = "2.0.0" agent_id = coder_agent.main.id # Default is "next"; set to a specific version to pin. # Only versions published as @coder/xum (0.28.3 or newer) are available. @@ -64,7 +64,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.5.0" + version = "2.0.0" agent_id = coder_agent.main.id add_project = "/path/to/project" } @@ -79,7 +79,7 @@ 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.5.0" + version = "2.0.0" agent_id = coder_agent.main.id additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'" } @@ -93,7 +93,7 @@ Enable automatic restarts after Mux exits, including clean exits and intentional module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.5.0" + version = "2.0.0" agent_id = coder_agent.main.id restart_on_kill = true restart_delay_seconds = 3 @@ -107,7 +107,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.5.0" + version = "2.0.0" agent_id = coder_agent.main.id port = 8080 } @@ -121,7 +121,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.5.0" + version = "2.0.0" agent_id = coder_agent.main.id package_manager = "pnpm" # or "npm", "bun" } @@ -135,7 +135,7 @@ Use a private or mirrored npm registry. The registry must serve the scoped `@cod module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.5.0" + version = "2.0.0" agent_id = coder_agent.main.id registry_url = "https://npm.pkg.github.com" } @@ -149,7 +149,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.5.0" + version = "2.0.0" agent_id = coder_agent.main.id use_cached = true } @@ -163,7 +163,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.5.0" + version = "2.0.0" agent_id = coder_agent.main.id install = false } From a1b2fb41b11c0a8e19e51288b09aa80a635e269b Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:37:59 +0000 Subject: [PATCH 3/5] test(mux): remove install containers inside the tests The two install tests left their containers to the global afterAll hook in setup.ts, which has a 5 second timeout. A container holding a full @coder/xum install (about 800 MB, 48k files) takes longer than that to delete on the CI runner, so the hook timed out under bun 1.4.1 and failed the run after all tests had passed. --- registry/coder/modules/mux/main.test.ts | 32 +++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/mux/main.test.ts b/registry/coder/modules/mux/main.test.ts index e8559d43e..52dcba707 100644 --- a/registry/coder/modules/mux/main.test.ts +++ b/registry/coder/modules/mux/main.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "bun:test"; import { - executeScriptInContainer, execContainer, findResourceInstance, readFileContainer, @@ -8,9 +7,34 @@ import { runContainer, runTerraformApply, runTerraformInit, + type scriptOutput, + type TerraformState, testRequiredVariables, } from "~test"; +// Like executeScriptInContainer, but removes the container inside the test: +// deleting a container that holds a full @coder/xum install takes longer than +// the few seconds the global afterAll cleanup hook allows. +const executeInstallScriptInContainer = async ( + state: TerraformState, + image: string, + before: string, +): Promise => { + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer(image); + try { + await execContainer(id, ["sh", "-c", before]); + const resp = await execContainer(id, ["sh", "-c", instance.script]); + return { + exitCode: resp.exitCode, + stdout: resp.stdout.trim().split("\n"), + stderr: resp.stderr.trim().split("\n"), + }; + } finally { + await removeContainer(id); + } +}; + describe("mux", async () => { await runTerraformInit(import.meta.dir); @@ -23,10 +47,9 @@ describe("mux", async () => { agent_id: "foo", }); - const output = await executeScriptInContainer( + const output = await executeInstallScriptInContainer( state, "alpine/curl", - "sh", "apk add --no-cache bash tar gzip ca-certificates findutils nodejs && update-ca-certificates", ); if (output.exitCode !== 0) { @@ -287,10 +310,9 @@ chmod +x /tmp/mux/mux`, agent_id: "foo", }); - const output = await executeScriptInContainer( + const output = await executeInstallScriptInContainer( state, "node:20-alpine", - "sh", "apk add bash", ); From ea9046a133bc2473bc17cf97bf5c6450c6fb1106 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:59:38 +0000 Subject: [PATCH 4/5] fix(mux): move install and logs under the module root Default install_prefix and log_path now live under $HOME/.coder-modules/coder/mux (Module Data Layout in AGENTS.md) instead of /tmp, so restarts that clear /tmp no longer discard the install or the diagnostics. Tests assert the new defaults; README links the renamed upstream repo coder/xum. --- registry/coder/modules/mux/README.md | 5 +- registry/coder/modules/mux/main.test.ts | 58 +++++++++++------------ registry/coder/modules/mux/main.tf | 6 +-- registry/coder/modules/mux/mux.tftest.hcl | 45 ++++++++++++++++++ 4 files changed, 80 insertions(+), 34 deletions(-) diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index a490d7d77..cdf52e590 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -8,7 +8,7 @@ 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 `@coder/xum@next`, the npm package that ships the `mux` CLI (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. +Automatically install and run [Mux](https://github.com/coder/xum) in a Coder workspace. By default, the module auto-detects an available package manager (`npm`, `pnpm`, or `bun`) to install `@coder/xum@next`, the npm package that ships the `mux` CLI (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" { @@ -157,7 +157,7 @@ module "mux" { ### Skip Install -Run without installing from the network (requires Mux to be pre-installed): +Run without installing from the network (requires a `mux` binary at `/mux`, by default `~/.coder-modules/coder/mux/mux`): ```tf module "mux" { @@ -181,6 +181,7 @@ module "mux" { - Requires a Node.js runtime; if `node` is not on the workspace `PATH`, the module bootstraps a pinned Node.js runtime into `~/.local/share/coder-mux` (override the version with the `MUX_NODE_VERSION` environment variable) - Installs `@coder/xum@next` from the npm registry by default (this package ships the `mux` binary); set `registry_url` to use a private or mirrored registry - `install_version` must be a version or dist-tag published as `@coder/xum` (0.28.3 or newer); older releases were only published under the legacy `mux` package name +- Installs into `~/.coder-modules/coder/mux` and logs to `~/.coder-modules/coder/mux/logs/mux.log` by default, so the install survives restarts that clear `/tmp`; override with `install_prefix` and `log_path` - 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 diff --git a/registry/coder/modules/mux/main.test.ts b/registry/coder/modules/mux/main.test.ts index 52dcba707..7d26cebb5 100644 --- a/registry/coder/modules/mux/main.test.ts +++ b/registry/coder/modules/mux/main.test.ts @@ -12,6 +12,10 @@ import { testRequiredVariables, } from "~test"; +// Default install_prefix and log_path with HOME=/root inside the test containers. +const MODULE_ROOT = "/root/.coder-modules/coder/mux"; +const LOG_PATH = `${MODULE_ROOT}/logs/mux.log`; + // Like executeScriptInContainer, but removes the container inside the test: // deleting a container that holds a full @coder/xum install takes longer than // the few seconds the global afterAll cleanup hook allows. @@ -59,9 +63,9 @@ describe("mux", async () => { expect(output.exitCode).toBe(0); const expectedLines = [ "📥 No package manager found; downloading tarball from registry...", - "🥳 mux has been installed in /tmp/mux", + `🥳 mux has been installed in ${MODULE_ROOT}`, "🚀 Starting mux server on port 4000...", - "Check logs at /tmp/mux.log!", + `Check logs at ${LOG_PATH}!`, ]; for (const line of expectedLines) { expect(output.stdout).toContain(line); @@ -72,7 +76,6 @@ describe("mux", 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'", }); @@ -85,8 +88,8 @@ describe("mux", async () => { "sh", "-c", `apk add --no-cache bash >/dev/null -mkdir -p /tmp/mux -cat <<'EOF' > /tmp/mux/mux +mkdir -p ${MODULE_ROOT} +cat <<'EOF' > ${MODULE_ROOT}/mux #!/usr/bin/env sh i=1 for arg in "$@"; do @@ -94,7 +97,7 @@ for arg in "$@"; do i=$((i + 1)) done EOF -chmod +x /tmp/mux/mux`, +chmod +x ${MODULE_ROOT}/mux`, ]); expect(setup.exitCode).toBe(0); @@ -106,7 +109,7 @@ chmod +x /tmp/mux/mux`, expect(output.exitCode).toBe(0); await execContainer(id, ["sh", "-c", "sleep 1"]); - const log = await readFileContainer(id, "/tmp/mux.log"); + const log = await readFileContainer(id, LOG_PATH); expect(log).toContain("arg1=server"); expect(log).toContain("arg2=--port"); expect(log).toContain("arg3=4000"); @@ -123,7 +126,6 @@ chmod +x /tmp/mux/mux`, const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", install: false, - log_path: "/tmp/mux.log", }); const instance = findResourceInstance(state, "coder_script"); @@ -134,8 +136,8 @@ chmod +x /tmp/mux/mux`, "sh", "-c", `apk add --no-cache bash >/dev/null -mkdir -p /tmp/mux -cat <<'EOF' > /tmp/mux/mux +mkdir -p ${MODULE_ROOT} +cat <<'EOF' > ${MODULE_ROOT}/mux #!/usr/bin/env sh target_pid="$$" ( @@ -146,7 +148,7 @@ while true; do sleep 1 done EOF -chmod +x /tmp/mux/mux`, +chmod +x ${MODULE_ROOT}/mux`, ]); expect(setup.exitCode).toBe(0); @@ -158,7 +160,7 @@ chmod +x /tmp/mux/mux`, expect(output.exitCode).toBe(0); await execContainer(id, ["sh", "-c", "sleep 2"]); - const log = await readFileContainer(id, "/tmp/mux.log"); + const log = await readFileContainer(id, LOG_PATH); expect(log).toContain("shell exit code 137"); expect(log).toContain( "SIGKILL usually means the process was killed externally or by the OOM killer.", @@ -172,7 +174,6 @@ chmod +x /tmp/mux/mux`, 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, @@ -186,10 +187,10 @@ chmod +x /tmp/mux/mux`, "sh", "-c", `apk add --no-cache bash >/dev/null -mkdir -p /tmp/mux -cat <<'EOF' > /tmp/mux/mux +mkdir -p ${MODULE_ROOT} +cat <<'EOF' > ${MODULE_ROOT}/mux #!/usr/bin/env sh -run_count_file="/tmp/mux-run-count" +run_count_file="${MODULE_ROOT}/run-count" run_count=0 if [ -f "$run_count_file" ]; then run_count=$(cat "$run_count_file") @@ -209,7 +210,7 @@ else fi exit 0 EOF -chmod +x /tmp/mux/mux`, +chmod +x ${MODULE_ROOT}/mux`, ]); expect(setup.exitCode).toBe(0); @@ -221,8 +222,8 @@ chmod +x /tmp/mux/mux`, 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"); + const log = await readFileContainer(id, LOG_PATH); + const runCount = await readFileContainer(id, `${MODULE_ROOT}/run-count`); expect(log).toContain("run=1"); expect(log).toContain("mux server exited cleanly."); expect(log).toContain( @@ -246,7 +247,6 @@ chmod +x /tmp/mux/mux`, 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, @@ -260,10 +260,10 @@ chmod +x /tmp/mux/mux`, "sh", "-c", `apk add --no-cache bash >/dev/null -mkdir -p /tmp/mux -cat <<'EOF' > /tmp/mux/mux +mkdir -p ${MODULE_ROOT} +cat <<'EOF' > ${MODULE_ROOT}/mux #!/usr/bin/env sh -run_count_file="/tmp/mux-run-count" +run_count_file="${MODULE_ROOT}/run-count" run_count=0 if [ -f "$run_count_file" ]; then run_count=$(cat "$run_count_file") @@ -276,7 +276,7 @@ if [ "$run_count" -eq 1 ]; then fi exit 0 EOF -chmod +x /tmp/mux/mux`, +chmod +x ${MODULE_ROOT}/mux`, ]); expect(setup.exitCode).toBe(0); @@ -288,8 +288,8 @@ chmod +x /tmp/mux/mux`, 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"); + const log = await readFileContainer(id, LOG_PATH); + const runCount = await readFileContainer(id, `${MODULE_ROOT}/run-count`); expect(log).toContain("run=1"); expect(log).toContain("signal TERM (15); shell exit code 143."); expect(log).toContain( @@ -318,11 +318,11 @@ chmod +x /tmp/mux/mux`, expect(output.exitCode).toBe(0); const expectedLines = [ - "📦 Installing @coder/xum via npm into /tmp/mux...", + `📦 Installing @coder/xum via npm into ${MODULE_ROOT}...`, "⏭️ Skipping lifecycle scripts with --ignore-scripts", - "🥳 mux has been installed in /tmp/mux", + `🥳 mux has been installed in ${MODULE_ROOT}`, "🚀 Starting mux server on port 4000...", - "Check logs at /tmp/mux.log!", + `Check logs at ${LOG_PATH}!`, ]; for (const line of expectedLines) { expect(output.stdout).toContain(line); diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index 4413ebc87..40a941fc8 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -39,14 +39,14 @@ variable "slug" { variable "install_prefix" { type = string - description = "The prefix to install Mux to." - default = "/tmp/mux" + description = "The directory to install Mux into." + default = "$HOME/.coder-modules/coder/mux" } variable "log_path" { type = string description = "The path for Mux logs." - default = "/tmp/mux.log" + default = "$HOME/.coder-modules/coder/mux/logs/mux.log" } variable "restart_on_kill" { diff --git a/registry/coder/modules/mux/mux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl index 40995b943..a7ffe58a4 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -245,6 +245,51 @@ run "use_cached_only_success" { } } +# Module-controlled paths default to the per-module root (AGENTS.md Module +# Data Layout) so the install and logs survive restarts that clear /tmp. +run "default_paths_under_module_root" { + command = plan + + variables { + agent_id = "foo" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "MUX_BINARY=\"$HOME/.coder-modules/coder/mux/mux\"") + error_message = "mux must install under $HOME/.coder-modules/coder/mux by default" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "LOG_PATH=\"$HOME/.coder-modules/coder/mux/logs/mux.log\"") + error_message = "mux must log to $HOME/.coder-modules/coder/mux/logs/mux.log by default" + } + + assert { + condition = !strcontains(resource.coder_script.mux.script, "/tmp/mux") + error_message = "mux script must not default any module path to /tmp" + } +} + +run "custom_install_prefix_and_log_path" { + command = plan + + variables { + agent_id = "foo" + install_prefix = "/opt/mux" + log_path = "/var/log/mux.log" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "MUX_BINARY=\"/opt/mux/mux\"") + error_message = "mux must honor a custom install_prefix" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "LOG_PATH=\"/var/log/mux.log\"") + error_message = "mux must honor a custom log_path" + } +} + # The installed npm package must be @coder/xum (it ships the mux bin); # the legacy mux package is only a compat shim with an exact-pinned # @coder/xum dependency that is published separately. From 2b8055a4960029312227307ffffb49e07e7c9d88 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:11:03 +0000 Subject: [PATCH 5/5] fix(mux): keep the Node.js bootstrap under the module root The bootstrapped Node.js runtime moves from ~/.local/share/coder-mux to $HOME/.coder-modules/coder/mux so all module data shares one root. Also document that @coder/xum prereleases exist from 0.28.2-next.24. --- registry/coder/modules/mux/README.md | 7 ++++--- registry/coder/modules/mux/main.tf | 2 +- registry/coder/modules/mux/mux.tftest.hcl | 5 +++++ registry/coder/modules/mux/run.sh | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index cdf52e590..977cbbaad 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -51,7 +51,8 @@ module "mux" { version = "2.0.0" agent_id = coder_agent.main.id # Default is "next"; set to a specific version to pin. - # Only versions published as @coder/xum (0.28.3 or newer) are available. + # Only versions published as @coder/xum are available: 0.28.3 or newer, + # or prereleases from 0.28.2-next.24. install_version = "0.28.4" } ``` @@ -178,9 +179,9 @@ module "mux" { - Mux is currently in preview and you may encounter bugs - Requires internet connectivity for agent operations (unless `install` is set to false) - Auto-detects `npm`, `pnpm`, or `bun` by default; set `package_manager` to force a specific one -- Requires a Node.js runtime; if `node` is not on the workspace `PATH`, the module bootstraps a pinned Node.js runtime into `~/.local/share/coder-mux` (override the version with the `MUX_NODE_VERSION` environment variable) +- Requires a Node.js runtime; if `node` is not on the workspace `PATH`, the module bootstraps a pinned Node.js runtime into `~/.coder-modules/coder/mux` (override the version with the `MUX_NODE_VERSION` environment variable) - Installs `@coder/xum@next` from the npm registry by default (this package ships the `mux` binary); set `registry_url` to use a private or mirrored registry -- `install_version` must be a version or dist-tag published as `@coder/xum` (0.28.3 or newer); older releases were only published under the legacy `mux` package name +- `install_version` must be a version or dist-tag published as `@coder/xum` (0.28.3 or newer, or a prerelease from 0.28.2-next.24); older releases were only published under the legacy `mux` package name - Installs into `~/.coder-modules/coder/mux` and logs to `~/.coder-modules/coder/mux/logs/mux.log` by default, so the install survives restarts that clear `/tmp`; override with `install_prefix` and `log_path` - 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 diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index 40a941fc8..787df6934 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -91,7 +91,7 @@ variable "additional_arguments" { variable "install_version" { type = string - description = "The version or dist-tag of the @coder/xum npm package to install. Only versions published as @coder/xum (0.28.3 or newer) are available." + description = "The version or dist-tag of the @coder/xum npm package to install. Only versions published as @coder/xum are available: 0.28.3 or newer, or prereleases from 0.28.2-next.24." default = "next" } diff --git a/registry/coder/modules/mux/mux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl index a7ffe58a4..4a0db7edc 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -264,6 +264,11 @@ run "default_paths_under_module_root" { error_message = "mux must log to $HOME/.coder-modules/coder/mux/logs/mux.log by default" } + assert { + condition = strcontains(resource.coder_script.mux.script, "node_dir=\"$HOME/.coder-modules/coder/mux/node-v") + error_message = "the Node.js bootstrap must live under the module root" + } + assert { condition = !strcontains(resource.coder_script.mux.script, "/tmp/mux") error_message = "mux script must not default any module path to /tmp" diff --git a/registry/coder/modules/mux/run.sh b/registry/coder/modules/mux/run.sh index a40f63ab0..839a07c21 100644 --- a/registry/coder/modules/mux/run.sh +++ b/registry/coder/modules/mux/run.sh @@ -196,7 +196,7 @@ EOF_LAUNCHER } # Ensure a Node.js runtime is available (mux is a Node application launched # via "#!/usr/bin/env node"). When the workspace image does not provide node, -# bootstrap a pinned runtime into $HOME so it persists across restarts. +# bootstrap a pinned runtime into the module root so it persists across restarts. ensure_node() { if command -v node > /dev/null 2>&1; then return 0 @@ -213,7 +213,7 @@ ensure_node() { ;; esac - node_dir="$HOME/.local/share/coder-mux/node-v$node_version-linux-$node_arch" + node_dir="$HOME/.coder-modules/coder/mux/node-v$node_version-linux-$node_arch" if [ ! -x "$node_dir/bin/node" ]; then echo "⚠️ node not found on PATH; bootstrapping Node.js v$node_version into $node_dir..." mkdir -p "$(dirname "$node_dir")"