From a35986d7dfd485758f4e32a275d9da84eba8b315 Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:44:26 -0500 Subject: [PATCH 01/22] feat: initial boundary integration with claude code (#455) Closes # ## Description ## Type of Change - [ ] New module - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/[namespace]/modules/[module-name]` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [ ] Changes tested locally ## Related Issues --------- Co-authored-by: YEVHENII SHCHERBINA --- .github/typos.toml | 1 + registry/coder/modules/claude-code/main.tf | 45 ++++++++++++++ .../coder/modules/claude-code/main.tftest.hcl | 28 ++++++++- .../modules/claude-code/scripts/start.sh | 62 ++++++++++++++++++- 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/.github/typos.toml b/.github/typos.toml index fdb74748..600a39ba 100644 --- a/.github/typos.toml +++ b/.github/typos.toml @@ -5,6 +5,7 @@ Hashi = "Hashi" HashiCorp = "HashiCorp" mavrickrishi = "mavrickrishi" # Username mavrick = "mavrick" # Username +inh = "inh" # Option in setpriv command [files] extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive \ No newline at end of file diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 8909a024..df3eaaa5 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -192,6 +192,42 @@ variable "claude_md_path" { default = "$HOME/.claude/CLAUDE.md" } +variable "enable_boundary" { + type = bool + description = "Whether to enable coder boundary for network filtering" + default = false +} + +variable "boundary_version" { + type = string + description = "Boundary version, valid git reference should be provided (tag, commit, branch)" + default = "main" +} + +variable "boundary_log_dir" { + type = string + description = "Directory for boundary logs" + default = "/tmp/boundary_logs" +} + +variable "boundary_log_level" { + type = string + description = "Log level for boundary process" + default = "WARN" +} + +variable "boundary_additional_allowed_urls" { + type = list(string) + description = "Additional URLs to allow through boundary (in addition to default allowed URLs)" + default = [] +} + +variable "boundary_proxy_port" { + type = string + description = "Port for HTTP Proxy used by Boundary" + default = "8087" +} + resource "coder_env" "claude_code_md_path" { count = var.claude_md_path == "" ? 0 : 1 @@ -229,6 +265,8 @@ locals { start_script = file("${path.module}/scripts/start.sh") module_dir_name = ".claude-module" remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh")) + # Extract hostname from access_url for boundary --allow flag + coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "") # Required prompts for the module to properly report task status to Coder report_tasks_system_prompt = <<-EOT @@ -299,6 +337,13 @@ module "agentapi" { ARG_PERMISSION_MODE='${var.permission_mode}' \ ARG_WORKDIR='${local.workdir}' \ ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ + ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \ + ARG_BOUNDARY_VERSION='${var.boundary_version}' \ + ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \ + ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \ + ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \ + ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \ + ARG_CODER_HOST='${local.coder_host}' \ /tmp/start.sh EOT diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index 9999c1b1..6994caf2 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -188,6 +188,32 @@ run "test_claude_code_permission_mode_validation" { } } +run "test_claude_code_with_boundary" { + command = plan + + variables { + agent_id = "test-agent-boundary" + workdir = "/home/coder/boundary-test" + enable_boundary = true + boundary_log_dir = "/tmp/test-boundary-logs" + } + + assert { + condition = var.enable_boundary == true + error_message = "Boundary should be enabled" + } + + assert { + condition = var.boundary_log_dir == "/tmp/test-boundary-logs" + error_message = "Boundary log dir should be set correctly" + } + + assert { + condition = local.coder_host != "" + error_message = "Coder host should be extracted from access URL" + } +} + run "test_claude_code_system_prompt" { command = plan @@ -267,4 +293,4 @@ run "test_claude_report_tasks_disabled" { condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "") error_message = "System prompt should end with " } -} \ No newline at end of file +} diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 6eeb411b..daef71a3 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -17,6 +17,12 @@ ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-} ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d) +ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} +ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"} +ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"} +ARG_BOUNDARY_LOG_LEVEL=${ARG_BOUNDARY_LOG_LEVEL:-"WARN"} +ARG_BOUNDARY_PROXY_PORT=${ARG_BOUNDARY_PROXY_PORT:-"8087"} +ARG_CODER_HOST=${ARG_CODER_HOST:-} echo "--------------------------------" @@ -27,6 +33,12 @@ printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIO printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE" printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT" printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" +printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY" +printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION" +printf "ARG_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR" +printf "ARG_BOUNDARY_LOG_LEVEL: %s\n" "$ARG_BOUNDARY_LOG_LEVEL" +printf "ARG_BOUNDARY_PROXY_PORT: %s\n" "$ARG_BOUNDARY_PROXY_PORT" +printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" echo "--------------------------------" @@ -35,6 +47,14 @@ echo "--------------------------------" # avoid exiting if the script fails bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true +function install_boundary() { + # Install boundary from public github repo + git clone https://github.com/coder/boundary + cd boundary + git checkout $ARG_BOUNDARY_VERSION + go install ./cmd/... +} + function validate_claude_installation() { if command_exists claude; then printf "Claude Code is installed\n" @@ -76,7 +96,47 @@ function start_agentapi() { fi fi printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" - agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" + + if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then + install_boundary + + mkdir -p "$ARG_BOUNDARY_LOG_DIR" + printf "Starting with coder boundary enabled\n" + + # Build boundary args with conditional --unprivileged flag + BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR") + # Add default allowed URLs + BOUNDARY_ARGS+=(--allow "*anthropic.com" --allow "registry.npmjs.org" --allow "*sentry.io" --allow "claude.ai" --allow "$ARG_CODER_HOST") + + # Add any additional allowed URLs from the variable + if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then + IFS=' ' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" + for url in "${ADDITIONAL_URLS[@]}"; do + BOUNDARY_ARGS+=(--allow "$url") + done + fi + + # Set HTTP Proxy port used by Boundary + BOUNDARY_ARGS+=(--proxy-port $ARG_BOUNDARY_PROXY_PORT) + + # Set log level for boundary + BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL) + + # Remove --dangerously-skip-permissions from ARGS when using boundary (it doesn't work with elevated permissions) + # Create a new array without the dangerous permissions flag + CLAUDE_ARGS=() + for arg in "${ARGS[@]}"; do + if [ "$arg" != "--dangerously-skip-permissions" ]; then + CLAUDE_ARGS+=("$arg") + fi + done + + agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \ + sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \ + claude "${CLAUDE_ARGS[@]}" + else + agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" + fi } validate_claude_installation From a1786a09ea48d9c3ab62ef141a9d9f51f5ffeb1f Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:46:32 -0500 Subject: [PATCH 02/22] update claude-code module version (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The version for the claude-code module should have been updated in https://github.com/coder/registry/pull/455. This PR updates the module version so we can cut a release 😎 --- registry/coder/modules/claude-code/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index ed10e2a5..4477eb63 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 = "3.1.1" + version = "3.2.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -49,7 +49,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.1.1" + version = "3.2.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -85,7 +85,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 = "3.1.1" + version = "3.2.0" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -108,7 +108,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.1.1" + version = "3.2.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -181,7 +181,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.1.1" + version = "3.2.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -238,7 +238,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.1.1" + version = "3.2.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" From 583918bfef28a121241af5b33b7bb0858961ee51 Mon Sep 17 00:00:00 2001 From: Jiachen Jiang Date: Tue, 21 Oct 2025 14:33:15 -0700 Subject: [PATCH 03/22] added example of boundary to claude code module (#500) --- registry/coder/modules/claude-code/README.md | 29 ++++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 4477eb63..3e690ce2 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 = "3.2.0" + version = "3.2.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -34,6 +34,23 @@ module "claude-code" { ## Examples +### Usage with Agent Boundaries + +This example shows how to configure the Claude Code module to run the agent behind a process-level boundary that restricts its network access. + +```tf +module "claude-code" { + source = "dev.registry.coder.com/coder/claude-code/coder" + enable_boundary = true + boundary_version = "main" + boundary_log_dir = "/tmp/boundary_logs" + boundary_log_level = "WARN" + boundary_additional_allowed_urls = ["GET *google.com"] + boundary_proxy_port = "8087" + version = "3.2.1" +} +``` + ### Usage with Tasks and Advanced Configuration This example shows how to configure the Claude Code module with an AI prompt, API key shared by all users of the template, and other custom settings. @@ -49,7 +66,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.0" + version = "3.2.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -85,7 +102,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 = "3.2.0" + version = "3.2.1" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -108,7 +125,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.0" + version = "3.2.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -181,7 +198,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.0" + version = "3.2.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -238,7 +255,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.0" + version = "3.2.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" From 843b1f1e5acb18bf70d493d4e6aa63ce5c161c52 Mon Sep 17 00:00:00 2001 From: DevCats Date: Wed, 22 Oct 2025 07:33:09 -0500 Subject: [PATCH 04/22] chore: change copilot default version to latest (#499) ## Description Changes `copilot_version` default to `latest` ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder-labs/modules/copilot` **New version:** `v0.2.2` **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/copilot/README.md | 14 +++++++------- registry/coder-labs/modules/copilot/main.tf | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/registry/coder-labs/modules/copilot/README.md b/registry/coder-labs/modules/copilot/README.md index 83f59c7c..e0b520e0 100644 --- a/registry/coder-labs/modules/copilot/README.md +++ b/registry/coder-labs/modules/copilot/README.md @@ -13,7 +13,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/projects" } @@ -51,7 +51,7 @@ data "coder_parameter" "ai_prompt" { module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/projects" @@ -71,12 +71,12 @@ Customize tool permissions, MCP servers, and Copilot settings: ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/projects" - # Version pinning (defaults to "0.0.334", use "latest" for newest version) - copilot_version = "latest" + # Version pinning (defaults to "latest", use specific version if desired) + copilot_version = "0.0.334" # Tool permissions allow_tools = ["shell(git)", "shell(npm)", "write"] @@ -142,7 +142,7 @@ variable "github_token" { module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/projects" github_token = var.github_token @@ -156,7 +156,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder" report_tasks = false diff --git a/registry/coder-labs/modules/copilot/main.tf b/registry/coder-labs/modules/copilot/main.tf index eb9f78d4..41a83d53 100644 --- a/registry/coder-labs/modules/copilot/main.tf +++ b/registry/coder-labs/modules/copilot/main.tf @@ -104,7 +104,7 @@ variable "agentapi_version" { variable "copilot_version" { type = string description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'." - default = "0.0.334" + default = "latest" } variable "report_tasks" { From 51ec6e3212229fb8b2108995f0112acf4efdb341 Mon Sep 17 00:00:00 2001 From: DevCats Date: Wed, 22 Oct 2025 10:58:01 -0500 Subject: [PATCH 05/22] fix: resolve issues with claude-code session resumption (#496) ## Description Fixes session resumption logic by having the continue flag decide whether to continue a workspace based on session history ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [X] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v3.2.2` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- registry/coder/modules/claude-code/README.md | 16 +++-- .../coder/modules/claude-code/main.test.ts | 23 ++++-- registry/coder/modules/claude-code/main.tf | 4 +- .../modules/claude-code/scripts/start.sh | 70 ++++++++++++++----- 4 files changed, 81 insertions(+), 32 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 3e690ce2..d3cee145 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 = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -32,6 +32,10 @@ module "claude-code" { - You can get the API key from the [Anthropic Console](https://console.anthropic.com/dashboard). - You can get the Session Token using the `claude setup-token` command. This is a long-lived authentication token (requires Claude subscription) +### Session Resumption Behavior + +By default, Claude Code automatically resumes existing conversations when your workspace restarts. Sessions are tracked per workspace directory, so conversations continue where you left off. If no session exists (first start), your `ai_prompt` will run normally. To disable this behavior and always start fresh, set `continue = false` + ## Examples ### Usage with Agent Boundaries @@ -66,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -102,7 +106,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 = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -125,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -198,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -255,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 9c132f1a..a7c2dd14 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -167,7 +167,7 @@ describe("claude-code", async () => { const { id } = await setup({ moduleVariables: { permission_mode: mode, - task_prompt: "test prompt", + ai_prompt: "test prompt", }, }); await execModuleScript(id); @@ -185,7 +185,7 @@ describe("claude-code", async () => { const { id } = await setup({ moduleVariables: { model: model, - task_prompt: "test prompt", + ai_prompt: "test prompt", }, }); await execModuleScript(id); @@ -198,13 +198,24 @@ describe("claude-code", async () => { expect(startLog.stdout).toContain(`--model ${model}`); }); - test("claude-continue-previous-conversation", async () => { + test("claude-continue-resume-existing-session", async () => { const { id } = await setup({ moduleVariables: { continue: "true", - task_prompt: "test prompt", + ai_prompt: "test prompt", }, }); + + // Create a mock session file with the predefined task session ID + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + await execContainer(id, [ + "bash", + "-c", + `touch ${sessionDir}/session-${taskSessionId}.jsonl`, + ]); + await execModuleScript(id); const startLog = await execContainer(id, [ @@ -212,7 +223,9 @@ describe("claude-code", async () => { "-c", "cat /home/coder/.claude-module/agentapi-start.log", ]); - expect(startLog.stdout).toContain("--continue"); + expect(startLog.stdout).toContain("--resume"); + expect(startLog.stdout).toContain(taskSessionId); + expect(startLog.stdout).toContain("Resuming existing task session"); }); test("pre-post-install-scripts", async () => { diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index df3eaaa5..20a0cfee 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -134,8 +134,8 @@ variable "resume_session_id" { variable "continue" { type = bool - description = "Load the most recent conversation in the current directory. Task will fail in a new workspace with no conversation/session to continue" - default = false + description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)." + default = true } variable "dangerously_skip_permissions" { diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index daef71a3..fb3180af 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -64,37 +64,70 @@ function validate_claude_installation() { fi } +TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" + +task_session_exists() { + if find "$HOME/.claude" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then + return 0 + else + return 1 + fi +} + ARGS=() -function build_claude_args() { +function start_agentapi() { + mkdir -p "$ARG_WORKDIR" + cd "$ARG_WORKDIR" + if [ -n "$ARG_MODEL" ]; then ARGS+=(--model "$ARG_MODEL") fi - if [ -n "$ARG_RESUME_SESSION_ID" ]; then - ARGS+=(--resume "$ARG_RESUME_SESSION_ID") - fi - - if [ "$ARG_CONTINUE" = "true" ]; then - ARGS+=(--continue) - fi - if [ -n "$ARG_PERMISSION_MODE" ]; then ARGS+=(--permission-mode "$ARG_PERMISSION_MODE") fi -} - -function start_agentapi() { - mkdir -p "$ARG_WORKDIR" - cd "$ARG_WORKDIR" - if [ -n "$ARG_AI_PROMPT" ]; then - ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT") - else - if [ -n "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" ]; then + if [ -n "$ARG_RESUME_SESSION_ID" ]; then + echo "Using explicit resume_session_id: $ARG_RESUME_SESSION_ID" + ARGS+=(--resume "$ARG_RESUME_SESSION_ID") + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then ARGS+=(--dangerously-skip-permissions) fi + elif [ "$ARG_CONTINUE" = "true" ]; then + if task_session_exists; then + echo "Task session detected (ID: $TASK_SESSION_ID)" + ARGS+=(--resume "$TASK_SESSION_ID") + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions) + fi + echo "Resuming existing task session" + else + echo "No existing task session found" + ARGS+=(--session-id "$TASK_SESSION_ID") + if [ -n "$ARG_AI_PROMPT" ]; then + ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT") + echo "Starting new task session with prompt" + else + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions) + fi + echo "Starting new task session" + fi + fi + else + echo "Continue disabled, starting fresh session" + if [ -n "$ARG_AI_PROMPT" ]; then + ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT") + echo "Starting new session with prompt" + else + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions) + fi + echo "Starting claude code session" + fi fi + printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then @@ -140,5 +173,4 @@ function start_agentapi() { } validate_claude_installation -build_claude_args start_agentapi From 0c5a8a2354f6af1938ccc901715899143370fafe Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Wed, 22 Oct 2025 17:51:58 +0100 Subject: [PATCH 06/22] add nfs-deployment template (#502) ## Description this PR adds a new template to the registry, which shows how to mount an NFS share to a K8s deployment workspace. ## Type of Change - [ ] New module - [x] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Template Information **Path:** `registry/ericpaulsen/templates/nfs-deployment` ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --------- Co-authored-by: DevCats --- .github/typos.toml | 1 + .../templates/nfs-deployment/README.md | 70 ++++ .../templates/nfs-deployment/main.tf | 348 ++++++++++++++++++ 3 files changed, 419 insertions(+) create mode 100644 registry/ericpaulsen/templates/nfs-deployment/README.md create mode 100644 registry/ericpaulsen/templates/nfs-deployment/main.tf diff --git a/.github/typos.toml b/.github/typos.toml index 600a39ba..7ebdacef 100644 --- a/.github/typos.toml +++ b/.github/typos.toml @@ -6,6 +6,7 @@ HashiCorp = "HashiCorp" mavrickrishi = "mavrickrishi" # Username mavrick = "mavrick" # Username inh = "inh" # Option in setpriv command +exportfs = "exportfs" # nfs related binary [files] extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive \ No newline at end of file diff --git a/registry/ericpaulsen/templates/nfs-deployment/README.md b/registry/ericpaulsen/templates/nfs-deployment/README.md new file mode 100644 index 00000000..c2bdbdc6 --- /dev/null +++ b/registry/ericpaulsen/templates/nfs-deployment/README.md @@ -0,0 +1,70 @@ +--- +display_name: "NFS K8s Deployment" +description: "Mount an NFS share to a Coder K8s workspace" +icon: "../../../../.icons/folder.svg" +verified: false +tags: ["kubernetes", "shared-dir", "nfs"] +--- + +# NFS K8s Deployment + +This template provisions a Coder workspace as a Kubernetes Deployment, with an NFS share mounted +as a volume. The NFS share will synchronize the server-side files onto the client (Coder workspace) +When you stop the Coder workspace and rebuild, the NFS share will be re-mounted, and the changes persisted. + +Note the `volume` and `volume_mount` blocks in the deployment and container spec, +respectively: + +```terraform +resource "kubernetes_deployment" "main" { + spec { + template { + spec { + container { + volume_mount { + mount_path = data.coder_parameter.nfs_mount_path.value # mount path in the container + name = "nfs-share" + } + } + volume { + name = "nfs-share" + nfs { + path = data.coder_parameter.nfs_mount_path.value # path to be exported from the server + server = data.coder_parameter.nfs_server.value # server IP address + } + } + } + } + } +} +``` + +## server-side configuration + +1. Create an NFS mount on the server for the clients to access: + + ```console + export NFS_MNT_PATH=/mnt/nfs_share + # Create directory to shaare + sudo mkdir -p $NFS_MNT_PATH + # Assign UID & GIDs access + sudo chown -R uid:gid $NFS_MNT_PATH + sudo chmod 777 $NFS_MNT_PATH + ``` + +1. Grant access to the client by updating the `/etc/exports` file, which + controls the directories shared with remote clients. See + [Red Hat's docs for more information about the configuration options](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/5/html/deployment_guide/s1-nfs-server-config-exports). + + ```console + # Provides read/write access to clients accessing the NFS from any IP address. + /mnt/nfs_share *(rw,sync,no_subtree_check) + ``` + +1. Export the NFS file share directory. You must do this every time you change + `/etc/exports`. + + ```console + sudo exportfs -a + sudo systemctl restart + ``` diff --git a/registry/ericpaulsen/templates/nfs-deployment/main.tf b/registry/ericpaulsen/templates/nfs-deployment/main.tf new file mode 100644 index 00000000..e8c395e6 --- /dev/null +++ b/registry/ericpaulsen/templates/nfs-deployment/main.tf @@ -0,0 +1,348 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + } +} + +provider "coder" { +} + +provider "kubernetes" { + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} + +variable "use_kubeconfig" { + type = bool + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF + default = false +} + +variable "namespace" { + type = string + description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace." +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPU cores" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 Cores" + value = "2" + } + option { + name = "4 Cores" + value = "4" + } + option { + name = "6 Cores" + value = "6" + } + option { + name = "8 Cores" + value = "8" + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory in GB" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } +} + +data "coder_parameter" "home_disk_size" { + name = "home_disk_size" + display_name = "Home disk size" + description = "The size of the home disk in GB" + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 99999 + } +} + +data "coder_parameter" "nfs_server" { + name = "nfs_server" + type = "string" + display_name = "NFS Server IP" + description = "The NFS server IP address to use for the workspace" +} + +data "coder_parameter" "nfs_mount_path" { + name = "nfs_mount_path" + type = "string" + display_name = "NFS Mount Path" + description = "The path in your workspace container to mount the NFS share to" + default = "/mnt/nfs-share" + validation { + regex = "^/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*$" + error = "NFS mount path must be a valid path in your workspace container" + } +} + +resource "coder_agent" "coder" { + os = "linux" + arch = "amd64" + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = < Date: Thu, 23 Oct 2025 15:28:58 +1100 Subject: [PATCH 07/22] chore: Update templates from Always to IfNotPresent for image_pull_policy (#501) ## Description Change `image_pull_policy` from `Always` to `IfNotPresent` on Coder owned templates. Given these are a reference point for users and customers and they copy them into their own templates I think it makes sense to encourage the use of caching of images. ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Template Information **Path:** https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-devcontainer https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-envbox https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --- registry/coder/templates/kubernetes-devcontainer/main.tf | 4 ++-- registry/coder/templates/kubernetes-envbox/main.tf | 4 ++-- registry/coder/templates/kubernetes/main.tf | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/registry/coder/templates/kubernetes-devcontainer/main.tf b/registry/coder/templates/kubernetes-devcontainer/main.tf index 5e36226d..d391c75a 100644 --- a/registry/coder/templates/kubernetes-devcontainer/main.tf +++ b/registry/coder/templates/kubernetes-devcontainer/main.tf @@ -264,7 +264,7 @@ resource "kubernetes_deployment" "main" { container { name = "dev" image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image - image_pull_policy = "Always" + image_pull_policy = "IfNotPresent" security_context { privileged = true } @@ -455,4 +455,4 @@ resource "coder_metadata" "container_info" { key = "cache repo" value = var.cache_repo == "" ? "not enabled" : var.cache_repo } -} \ No newline at end of file +} diff --git a/registry/coder/templates/kubernetes-envbox/main.tf b/registry/coder/templates/kubernetes-envbox/main.tf index e70ad2a3..98543d9c 100644 --- a/registry/coder/templates/kubernetes-envbox/main.tf +++ b/registry/coder/templates/kubernetes-envbox/main.tf @@ -152,7 +152,7 @@ resource "kubernetes_pod" "main" { name = "dev" # We highly recommend pinning this to a specific release of envbox, as the latest tag may change. image = "ghcr.io/coder/envbox:latest" - image_pull_policy = "Always" + image_pull_policy = "IfNotPresent" command = ["/envbox", "docker"] security_context { @@ -310,4 +310,4 @@ resource "kubernetes_pod" "main" { } } } -} \ No newline at end of file +} diff --git a/registry/coder/templates/kubernetes/main.tf b/registry/coder/templates/kubernetes/main.tf index c72316ff..7d7c0aa8 100644 --- a/registry/coder/templates/kubernetes/main.tf +++ b/registry/coder/templates/kubernetes/main.tf @@ -287,7 +287,7 @@ resource "kubernetes_deployment" "main" { container { name = "dev" image = "codercom/enterprise-base:ubuntu" - image_pull_policy = "Always" + image_pull_policy = "IfNotPresent" command = ["sh", "-c", coder_agent.main.init_script] security_context { run_as_user = "1000" From 19519a0a1302a599b32ae0b76de0462b0746afea Mon Sep 17 00:00:00 2001 From: DevCats Date: Thu, 23 Oct 2025 07:39:27 -0500 Subject: [PATCH 08/22] fix: add shebang to zed coder_script (#504) ## Description Add `#!/bin/sh` to zed_settings coder_script ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/zed` **New version:** `v1.1.1` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues https://github.com/coder/registry/issues/482 --- registry/coder/modules/zed/README.md | 10 +++++----- registry/coder/modules/zed/main.tf | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/zed/README.md b/registry/coder/modules/zed/README.md index 989df54d..132e489c 100644 --- a/registry/coder/modules/zed/README.md +++ b/registry/coder/modules/zed/README.md @@ -19,7 +19,7 @@ Zed is a high-performance, multiplayer code editor from the creators of Atom and module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id } ``` @@ -32,7 +32,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" } @@ -44,7 +44,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id display_name = "Zed Editor" order = 1 @@ -57,7 +57,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id agent_name = coder_agent.example.name } @@ -73,7 +73,7 @@ You can declaratively set/merge settings with the `settings` input. Provide a JS module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id settings = jsonencode({ diff --git a/registry/coder/modules/zed/main.tf b/registry/coder/modules/zed/main.tf index 94ec69a6..0029672a 100644 --- a/registry/coder/modules/zed/main.tf +++ b/registry/coder/modules/zed/main.tf @@ -73,6 +73,7 @@ resource "coder_script" "zed_settings" { icon = "/icon/zed.svg" run_on_start = true script = <<-EOT + #!/bin/sh set -eu SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}' if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then From f7c1be71f76b418c4940f31e70dd63a759aa3420 Mon Sep 17 00:00:00 2001 From: djarbz <30350993+djarbz@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:19:05 -0500 Subject: [PATCH 09/22] Add [copyparty] module (#486) ## Description This PR adds a module to install Copyparty as an alternative to Filebrowser. ## Type of Change - [x] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/djarbz/modules/copyparty` **New version:** `v0.1.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [N/A] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --------- Co-authored-by: DevCats --- .icons/copyparty.svg | 210 ++++++++++++++++++ registry/djarbz/.images/avatar.png | Bin 0 -> 1557 bytes .../djarbz/.images/copyparty_screenshot.png | Bin 0 -> 39447 bytes registry/djarbz/README.md | 11 + registry/djarbz/modules/copyparty/README.md | 68 ++++++ .../modules/copyparty/copyparty.tftest.hcl | 181 +++++++++++++++ registry/djarbz/modules/copyparty/main.tf | 174 +++++++++++++++ registry/djarbz/modules/copyparty/run.sh | 100 +++++++++ 8 files changed, 744 insertions(+) create mode 100644 .icons/copyparty.svg create mode 100644 registry/djarbz/.images/avatar.png create mode 100644 registry/djarbz/.images/copyparty_screenshot.png create mode 100644 registry/djarbz/README.md create mode 100644 registry/djarbz/modules/copyparty/README.md create mode 100644 registry/djarbz/modules/copyparty/copyparty.tftest.hcl create mode 100644 registry/djarbz/modules/copyparty/main.tf create mode 100755 registry/djarbz/modules/copyparty/run.sh diff --git a/.icons/copyparty.svg b/.icons/copyparty.svg new file mode 100644 index 00000000..2c4f0d04 --- /dev/null +++ b/.icons/copyparty.svg @@ -0,0 +1,210 @@ + + + copyparty_logo + + + + + + + + + + + + image/svg+xml + + copyparty_logo + github.com/9001/copyparty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/djarbz/.images/avatar.png b/registry/djarbz/.images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..f60192032e0581ddbb88633f9c8efaa893d41953 GIT binary patch literal 1557 zcmeAS@N?(olHy`uVBq!ia0y~yU|a&i985rwk9xzp%(1l;$4Bkqz=}n|~?D z=Uv~5XV3dEF=jZYYG{^)MzPhqmz?(bpe$|Ebeev}3B8If6niSxZe3>1@W*O8{mQKm e`V0*J|E~~M^7r`XTnjAj89ZJ6T-G@yGywqQMEvFe literal 0 HcmV?d00001 diff --git a/registry/djarbz/.images/copyparty_screenshot.png b/registry/djarbz/.images/copyparty_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..690c716f9e2184197498a6d6a6a70a426f7c63ed GIT binary patch literal 39447 zcmd42bx@qm8ZU@zkl=2?-CY6!65M5QNMH!=8r(I(ZE$zO;7+h$GdK(cw*Z3^Bm`LU zopZMC-E+6<*4=-$Yl>-k`ti4a-90Zcn(A+Huqdz)5D;*b-pOktAfN&f5Rh;%ke_?3 zMWsLp2*?PUs=5kqT~wcY{(oC2X*g-bkEIaE2wBVo+b@_vfrI_B8a@DCm^Dpm^kq-P$k8B5So$g>3!ul&8wyh7|%Xh5#j@ z!Z9($MMb>~jJ$0nuPZX7LGe;vW-JmaOm>dppVADyzUT($8241kDJcV-qcv2_cuP~I zY=e|Od~lbRerM?|A6;qu^z>xpEG@0U>cpk@z3^>*e*5avL}c*?4J)C&HVt|Iz*PtW2706{*^tUc

WVLNJcUG;# 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 literal 0 HcmV?d00001 diff --git a/registry/djarbz/README.md b/registry/djarbz/README.md new file mode 100644 index 00000000..2319441a --- /dev/null +++ b/registry/djarbz/README.md @@ -0,0 +1,11 @@ +--- +display_name: "Austin" +bio: "IT Pro by day, script kiddie at night." +avatar: "./.images/avatar.png" +github: "djarbz" +status: "community" +--- + +# Austin + +I like to program as a hobby. diff --git a/registry/djarbz/modules/copyparty/README.md b/registry/djarbz/modules/copyparty/README.md new file mode 100644 index 00000000..fdd3ad56 --- /dev/null +++ b/registry/djarbz/modules/copyparty/README.md @@ -0,0 +1,68 @@ +--- +display_name: copyparty +description: A web based file explorer alternative to Filebrowser. +icon: ../../../../.icons/copyparty.svg +verified: false +tags: [files, filebrowser, web, copyparty] +--- + +# copyparty + + + +This module installs Copyparty, an alternative to Filebrowser. +[Copyparty](https://github.com/9001/copyparty) is a portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" +} +``` + + + +![copyparty-browser-fs8](../../.images/copyparty_screenshot.png) + +## Examples + +### Example 1 + +Some basic command line options: + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + arguments = [ + "-v", "/home/coder/:/home:r", # Share home directory (read-only) + "-v", "${local.repo_dir}:/repo:rw", # Share project directory (read-write) + "-e2dsa", # Enables general file indexing" + ] +} +``` + +### Example 2 + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + subdomain = true + arguments = [ + "-v", "/tmp:/tmp:r", # Share tmp directory (read-only) + "-v", "/home/coder/:/home:rw", # Share home directory (read-write) + "-v", "${local.root_dir}:/work:A:c,dotsrch", # Share work directory (All Perms) + "-e2dsa", # Enables general file indexing" + "--re-maxage", "900", # Rescan filesystem for changes every SEC + "--see-dots", # Show dotfiles by default if user has correct permissions on volume + "--xff-src=lan", # List of trusted reverse-proxy CIDRs (comma-separated) or `lan` for private IPs. + "--rproxy", "1", # Which ip to associate clients with, index of X-FWD IP. + ] +} +``` diff --git a/registry/djarbz/modules/copyparty/copyparty.tftest.hcl b/registry/djarbz/modules/copyparty/copyparty.tftest.hcl new file mode 100644 index 00000000..e6fab843 --- /dev/null +++ b/registry/djarbz/modules/copyparty/copyparty.tftest.hcl @@ -0,0 +1,181 @@ +# --- Test Case 1: Required Variables --- +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + } +} + +# --- Test Case 2: Coder App URL uses custom port --- +run "app_url_uses_port" { + command = plan + + variables { + agent_id = "example-agent-id" + port = 19999 + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:19999" + error_message = "Expected copyparty app URL to include configured port" + } +} + +# --- Test Case 3: Default Values --- +run "test_defaults" { + # This run block applies the module with default values + # (except for the required 'agent_id' provided above). + + variables { + agent_id = "example-agent-id" + } + + # --- Asserts for coder_app "copyparty" --- + assert { + condition = resource.coder_app.copyparty.display_name == "copyparty" + error_message = "Default display_name is incorrect" + } + + assert { + condition = resource.coder_app.copyparty.slug == "copyparty" + error_message = "Default slug is incorrect" + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:3923" + error_message = "Default URL is incorrect, expected port 3923" + } + + assert { + condition = resource.coder_app.copyparty.subdomain == false + error_message = "Default subdomain should be false" + } + + assert { + condition = resource.coder_app.copyparty.share == "owner" + error_message = "Default share value should be 'owner'" + } + + assert { + condition = resource.coder_app.copyparty.open_in == "slim-window" + error_message = "Default open_in value should be 'slim-window'" + } + + # --- Asserts for coder_script "copyparty" --- + assert { + condition = coder_script.copyparty.display_name == "copyparty" + error_message = "Script display_name is incorrect" + } + + # Check rendered script content (this assumes your run.sh uses the variables) + assert { + condition = strcontains(coder_script.copyparty.script, "PORT=\"3923\"") + error_message = "Script content does not reflect default port" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/tmp/copyparty.log\"") + error_message = "Script content does not reflect default log_path" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"\"") + error_message = "Script content does not reflect default empty arguments" + } +} + +# --- Test Case 4: Custom Values --- +run "test_custom_values" { + # Override default variables for this specific run + variables { + agent_id = "example-agent-id" + port = 8080 + slug = "my-custom-app" + display_name = "My Custom App" + share = "authenticated" + open_in = "tab" + pinned_version = "v1.2.3" + arguments = ["--verbose", "-v"] + log_path = "/var/log/custom.log" + } + + # --- Asserts for coder_app "copyparty" --- + assert { + condition = resource.coder_app.copyparty.display_name == "My Custom App" + error_message = "Custom display_name was not applied" + } + + assert { + condition = resource.coder_app.copyparty.slug == "my-custom-app" + error_message = "Custom slug was not applied" + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:8080" + error_message = "Custom port was not applied to URL" + } + + assert { + condition = resource.coder_app.copyparty.share == "authenticated" + error_message = "Custom share value was not applied" + } + + assert { + condition = resource.coder_app.copyparty.open_in == "tab" + error_message = "Custom open_in value was not applied" + } + + # --- Asserts for coder_script "copyparty" --- + assert { + condition = strcontains(coder_script.copyparty.script, "PORT=\"8080\"") + error_message = "Script content does not reflect custom port" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "PINNED_VERSION=\"v1.2.3\"") + error_message = "Script content does not reflect custom pinned_version" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"--verbose,-v\"") + error_message = "Script content does not reflect custom arguments" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/var/log/custom.log\"") + error_message = "Script content does not reflect custom log_path" + } +} + +# --- Test Case 5: Validation Failure (open_in) --- +run "test_invalid_open_in" { + # This is a 'plan' test that expects a failure + command = plan + + variables { + agent_id = "example-agent-id" + open_in = "invalid-value" + } + + # Expect this plan to fail due to the validation rule in 'var.open_in' + expect_failures = [ + var.open_in, + ] +} + +# --- Test Case 6: Validation Failure (share) --- +run "test_invalid_share" { + # This is a 'plan' test that expects a failure + command = plan + + variables { + agent_id = "example-agent-id" + share = "everyone" # This is not 'owner', 'authenticated', or 'public' + } + + # Expect this plan to fail due to the validation rule in 'var.share' + expect_failures = [ + var.share, + ] +} diff --git a/registry/djarbz/modules/copyparty/main.tf b/registry/djarbz/modules/copyparty/main.tf new file mode 100644 index 00000000..822387c1 --- /dev/null +++ b/registry/djarbz/modules/copyparty/main.tf @@ -0,0 +1,174 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +locals { + # A built-in icon like "/icon/code.svg" or a full URL of icon + icon_url = "/icon/copyparty.svg" + # a map of all possible values + # options = { + # "Option 1" = { + # "name" = "Option 1", + # "value" = "1" + # "icon" = "/emojis/1.png" + # } + # "Option 2" = { + # "name" = "Option 2", + # "value" = "2" + # "icon" = "/emojis/2.png" + # } + # } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "log_path" { + type = string + description = "The path to log copyparty to." + default = "/tmp/copyparty.log" +} + +variable "port" { + type = number + description = "ports to listen on (comma/range); ignored for unix-sockets (default: 3923)" + default = 3923 +} + +variable "slug" { + type = string + description = "The slug for the copyparty application." + default = "copyparty" +} + +variable "display_name" { + type = string + description = "The display name for the copyparty application." + default = "copyparty" +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "open_in" { + type = string + description = <<-EOT + Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`. + `"tab"` opens in a new tab in the same browser window. + `"slim-window"` opens a new browser window without navigation controls. + EOT + default = "slim-window" + validation { + condition = contains(["tab", "slim-window"], var.open_in) + error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'." + } +} + +variable "subdomain" { + type = bool + description = <<-EOT + Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. + If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. + EOT + default = false +} + +variable "share" { + type = string + default = "owner" + validation { + condition = var.share == "owner" || var.share == "authenticated" || var.share == "public" + error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'." + } +} + +# variable "mutable" { +# type = bool +# description = "Whether the parameter is mutable." +# default = true +# } + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} +# Add other variables here + +variable "pinned_version" { + type = string + description = "Install a specific version in semver format (v1.19.16)." + default = "" +} + +variable "arguments" { + type = list(string) + description = "A list of arguments to pass to the application." + default = [] +} + + +resource "coder_script" "copyparty" { + agent_id = var.agent_id + display_name = "copyparty" + icon = local.icon_url + script = templatefile("${path.module}/run.sh", { + LOG_PATH : var.log_path, + PORT : var.port, + PINNED_VERSION : var.pinned_version, + ARGUMENTS : join(",", var.arguments), + }) + run_on_start = true + run_on_stop = false +} + +resource "coder_app" "copyparty" { + agent_id = var.agent_id + slug = var.slug + display_name = var.display_name + url = "http://localhost:${var.port}" + icon = local.icon_url + subdomain = var.subdomain + share = var.share + order = var.order + group = var.group + open_in = var.open_in + + # Remove if the app does not have a healthcheck endpoint + healthcheck { + url = "http://localhost:${var.port}" + interval = 5 + threshold = 6 + } +} + +# data "coder_parameter" "copyparty" { +# type = "list(string)" +# name = "copyparty" +# display_name = "copyparty" +# icon = local.icon_url +# mutable = var.mutable +# default = local.options["Option 1"]["value"] + +# dynamic "option" { +# for_each = local.options +# content { +# icon = option.value.icon +# name = option.value.name +# value = option.value.value +# } +# } +# } diff --git a/registry/djarbz/modules/copyparty/run.sh b/registry/djarbz/modules/copyparty/run.sh new file mode 100755 index 00000000..a138f540 --- /dev/null +++ b/registry/djarbz/modules/copyparty/run.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Convert templated variables to shell variables +# This variable is assigned to itself, so the assignment does nothing. +# shellcheck disable=SC2269 +LOG_PATH="${LOG_PATH}" + +# Ports to listen on (comma/range); ignored for unix-sockets (default: 3923) +PORT="${PORT}" +# Pinned version (e.g., v1.19.16); overrides latest release discovery if set +PINNED_VERSION="${PINNED_VERSION}" +# Custom CLI Arguments# The variable from Terraform is a single, comma-separated string. +# We need to split it into a proper bash array using the comma (,) as the delimiter. +IFS=',' read -r -a ARGUMENTS <<< "${ARGUMENTS}" + +# VARIABLE appears unused. Verify use (or export if used externally). +# shellcheck disable=SC2034 +MODULE_NAME="Copyparty" + +# VARIABLE appears unused. Verify use (or export if used externally). +# shellcheck disable=SC2034 +BOLD='\033[0;1m' + +printf '%sInstalling %s ...\n\n' "$${BOLD}" "$${MODULE_NAME}" + +# Add code here +# Use variables from the templatefile function in main.tf +# e.g. LOG_PATH, PORT, etc. + +printf "🐍 Verifying Python 3 installation...\n" +if ! command -v python3 &> /dev/null; then + printf "❌ Python3 could not be found. Please install it to continue.\n" + exit 1 +fi +printf "✅ Python3 is installed.\n\n" + +RELEASE_TO_INSTALL="" +# Install provided version to pin, otherwise discover latest github release from `https://github.com/9001/copyparty`. +if [[ -n "$${PINNED_VERSION}" ]]; then + printf "📌 Pinned version specified: %s\n" "$${PINNED_VERSION}" + # Verify that it is in v#.#.# format + if [[ ! "$${PINNED_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + printf "❌ Invalid format for PINNED_VERSION. Expected 'v#.#.#' (e.g., v1.19.16).\n" + exit 1 + fi + RELEASE_TO_INSTALL="$${PINNED_VERSION}" + printf "✅ Using pinned version %s.\n\n" "$${RELEASE_TO_INSTALL}" +else + printf "🔎 Discovering latest release from GitHub...\n" + # Use curl to get the latest release tag from the GitHub API and sed to parse it + LATEST_RELEASE=$(curl -fsSL https://api.github.com/repos/9001/copyparty/releases/latest | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\1/') + if [[ -z "$${LATEST_RELEASE}" ]]; then + printf "❌ Could not determine the latest release. Please check your internet connection.\n" + exit 1 + fi + RELEASE_TO_INSTALL="$${LATEST_RELEASE}" + printf "🏷️ Latest release is %s.\n\n" "$${RELEASE_TO_INSTALL}" +fi + +# Download appropriate release version assets: `copyparty-sfx.py` and `helptext.html`. +printf "🚀 Downloading copyparty v%s...\n" "$${RELEASE_TO_INSTALL}" +DOWNLOAD_URL="https://github.com/9001/copyparty/releases/download/$${RELEASE_TO_INSTALL}" + +printf "⏬ Downloading copyparty-sfx.py...\n" +if ! curl -fsSL -o /tmp/copyparty-sfx.py "$${DOWNLOAD_URL}/copyparty-sfx.py"; then + printf "❌ Failed to download copyparty-sfx.py.\n" + exit 1 +fi + +printf "⏬ Downloading helptext.html...\n" +if ! curl -fsSL -o /tmp/helptext.html "$${DOWNLOAD_URL}/helptext.html"; then + # This is not a fatal error, just a warning. + printf "⚠️ Could not download helptext.html. The application will still work.\n" +fi + +chmod +x /tmp/copyparty-sfx.py +printf "✅ Download complete.\n\n" + +printf "🥳 Installation complete!\n\n" + +# Build a clean, quoted string of the command for logging purposes only. +log_command="python3 /tmp/copyparty-sfx.py -p '$${PORT}'" +for arg in "$${ARGUMENTS[@]}"; do + # printf "DEBUG: ARG [$${arg}]\n" + log_command+=" '$${arg}'" +done + +# Clear the log file and write the header and command string using printf. +{ + printf "=== Starting copyparty at %s ===\n" "$(date)" + printf "EXECUTING: %s\n" "$${log_command}" +} > "$${LOG_PATH}" + +printf "👷 Starting %s in background...\n\n" "$${MODULE_NAME}" + +# Execute the actual command using the robust array expansion. +# Then, append its output (stdout and stderr) to the log file. +python3 /tmp/copyparty-sfx.py -p "$${PORT}" "$${ARGUMENTS[@]}" >> "$${LOG_PATH}" 2>&1 & + +printf "✅ Service started. Check logs at %s\n\n" "$${LOG_PATH}" From 30123e7ea3891c6b3a0a013f413719e1695ba6c8 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 23 Oct 2025 14:18:30 -0400 Subject: [PATCH 10/22] feat: add boundary pprof server in claude-code module (#503) --- registry/coder/modules/claude-code/README.md | 14 +++++++------- registry/coder/modules/claude-code/main.tf | 14 ++++++++++++++ .../coder/modules/claude-code/scripts/start.sh | 8 ++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index d3cee145..1c17fab8 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 = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.2.1" + version = "3.3.0" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,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 = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 20a0cfee..926b2402 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -228,6 +228,18 @@ variable "boundary_proxy_port" { default = "8087" } +variable "enable_boundary_pprof" { + type = bool + description = "Whether to enable coder boundary pprof server" + default = false +} + +variable "boundary_pprof_port" { + type = string + description = "Port for pprof server used by Boundary" + default = "6067" +} + resource "coder_env" "claude_code_md_path" { count = var.claude_md_path == "" ? 0 : 1 @@ -343,6 +355,8 @@ module "agentapi" { ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \ ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \ ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \ + ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \ + ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \ ARG_CODER_HOST='${local.coder_host}' \ /tmp/start.sh EOT diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index fb3180af..3ac840bd 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -22,6 +22,8 @@ ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"} ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"} ARG_BOUNDARY_LOG_LEVEL=${ARG_BOUNDARY_LOG_LEVEL:-"WARN"} ARG_BOUNDARY_PROXY_PORT=${ARG_BOUNDARY_PROXY_PORT:-"8087"} +ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false} +ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"} ARG_CODER_HOST=${ARG_CODER_HOST:-} echo "--------------------------------" @@ -155,6 +157,12 @@ function start_agentapi() { # Set log level for boundary BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL) + if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then + # Enable boundary pprof server on specified port + BOUNDARY_ARGS+=(--pprof) + BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT}) + fi + # Remove --dangerously-skip-permissions from ARGS when using boundary (it doesn't work with elevated permissions) # Create a new array without the dangerous permissions flag CLAUDE_ARGS=() From e3ff43c0a64482e909109bc1d598b6bf00a40ac1 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 24 Oct 2025 11:54:12 +0100 Subject: [PATCH 11/22] refactor(coder/agentapi): support terraform-provider-coder v2.12.0 (#485) In terraform-provider-coder v2.12.0 and the up-coming coder v2.28 release we have removed the requirement for the "AI Prompt" parameter, and are intending on slightly re-designing the API of the AI task modules. Instead of `agentapi` defining the `coder_ai_task` resource, it will output the `task_app_id`. Consumers of the module will then be expected to create the `coder_ai_task` resource themselves with this `task_app_id`. --- registry/coder/modules/agentapi/README.md | 2 +- registry/coder/modules/agentapi/main.tf | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md index d68af511..954db1ce 100644 --- a/registry/coder/modules/agentapi/README.md +++ b/registry/coder/modules/agentapi/README.md @@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI ```tf module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id web_app_slug = local.app_slug diff --git a/registry/coder/modules/agentapi/main.tf b/registry/coder/modules/agentapi/main.tf index e73f45f6..5c3ab9c4 100644 --- a/registry/coder/modules/agentapi/main.tf +++ b/registry/coder/modules/agentapi/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -239,8 +239,6 @@ resource "coder_app" "agentapi_cli" { group = var.cli_app_group } -resource "coder_ai_task" "agentapi" { - sidebar_app { - id = coder_app.agentapi_web.id - } +output "task_app_id" { + value = coder_app.agentapi_web.id } From bc39c2ee293bd2cb45dcba7ffbba0d50427a6242 Mon Sep 17 00:00:00 2001 From: Harsh Singh Panwar Date: Fri, 24 Oct 2025 20:55:40 +0530 Subject: [PATCH 12/22] Aider module support agentAPI (#356) Closes #239 /claim #239 ## Description video :- https://www.loom.com/share/d1d1d54d48bc45c4a48271ca9a387a88?sid=933e250d-78f8-4a7f-9745-0e908c0ee4d9 ## Type of Change - [x] New module - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/aider` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun run fmt`) - [ ] Changes tested locally ## Related Issues --------- Co-authored-by: DevCats --- registry/coder/modules/aider/README.md | 282 ++------- registry/coder/modules/aider/main.test.ts | 197 +++--- registry/coder/modules/aider/main.tf | 559 ++++++------------ registry/coder/modules/aider/main.tftest.hcl | 149 +++++ .../coder/modules/aider/scripts/install.sh | 49 ++ registry/coder/modules/aider/scripts/start.sh | 55 ++ .../modules/aider/testdata/aider-mock.sh | 14 + 7 files changed, 600 insertions(+), 705 deletions(-) create mode 100644 registry/coder/modules/aider/main.tftest.hcl create mode 100644 registry/coder/modules/aider/scripts/install.sh create mode 100644 registry/coder/modules/aider/scripts/start.sh create mode 100644 registry/coder/modules/aider/testdata/aider-mock.sh diff --git a/registry/coder/modules/aider/README.md b/registry/coder/modules/aider/README.md index c93fb89c..3ce9c88e 100644 --- a/registry/coder/modules/aider/README.md +++ b/registry/coder/modules/aider/README.md @@ -8,76 +8,58 @@ tags: [agent, ai, aider] # Aider -Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux. +Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider with AgentAPI for seamless Coder Tasks Support. ```tf -module "aider" { - source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" - agent_id = coder_agent.example.id -} -``` - -## Features - -- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace -- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter -- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background -- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding -- **Project Integration**: Works with any project directory, including Git repositories -- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal -- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable - -## Module Parameters - -> [!NOTE] -> The `use_screen` and `use_tmux` parameters cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`. - -## Usage Examples - -### Basic setup with API key - -```tf -variable "anthropic_api_key" { +variable "api_key" { type = string - description = "Anthropic API key" + description = "API key" sensitive = true } module "aider" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" - agent_id = coder_agent.example.id - ai_api_key = var.anthropic_api_key -} -``` - -This basic setup will: - -- Install Aider in the workspace -- Create a persistent screen session named "aider" -- Configure Aider to use Anthropic Claude 3.7 Sonnet model -- Enable task reporting (configures Aider to report tasks to Coder MCP) - -### Using OpenAI with tmux - -```tf -variable "openai_api_key" { - type = string - description = "OpenAI API key" - sensitive = true -} - -module "aider" { - count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" + version = "2.0.0" agent_id = coder_agent.example.id - use_tmux = true - ai_provider = "openai" - ai_model = "4o" # Uses Aider's built-in alias for gpt-4o - ai_api_key = var.openai_api_key + api_key = var.api_key + ai_provider = "google" + model = "gemini" +} +``` + +## Prerequisites + +- pipx is automatically installed if not already available + +## Usage Example + +```tf +data "coder_parameter" "ai_prompt" { + name = "AI Prompt" + description = "Write an initial prompt for Aider to work on." + type = "string" + default = "" + mutable = true +} + +variable "gemini_api_key" { + type = string + description = "Gemini API key" + sensitive = true +} + +module "aider" { + source = "registry.coder.com/coder/aider/coder" + version = "2.0.0" + agent_id = coder_agent.example.id + api_key = var.gemini_api_key + install_aider = true + workdir = "/home/coder" + ai_provider = "google" + model = "gemini" + install_agentapi = true + ai_prompt = data.coder_parameter.ai_prompt.value + system_prompt = "..." } ``` @@ -93,174 +75,16 @@ variable "custom_api_key" { module "aider" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" + version = "2.0.0" agent_id = coder_agent.example.id + workdir = "/home/coder" ai_provider = "custom" custom_env_var_name = "MY_CUSTOM_API_KEY" - ai_model = "custom-model" - ai_api_key = var.custom_api_key + model = "custom-model" + api_key = var.custom_api_key } ``` -### Adding Custom Extensions (Experimental) - -You can extend Aider's capabilities by adding custom extensions: - -```tf -module "aider" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" - agent_id = coder_agent.example.id - ai_api_key = var.anthropic_api_key - - experiment_pre_install_script = <<-EOT - pip install some-custom-dependency - EOT - - experiment_additional_extensions = <<-EOT - custom-extension: - args: [] - cmd: custom-extension-command - description: A custom extension for Aider - enabled: true - envs: {} - name: custom-extension - timeout: 300 - type: stdio - EOT -} -``` - -Note: The indentation in the heredoc is preserved, so you can write the YAML naturally. - -## Task Reporting (Experimental) - -> This functionality is in early access as of Coder v2.21 and is still evolving. -> For now, we recommend testing it in a demo or staging environment, -> rather than deploying to production -> -> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents) -> -> Join our [Discord channel](https://discord.gg/coder) or -> [contact us](https://coder.com/contact) to get help or share feedback. - -Your workspace must have either `screen` or `tmux` installed to use this. - -Task reporting is **enabled by default** in this module, allowing you to: - -- Send an initial prompt to Aider during workspace creation -- Monitor task progress in the Coder UI -- Use the `coder_parameter` resource to collect prompts from users - -### Setting up Task Reporting - -To use task reporting effectively: - -1. Add the Coder Login module to your template -2. Configure the necessary variables to pass the task prompt -3. Optionally add a coder_parameter to collect prompts from users - -Here's a complete example: - -```tf -module "coder-login" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/modules/coder-login/coder" - version = "1.0.15" - agent_id = coder_agent.example.id -} - -variable "anthropic_api_key" { - type = string - description = "Anthropic API key" - sensitive = true -} - -data "coder_parameter" "ai_prompt" { - type = "string" - name = "AI Prompt" - default = "" - description = "Write a prompt for Aider" - mutable = true - ephemeral = true -} - -module "aider" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" - agent_id = coder_agent.example.id - ai_api_key = var.anthropic_api_key - task_prompt = data.coder_parameter.ai_prompt.value - - # Optionally customize the system prompt - system_prompt = <<-EOT -You are a helpful Coding assistant. Aim to autonomously investigate -and solve issues the user gives you and test your work, whenever possible. -Avoid shortcuts like mocking tests. When you get stuck, you can ask the user -but opt for autonomy. -YOU MUST REPORT ALL TASKS TO CODER. -When reporting tasks, you MUST follow these EXACT instructions: -- IMMEDIATELY report status after receiving ANY user message. -- Be granular. If you are investigating with multiple steps, report each step to coder. -Task state MUST be one of the following: -- Use "state": "working" when actively processing WITHOUT needing additional user input. -- Use "state": "complete" only when finished with a task. -- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers. -Task summaries MUST: -- Include specifics about what you're doing. -- Include clear and actionable steps for the user. -- Be less than 160 characters in length. - EOT -} -``` - -When a task prompt is provided via the `task_prompt` variable, the module automatically: - -1. Combines the system prompt with the task prompt into a single message in the format: - -``` -SYSTEM PROMPT: -[system_prompt content] - -This is your current task: [task_prompt] -``` - -2. Executes the task during workspace creation using the `--message` and `--yes-always` flags -3. Logs task output to `$HOME/.aider.log` for reference - -If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration. - -## Using Aider in Your Workspace - -After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation. - -### Session Options - -You can run Aider in three different ways: - -1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button - -- Simple setup without persistent context -- Suitable for quick coding sessions - -2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections - -- Session name: "aider" (or configured via `session_name`) - -3. **Tmux Mode**: Run Aider in a tmux session instead of screen - -- Set `use_tmux = true` to enable -- Session name: "aider" (or configured via `session_name`) -- Configures tmux with mouse support for shared sessions - -Persistent sessions (screen/tmux) allow you to: - -- Disconnect and reconnect without losing context -- Run Aider in the background while doing other work -- Switch between terminal and browser interfaces - ### Available AI Providers and Models Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases: @@ -280,10 +104,12 @@ For a complete and up-to-date list of supported aliases and models, please refer ## Troubleshooting -If you encounter issues: +- If `aider` is not found, ensure `install_aider = true` and your API key is valid +- Logs are written under `/home/coder/.aider-module/` (`install.log`, `agentapi-start.log`) for debugging +- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts -1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions` -2. **API key issues**: Ensure you've entered the correct API key for your selected provider -3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace +## References -For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/). +- [Aider Documentation](https://aider.chat/docs) +- [AgentAPI Documentation](https://github.com/coder/agentapi) +- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) diff --git a/registry/coder/modules/aider/main.test.ts b/registry/coder/modules/aider/main.test.ts index c25513a5..c0aa51df 100644 --- a/registry/coder/modules/aider/main.test.ts +++ b/registry/coder/modules/aider/main.test.ts @@ -1,107 +1,138 @@ -import { describe, expect, it } from "bun:test"; import { - findResourceInstance, - runTerraformApply, - runTerraformInit, - testRequiredVariables, -} from "~test"; + test, + afterEach, + describe, + setDefaultTimeout, + beforeAll, + expect, +} from "bun:test"; +import { execContainer, readFileContainer, runTerraformInit } from "~test"; +import { + loadTestFile, + writeExecutable, + setup as setupUtil, + execModuleScript, + expectAgentAPIStarted, +} from "../../../coder/modules/agentapi/test-util"; -describe("aider", async () => { - await runTerraformInit(import.meta.dir); +let cleanupFunctions: (() => Promise)[] = []; +const registerCleanup = (cleanup: () => Promise) => { + cleanupFunctions.push(cleanup); +}; +afterEach(async () => { + const cleanupFnsCopy = cleanupFunctions.slice().reverse(); + cleanupFunctions = []; + for (const cleanup of cleanupFnsCopy) { + try { + await cleanup(); + } catch (error) { + console.error("Error during cleanup:", error); + } + } +}); - testRequiredVariables(import.meta.dir, { - agent_id: "foo", +interface SetupProps { + skipAgentAPIMock?: boolean; + skipAiderMock?: boolean; + moduleVariables?: Record; + agentapiMockScript?: string; +} + +const setup = async (props?: SetupProps): Promise<{ id: string }> => { + const projectDir = "/home/coder/project"; + const { id } = await setupUtil({ + moduleDir: import.meta.dir, + moduleVariables: { + install_aider: props?.skipAiderMock ? "true" : "false", + install_agentapi: props?.skipAgentAPIMock ? "true" : "false", + aider_model: "test-model", + ...props?.moduleVariables, + }, + registerCleanup, + projectDir, + skipAgentAPIMock: props?.skipAgentAPIMock, + agentapiMockScript: props?.agentapiMockScript, }); - it("configures task prompt correctly", async () => { - const testPrompt = "Add a hello world function"; - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - task_prompt: testPrompt, + // Place the Aider mock CLI binary inside the container + if (!props?.skipAiderMock) { + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/aider", + content: await loadTestFile(`${import.meta.dir}`, "aider-mock.sh"), }); + } - const instance = findResourceInstance(state, "coder_script"); - expect(instance.script).toContain( - `This is your current task: ${testPrompt}`, - ); - expect(instance.script).toContain("aider --architect --yes-always"); + return { id }; +}; + +setDefaultTimeout(60 * 1000); + +describe("Aider", async () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); }); - it("handles custom system prompt", async () => { - const customPrompt = "Report all tasks with state: working"; - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - system_prompt: customPrompt, + test("happy-path", async () => { + const { id } = await setup({ + moduleVariables: { + model: "gemini", + }, }); - - const instance = findResourceInstance(state, "coder_script"); - expect(instance.script).toContain(customPrompt); + await execModuleScript(id); + await expectAgentAPIStarted(id); }); - it("handles pre and post install scripts", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - experiment_pre_install_script: "echo 'Pre-install script executed'", - experiment_post_install_script: "echo 'Post-install script executed'", + test("api-key", async () => { + const apiKey = "test-api-key-123"; + const { id } = await setup({ + moduleVariables: { + api_key: apiKey, + model: "gemini", + }, }); - - const instance = findResourceInstance(state, "coder_script"); - - expect(instance.script).toContain("Running pre-install script"); - expect(instance.script).toContain("Running post-install script"); - expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh"); - expect(instance.script).toContain("base64 -d > /tmp/post_install.sh"); + await execModuleScript(id); + const resp = await readFileContainer( + id, + "/home/coder/.aider-module/agentapi-start.log", + ); + expect(resp).toContain("API key provided!"); }); - it("validates that use_screen and use_tmux cannot both be true", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - use_screen: true, - use_tmux: true, + test("custom-folder", async () => { + const workdir = "/tmp/aider-test"; + const { id } = await setup({ + moduleVariables: { + workdir, + model: "gemini", + }, }); - - const instance = findResourceInstance(state, "coder_script"); - - expect(instance.script).toContain( - "Error: Both use_screen and use_tmux cannot be enabled at the same time", + await execModuleScript(id); + const resp = await readFileContainer( + id, + "/home/coder/.aider-module/install.log", ); - expect(instance.script).toContain("exit 1"); + expect(resp).toContain(workdir); }); - it("configures Aider with known provider and model", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - ai_provider: "anthropic", - ai_model: "sonnet", - ai_api_key: "test-anthropic-key", + test("pre-post-install-scripts", async () => { + const { id } = await setup({ + moduleVariables: { + pre_install_script: "#!/bin/bash\necho 'pre-install-script'", + post_install_script: "#!/bin/bash\necho 'post-install-script'", + model: "gemini", + }, }); - - const instance = findResourceInstance(state, "coder_script"); - expect(instance.script).toContain( - 'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"', + await execModuleScript(id); + const preLog = await readFileContainer( + id, + "/home/coder/.aider-module/pre_install.log", ); - expect(instance.script).toContain("--model sonnet"); - expect(instance.script).toContain( - "Starting Aider using anthropic provider and model: sonnet", - ); - }); - - it("handles custom provider with custom env var and API key", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - ai_provider: "custom", - custom_env_var_name: "MY_CUSTOM_API_KEY", - ai_model: "custom-model", - ai_api_key: "test-custom-key", - }); - - const instance = findResourceInstance(state, "coder_script"); - expect(instance.script).toContain( - 'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"', - ); - expect(instance.script).toContain("--model custom-model"); - expect(instance.script).toContain( - "Starting Aider using custom provider and model: custom-model", + expect(preLog).toContain("pre-install-script"); + const postLog = await readFileContainer( + id, + "/home/coder/.aider-module/post_install.log", ); + expect(postLog).toContain("post-install-script"); }); }); diff --git a/registry/coder/modules/aider/main.tf b/registry/coder/modules/aider/main.tf index e1f2eccd..70274cb8 100644 --- a/registry/coder/modules/aider/main.tf +++ b/registry/coder/modules/aider/main.tf @@ -36,87 +36,84 @@ variable "icon" { default = "/icon/aider.svg" } -variable "folder" { +variable "workdir" { type = string description = "The folder to run Aider in." default = "/home/coder" } +variable "report_tasks" { + type = bool + description = "Whether to enable task reporting to Coder UI via AgentAPI" + default = false +} + +variable "subdomain" { + type = bool + description = "Whether to use a subdomain for AgentAPI." + default = false +} + +variable "cli_app" { + type = bool + description = "Whether to create a CLI app for Aider" + default = false +} + +variable "web_app_display_name" { + type = string + description = "Display name for the web app" + default = "Aider" +} + +variable "cli_app_display_name" { + type = string + description = "Display name for the CLI app" + default = "Aider CLI" +} + +variable "pre_install_script" { + type = string + description = "Custom script to run before installing Aider." + default = null +} + +variable "post_install_script" { + type = string + description = "Custom script to run after installing Aider." + default = null +} + +variable "install_agentapi" { + type = bool + description = "Whether to install AgentAPI." + default = true +} + +variable "agentapi_version" { + type = string + description = "The version of AgentAPI to install." + default = "v0.10.0" +} + +variable "ai_prompt" { + type = string + description = "Initial task prompt for Aider." + default = "" +} + +# --------------------------------------------- + variable "install_aider" { type = bool description = "Whether to install Aider." default = true } -variable "aider_version" { - type = string - description = "The version of Aider to install." - default = "latest" -} - -variable "use_screen" { - type = bool - description = "Whether to use screen for running Aider in the background" - default = true -} - -variable "use_tmux" { - type = bool - description = "Whether to use tmux instead of screen for running Aider in the background" - default = false -} - -variable "session_name" { - type = string - description = "Name for the persistent session (screen or tmux)" - default = "aider" -} - -variable "experiment_report_tasks" { - type = bool - description = "Whether to enable task reporting." - default = true -} - variable "system_prompt" { type = string description = "System prompt for instructing Aider on task reporting and behavior" - default = <<-EOT -You are a helpful Coding assistant. Aim to autonomously investigate -and solve issues the user gives you and test your work, whenever possible. -Avoid shortcuts like mocking tests. When you get stuck, you can ask the user -but opt for autonomy. -YOU MUST REPORT ALL TASKS TO CODER. -When reporting tasks, you MUST follow these EXACT instructions: -- IMMEDIATELY report status after receiving ANY user message. -- Be granular. If you are investigating with multiple steps, report each step to coder. -Task state MUST be one of the following: -- Use "state": "working" when actively processing WITHOUT needing additional user input. -- Use "state": "complete" only when finished with a task. -- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers. -Task summaries MUST: -- Include specifics about what you're doing. -- Include clear and actionable steps for the user. -- Be less than 160 characters in length. -EOT -} - -variable "task_prompt" { - type = string - description = "Task prompt to use with Aider" - default = "" -} - -variable "experiment_pre_install_script" { - type = string - description = "Custom script to run before installing Aider." - default = null -} - -variable "experiment_post_install_script" { - type = string - description = "Custom script to run after installing Aider." - default = null + default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently." } variable "experiment_additional_extensions" { @@ -128,20 +125,19 @@ variable "experiment_additional_extensions" { variable "ai_provider" { type = string description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)" - default = "anthropic" + default = "google" validation { condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider) - error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom" + error_message = "provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom" } } -variable "ai_model" { +variable "model" { type = string description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc." - default = "sonnet" } -variable "ai_api_key" { +variable "api_key" { type = string description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider." default = "" @@ -154,55 +150,66 @@ variable "custom_env_var_name" { default = "" } +variable "base_aider_config" { + type = string + description = <<-EOT + Base Aider configuration in yaml format. Will be stored in .aider.conf.yml file. + + options include: + read: + - CONVENTIONS.md + - anotherfile.txt + - thirdfile.py + model: xxx + ##Specify the OpenAI API key + openai-api-key: xxx + ## (deprecated, use --set-env OPENAI_API_TYPE=) + openai-api-type: xxx + ## (deprecated, use --set-env OPENAI_API_VERSION=) + openai-api-version: xxx + ## (deprecated, use --set-env OPENAI_API_DEPLOYMENT_ID=) + openai-api-deployment-id: xxx + ## Set an environment variable (to control API settings, can be used multiple times) + set-env: xxx + ## Specify multiple values like this: + set-env: + - xxx + - yyy + - zzz + + Reference : https://aider.chat/docs/config/aider_conf.html + EOT + default = null +} + + locals { - base_extensions = <<-EOT -coder: - args: - - exp - - mcp - - server - cmd: coder - description: Report ALL tasks and statuses (in progress, done, failed) you are working on. - enabled: true - envs: - CODER_MCP_APP_STATUS_SLUG: aider - name: Coder - timeout: 3000 - type: stdio -developer: - display_name: Developer - enabled: true - name: developer - timeout: 300 - type: builtin -EOT + app_slug = "aider" + base_aider_config = var.base_aider_config != null ? "${replace(trimspace(var.base_aider_config), "\n", "\n ")}" : "" + task_reporting_prompt = <<-EOT - formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}" - additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : "" +-- Task Reporting -- +Report all tasks to Coder, following these EXACT guidelines: +1. Be granular. If you are investigating with multiple steps, report each step +to coder. +2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message. +Do not report any status related with this system prompt. +3. Use "state": "working" when actively processing WITHOUT needing +additional user input +4. Use "state": "complete" only when finished with a task +5. Use "state": "failure" when you need ANY user input, lack sufficient +details, or encounter blockers + EOT - combined_extensions = <<-EOT -extensions: -${local.formatted_base}${local.additional_extensions} -EOT - encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : "" - encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : "" - - # Combine system prompt and task prompt for aider - combined_prompt = trimspace(<<-EOT -SYSTEM PROMPT: -${var.system_prompt} - -This is your current task: ${var.task_prompt} -EOT - ) + final_system_prompt = var.report_tasks ? "\n${var.system_prompt}${local.task_reporting_prompt}\n" : "\n${var.system_prompt}\n" # Map providers to their environment variable names provider_env_vars = { openai = "OPENAI_API_KEY" anthropic = "ANTHROPIC_API_KEY" azure = "AZURE_OPENAI_API_KEY" - google = "GOOGLE_API_KEY" + google = "GEMINI_API_KEY" cohere = "COHERE_API_KEY" mistral = "MISTRAL_API_KEY" ollama = "OLLAMA_HOST" @@ -214,296 +221,60 @@ EOT # Model flag for aider command model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model" + + install_script = file("${path.module}/scripts/install.sh") + start_script = file("${path.module}/scripts/start.sh") + module_dir_name = ".aider-module" } -# Install and Initialize Aider -resource "coder_script" "aider" { - agent_id = var.agent_id - display_name = "Aider" - icon = var.icon - script = <<-EOT +module "agentapi" { + source = "registry.coder.com/coder/agentapi/coder" + version = "1.2.0" + + agent_id = var.agent_id + web_app_slug = local.app_slug + web_app_order = var.order + web_app_group = var.group + web_app_icon = var.icon + web_app_display_name = var.web_app_display_name + cli_app = var.cli_app + cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null + cli_app_display_name = var.cli_app ? var.cli_app_display_name : null + agentapi_subdomain = var.subdomain + module_dir_name = local.module_dir_name + install_agentapi = var.install_agentapi + agentapi_version = var.agentapi_version + pre_install_script = var.pre_install_script + post_install_script = var.post_install_script + start_script = <<-EOT #!/bin/bash - set -e + set -o errexit + set -o pipefail - command_exists() { - command -v "$1" >/dev/null 2>&1 - } - - echo "Setting up Aider AI pair programming..." - - if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then - echo "Error: Both use_screen and use_tmux cannot be enabled at the same time." - exit 1 - fi - - mkdir -p "${var.folder}" - - if [ "$(uname)" = "Linux" ]; then - echo "Checking dependencies for Linux..." - - if [ "${var.use_tmux}" = "true" ]; then - if ! command_exists tmux; then - echo "Installing tmux for persistent sessions..." - if command -v apt-get >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y -qq tmux - else - apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges" - apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges" - fi - elif command -v dnf >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo dnf install -y -q tmux - else - dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges" - fi - else - echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found." - fi - else - echo "tmux is already installed, skipping installation." - fi - elif [ "${var.use_screen}" = "true" ]; then - if ! command_exists screen; then - echo "Installing screen for persistent sessions..." - if command -v apt-get >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y -qq screen - else - apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges" - apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges" - fi - elif command -v dnf >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo dnf install -y -q screen - else - dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges" - fi - else - echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found." - fi - else - echo "screen is already installed, skipping installation." - fi - fi - else - echo "This module currently only supports Linux workspaces." - exit 1 - fi - - if [ -n "${local.encoded_pre_install_script}" ]; then - echo "Running pre-install script..." - echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh - chmod +x /tmp/pre_install.sh - /tmp/pre_install.sh - fi - - if [ "${var.install_aider}" = "true" ]; then - echo "Installing Aider..." - - if ! command_exists python3 || ! command_exists pip3; then - echo "Installing Python dependencies required for Aider..." - if command -v apt-get >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y -qq python3-pip python3-venv - else - apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges" - apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges" - fi - elif command -v dnf >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo dnf install -y -q python3-pip python3-virtualenv - else - dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges" - fi - else - echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found." - fi - else - echo "Python is already installed, skipping installation." - fi - - if ! command_exists aider; then - curl -LsSf https://aider.chat/install.sh | sh - fi - - if [ -f "$HOME/.bashrc" ]; then - if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then - echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc" - fi - fi - - if [ -f "$HOME/.zshrc" ]; then - if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then - echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc" - fi - fi - - fi - - if [ -n "${local.encoded_post_install_script}" ]; then - echo "Running post-install script..." - echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh - chmod +x /tmp/post_install.sh - /tmp/post_install.sh - fi - - if [ "${var.experiment_report_tasks}" = "true" ]; then - echo "Configuring Aider to report tasks via Coder MCP..." - - mkdir -p "$HOME/.config/aider" - - cat > "$HOME/.config/aider/config.yml" << EOL -${trimspace(local.combined_extensions)} -EOL - echo "Added Coder MCP extension to Aider config.yml" - fi - - echo "Starting persistent Aider session..." - - touch "$HOME/.aider.log" - - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 - - export PATH="$HOME/bin:$PATH" - - if [ "${var.use_tmux}" = "true" ]; then - if [ -n "${var.task_prompt}" ]; then - echo "Running Aider with message in tmux session..." - - # Configure tmux for shared sessions - if [ ! -f "$HOME/.tmux.conf" ]; then - echo "Creating ~/.tmux.conf with shared session settings..." - echo "set -g mouse on" > "$HOME/.tmux.conf" - fi - - if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then - echo "Adding 'set -g mouse on' to ~/.tmux.conf..." - echo "set -g mouse on" >> "$HOME/.tmux.conf" - fi - - echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}" - tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"" - echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress." - else - # Configure tmux for shared sessions - if [ ! -f "$HOME/.tmux.conf" ]; then - echo "Creating ~/.tmux.conf with shared session settings..." - echo "set -g mouse on" > "$HOME/.tmux.conf" - fi - - if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then - echo "Adding 'set -g mouse on' to ~/.tmux.conf..." - echo "set -g mouse on" >> "$HOME/.tmux.conf" - fi - - echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}" - tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\"" - echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button." - fi - else - if [ -n "${var.task_prompt}" ]; then - echo "Running Aider with message in screen session..." - - if [ ! -f "$HOME/.screenrc" ]; then - echo "Creating ~/.screenrc and adding multiuser settings..." - echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" - fi - - if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then - echo "Adding 'multiuser on' to ~/.screenrc..." - echo "multiuser on" >> "$HOME/.screenrc" - fi - - if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then - echo "Adding 'acladd $(whoami)' to ~/.screenrc..." - echo "acladd $(whoami)" >> "$HOME/.screenrc" - fi - - echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}" - screen -U -dmS ${var.session_name} bash -c " - cd ${var.folder} - export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\" - export ${local.env_var_name}=\"${var.ai_api_key}\" - aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\" - /bin/bash - " - - echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress." - else - - if [ ! -f "$HOME/.screenrc" ]; then - echo "Creating ~/.screenrc and adding multiuser settings..." - echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" - fi - - if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then - echo "Adding 'multiuser on' to ~/.screenrc..." - echo "multiuser on" >> "$HOME/.screenrc" - fi - - if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then - echo "Adding 'acladd $(whoami)' to ~/.screenrc..." - echo "acladd $(whoami)" >> "$HOME/.screenrc" - fi - - echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}" - screen -U -dmS ${var.session_name} bash -c " - cd ${var.folder} - export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\" - export ${local.env_var_name}=\"${var.ai_api_key}\" - aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\" - /bin/bash - " - echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button." - fi - fi - - echo "Aider setup complete!" + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + chmod +x /tmp/start.sh + ARG_WORKDIR='${var.workdir}' \ + ARG_API_KEY='${base64encode(var.api_key)}' \ + ARG_MODEL='${var.model}' \ + ARG_PROVIDER='${var.ai_provider}' \ + ARG_ENV_API_NAME_HOLDER='${local.env_var_name}' \ + ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \ + ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ + /tmp/start.sh EOT - run_on_start = true -} -# Aider CLI app -resource "coder_app" "aider_cli" { - agent_id = var.agent_id - slug = "aider" - display_name = "Aider" - icon = var.icon - command = <<-EOT + install_script = <<-EOT #!/bin/bash - set -e + set -o errexit + set -o pipefail - export PATH="$HOME/bin:$HOME/.local/bin:$PATH" - - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 - - if [ "${var.use_tmux}" = "true" ]; then - if tmux has-session -t ${var.session_name} 2>/dev/null; then - echo "Attaching to existing Aider tmux session..." - tmux attach-session -t ${var.session_name} - else - echo "Starting new Aider tmux session..." - tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash" - fi - elif [ "${var.use_screen}" = "true" ]; then - if ! screen -list | grep -q "${var.session_name}"; then - echo "Error: No existing Aider session found. Please wait for the script to start it." - exit 1 - fi - screen -xRR ${var.session_name} - else - cd "${var.folder}" - echo "Starting Aider directly..." - export ${local.env_var_name}="${var.ai_api_key}" - aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}" - fi + echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh + chmod +x /tmp/install.sh + ARG_WORKDIR='${var.workdir}' \ + ARG_INSTALL_AIDER='${var.install_aider}' \ + ARG_REPORT_TASKS='${var.report_tasks}' \ + ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.base_aider_config)}' | base64 -d)" \ + /tmp/install.sh EOT - order = var.order - group = var.group } + diff --git a/registry/coder/modules/aider/main.tftest.hcl b/registry/coder/modules/aider/main.tftest.hcl new file mode 100644 index 00000000..281bde86 --- /dev/null +++ b/registry/coder/modules/aider/main.tftest.hcl @@ -0,0 +1,149 @@ +run "test_aider_basic" { + command = plan + + variables { + agent_id = "test-agent-123" + workdir = "/home/coder" + model = "gemini" + } + + assert { + condition = var.workdir == "/home/coder" + error_message = "Workdir variable should default to /home/coder" + } + + assert { + condition = var.agent_id == "test-agent-123" + error_message = "Agent ID variable should be set correctly" + } + + assert { + condition = var.install_aider == true + error_message = "install_aider should default to true" + } + + assert { + condition = var.install_agentapi == true + error_message = "install_agentapi should default to true" + } + + assert { + condition = var.report_tasks == false + error_message = "report_tasks should default to false" + } +} + +run "test_with_api_key" { + command = plan + + variables { + agent_id = "test-agent-456" + workdir = "/home/coder/workspace" + api_key = "test-api-key-123" + model = "gemini" + } + + assert { + condition = var.api_key == "test-api-key-123" + error_message = "API key value should match the input" + } +} + +run "test_custom_options" { + command = plan + + variables { + agent_id = "test-agent-789" + workdir = "/home/coder/custom" + order = 5 + group = "development" + icon = "/icon/custom.svg" + model = "4o" + ai_prompt = "Help me write better code" + install_aider = false + install_agentapi = false + agentapi_version = "v0.10.0" + api_key = "" + base_aider_config = "read:\n - CONVENTIONS.md" + } + + assert { + condition = var.order == 5 + error_message = "Order variable should be set to 5" + } + + assert { + condition = var.group == "development" + error_message = "Group variable should be set to 'development'" + } + + assert { + condition = var.icon == "/icon/custom.svg" + error_message = "Icon variable should be set to custom icon" + } + + assert { + condition = var.model == "4o" + error_message = "Model variable should be set to '4o'" + } + + assert { + condition = var.ai_prompt == "Help me write better code" + error_message = "AI prompt variable should be set correctly" + } + + assert { + condition = var.install_aider == false + error_message = "install_aider should be set to false" + } + + assert { + condition = var.install_agentapi == false + error_message = "install_agentapi should be set to false" + } + + assert { + condition = var.agentapi_version == "v0.10.0" + error_message = "AgentAPI version should be set to 'v0.10.0'" + } +} + +run "test_with_scripts" { + command = plan + + variables { + agent_id = "test-agent-scripts" + workdir = "/home/coder/scripts" + model = "gemini" + pre_install_script = "echo 'Pre-install script'" + post_install_script = "echo 'Post-install script'" + } + + assert { + condition = var.pre_install_script == "echo 'Pre-install script'" + error_message = "Pre-install script should be set correctly" + } + + assert { + condition = var.post_install_script == "echo 'Post-install script'" + error_message = "Post-install script should be set correctly" + } +} + +run "test_ai_provider_env_mapping" { + command = plan + + variables { + agent_id = "test-agent-provider" + workdir = "/home/coder/test" + ai_provider = "google" + model = "gemini" + custom_env_var_name = "" + } + + # Ensure provider -> env var mapping works as expected (based on locals.provider_env_vars) + assert { + condition = var.ai_provider == "google" + error_message = "AI provider should be set to 'google' for this test" + } +} diff --git a/registry/coder/modules/aider/scripts/install.sh b/registry/coder/modules/aider/scripts/install.sh new file mode 100644 index 00000000..b2244aa0 --- /dev/null +++ b/registry/coder/modules/aider/scripts/install.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + +# Function to check if a command exists +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +# Inputs +ARG_WORKDIR=${ARG_WORKDIR:-/home/coder} +ARG_INSTALL_AIDER=${ARG_INSTALL_AIDER:-true} +ARG_AIDER_CONFIG=${ARG_AIDER_CONFIG:-} + +echo "--------------------------------" +echo "Install flag: $ARG_INSTALL_AIDER" +echo "Workspace: $ARG_WORKDIR" +echo "--------------------------------" + +function install_aider() { + echo "pipx installing..." + sudo apt-get install -y pipx + echo "pipx installed!" + pipx ensurepath + mkdir -p "$ARG_WORKDIR/.local/bin" + export PATH="$HOME/.local/bin:$ARG_WORKDIR/.local/bin:$PATH" + + if ! command_exists aider; then + echo "Installing Aider via pipx..." + pipx install --force aider-install + aider-install + fi + echo "Aider installed: $(aider --version || echo 'Aider installation check failed')" +} + +function configure_aider_settings() { + if [ -n "${ARG_AIDER_CONFIG}" ]; then + echo "Configuring Aider environment variables and model" + + mkdir -p "$HOME/.config/aider" + + echo "$ARG_AIDER_CONFIG" > "$HOME/.config/aider/.aider.conf.yml" + echo "Aider config created at $HOME/.config/aider/.aider.conf.yml" + else + printf "No Aider environment variables or model configured\n" + fi +} + +install_aider +configure_aider_settings diff --git a/registry/coder/modules/aider/scripts/start.sh b/registry/coder/modules/aider/scripts/start.sh new file mode 100644 index 00000000..1bd18ffa --- /dev/null +++ b/registry/coder/modules/aider/scripts/start.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -euo pipefail + +# Ensure pipx-installed apps are in PATH +export PATH="$HOME/.local/bin:$PATH" + +ARG_WORKDIR=${ARG_WORKDIR:-/home/coder} +ARG_API_KEY=$(echo -n "${ARG_API_KEY:-}" | base64 -d) +ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "") +ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "") +ARG_MODEL=${ARG_MODEL:-} +ARG_PROVIDER=${ARG_PROVIDER:-} +ARG_ENV_API_NAME_HOLDER=${ARG_ENV_API_NAME_HOLDER:-} + +echo "--------------------------------" +echo "Provider: $ARG_PROVIDER" +echo "Model: $ARG_MODEL" +echo "--------------------------------" + +if [ -n "$ARG_API_KEY" ]; then + printf "API key provided!\n" + export $ARG_ENV_API_NAME_HOLDER=$ARG_API_KEY +else + printf "API key not provided.\n" +fi + +build_initial_prompt() { + local initial_prompt="" + if [ -n "$ARG_AI_PROMPT" ]; then + if [ -n "$ARG_SYSTEM_PROMPT" ]; then + initial_prompt="$ARG_SYSTEM_PROMPT $ARG_AI_PROMPT" + else + initial_prompt="$ARG_AI_PROMPT" + fi + fi + echo "$initial_prompt" +} + +start_agentapi() { + echo "Starting in directory: $ARG_WORKDIR" + cd "$ARG_WORKDIR" + + local initial_prompt + initial_prompt=$(build_initial_prompt) + if [ -n "$initial_prompt" ]; then + echo "Starting agentapi with initial prompt" + agentapi server -I="$initial_prompt" --type aider --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always + else + agentapi server --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always + fi +} + +# TODO: Implement MCP server for coder when Aider support MCP servers. + +start_agentapi diff --git a/registry/coder/modules/aider/testdata/aider-mock.sh b/registry/coder/modules/aider/testdata/aider-mock.sh new file mode 100644 index 00000000..e021b2d2 --- /dev/null +++ b/registry/coder/modules/aider/testdata/aider-mock.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [[ "$1" == "--version" ]]; then + echo "HELLO: $(bash -c env)" + echo "aider version v0.86.0" + exit 0 +fi + +set -e + +while true; do + echo "$(date) - aider-agent-mock" + sleep 15 +done From a327e79bc4f2bb2c4b3483acea63618539b53c96 Mon Sep 17 00:00:00 2001 From: netsgnut <284779+netsgnut@users.noreply.github.com> Date: Fri, 24 Oct 2025 10:47:51 -0700 Subject: [PATCH 13/22] fix(kasmvnc): change installed check and bump default version (#505) ## Description This PR makes the following changes to the `coder/modules/kasmvnc`: - Change the installation check from checking `vncserver` to `kasmvncserver`. - Bump the default KasmVNC installation version to [1.4.0](https://docs.kasmvnc.com/docs/release_notes/1.4.0). In images where there is already TightVNC installed, the current installation check will erroneously report that KasmVNC is already installed. By checking `kasmvncserver` instead, it ensures KasmVNC is installed. Tested on Debian, Kali and Alpine-based images. ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/kasmvnc` **New version:** `v1.2.5` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues None --- registry/coder/modules/kasmvnc/README.md | 2 +- registry/coder/modules/kasmvnc/main.tf | 2 +- registry/coder/modules/kasmvnc/run.sh | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/registry/coder/modules/kasmvnc/README.md b/registry/coder/modules/kasmvnc/README.md index 2bc862d4..7f01b45b 100644 --- a/registry/coder/modules/kasmvnc/README.md +++ b/registry/coder/modules/kasmvnc/README.md @@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and module "kasmvnc" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/kasmvnc/coder" - version = "1.2.4" + version = "1.2.5" agent_id = coder_agent.example.id desktop_environment = "xfce" subdomain = true diff --git a/registry/coder/modules/kasmvnc/main.tf b/registry/coder/modules/kasmvnc/main.tf index ca7315ec..5a5b449b 100644 --- a/registry/coder/modules/kasmvnc/main.tf +++ b/registry/coder/modules/kasmvnc/main.tf @@ -23,7 +23,7 @@ variable "port" { variable "kasm_version" { type = string description = "Version of KasmVNC to install." - default = "1.3.2" + default = "1.4.0" } variable "desktop_environment" { diff --git a/registry/coder/modules/kasmvnc/run.sh b/registry/coder/modules/kasmvnc/run.sh index 04b8b9ee..089dce3e 100644 --- a/registry/coder/modules/kasmvnc/run.sh +++ b/registry/coder/modules/kasmvnc/run.sh @@ -8,10 +8,10 @@ error() { exit 1 } -# Function to check if vncserver is already installed +# Function to check if KasmVNC is already installed check_installed() { - if command -v vncserver &> /dev/null; then - echo "vncserver is already installed." + if command -v kasmvncserver &> /dev/null; then + echo "KasmVNC is already installed." return 0 # Don't exit, just indicate it's installed else return 1 # Indicates not installed @@ -158,7 +158,7 @@ case "$arch" in ;; esac -# Check if vncserver is installed, and install if not +# Check if KasmVNC is installed, and install if not if ! check_installed; then # Check for NOPASSWD sudo (required) if ! command -v sudo &> /dev/null || ! sudo -n true 2> /dev/null; then @@ -188,7 +188,7 @@ if ! check_installed; then ;; esac else - echo "vncserver already installed. Skipping installation." + echo "KasmVNC already installed. Skipping installation." fi if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then @@ -227,7 +227,7 @@ EOF # This password is not used since we start the server without auth. # The server is protected via the Coder session token / tunnel # and does not listen publicly -echo -e "password\npassword\n" | vncpasswd -wo -u "$USER" +echo -e "password\npassword\n" | kasmvncpasswd -wo -u "$USER" get_http_dir() { # determine the served file path @@ -290,7 +290,7 @@ VNC_LOG="/tmp/kasmvncserver.log" printf "🚀 Starting KasmVNC server...\n" set +e -vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1 +kasmvncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1 RETVAL=$? set -e From 0ff3dbcc48411c243aefb89215395e05988d1d18 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 24 Oct 2025 23:14:34 +0500 Subject: [PATCH 14/22] chore(claude-code): limit MCP tools for task reporting (#507) --- registry/coder/modules/claude-code/README.md | 14 +++++++------- .../coder/modules/claude-code/scripts/install.sh | 5 ----- .../coder/modules/claude-code/scripts/start.sh | 3 +++ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 1c17fab8..2058e917 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 = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.3.0" + version = "3.3.1" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,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 = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 1285df90..21133384 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -91,11 +91,6 @@ function report_tasks() { export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG" export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284" coder exp mcp configure claude-code "$ARG_WORKDIR" - else - export CODER_MCP_APP_STATUS_SLUG="" - export CODER_MCP_AI_AGENTAPI_URL="" - echo "Configuring Claude Code with Coder MCP..." - coder exp mcp configure claude-code "$ARG_WORKDIR" fi } diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 3ac840bd..525b8733 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -79,6 +79,9 @@ task_session_exists() { ARGS=() function start_agentapi() { + # For Task reporting + export CODER_MCP_ALLOWED_TOOLS="coder_report_task" + mkdir -p "$ARG_WORKDIR" cd "$ARG_WORKDIR" From 7e42a145faea94c10b890799dba2a2998fd30eaf Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Fri, 24 Oct 2025 16:35:20 -0400 Subject: [PATCH 15/22] feat: dropping perms before running claude (#509) Co-authored-by: DevCats Co-authored-by: Atif Ali --- registry/coder/modules/claude-code/README.md | 14 +++++++------- .../coder/modules/claude-code/scripts/start.sh | 11 +---------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 2058e917..af0e58e8 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 = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.3.1" + version = "3.3.2" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,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 = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.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 525b8733..1daae35f 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -166,18 +166,9 @@ function start_agentapi() { BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT}) fi - # Remove --dangerously-skip-permissions from ARGS when using boundary (it doesn't work with elevated permissions) - # Create a new array without the dangerous permissions flag - CLAUDE_ARGS=() - for arg in "${ARGS[@]}"; do - if [ "$arg" != "--dangerously-skip-permissions" ]; then - CLAUDE_ARGS+=("$arg") - fi - done - agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \ sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \ - claude "${CLAUDE_ARGS[@]}" + claude "${ARGS[@]}" else agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" fi From 01f5100068148aaf302ed72e76aab0b77b26694a Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Fri, 24 Oct 2025 21:23:42 -0400 Subject: [PATCH 16/22] fix: drop perms for boundary process (#512) --- registry/coder/modules/claude-code/README.md | 14 +++++++------- .../coder/modules/claude-code/scripts/start.sh | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index af0e58e8..c311eeb7 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 = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.3.2" + version = "3.3.3" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,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 = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.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 1daae35f..70452675 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -167,7 +167,8 @@ function start_agentapi() { fi agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \ - sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \ + sudo -E env PATH=$PATH setpriv --reuid=$(id -u) --regid=$(id -g) --clear-groups \ + --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \ claude "${ARGS[@]}" else agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" From d3b40c08f1b559f8f5fb349c9f3d1929cf6b58f7 Mon Sep 17 00:00:00 2001 From: DevCats Date: Mon, 27 Oct 2025 07:45:37 -0500 Subject: [PATCH 17/22] feat: add session resumption to codex (#506) ## Description Add continue variable, and logic for resuming task sessions ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [X] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder-labs/modules/codex` **New version:** `v3.1.0` **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 | 9 +- .../coder-labs/modules/codex/main.test.ts | 86 +++++++ registry/coder-labs/modules/codex/main.tf | 11 +- .../coder-labs/modules/codex/scripts/start.sh | 222 ++++++++++++++---- .../modules/codex/testdata/codex-mock.sh | 26 +- 5 files changed, 304 insertions(+), 50 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 9c749229..98062326 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 = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = var.openai_api_key workdir = "/home/coder/project" @@ -33,7 +33,7 @@ module "codex" { module "codex" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" @@ -61,7 +61,7 @@ module "coder-login" { module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = "..." ai_prompt = data.coder_parameter.ai_prompt.value @@ -84,6 +84,7 @@ module "codex" { - **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory - **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI - **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided) +- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions. ## Configuration @@ -107,7 +108,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" # ... other variables ... # Override default configuration diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 7d34a9c4..2041e36e 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -368,4 +368,90 @@ describe("codex", async () => { expect(prompt.exitCode).not.toBe(0); expect(prompt.stderr).toContain("No such file or directory"); }); + + test("codex-continue-capture-new-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + ai_prompt: "test task", + }, + }); + + const workdir = "/home/coder"; + const expectedSessionId = "019a1234-5678-9abc-def0-123456789012"; + const sessionsDir = "/home/coder/.codex/sessions"; + const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`; + + await execContainer(id, ["mkdir", "-p", sessionsDir]); + await execContainer(id, [ + "bash", + "-c", + `echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`, + ]); + + await execModuleScript(id); + + await expectAgentAPIStarted(id); + + const trackingFile = "/home/coder/.codex-module/.codex-task-session"; + const maxAttempts = 30; + let trackingFileContents = ""; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const result = await execContainer(id, [ + "bash", + "-c", + `cat ${trackingFile} 2>/dev/null || echo ""`, + ]); + if (result.stdout.trim().length > 0) { + trackingFileContents = result.stdout; + break; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`); + + const startLog = await readFileContainer( + id, + "/home/coder/.codex-module/agentapi-start.log", + ); + expect(startLog).toContain("Capturing new session ID"); + expect(startLog).toContain("Session tracked"); + expect(startLog).toContain(expectedSessionId); + }); + + test("codex-continue-resume-existing-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + ai_prompt: "test prompt", + }, + }); + + const workdir = "/home/coder"; + const mockSessionId = "019a1234-5678-9abc-def0-123456789012"; + const trackingFile = "/home/coder/.codex-module/.codex-task-session"; + + await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]); + await execContainer(id, [ + "bash", + "-c", + `echo "${workdir}|${mockSessionId}" > ${trackingFile}`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.codex-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("Found existing task session"); + expect(startLog.stdout).toContain(mockSessionId); + expect(startLog.stdout).toContain("Resuming existing session"); + expect(startLog.stdout).toContain( + `Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`, + ); + expect(startLog.stdout).not.toContain("test prompt"); + }); }); diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index d181c10f..a68cd79f 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -137,6 +137,12 @@ variable "ai_prompt" { default = "" } +variable "continue" { + type = bool + description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)." + default = true +} + variable "codex_system_prompt" { type = string description = "System instructions written to AGENTS.md in the ~/.codex directory" @@ -187,8 +193,9 @@ module "agentapi" { ARG_OPENAI_API_KEY='${var.openai_api_key}' \ ARG_REPORT_TASKS='${var.report_tasks}' \ ARG_CODEX_MODEL='${var.codex_model}' \ - ARG_CODEX_START_DIRECTORY='${var.workdir}' \ + ARG_CODEX_START_DIRECTORY='${local.workdir}' \ ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \ + ARG_CONTINUE='${var.continue}' \ /tmp/start.sh EOT @@ -206,7 +213,7 @@ module "agentapi" { ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \ ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \ ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \ - ARG_CODEX_START_DIRECTORY='${var.workdir}' \ + ARG_CODEX_START_DIRECTORY='${local.workdir}' \ ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \ /tmp/install.sh EOT diff --git a/registry/coder-labs/modules/codex/scripts/start.sh b/registry/coder-labs/modules/codex/scripts/start.sh index be54d575..663e80e5 100644 --- a/registry/coder-labs/modules/codex/scripts/start.sh +++ b/registry/coder-labs/modules/codex/scripts/start.sh @@ -3,6 +3,7 @@ source "$HOME"/.bashrc set -o errexit set -o pipefail + command_exists() { command -v "$1" > /dev/null 2>&1 } @@ -16,6 +17,7 @@ fi printf "Version: %s\n" "$(codex --version)" set -o nounset ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d) +ARG_CONTINUE=${ARG_CONTINUE:-true} echo "=== Codex Launch Configuration ===" printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")" @@ -23,53 +25,187 @@ printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}" printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY" printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")" printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS" +printf "Continue Sessions: %s\n" "$ARG_CONTINUE" echo "======================================" set +o nounset -CODEX_ARGS=() -if command_exists codex; then - printf "Codex is installed\n" -else - printf "Error: Codex is not installed. Please enable install_codex or install it manually\n" - exit 1 -fi +SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session" -if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then - printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" - cd "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } -else - printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" - mkdir -p "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } - cd "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } -fi +find_session_for_directory() { + local target_dir="$1" -if [ -n "$ARG_CODEX_MODEL" ]; then - CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL") -fi - -if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then - printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT" - if [ "${ARG_REPORT_TASKS}" == "true" ]; then - PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" - else - PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + if [ ! -f "$SESSION_TRACKING_FILE" ]; then + return 1 fi - CODEX_ARGS+=("$PROMPT") -else - printf "No task prompt given.\n" -fi -# Terminal dimensions optimized for Coder Tasks UI sidebar: -# - Width 67: fits comfortably in sidebar -# - Height 1190: adjusted due to Codex terminal height bug -printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}" -agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" + local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1) + + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + + return 1 +} + +store_session_mapping() { + local dir="$1" + local session_id="$2" + + mkdir -p "$(dirname "$SESSION_TRACKING_FILE")" + + if [ -f "$SESSION_TRACKING_FILE" ]; then + grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true + mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE" + fi + + echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE" +} + +find_recent_session_file() { + local target_dir="$1" + local sessions_dir="$HOME/.codex/sessions" + + if [ ! -d "$sessions_dir" ]; then + return 1 + fi + + local latest_file="" + local latest_time=0 + + while IFS= read -r session_file; do + local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0") + local first_line=$(head -n 1 "$session_file" 2> /dev/null) + local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4) + + if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then + latest_file="$session_file" + latest_time="$file_time" + fi + done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null) + + if [ -n "$latest_file" ]; then + local first_line=$(head -n 1 "$latest_file") + local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + fi + + return 1 +} + +wait_for_session_file() { + local target_dir="$1" + local max_attempts=20 + local attempt=0 + + while [ $attempt -lt $max_attempts ]; do + local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "") + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + sleep 0.5 + attempt=$((attempt + 1)) + done + + return 1 +} + +validate_codex_installation() { + if command_exists codex; then + printf "Codex is installed\n" + else + printf "Error: Codex is not installed. Please enable install_codex or install it manually\n" + exit 1 + fi +} + +setup_workdir() { + if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then + printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" + cd "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + else + printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" + mkdir -p "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + cd "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + fi +} + +build_codex_args() { + CODEX_ARGS=() + + if [ -n "$ARG_CODEX_MODEL" ]; then + CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL") + fi + + if [ "$ARG_CONTINUE" = "true" ]; then + existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "") + + if [ -n "$existing_session" ]; then + printf "Found existing task session for this directory: %s\n" "$existing_session" + printf "Resuming existing session...\n" + CODEX_ARGS+=("resume" "$existing_session") + else + printf "No existing task session found for this directory\n" + printf "Starting new task session...\n" + + if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then + if [ "${ARG_REPORT_TASKS}" == "true" ]; then + PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" + else + PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + fi + CODEX_ARGS+=("$PROMPT") + fi + fi + else + printf "Continue disabled, starting fresh session\n" + + if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then + if [ "${ARG_REPORT_TASKS}" == "true" ]; then + PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" + else + PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + fi + CODEX_ARGS+=("$PROMPT") + fi + fi +} + +capture_session_id() { + if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then + printf "Capturing new session ID...\n" + new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "") + + if [ -n "$new_session" ]; then + store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session" + printf "✓ Session tracked: %s\n" "$new_session" + printf "This session will be automatically resumed on next restart\n" + else + printf "⚠ Could not capture session ID after 10s timeout\n" + fi + fi +} + +start_codex() { + printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}" + agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" & + capture_session_id +} + +validate_codex_installation +setup_workdir +build_codex_args +start_codex diff --git a/registry/coder-labs/modules/codex/testdata/codex-mock.sh b/registry/coder-labs/modules/codex/testdata/codex-mock.sh index 8c1c7366..fe8f3806 100644 --- a/registry/coder-labs/modules/codex/testdata/codex-mock.sh +++ b/registry/coder-labs/modules/codex/testdata/codex-mock.sh @@ -1,5 +1,6 @@ #!/bin/bash +# Handle --version flag if [[ "$1" == "--version" ]]; then echo "HELLO: $(bash -c env)" echo "codex version v1.0.0" @@ -8,7 +9,30 @@ fi set -e +SESSION_ID="" +IS_RESUME=false + +while [[ $# -gt 0 ]]; do + case $1 in + resume) + IS_RESUME=true + SESSION_ID="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +if [ "$IS_RESUME" = false ]; then + SESSION_ID="019a1234-5678-9abc-def0-123456789012" + echo "Created new session: $SESSION_ID" +else + echo "Resuming session: $SESSION_ID" +fi + while true; do - echo "$(date) - codex-mock" + echo "$(date) - codex-mock (session: $SESSION_ID)" sleep 15 done From d64851774bd902f78fadd6e7bcac38d675345b70 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 27 Oct 2025 18:36:19 +0500 Subject: [PATCH 18/22] fix(jetbrains): update Terraform version requirement to 1.9+ (#513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Updated `required_version` constraint from `>= 1.0` to `>= 1.9` in jetbrains module - Added inline comment explaining the cross-variable validation requirement - Bumped module version from `1.1.0` to `1.1.1` (patch version) ## Issue The jetbrains module uses cross-variable validation at line 169-171 where `var.options` is referenced within the `var.ide_config` validation block: ```tf validation { condition = alltrue([ for code in var.options : contains(keys(var.ide_config), code) ]) error_message = "The ide_config must be a superset of var.options." } ``` This pattern requires Terraform 1.9+ and fails on earlier versions with: ``` Error: Invalid reference in variable validation The condition for variable "ide_config" can only refer to the variable itself, using var.ide_config. ``` ## References - Terrafomr release blog that talks abut this feature: https://www.hashicorp.com/en/blog/terraform-1-9-enhances-input-variable-validations - Terraform PR that added this feature: https://github.com/hashicorp/terraform/pull/34955 - HashiCorp Support Article: https://support.hashicorp.com/hc/en-us/articles/43291233547027 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Co-authored-by: DevCats --- registry/coder/modules/jetbrains/README.md | 14 +++++++------- registry/coder/modules/jetbrains/main.tf | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md index ef19ec20..9d08e645 100644 --- a/registry/coder/modules/jetbrains/README.md +++ b/registry/coder/modules/jetbrains/README.md @@ -14,7 +14,7 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" # tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional @@ -40,7 +40,7 @@ When `default` contains IDE codes, those IDEs are created directly without user module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA @@ -53,7 +53,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" # Show parameter with limited options @@ -67,7 +67,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["IU", "PY"] @@ -82,7 +82,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/workspace/project" @@ -108,7 +108,7 @@ module "jetbrains" { module "jetbrains_pycharm" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/workspace/project" @@ -128,7 +128,7 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons: module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["IU", "PY"] diff --git a/registry/coder/modules/jetbrains/main.tf b/registry/coder/modules/jetbrains/main.tf index d33fc6b2..8f0e0ac7 100644 --- a/registry/coder/modules/jetbrains/main.tf +++ b/registry/coder/modules/jetbrains/main.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.9" required_providers { coder = { @@ -163,7 +163,8 @@ variable "ide_config" { condition = length(var.ide_config) > 0 error_message = "The ide_config must not be empty." } - # ide_config must be a superset of var.. options + # ide_config must be a superset of var.options + # Requires Terraform 1.9+ for cross-variable validation references validation { condition = alltrue([ for code in var.options : contains(keys(var.ide_config), code) From 1a15ad650a43a985d92f76da7f33264fdfd22095 Mon Sep 17 00:00:00 2001 From: Luis <25037200+angwdev@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:12:24 -0500 Subject: [PATCH 19/22] Update Vault CLI download link to use architecture (#514) ## Description The download command was downloading only the amd64 version, ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/[namespace]/modules/[module-name]` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [ ] No ## Template Information **Path:** `registry/[namespace]/templates/[template-name]` ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues --- registry/coder/modules/vault-token/README.md | 4 ++-- registry/coder/modules/vault-token/run.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/coder/modules/vault-token/README.md b/registry/coder/modules/vault-token/README.md index e30abdc5..4561a170 100644 --- a/registry/coder/modules/vault-token/README.md +++ b/registry/coder/modules/vault-token/README.md @@ -19,7 +19,7 @@ variable "vault_token" { module "vault" { source = "registry.coder.com/coder/vault-token/coder" - version = "1.2.1" + version = "1.2.2" agent_id = coder_agent.example.id vault_token = var.token # optional vault_addr = "https://vault.example.com" @@ -73,7 +73,7 @@ variable "vault_token" { module "vault" { source = "registry.coder.com/coder/vault-token/coder" - version = "1.2.1" + version = "1.2.2" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_token = var.token diff --git a/registry/coder/modules/vault-token/run.sh b/registry/coder/modules/vault-token/run.sh index e1da6ee8..9b83f32f 100644 --- a/registry/coder/modules/vault-token/run.sh +++ b/registry/coder/modules/vault-token/run.sh @@ -68,7 +68,7 @@ install() { else printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${INSTALL_VERSION}" fi - fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_amd64.zip" + fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_$${ARCH}.zip" if [ $? -ne 0 ]; then printf "Failed to download Vault.\n" return 1 From d6d0101f096259c93c982f02693fb1ded2e6fdc7 Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:00:41 +0000 Subject: [PATCH 20/22] Fix Devolutions Auto-Complete (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description I’ve completed a set of modifications to improve the user experience and session behaviour within Devolutions Gateway: - Auto-Complete Fix: Resolved issues with auto-complete functionality. - Container Visibility: Implemented logic to hide the app-net-scan container, preventing it from displaying during the initial session load. - Default Settings: Enabled Unicode keyboard mode and dynamic window resizing by default to enhance usability. - Session Closure Behaviour: Modified the "Close Session" button to fully close the session window, avoiding returns to the session manager. - Dynamic Module Path Construction: Refactored the PowerShell module path setup to be dynamically constructed. - Input Variables: Added `slug` and `display_name` as input variables. ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/windows-rdp` **New version:** `v1.3.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" --------- Co-authored-by: DevCats Co-authored-by: DevelopmentCats Co-authored-by: Eric Paulsen Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- registry/coder/modules/windows-rdp/README.md | 8 +- .../modules/windows-rdp/devolutions-patch.js | 743 +++++++++--------- .../coder/modules/windows-rdp/main.test.ts | 10 +- registry/coder/modules/windows-rdp/main.tf | 28 +- .../powershell-installation-script.tftpl | 49 +- 5 files changed, 462 insertions(+), 376 deletions(-) diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index f19afc47..92c5ac17 100644 --- a/registry/coder/modules/windows-rdp/README.md +++ b/registry/coder/modules/windows-rdp/README.md @@ -15,7 +15,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id } ``` @@ -32,7 +32,7 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id } ``` @@ -43,7 +43,7 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id } ``` @@ -54,7 +54,7 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id devolutions_gateway_version = "2025.2.2" # Specify a specific version } diff --git a/registry/coder/modules/windows-rdp/devolutions-patch.js b/registry/coder/modules/windows-rdp/devolutions-patch.js index ef736452..1f231ca3 100644 --- a/registry/coder/modules/windows-rdp/devolutions-patch.js +++ b/registry/coder/modules/windows-rdp/devolutions-patch.js @@ -25,401 +25,426 @@ * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry * @typedef {Readonly>} FormFieldEntries */ +(function () { + /** + * The communication protocol to set Devolutions to. + */ + const PROTOCOL = "RDP"; -/** - * The communication protocol to set Devolutions to. - */ -const PROTOCOL = "RDP"; + /** + * The hostname to use with Devolutions. + */ + const HOSTNAME = "localhost"; -/** - * The hostname to use with Devolutions. - */ -const HOSTNAME = "localhost"; + /** + * How often to poll the screen for the main Devolutions form. + */ + const POLL_INTERVAL_MS = 500; -/** - * How often to poll the screen for the main Devolutions form. - */ -const SCREEN_POLL_INTERVAL_MS = 500; - -/** - * The fields in the Devolutions sign-in form that should be populated with - * values from the Coder workspace. - * - * All properties should be defined as placeholder templates in the form - * VALUE_NAME. The Coder module, when spun up, should then run some logic to - * replace the template slots with actual values. These values should never - * change from within JavaScript itself. - * - * @satisfies {FormFieldEntries} - */ -const formFieldEntries = { - /** @readonly */ - username: { + /** + * The fields in the Devolutions sign-in form that should be populated with + * values from the Coder workspace. + * + * All properties should be defined as placeholder templates in the form + * VALUE_NAME. The Coder module, when spun up, should then run some logic to + * replace the template slots with actual values. These values should never + * change from within JavaScript itself. + * + * @satisfies {FormFieldEntries} + */ + const formFieldEntries = { /** @readonly */ - querySelector: "web-client-username-control input", + username: { + /** @readonly */ + querySelector: "web-client-username-control input", + /** @readonly */ + value: "${CODER_USERNAME}", + }, /** @readonly */ - value: "${CODER_USERNAME}", - }, + password: { + /** @readonly */ + querySelector: "web-client-password-control input", - /** @readonly */ - password: { - /** @readonly */ - querySelector: "web-client-password-control input", - - /** @readonly */ - value: "${CODER_PASSWORD}", - }, -}; - -/** - * Handles typing in the values for the input form. All values are written - * immediately, even though that would be physically impossible with a real - * keyboard. - * - * Note: this code will never break, but you might get warnings in the console - * from Angular about unexpected value changes. Angular patches over a lot of - * the built-in browser APIs to support its component change detection system. - * As part of that, it has validations for checking whether an input it - * previously had control over changed without it doing anything. - * - * But the only way to simulate a keyboard input is by setting the input's - * .value property, and then firing an input event. So basically, the inner - * value will change, which Angular won't be happy about, but then the input - * event will fire and sync everything back together. - * - * @param {HTMLInputElement} inputField - * @param {string} inputText - * @returns {Promise} - */ -function setInputValue(inputField, inputText) { - return new Promise((resolve, reject) => { - // Adding timeout for input event, even though we'll be dispatching it - // immediately, just in the off chance that something in the Angular app - // intercepts it or stops it from propagating properly - const timeoutId = window.setTimeout(() => { - reject(new Error("Input event did not get processed correctly in time.")); - }, 3_000); - - const handleSuccessfulDispatch = () => { - window.clearTimeout(timeoutId); - inputField.removeEventListener("input", handleSuccessfulDispatch); - resolve(); - }; - - inputField.addEventListener("input", handleSuccessfulDispatch); - - // Code assumes that Angular will have an event handler in place to handle - // the new event - const inputEvent = new Event("input", { - bubbles: true, - cancelable: true, - }); - - inputField.value = inputText; - inputField.dispatchEvent(inputEvent); - }); -} - -/** - * Takes a Devolutions remote session form, auto-fills it with data, and then - * submits it. - * - * The logic here is more convoluted than it should be for two main reasons: - * 1. Devolutions' HTML markup has errors. There are labels, but they aren't - * bound to the inputs they're supposed to describe. This means no easy hooks - * for selecting the elements, unfortunately. - * 2. Trying to modify the .value properties on some of the inputs doesn't - * work. Probably some combo of Angular data-binding and some inputs having - * the readonly attribute. Have to simulate user input to get around this. - * - * @param {HTMLFormElement} myForm - * @returns {Promise} - */ -async function autoSubmitForm(myForm) { - const setProtocolValue = () => { - /** @type {HTMLDivElement | null} */ - const protocolDropdownTrigger = myForm.querySelector('div[role="button"]'); - if (protocolDropdownTrigger === null) { - throw new Error("No clickable trigger for setting protocol value"); - } - - protocolDropdownTrigger.click(); - - // Can't use form as container for querying the list of dropdown options, - // because the elements don't actually exist inside the form. They're placed - // in the top level of the HTML doc, and repositioned to make it look like - // they're part of the form. Avoids CSS stacking context issues, maybe? - /** @type {HTMLLIElement | null} */ - const protocolOption = document.querySelector( - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li', - ); - - if (protocolOption === null) { - throw new Error( - "Unable to find protocol option on screen that matches desired protocol", - ); - } - - protocolOption.click(); + /** @readonly */ + value: "${CODER_PASSWORD}", + }, }; - const setHostname = () => { - /** @type {HTMLInputElement | null} */ - const hostnameInput = myForm.querySelector("p-autocomplete#hostname input"); + /** + * This ensures that the Devolutions login form (which by default, always shows + * up on screen when the app first launches) stays visually hidden from the user + * when they open Devolutions via the Coder module. + * + * The form will still be filled out automatically and submitted in the + * background via the rest of the logic in this file, so this function is mainly + * to help avoid screen flickering and make the overall experience feel a little + * more polished (even though it's just one giant hack). + * + * @returns {void} + */ + function hideFormForInitialSubmission() { + const styleId = "coder-patch--styles-initial-submission"; + const cssOpacityVariableName = "--coder-opacity-multiplier"; - if (hostnameInput === null) { - throw new Error("Unable to find field for adding hostname"); + /** @type {HTMLStyleElement | null} */ + // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation + let styleContainer = document.querySelector("#" + styleId); + if (!styleContainer) { + styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` + /* + Have to use opacity instead of visibility, because the element still + needs to be interactive via the script so that it can be auto-filled. + */ + :root { + /* + Can be 0 or 1. Start off invisible to avoid risks of UI flickering, + but the rest of the function should be in charge of making the form + container visible again if something goes wrong during setup. + + Double dollar sign needed to avoid Terraform script false positives + */ + $${cssOpacityVariableName}: 0; + } + + /* + web-client-form is the container for the main session form, while + the div is for the dropdown that is used for selecting the protocol. + The dropdown is not inside of the form for CSS styling reasons, so we + need to select both. + */ + web-client-form, + body > div.p-overlay { + /* + Double dollar sign needed to avoid Terraform script false positives + */ + opacity: calc(100% * var($${cssOpacityVariableName})) !important; + } + `; + + document.head.appendChild(styleContainer); } - return setInputValue(hostnameInput, HOSTNAME); - }; - - const setCoderFormFieldValues = async () => { - // The RDP form will not appear on screen unless the dropdown is set to use - // the RDP protocol - const rdpSubsection = myForm.querySelector("rdp-form"); - if (rdpSubsection === null) { - throw new Error( - "Unable to find RDP subsection. Is the value of the protocol set to RDP?", - ); - } - - for (const { value, querySelector } of Object.values(formFieldEntries)) { - /** @type {HTMLInputElement | null} */ - const input = document.querySelector(querySelector); - - if (input === null) { - throw new Error( - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - 'Unable to element that matches query "' + querySelector + '"', - ); - } - - await setInputValue(input, value); - } - }; - - const triggerSubmission = () => { - /** @type {HTMLButtonElement | null} */ - const submitButton = myForm.querySelector( - 'p-button[ng-reflect-type="submit"] button', - ); - - if (submitButton === null) { - throw new Error("Unable to find submission button"); - } - - if (submitButton.disabled) { - throw new Error( - "Unable to submit form because submit button is disabled. Are all fields filled out correctly?", - ); - } - - submitButton.click(); - }; - - setProtocolValue(); - await setHostname(); - await setCoderFormFieldValues(); - triggerSubmission(); -} - -/** - * Sets up logic for auto-populating the form data when the form appears on - * screen. - * - * @returns {void} - */ -function setupFormDetection() { - /** @type {HTMLFormElement | null} */ - let formValueFromLastMutation = null; - - /** @returns {void} */ - const onDynamicTabMutation = () => { - /** @type {HTMLFormElement | null} */ - const latestForm = document.querySelector("web-client-form > form"); - - // Only try to auto-fill if we went from having no form on screen to - // having a form on screen. That way, we don't accidentally override the - // form if the user is trying to customize values, and this essentially - // makes the script values function as default values - const mounted = formValueFromLastMutation === null && latestForm !== null; - if (mounted) { - autoSubmitForm(latestForm); - } - - formValueFromLastMutation = latestForm; - }; - - /** @type {number | undefined} */ - let pollingId = undefined; - - /** @returns {void} */ - const checkScreenForDynamicTab = () => { - const dynamicTab = document.querySelector("web-client-dynamic-tab"); - - // Keep polling until the main content container is on screen - if (dynamicTab === null) { + // The root node being undefined should be physically impossible (if it's + // undefined, the browser itself is busted), but we need to do a type check + // here so that the rest of the function doesn't need to do type checks over + // and over. + const rootNode = document.querySelector(":root"); + if (!(rootNode instanceof HTMLHtmlElement)) { + // Remove the container entirely because if the browser is busted, who knows + // if the CSS variables can be applied correctly. Better to have something + // be a bit more ugly/painful to use, than have it be impossible to use + styleContainer.remove(); return; } - window.clearInterval(pollingId); + // It's safe to make the form visible preemptively because Devolutions + // outputs the Windows view through an HTML canvas that it overlays on top + // of the rest of the app. Even if the form isn't hidden at the style level, + // it will still be covered up. + const restoreOpacity = () => { + rootNode.style.setProperty(cssOpacityVariableName, "1"); + }; - // Call the mutation callback manually, to ensure it runs at least once - onDynamicTabMutation(); + // If this file gets more complicated, it might make sense to set up the + // timeout and event listener so that if one triggers, it cancels the other, + // but having restoreOpacity run more than once is a no-op for right now. + // Not a big deal if these don't get cleaned up. - // Having the mutation observer is kind of an extra safety net that isn't - // really expected to run that often. Most of the content in the dynamic - // tab is being rendered through Canvas, which won't trigger any mutations - // that the observer can detect - const dynamicTabObserver = new MutationObserver(onDynamicTabMutation); - dynamicTabObserver.observe(dynamicTab, { - subtree: true, - childList: true, - }); - }; + // Have the form automatically reappear no matter what, so that if something + // does break, the user isn't left out to dry + window.setTimeout(restoreOpacity, 5_000); - pollingId = window.setInterval( - checkScreenForDynamicTab, - SCREEN_POLL_INTERVAL_MS, - ); -} - -/** - * Sets up custom styles for hiding default Devolutions elements that Coder - * users shouldn't need to care about. - * - * @returns {void} - */ -function setupAlwaysOnStyles() { - const styleId = "coder-patch--styles-always-on"; - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - const existingContainer = document.querySelector("#" + styleId); - if (existingContainer) { - return; + /** @type {HTMLFormElement | null} */ + const form = document.querySelector("web-client-form > form"); + form?.addEventListener( + "submit", + () => { + // Not restoring opacity right away just to give the HTML canvas a little + // bit of time to get spun up and cover up the main form + window.setTimeout(restoreOpacity, 1_000); + }, + { once: true }, + ); } - const styleContainer = document.createElement("style"); - styleContainer.id = styleId; - styleContainer.innerHTML = ` - /* app-menu corresponds to the sidebar of the default view. */ - app-menu { - display: none !important; + /** + * Sets up custom styles for hiding default Devolutions elements that Coder + * users shouldn't need to care about. + * + * @returns {void} + */ + function setupAlwaysOnStyles() { + const styleId = "coder-patch--styles-always-on"; + // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation + const existingContainer = document.querySelector("#" + styleId); + if (existingContainer) { + return; } - `; - document.head.appendChild(styleContainer); -} - -/** - * This ensures that the Devolutions login form (which by default, always shows - * up on screen when the app first launches) stays visually hidden from the user - * when they open Devolutions via the Coder module. - * - * The form will still be filled out automatically and submitted in the - * background via the rest of the logic in this file, so this function is mainly - * to help avoid screen flickering and make the overall experience feel a little - * more polished (even though it's just one giant hack). - * - * @returns {void} - */ -function hideFormForInitialSubmission() { - const styleId = "coder-patch--styles-initial-submission"; - const cssOpacityVariableName = "--coder-opacity-multiplier"; - - /** @type {HTMLStyleElement | null} */ - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - let styleContainer = document.querySelector("#" + styleId); - if (!styleContainer) { - styleContainer = document.createElement("style"); + const styleContainer = document.createElement("style"); styleContainer.id = styleId; styleContainer.innerHTML = ` - /* - Have to use opacity instead of visibility, because the element still - needs to be interactive via the script so that it can be auto-filled. - */ - :root { - /* - Can be 0 or 1. Start off invisible to avoid risks of UI flickering, - but the rest of the function should be in charge of making the form - container visible again if something goes wrong during setup. - - Double dollar sign needed to avoid Terraform script false positives - */ - $${cssOpacityVariableName}: 0; + /* app-menu corresponds to the sidebar of the default view. */ + app-menu { + display: none !important; } - /* - web-client-form is the container for the main session form, while - the div is for the dropdown that is used for selecting the protocol. - The dropdown is not inside of the form for CSS styling reasons, so we - need to select both. - */ - web-client-form, - body > div.p-overlay { - /* - Double dollar sign needed to avoid Terraform script false positives - */ - opacity: calc(100% * var($${cssOpacityVariableName})) !important; + /* app-net-scan corresponds to the auto-discovery feature. */ + app-net-scan { + display: none !important; } `; document.head.appendChild(styleContainer); } - // The root node being undefined should be physically impossible (if it's - // undefined, the browser itself is busted), but we need to do a type check - // here so that the rest of the function doesn't need to do type checks over - // and over. - const rootNode = document.querySelector(":root"); - if (!(rootNode instanceof HTMLHtmlElement)) { - // Remove the container entirely because if the browser is busted, who knows - // if the CSS variables can be applied correctly. Better to have something - // be a bit more ugly/painful to use, than have it be impossible to use - styleContainer.remove(); - return; + /** + * Handles typing in the values for the input form. All values are written + * immediately, even though that would be physically impossible with a real + * keyboard. + * + * Note: this code will never break, but you might get warnings in the console + * from Angular about unexpected value changes. Angular patches over a lot of + * the built-in browser APIs to support its component change detection system. + * As part of that, it has validations for checking whether an input it + * previously had control over changed without it doing anything. + * + * But the only way to simulate a keyboard input is by setting the input's + * .value property, and then firing an input event. So basically, the inner + * value will change, which Angular won't be happy about, but then the input + * event will fire and sync everything back together. + * + * @param {HTMLInputElement} inputField + * @param {string} inputText + * @returns {Promise} + */ + function setInputValue(inputField, inputText) { + return new Promise((resolve, reject) => { + // Adding timeout for input event, even though we'll be dispatching it + // immediately, just in the off chance that something in the Angular app + // intercepts it or stops it from propagating properly + const timeoutId = window.setTimeout(() => { + reject( + new Error("Input event did not get processed correctly in time."), + ); + }, 3_000); + + const handleSuccessfulDispatch = () => { + window.clearTimeout(timeoutId); + inputField.removeEventListener("input", handleSuccessfulDispatch); + resolve(); + }; + + inputField.addEventListener("input", handleSuccessfulDispatch); + + // Code assumes that Angular will have an event handler in place to handle + // the new event + const inputEvent = new Event("input", { + bubbles: true, + cancelable: true, + }); + + inputField.value = inputText; + inputField.dispatchEvent(inputEvent); + }); } - // It's safe to make the form visible preemptively because Devolutions - // outputs the Windows view through an HTML canvas that it overlays on top - // of the rest of the app. Even if the form isn't hidden at the style level, - // it will still be covered up. - const restoreOpacity = () => { - rootNode.style.setProperty(cssOpacityVariableName, "1"); - }; + /** + * Takes a Devolutions remote session form, auto-fills it with data, and then + * submits it. + * + * The logic here is more convoluted than it should be for two main reasons: + * 1. Devolutions' HTML markup has errors. There are labels, but they aren't + * bound to the inputs they're supposed to describe. This means no easy hooks + * for selecting the elements, unfortunately. + * 2. Trying to modify the .value properties on some of the inputs doesn't + * work. Probably some combo of Angular data-binding and some inputs having + * the readonly attribute. Have to simulate user input to get around this. + * + * @param {HTMLFormElement} form + */ + async function fillForm(form) { + try { + log("Form detected. Starting auto-fill..."); - // If this file gets more complicated, it might make sense to set up the - // timeout and event listener so that if one triggers, it cancels the other, - // but having restoreOpacity run more than once is a no-op for right now. - // Not a big deal if these don't get cleaned up. + // By default, RDP is selected. Leaving this here if needed + // in the future. + const protocolTrigger = form.querySelector('p-dropdown[id="protocol"]'); + if (protocolTrigger) { + protocolTrigger.click(); + const protocolOption = document.querySelector( + `li[aria-label="$${PROTOCOL}"]`, + ); + if (protocolOption) { + protocolOption.click(); + log(`Protocol set to $${PROTOCOL}`); + } else { + log("Protocol option not found."); + } + } else { + log("Protocol dropdown trigger not found."); + } - // Have the form automatically reappear no matter what, so that if something - // does break, the user isn't left out to dry - window.setTimeout(restoreOpacity, 5_000); + const hostnameInput = form.querySelector("p-autocomplete#hostname input"); + if (hostnameInput) { + await setInputValue(hostnameInput, HOSTNAME); + log(`Hostname set to $${HOSTNAME}`); + } else { + log("Hostname input not found."); + } - /** @type {HTMLFormElement | null} */ - const form = document.querySelector("web-client-form > form"); - form?.addEventListener( - "submit", - () => { - // Not restoring opacity right away just to give the HTML canvas a little - // bit of time to get spun up and cover up the main form - window.setTimeout(restoreOpacity, 1_000); - }, - { once: true }, - ); -} + for (const [key, { querySelector, value }] of Object.entries( + formFieldEntries, + )) { + const input = document.querySelector(querySelector); + if (input) { + await setInputValue(input, value); + log(`Set $${key} to $${value}`); + } else { + log(`Input for $${key} not found with selector: $${querySelector}`); + } + } -// Always safe to call these immediately because even if the Angular app isn't -// loaded by the time the function gets called, the CSS will always be globally -// available for when Angular is finally ready -setupAlwaysOnStyles(); -hideFormForInitialSubmission(); + const submitButton = form.querySelector( + 'p-button[class="p-element"] button', + ); + if (submitButton && !submitButton.disabled) { + submitButton.click(); + log("Form submitted."); + } else { + log("Submit button not found or disabled."); + } + } catch (err) { + console.error("[Devolutions Patch] Error during form fill:", err); + } + } -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupFormDetection); -} else { - setupFormDetection(); -} + /** + * Attaches a click event listener to the "Close Session" button within the provided top bar element. + * When clicked, the listener triggers the window to close. + * Logs a message indicating whether the listener was successfully attached or if the button was not found. + * + * @param {HTMLElement} topBar - The container element that includes the "Close Session" button. + * @returns {void} + */ + function attachCloseListener(topBar) { + const buttons = topBar.querySelectorAll("button"); + + const closeButton = Array.from(buttons).find((button) => { + const labelSpan = button.querySelector(".p-button-label"); + return labelSpan && labelSpan.textContent.trim() === "Close Session"; + }); + + if (closeButton) { + closeButton.parentElement.addEventListener("click", () => { + window.close(); + }); + log("Close listener attached."); + } else { + log("Close button not found in top bar."); + } + } + + /** + * Sets the checked state of a checkbox based on its label text. + * Searches all components in the document and identifies the one + * whose label matches the provided `filterText`. Once found, it sets the checkbox + * to the specified `checked` state (true or false) and dispatches a change event + * to ensure any bound listeners (e.g., Angular change detection) are triggered. + * Logs the outcome of the operation for debugging or audit purposes. + * + * @param {string} filterText - The exact label text of the checkbox to target. + * @param {boolean} checked - The desired checked state (true to check, false to uncheck). + * @returns {void} + */ + function setCheckbox(filterText, checked) { + const checkboxes = document.querySelectorAll("p-checkbox"); + + const targetCheckbox = Array.from(checkboxes).find((checkbox) => { + const label = checkbox.querySelector(".p-checkbox-label"); + return label && label.textContent.trim() === filterText; + }); + + if (targetCheckbox) { + const input = targetCheckbox.querySelector('input[type="checkbox"]'); + if (input) { + input.checked = checked; + input.dispatchEvent(new Event("change", { bubbles: true })); + } + log(`$${filterText} set to $${checked}.`); + } else { + log(`$${filterText} checkbox not found in top bar.`); + } + } + + /** + * Continuously polls the DOM for a specific form element. + * - Searches for a

inside a element. + * - If found, calls `fillForm(form)` to process it. + * - If not found, logs a retry message and schedules another check after a delay. + * + * @returns {void} + */ + function pollForForm() { + const form = document.querySelector("web-client-form form"); + if (form) { + fillForm(form); + + // Start polling for top bar after form is filled + pollForSessionToolBar(); + } else { + log("Form not yet available. Retrying..."); + setTimeout(pollForForm, POLL_INTERVAL_MS); + } + } + + /** + * Continuously polls the DOM for a specific form element. + * - Searches for a element. + * - If found, adds another listener to session toolbar + * - If not found, logs a retry message and schedules another check after a delay. + * + * @returns {void} + */ + function pollForSessionToolBar() { + const sessionToolBar = document.querySelector("session-toolbar"); + if (sessionToolBar) { + log("Top bar detected. Proceeding with next steps..."); + attachCloseListener(sessionToolBar); + + // Automatically set checkboxes to improve user experience + setCheckbox("Unicode Keyboard Mode", true); + setCheckbox("Dynamic Resize", true); + } else { + log("Top bar not yet available. Retrying..."); + setTimeout(pollForSessionToolBar, POLL_INTERVAL_MS); + } + } + + /** + * Logs a message to the console with a standardized prefix. + * Format: [Devolutions Patch] $ + * + * @param {string} msg - The message to log. + * @returns {void} + */ + function log(msg) { + console.log(`[Devolutions Patch] $${msg}`); + } + + // Always safe to call these immediately because even if the Angular app isn't + // loaded by the time the function gets called, the CSS will always be globally + // available for when Angular is finally ready + setupAlwaysOnStyles(); + hideFormForInitialSubmission(); + + log("Script loaded. Starting form detection..."); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", pollForForm); + } else { + pollForForm(); + } +})(); diff --git a/registry/coder/modules/windows-rdp/main.test.ts b/registry/coder/modules/windows-rdp/main.test.ts index 125b3b3b..80c09fd0 100644 --- a/registry/coder/modules/windows-rdp/main.test.ts +++ b/registry/coder/modules/windows-rdp/main.test.ts @@ -59,9 +59,11 @@ describe("Web RDP", async () => { expect(lines).toEqual( expect.arrayContaining([ '$moduleName = "DevolutionsGateway"', - // Devolutions does versioning in the format year.minor.patch - expect.stringMatching(/^\$moduleVersion = "\d{4}\.\d+\.\d+"$/), - "Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force", + // Default is "latest" to automatically get the newest version + '$moduleVersion = "latest"', + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12", + "Set-PSRepository -Name PSGallery -InstallationPolicy Trusted", + "Install-Module -Name $moduleName -Force", ]), ); }); @@ -86,7 +88,7 @@ describe("Web RDP", async () => { * @see {@link https://regex101.com/r/UMgQpv/2} */ const formEntryValuesRe = - /^const formFieldEntries = \{$.*?^\s+username: \{$.*?^\s*?querySelector.*?,$.*?^\s*value: "(?.+?)",$.*?password: \{$.*?^\s+querySelector: .*?,$.*?^\s*value: "(?.+?)",$.*?^};$/ms; + /username:\s*\{[\s\S]*?value:\s*"(?[^"]+)"[\s\S]*?password:\s*\{[\s\S]*?value:\s*"(?[^"]+)"/; // Test that things work with the default username/password const defaultState = await runTerraformApply( diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index c1b996dd..3c83d195 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -9,6 +9,24 @@ terraform { } } +variable "display_name" { + type = string + description = "The display name for the Web RDP application." + default = "Web RDP" +} + +variable "slug" { + type = string + description = "The slug for the Web RDP application." + default = "web-rdp" +} + +variable "icon" { + type = string + description = "The icon for the Web RDP application." + default = "/icon/desktop.svg" +} + variable "order" { type = number description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." @@ -48,8 +66,8 @@ variable "admin_password" { variable "devolutions_gateway_version" { type = string - default = "2025.2.2" - description = "Version of Devolutions Gateway to install. Defaults to the latest available version." + default = "latest" + description = "Version of Devolutions Gateway to install. Use 'latest' for the most recent version, or specify a version like '2025.3.2'." } resource "coder_script" "windows-rdp" { @@ -77,10 +95,10 @@ resource "coder_script" "windows-rdp" { resource "coder_app" "windows-rdp" { agent_id = var.agent_id share = var.share - slug = "web-rdp" - display_name = "Web RDP" + slug = var.slug + display_name = var.display_name url = "http://localhost:7171" - icon = "/icon/desktop.svg" + icon = var.icon subdomain = true order = var.order group = var.group diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index 27c45b45..1657b878 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -2,6 +2,9 @@ function Set-AdminPassword { param ( [string]$adminPassword ) + # Explicitly import LocalAccounts module + Import-Module Microsoft.PowerShell.LocalAccounts -ErrorAction SilentlyContinue + # Set admin password Get-LocalUser -Name "${admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) # Enable admin user @@ -28,23 +31,61 @@ function Install-DevolutionsGateway { $moduleName = "DevolutionsGateway" $moduleVersion = "${devolutions_gateway_version}" +# Ensure TLS 1.2 is enabled for PSGallery +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + # Install the module with the specified version for all users # This requires administrator privileges try { # Install-PackageProvider is required for AWS. Need to set command to # terminate on failure so that try/catch actually triggers Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + + # Set PSGallery as trusted after NuGet is installed + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + + if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + Install-Module -Name $moduleName -Force + } else { + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } } catch { # If the first command failed, assume that we're on GCP and run # Install-Module only - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + Install-Module -Name $moduleName -Force + } else { + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } } # Construct the module path for system-wide installation -$moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" -$modulePath = Join-Path -Path $moduleBasePath -ChildPath "$moduleName.psd1" +$modulePath = $null # Declare outside the loop + +if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + $installedModule = Get-InstalledModule -Name $moduleName -ErrorAction SilentlyContinue + if ($installedModule) { + $installedVersion = $installedModule.Version.ToString() + } +} else { + $installedVersion = $moduleVersion +} + +$paths = $env:PSModulePath -split ';' + +foreach ($path in $paths) { + $candidatePath = Join-Path -Path $path -ChildPath $moduleName + if ($installedVersion) { + $candidatePath = Join-Path -Path $candidatePath -ChildPath $installedVersion + } + + $psd1Path = Join-Path -Path $candidatePath -ChildPath "$moduleName.psd1" + if (Test-Path $psd1Path) { + $modulePath = $psd1Path + break + } +} # Import the module using the full path Import-Module $modulePath From 92ab526733542e77e56cf0fc1c629b70ec22cadb Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Wed, 29 Oct 2025 19:57:15 -0400 Subject: [PATCH 21/22] feat: change boundary rules according to new spec (#517) --- registry/coder/modules/claude-code/README.md | 14 +++++++------- registry/coder/modules/claude-code/main.tf | 2 +- .../coder/modules/claude-code/scripts/start.sh | 5 +++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index c311eeb7..3a0ec420 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 = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.3.3" + version = "3.4.3" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,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 = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 926b2402..93b3761b 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -353,7 +353,7 @@ module "agentapi" { ARG_BOUNDARY_VERSION='${var.boundary_version}' \ ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \ ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \ - ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \ + ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join("|", var.boundary_additional_allowed_urls)}' \ ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \ ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \ ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \ diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 70452675..783e908d 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -144,12 +144,13 @@ function start_agentapi() { # Build boundary args with conditional --unprivileged flag BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR") # Add default allowed URLs - BOUNDARY_ARGS+=(--allow "*anthropic.com" --allow "registry.npmjs.org" --allow "*sentry.io" --allow "claude.ai" --allow "$ARG_CODER_HOST") + BOUNDARY_ARGS+=(--allow "domain=anthropic.com" --allow "domain=registry.npmjs.org" --allow "domain=sentry.io" --allow "domain=claude.ai" --allow "domain=$ARG_CODER_HOST") # Add any additional allowed URLs from the variable if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then - IFS=' ' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" + IFS='|' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" for url in "${ADDITIONAL_URLS[@]}"; do + # Quote the URL to preserve spaces within the allow rule BOUNDARY_ARGS+=(--allow "$url") done fi From 0ce65b2b586ffb4abedbee282b28b4a277c98979 Mon Sep 17 00:00:00 2001 From: uzair-coder07 Date: Thu, 30 Oct 2025 00:28:52 -0500 Subject: [PATCH 22/22] fix(coder-labs/modules/sourcegraph-amp): explicitly require external provider (#519) Co-authored-by: Atif Ali --- registry/coder-labs/modules/sourcegraph-amp/README.md | 4 ++-- registry/coder-labs/modules/sourcegraph-amp/main.tf | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/registry/coder-labs/modules/sourcegraph-amp/README.md b/registry/coder-labs/modules/sourcegraph-amp/README.md index 5a5039f0..608defd6 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/README.md +++ b/registry/coder-labs/modules/sourcegraph-amp/README.md @@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI ```tf module "amp-cli" { source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" - version = "2.0.0" + version = "2.0.1" agent_id = coder_agent.example.id sourcegraph_amp_api_key = var.sourcegraph_amp_api_key install_sourcegraph_amp = true @@ -48,7 +48,7 @@ variable "amp_api_key" { module "amp-cli" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" - amp_version = "2.0.0" + amp_version = "2.0.1" agent_id = coder_agent.example.id amp_api_key = var.amp_api_key # recommended for tasks usage workdir = "/home/coder/project" diff --git a/registry/coder-labs/modules/sourcegraph-amp/main.tf b/registry/coder-labs/modules/sourcegraph-amp/main.tf index ddaa475d..fc36ea8d 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/main.tf +++ b/registry/coder-labs/modules/sourcegraph-amp/main.tf @@ -6,7 +6,12 @@ terraform { source = "coder/coder" version = ">= 2.7" } + external = { + source = "hashicorp/external" + version = "2.3.5" + } } + } variable "agent_id" {