From 213aabb3b07a004bcc893c9b5b27d78a9e40a605 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 15 Sep 2025 09:00:07 +0100 Subject: [PATCH 01/11] fix(registry/modules/goose): default subdomain to false (#420) Relates to https://github.com/coder/coder/issues/18779 See also https://github.com/coder/registry/pull/419 By default, we set subdomain = true. Most folks testing this out don't have a wildcard subdomain setup. This switches to path-based behaviour by default and adds a note to the troubleshooting section. --- registry/coder/modules/goose/README.md | 6 ++- registry/coder/modules/goose/main.test.ts | 48 ++++++++++++++++------- registry/coder/modules/goose/main.tf | 2 +- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/registry/coder/modules/goose/README.md b/registry/coder/modules/goose/README.md index cdc63c3f..a1dbfefe 100644 --- a/registry/coder/modules/goose/README.md +++ b/registry/coder/modules/goose/README.md @@ -13,7 +13,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener ```tf module "goose" { source = "registry.coder.com/coder/goose/coder" - version = "2.1.1" + version = "2.1.2" agent_id = coder_agent.example.id folder = "/home/coder" install_goose = true @@ -79,7 +79,7 @@ resource "coder_agent" "main" { module "goose" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/goose/coder" - version = "2.1.1" + version = "2.1.2" agent_id = coder_agent.example.id folder = "/home/coder" install_goose = true @@ -123,4 +123,6 @@ Note: The indentation in the heredoc is preserved, so you can write the YAML nat ## Troubleshooting +By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details. + The module will create log files in the workspace's `~/.goose-module` directory. If you run into any issues, look at them for more information. diff --git a/registry/coder/modules/goose/main.test.ts b/registry/coder/modules/goose/main.test.ts index 9dd4dc79..ae5635b0 100644 --- a/registry/coder/modules/goose/main.test.ts +++ b/registry/coder/modules/goose/main.test.ts @@ -2,6 +2,7 @@ import { test, afterEach, describe, + it, setDefaultTimeout, beforeAll, expect, @@ -253,22 +254,41 @@ describe("goose", async () => { expect(prompt.stderr).toContain("No such file or directory"); }); - test("subdomain-false", async () => { - const { id } = await setup({ - agentapiMockScript: await loadTestFile( - import.meta.dir, - "agentapi-mock-print-args.js", - ), - moduleVariables: { - subdomain: "false", - }, + describe("subdomain", async () => { + it("sets AGENTAPI_CHAT_BASE_PATH when false", async () => { + const { id } = await setup({ + agentapiMockScript: await loadTestFile( + import.meta.dir, + "agentapi-mock-print-args.js", + ), + moduleVariables: { + subdomain: "false", + }, + }); + + await execModuleScript(id); + + const agentapiMockOutput = await readFileContainer(id, agentapiStartLog); + expect(agentapiMockOutput).toContain( + "AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/goose/chat", + ); }); - await execModuleScript(id); + it("does not set AGENTAPI_CHAT_BASE_PATH when true", async () => { + const { id } = await setup({ + agentapiMockScript: await loadTestFile( + import.meta.dir, + "agentapi-mock-print-args.js", + ), + moduleVariables: { + subdomain: "true", + }, + }); - const agentapiMockOutput = await readFileContainer(id, agentapiStartLog); - expect(agentapiMockOutput).toContain( - "AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/goose/chat", - ); + await execModuleScript(id); + + const agentapiMockOutput = await readFileContainer(id, agentapiStartLog); + expect(agentapiMockOutput).toMatch(/AGENTAPI_CHAT_BASE_PATH=$/m); + }); }); }); diff --git a/registry/coder/modules/goose/main.tf b/registry/coder/modules/goose/main.tf index 69f93aa3..2e015e76 100644 --- a/registry/coder/modules/goose/main.tf +++ b/registry/coder/modules/goose/main.tf @@ -69,7 +69,7 @@ variable "agentapi_version" { variable "subdomain" { type = bool description = "Whether to use a subdomain for AgentAPI." - default = true + default = false } variable "goose_provider" { From cb990bbee0119449e25ba1cf4a9e6d199381940c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 15 Sep 2025 13:09:12 +0100 Subject: [PATCH 02/11] fix(registry/modules/claude-code): default subdomain to false (#419) Relates to https://github.com/coder/coder/issues/18779 By default, we set `subdomain = true`. Most folks testing this out don't have a wildcard subdomain setup. This switches to path-based behaviour by default and adds a note to the troubleshooting section. --- registry/coder/modules/claude-code/README.md | 9 +++-- .../coder/modules/claude-code/main.test.ts | 35 ++++++++++++++++++- registry/coder/modules/claude-code/main.tf | 8 ++--- .../claude-code/testdata/agentapi-mock.js | 3 +- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 91e96636..e5223e4d 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 = "2.2.0" + version = "2.2.1" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true @@ -26,6 +26,9 @@ module "claude-code" { > this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as > the user running it. Use this module _only_ in trusted environments and be aware of the security implications. +> [!NOTE] +> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details. + ## Prerequisites - You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template @@ -83,7 +86,7 @@ resource "coder_agent" "main" { module "claude-code" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/claude-code/coder" - version = "2.2.0" + version = "2.2.1" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true @@ -101,7 +104,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "2.2.0" + version = "2.2.1" agent_id = coder_agent.example.id folder = "/home/coder" install_claude_code = true diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 08e4b488..e1647022 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -3,6 +3,7 @@ import { afterEach, expect, describe, + it, setDefaultTimeout, beforeAll, } from "bun:test"; @@ -100,6 +101,7 @@ const writeAgentAPIMockControl = async ({ interface SetupProps { skipAgentAPIMock?: boolean; skipClaudeMock?: boolean; + extraVars?: Record; } const projectDir = "/home/coder/project"; @@ -112,6 +114,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => { install_claude_code: "false", agentapi_version: "preview", folder: projectDir, + ...props?.extraVars, }, }); await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]); @@ -335,6 +338,36 @@ describe("claude-code", async () => { id, "/home/coder/agentapi-mock.log", ); - expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *"); + expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS=*"); + }); + + describe("subdomain", async () => { + it("sets AGENTAPI_CHAT_BASE_PATH when false", async () => { + const { id } = await setup(); + const respModuleScript = await execModuleScript(id); + expect(respModuleScript.exitCode).toBe(0); + await expectAgentAPIStarted(id); + const agentApiStartLog = await readFileContainer( + id, + "/home/coder/agentapi-mock.log", + ); + expect(agentApiStartLog).toContain( + "AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", + ); + }); + + it("does not set AGENTAPI_CHAT_BASE_PATH when true", async () => { + const { id } = await setup({ + extraVars: { subdomain: "true" }, + }); + const respModuleScript = await execModuleScript(id); + expect(respModuleScript.exitCode).toBe(0); + await expectAgentAPIStarted(id); + const agentApiStartLog = await readFileContainer( + id, + "/home/coder/agentapi-mock.log", + ); + expect(agentApiStartLog).toMatch(/AGENTAPI_CHAT_BASE_PATH=$/m); + }); }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index e56b8844..52fd3295 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -106,12 +106,12 @@ variable "agentapi_version" { variable "subdomain" { type = bool description = "Whether to use a subdomain for the Claude Code app." - default = true + default = false } locals { # we have to trim the slash because otherwise coder exp mcp will - # set up an invalid claude config + # set up an invalid claude config workdir = trimsuffix(var.folder, "/") 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) : "" @@ -244,7 +244,7 @@ resource "coder_script" "claude_code" { # Disable host header check since AgentAPI is proxied by Coder (which does its own validation) export AGENTAPI_ALLOWED_HOSTS="*" - + # Set chat base path for non-subdomain routing (only set if not using subdomain) export AGENTAPI_CHAT_BASE_PATH="${local.agentapi_chat_base_path}" @@ -295,4 +295,4 @@ resource "coder_ai_task" "claude_code" { sidebar_app { id = coder_app.claude_code_web.id } -} \ No newline at end of file +} diff --git a/registry/coder/modules/claude-code/testdata/agentapi-mock.js b/registry/coder/modules/claude-code/testdata/agentapi-mock.js index 5f37d3ca..e74f3c68 100644 --- a/registry/coder/modules/claude-code/testdata/agentapi-mock.js +++ b/registry/coder/modules/claude-code/testdata/agentapi-mock.js @@ -22,7 +22,8 @@ if ( fs.writeFileSync( "/home/coder/agentapi-mock.log", - `AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`, + `AGENTAPI_ALLOWED_HOSTS=${process.env.AGENTAPI_ALLOWED_HOSTS} + AGENTAPI_CHAT_BASE_PATH=${process.env.AGENTAPI_CHAT_BASE_PATH}`, ); console.log(`starting server on port ${port}`); From 54b9bf30388ffcc0d263eed07cc2e360cbb1d7fc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 18 Sep 2025 11:30:48 +0200 Subject: [PATCH 03/11] add: nextflow module (#416) --- .icons/nextflow.svg | 6 + .../coder-labs/modules/nextflow/README.md | 22 ++++ registry/coder-labs/modules/nextflow/main.tf | 106 ++++++++++++++++++ registry/coder-labs/modules/nextflow/run.sh | 49 ++++++++ 4 files changed, 183 insertions(+) create mode 100644 .icons/nextflow.svg create mode 100644 registry/coder-labs/modules/nextflow/README.md create mode 100644 registry/coder-labs/modules/nextflow/main.tf create mode 100644 registry/coder-labs/modules/nextflow/run.sh diff --git a/.icons/nextflow.svg b/.icons/nextflow.svg new file mode 100644 index 00000000..bcc10553 --- /dev/null +++ b/.icons/nextflow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/registry/coder-labs/modules/nextflow/README.md b/registry/coder-labs/modules/nextflow/README.md new file mode 100644 index 00000000..7a62a3ab --- /dev/null +++ b/registry/coder-labs/modules/nextflow/README.md @@ -0,0 +1,22 @@ +--- +display_name: Nextflow +description: A module that adds Nextflow to your Coder template. +icon: ../../../../.icons/nextflow.svg +verified: true +tags: [nextflow, workflow, hpc, bioinformatics] +--- + +# Nextflow + +A module that adds Nextflow to your Coder template. + +![Nextflow](../../.images/nextflow.png) + +```tf +module "nextflow" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder-labs/nextflow/coder" + version = "0.9.0" + agent_id = coder_agent.example.id +} +``` diff --git a/registry/coder-labs/modules/nextflow/main.tf b/registry/coder-labs/modules/nextflow/main.tf new file mode 100644 index 00000000..306b2110 --- /dev/null +++ b/registry/coder-labs/modules/nextflow/main.tf @@ -0,0 +1,106 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "nextflow_version" { + type = string + description = "Nextflow version" + default = "25.04.7" +} + +variable "project_path" { + type = string + description = "The path to Nextflow project, it will be mounted in the container." +} + +variable "http_server_port" { + type = number + description = "The port to run HTTP server on." + default = 9876 +} + +variable "http_server_reports_dir" { + type = string + description = "Subdirectory for HTTP server reports, relative to the project path." + default = "reports" +} + +variable "http_server_log_path" { + type = string + description = "HTTP server logs" + default = "/tmp/nextflow_reports.log" +} + +variable "stub_run" { + type = bool + description = "Execute a stub run?" + default = false +} + +variable "stub_run_command" { + type = string + description = "Nextflow command to be executed in the stub run." + default = "run rnaseq-nf -with-report reports/report.html -with-trace reports/trace.txt -with-timeline reports/timeline.html -with-dag reports/flowchart.png" +} + +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 +} + +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 "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +resource "coder_script" "nextflow" { + agent_id = var.agent_id + display_name = "nextflow" + icon = "/icon/nextflow.svg" + script = templatefile("${path.module}/run.sh", { + NEXTFLOW_VERSION : var.nextflow_version, + PROJECT_PATH : var.project_path, + HTTP_SERVER_PORT : var.http_server_port, + HTTP_SERVER_REPORTS_DIR : var.http_server_reports_dir, + HTTP_SERVER_LOG_PATH : var.http_server_log_path, + STUB_RUN : var.stub_run, + STUB_RUN_COMMAND : var.stub_run_command, + }) + run_on_start = true +} + +resource "coder_app" "nextflow" { + agent_id = var.agent_id + slug = "nextflow-reports" + display_name = "Nextflow Reports" + url = "http://localhost:${var.http_server_port}" + icon = "/icon/nextflow.svg" + subdomain = true + share = var.share + order = var.order + group = var.group +} diff --git a/registry/coder-labs/modules/nextflow/run.sh b/registry/coder-labs/modules/nextflow/run.sh new file mode 100644 index 00000000..83abbba5 --- /dev/null +++ b/registry/coder-labs/modules/nextflow/run.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env sh + +set -eu + +BOLD='\033[0;1m' +RESET='\033[0m' + +printf "$${BOLD}Starting Nextflow...$${RESET}\n" + +if ! command -v nextflow > /dev/null 2>&1; then + # Update system dependencies + sudo apt update + sudo apt install openjdk-21-jdk graphviz salmon fastqc multiqc -y + + # Install nextflow + export NXF_VER=${NEXTFLOW_VERSION} + curl -s https://get.nextflow.io | bash + sudo mv nextflow /usr/local/bin/ + sudo chmod +x /usr/local/bin/nextflow + + # Verify installation + tmp_verify=$(mktemp -d coder-nextflow-XXXXXX) + nextflow run hello \ + -with-report "$${tmp_verify}/report.html" \ + -with-trace "$${tmp_verify}/trace.txt" \ + -with-timeline "$${tmp_verify}/timeline.html" \ + -with-dag "$${tmp_verify}/flowchart.png" + rm -r "$${tmp_verify}" +else + echo "Nextflow is already installed\n\n" +fi + +if [ ! -z ${PROJECT_PATH} ]; then + # Project is located at PROJECT_PATH + echo "Change directory: ${PROJECT_PATH}" + cd ${PROJECT_PATH} +fi + +# Start a web server to preview reports +mkdir -p ${HTTP_SERVER_REPORTS_DIR} +echo "Starting HTTP server in background, check logs: ${HTTP_SERVER_LOG_PATH}" +python3 -m http.server --directory ${HTTP_SERVER_REPORTS_DIR} ${HTTP_SERVER_PORT} > "${HTTP_SERVER_LOG_PATH}" 2>&1 & + +# Stub run? +if [ "${STUB_RUN}" = "true" ]; then + nextflow ${STUB_RUN_COMMAND} -stub-run +fi + +printf "\n$${BOLD}Nextflow ${NEXTFLOW_VERSION} is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}$${RESET}\n" From d212de47ed75e4507f181482ef0cfc3ea2ebb1be Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Thu, 18 Sep 2025 20:34:52 +0530 Subject: [PATCH 04/11] feat: refactor claude code to use agentapi module (#402) Closes #302 ## Description ## Type of Change - [ ] New module - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/[namespace]/modules/[module-name]` **New version:** `v3.0.0` **Breaking change:** [ ] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [x] Changes tested locally ## Related Issues --------- Co-authored-by: DevCats Co-authored-by: Atif Ali --- registry/coder/modules/claude-code/README.md | 158 +++--- .../coder/modules/claude-code/main.test.ts | 517 ++++++++---------- registry/coder/modules/claude-code/main.tf | 430 +++++++-------- .../coder/modules/claude-code/main.tftest.hcl | 189 +++++++ .../claude-code/scripts/agentapi-start.sh | 63 --- .../scripts/agentapi-wait-for-start.sh | 30 - .../modules/claude-code/scripts/install.sh | 102 ++++ .../modules/claude-code/scripts/start.sh | 82 +++ .../claude-code/testdata/agentapi-mock.js | 40 -- .../claude-code/testdata/claude-mock.js | 9 - .../claude-code/testdata/claude-mock.sh | 13 + .../claude-code/testdata/coder-mock.js | 14 - 12 files changed, 916 insertions(+), 731 deletions(-) create mode 100644 registry/coder/modules/claude-code/main.tftest.hcl delete mode 100644 registry/coder/modules/claude-code/scripts/agentapi-start.sh delete mode 100644 registry/coder/modules/claude-code/scripts/agentapi-wait-for-start.sh create mode 100644 registry/coder/modules/claude-code/scripts/install.sh create mode 100644 registry/coder/modules/claude-code/scripts/start.sh delete mode 100644 registry/coder/modules/claude-code/testdata/agentapi-mock.js delete mode 100644 registry/coder/modules/claude-code/testdata/claude-mock.js create mode 100644 registry/coder/modules/claude-code/testdata/claude-mock.sh delete mode 100644 registry/coder/modules/claude-code/testdata/coder-mock.js diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index e5223e4d..04d8f8c8 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -1,120 +1,142 @@ --- display_name: Claude Code -description: Run Claude Code in your workspace +description: Run the Claude Code agent in your workspace. icon: ../../../../.icons/claude.svg verified: true -tags: [agent, claude-code, ai, tasks] +tags: [agent, claude-code, ai, tasks, anthropic] --- # Claude Code -Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. +Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI. ```tf module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "2.2.1" - agent_id = coder_agent.example.id - folder = "/home/coder" - install_claude_code = true - claude_code_version = "latest" + source = "registry.coder.com/coder/claude-code/coder" + version = "3.0.0" + agent_id = coder_agent.example.id + workdir = "/home/coder/project" + claude_api_key = "xxxx-xxxxx-xxxx" } ``` -> **Security Notice**: This module uses the [`--dangerously-skip-permissions`](https://docs.anthropic.com/en/docs/claude-code/cli-usage#cli-flags) flag when running Claude Code. This flag -> bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While -> this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as -> the user running it. Use this module _only_ in trusted environments and be aware of the security implications. +> [!WARNING] +> **Security Notice**: This module uses the `--dangerously-skip-permissions` flag when running Claude Code tasks. This flag bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as the user running it. Use this module _only_ in trusted environments and be aware of the security implications. > [!NOTE] > By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details. ## Prerequisites -- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template - -The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces. +- An **Anthropic API key** or a _Claude Session Token_ is required for tasks. + - 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) ## Examples -### Run in the background and report tasks (Experimental) +### Usage with Tasks and Advanced Configuration -> 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. +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. ```tf -variable "anthropic_api_key" { - type = string - description = "The Anthropic API key" - sensitive = true -} - -module "coder-login" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/coder-login/coder" - version = "1.0.15" - agent_id = coder_agent.example.id -} - data "coder_parameter" "ai_prompt" { type = "string" name = "AI Prompt" default = "" - description = "Write a prompt for Claude Code" + description = "Initial task prompt for Claude Code." mutable = true } -# Set the prompt and system prompt for Claude Code via environment variables -resource "coder_agent" "main" { - # ... - env = { - CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter - CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value - CODER_MCP_APP_STATUS_SLUG = "claude-code" - CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT - You are a helpful assistant that can help with code. - EOT - } -} - module "claude-code" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/claude-code/coder" - version = "2.2.1" - agent_id = coder_agent.example.id - folder = "/home/coder" - install_claude_code = true - claude_code_version = "1.0.40" + source = "registry.coder.com/coder/claude-code/coder" + version = "3.0.0" + agent_id = coder_agent.example.id + workdir = "/home/coder/project" - # Enable experimental features - experiment_report_tasks = true + claude_api_key = "xxxx-xxxxx-xxxx" + # OR + claude_code_oauth_token = "xxxxx-xxxx-xxxx" + + claude_code_version = "1.0.82" # Pin to a specific version + agentapi_version = "v0.6.1" + + ai_prompt = data.coder_parameter.ai_prompt.value + model = "sonnet" + + permission_mode = "plan" + + mcp = <<-EOF + { + "mcpServers": { + "my-custom-tool": { + "command": "my-tool-server" + "args": ["--port", "8080"] + } + } + } + EOF } ``` -## Run standalone +### Standalone Mode -Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it without any task reporting to the Coder UI. +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 = "2.2.1" + version = "3.0.0" agent_id = coder_agent.example.id - folder = "/home/coder" + workdir = "/home/coder" install_claude_code = true claude_code_version = "latest" + report_tasks = false + cli_app = true +} +``` - # Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL - icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png" +### Usage with Claude Code Subscription + +```tf + +variable "claude_code_oauth_token" { + type = string + description = "Generate one using `claude setup-token` command" + sensitive = true + value = "xxxx-xxx-xxxx" +} + +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "3.0.0" + agent_id = coder_agent.example.id + workdir = "/home/coder/project" + claude_code_oauth_token = var.claude_code_oauth_token } ``` ## Troubleshooting -The module will create log files in the workspace's `~/.claude-module` directory. If you run into any issues, look at them for more information. +If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information. + +```bash +# Installation logs +cat ~/.claude-module/install.log + +# Startup logs +cat ~/.claude-module/agentapi-start.log + +# Pre/post install script logs +cat ~/.claude-module/pre_install.log +cat ~/.claude-module/post_install.log +``` + +> [!NOTE] +> To use tasks with Claude Code, you must provide an `anthropic_api_key` or `claude_code_oauth_token`. +> The `workdir` variable is required and specifies the directory where Claude Code will run. + +## References + +- [Claude Code Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) +- [AgentAPI Documentation](https://github.com/coder/agentapi) +- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index e1647022..9c132f1a 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -1,38 +1,26 @@ import { test, afterEach, - expect, describe, - it, setDefaultTimeout, beforeAll, + expect, } from "bun:test"; -import path from "path"; +import { execContainer, readFileContainer, runTerraformInit } from "~test"; import { - execContainer, - findResourceInstance, - readFileContainer, - removeContainer, - runContainer, - runTerraformApply, - runTerraformInit, - writeCoder, - writeFileContainer, -} from "~test"; + loadTestFile, + writeExecutable, + setup as setupUtil, + execModuleScript, + expectAgentAPIStarted, +} from "../agentapi/test-util"; +import dedent from "dedent"; let cleanupFunctions: (() => Promise)[] = []; - const registerCleanup = (cleanup: () => Promise) => { cleanupFunctions.push(cleanup); }; - -// Cleanup logic depends on the fact that bun's built-in test runner -// runs tests sequentially. -// https://bun.sh/docs/test/discovery#execution-order -// Weird things would happen if tried to run tests in parallel. -// One test could clean up resources that another test was still using. afterEach(async () => { - // reverse the cleanup functions so that they are run in the correct order const cleanupFnsCopy = cleanupFunctions.slice().reverse(); cleanupFunctions = []; for (const cleanup of cleanupFnsCopy) { @@ -44,330 +32,281 @@ afterEach(async () => { } }); -const setupContainer = async ({ - image, - vars, -}: { - image?: string; - vars?: Record; -} = {}) => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - ...vars, - }); - const coderScript = findResourceInstance(state, "coder_script"); - const id = await runContainer(image ?? "codercom/enterprise-node:latest"); - registerCleanup(() => removeContainer(id)); - return { id, coderScript }; -}; - -const loadTestFile = async (...relativePath: string[]) => { - return await Bun.file( - path.join(import.meta.dir, "testdata", ...relativePath), - ).text(); -}; - -const writeExecutable = async ({ - containerId, - filePath, - content, -}: { - containerId: string; - filePath: string; - content: string; -}) => { - await writeFileContainer(containerId, filePath, content, { - user: "root", - }); - await execContainer( - containerId, - ["bash", "-c", `chmod 755 ${filePath}`], - ["--user", "root"], - ); -}; - -const writeAgentAPIMockControl = async ({ - containerId, - content, -}: { - containerId: string; - content: string; -}) => { - await writeFileContainer(containerId, "/tmp/agentapi-mock.control", content, { - user: "coder", - }); -}; - interface SetupProps { skipAgentAPIMock?: boolean; skipClaudeMock?: boolean; - extraVars?: Record; + moduleVariables?: Record; + agentapiMockScript?: string; } -const projectDir = "/home/coder/project"; - const setup = async (props?: SetupProps): Promise<{ id: string }> => { - const { id, coderScript } = await setupContainer({ - vars: { - experiment_report_tasks: "true", + const projectDir = "/home/coder/project"; + const { id } = await setupUtil({ + moduleDir: import.meta.dir, + moduleVariables: { + install_claude_code: props?.skipClaudeMock ? "true" : "false", install_agentapi: props?.skipAgentAPIMock ? "true" : "false", - install_claude_code: "false", - agentapi_version: "preview", - folder: projectDir, - ...props?.extraVars, + workdir: projectDir, + ...props?.moduleVariables, }, + registerCleanup, + projectDir, + skipAgentAPIMock: props?.skipAgentAPIMock, + agentapiMockScript: props?.agentapiMockScript, }); - await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]); - // the module script assumes that there is a coder executable in the PATH - await writeCoder(id, await loadTestFile("coder-mock.js")); - if (!props?.skipAgentAPIMock) { - await writeExecutable({ - containerId: id, - filePath: "/usr/bin/agentapi", - content: await loadTestFile("agentapi-mock.js"), - }); - } if (!props?.skipClaudeMock) { await writeExecutable({ containerId: id, filePath: "/usr/bin/claude", - content: await loadTestFile("claude-mock.js"), + content: await loadTestFile(import.meta.dir, "claude-mock.sh"), }); } - await writeExecutable({ - containerId: id, - filePath: "/home/coder/script.sh", - content: coderScript.script, - }); return { id }; }; -const expectAgentAPIStarted = async (id: string) => { - const resp = await execContainer(id, [ - "bash", - "-c", - `curl -fs -o /dev/null "http://localhost:3284/status"`, - ]); - if (resp.exitCode !== 0) { - console.log("agentapi not started"); - console.log(resp.stdout); - console.log(resp.stderr); - } - expect(resp.exitCode).toBe(0); -}; - -const execModuleScript = async (id: string) => { - const resp = await execContainer(id, [ - "bash", - "-c", - `set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`, - ]); - if (resp.exitCode !== 0) { - console.log(resp.stdout); - console.log(resp.stderr); - } - return resp; -}; - -// increase the default timeout to 60 seconds setDefaultTimeout(60 * 1000); -// we don't run these tests in CI because they take too long and make network -// calls. they are dedicated for local development. describe("claude-code", async () => { beforeAll(async () => { await runTerraformInit(import.meta.dir); }); - // test that the script runs successfully if claude starts without any errors test("happy-path", async () => { const { id } = await setup(); + await execModuleScript(id); + await expectAgentAPIStarted(id); + }); + + test("install-claude-code-version", async () => { + const version_to_install = "1.0.40"; + const { id } = await setup({ + skipClaudeMock: true, + moduleVariables: { + install_claude_code: "true", + claude_code_version: version_to_install, + }, + }); + await execModuleScript(id); + const resp = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/install.log", + ]); + expect(resp.stdout).toContain(version_to_install); + }); + + test("check-latest-claude-code-version-works", async () => { + const { id } = await setup({ + skipClaudeMock: true, + skipAgentAPIMock: true, + moduleVariables: { + install_claude_code: "true", + }, + }); + await execModuleScript(id); + await expectAgentAPIStarted(id); + }); + + test("claude-api-key", async () => { + const apiKey = "test-api-key-123"; + const { id } = await setup({ + moduleVariables: { + claude_api_key: apiKey, + }, + }); + await execModuleScript(id); + + const envCheck = await execContainer(id, [ + "bash", + "-c", + 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', + ]); + expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); + }); + + test("claude-mcp-config", async () => { + const mcpConfig = JSON.stringify({ + mcpServers: { + test: { + command: "test-cmd", + type: "stdio", + }, + }, + }); + const { id } = await setup({ + skipClaudeMock: true, + moduleVariables: { + mcp: mcpConfig, + }, + }); + await execModuleScript(id); + + const resp = await readFileContainer(id, "/home/coder/.claude.json"); + expect(resp).toContain("test-cmd"); + }); + + test("claude-task-prompt", async () => { + const prompt = "This is a task prompt for Claude."; + const { id } = await setup({ + moduleVariables: { + ai_prompt: prompt, + }, + }); + await execModuleScript(id); const resp = await execContainer(id, [ "bash", "-c", - "sudo /home/coder/script.sh", + "cat /home/coder/.claude-module/agentapi-start.log", ]); - expect(resp.exitCode).toBe(0); - - await expectAgentAPIStarted(id); + expect(resp.stdout).toContain(prompt); }); - // test that the script removes lastSessionId from the .claude.json file - test("last-session-id-removed", async () => { - const { id } = await setup(); + test("claude-permission-mode", async () => { + const mode = "plan"; + const { id } = await setup({ + moduleVariables: { + permission_mode: mode, + task_prompt: "test prompt", + }, + }); + await execModuleScript(id); - await writeFileContainer( - id, - "/home/coder/.claude.json", - JSON.stringify({ - projects: { - [projectDir]: { - lastSessionId: "123", - }, - }, - }), - ); - - const catResp = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude.json", - ]); - expect(catResp.exitCode).toBe(0); - expect(catResp.stdout).toContain("lastSessionId"); - - const respModuleScript = await execModuleScript(id); - expect(respModuleScript.exitCode).toBe(0); - - await expectAgentAPIStarted(id); - - const catResp2 = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude.json", - ]); - expect(catResp2.exitCode).toBe(0); - expect(catResp2.stdout).not.toContain("lastSessionId"); - }); - - // test that the script handles a .claude.json file that doesn't contain - // a lastSessionId field - test("last-session-id-not-found", async () => { - const { id } = await setup(); - - await writeFileContainer( - id, - "/home/coder/.claude.json", - JSON.stringify({ - projects: { - "/home/coder": {}, - }, - }), - ); - - const respModuleScript = await execModuleScript(id); - expect(respModuleScript.exitCode).toBe(0); - - await expectAgentAPIStarted(id); - - const catResp = await execContainer(id, [ + const startLog = await execContainer(id, [ "bash", "-c", "cat /home/coder/.claude-module/agentapi-start.log", ]); - expect(catResp.exitCode).toBe(0); - expect(catResp.stdout).toContain( - "No lastSessionId found in .claude.json - nothing to do", - ); + expect(startLog.stdout).toContain(`--permission-mode ${mode}`); }); - // test that if claude fails to run with the --continue flag and returns a - // no conversation found error, then the module script retries without the flag - test("no-conversation-found", async () => { - const { id } = await setup(); - await writeAgentAPIMockControl({ - containerId: id, - content: "no-conversation-found", + test("claude-model", async () => { + const model = "opus"; + const { id } = await setup({ + moduleVariables: { + model: model, + task_prompt: "test prompt", + }, }); - // check that mocking works - const respAgentAPI = await execContainer(id, [ + await execModuleScript(id); + + const startLog = await execContainer(id, [ "bash", "-c", - "agentapi --continue", + "cat /home/coder/.claude-module/agentapi-start.log", ]); - expect(respAgentAPI.exitCode).toBe(1); - expect(respAgentAPI.stderr).toContain("No conversation found to continue"); - - const respModuleScript = await execModuleScript(id); - expect(respModuleScript.exitCode).toBe(0); - - await expectAgentAPIStarted(id); + expect(startLog.stdout).toContain(`--model ${model}`); }); - test("install-agentapi", async () => { - const { id } = await setup({ skipAgentAPIMock: true }); - - const respModuleScript = await execModuleScript(id); - expect(respModuleScript.exitCode).toBe(0); - - await expectAgentAPIStarted(id); - const respAgentAPI = await execContainer(id, [ - "bash", - "-c", - "agentapi --version", - ]); - expect(respAgentAPI.exitCode).toBe(0); - }); - - // the coder binary should be executed with specific env vars - // that are set by the module script - test("coder-env-vars", async () => { - const { id } = await setup(); - - const respModuleScript = await execModuleScript(id); - expect(respModuleScript.exitCode).toBe(0); - - const respCoderMock = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/coder-mock-output.json", - ]); - if (respCoderMock.exitCode !== 0) { - console.log(respCoderMock.stdout); - console.log(respCoderMock.stderr); - } - expect(respCoderMock.exitCode).toBe(0); - expect(JSON.parse(respCoderMock.stdout)).toEqual({ - statusSlug: "ccw", - agentApiUrl: "http://localhost:3284", + test("claude-continue-previous-conversation", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + task_prompt: "test prompt", + }, }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("--continue"); }); - // verify that the agentapi binary has access to the AGENTAPI_ALLOWED_HOSTS environment variable - // set in main.tf - test("agentapi-allowed-hosts", async () => { - const { id } = await setup(); + test("pre-post-install-scripts", async () => { + const { id } = await setup({ + moduleVariables: { + pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", + post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", + }, + }); + await execModuleScript(id); - const respModuleScript = await execModuleScript(id); - expect(respModuleScript.exitCode).toBe(0); - - await expectAgentAPIStarted(id); - - const agentApiStartLog = await readFileContainer( + const preInstallLog = await readFileContainer( id, - "/home/coder/agentapi-mock.log", + "/home/coder/.claude-module/pre_install.log", ); - expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS=*"); + expect(preInstallLog).toContain("claude-pre-install-script"); + + const postInstallLog = await readFileContainer( + id, + "/home/coder/.claude-module/post_install.log", + ); + expect(postInstallLog).toContain("claude-post-install-script"); }); - describe("subdomain", async () => { - it("sets AGENTAPI_CHAT_BASE_PATH when false", async () => { - const { id } = await setup(); - const respModuleScript = await execModuleScript(id); - expect(respModuleScript.exitCode).toBe(0); - await expectAgentAPIStarted(id); - const agentApiStartLog = await readFileContainer( - id, - "/home/coder/agentapi-mock.log", - ); - expect(agentApiStartLog).toContain( - "AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", - ); + test("workdir-variable", async () => { + const workdir = "/home/coder/claude-test-folder"; + const { id } = await setup({ + skipClaudeMock: false, + moduleVariables: { + workdir, + }, + }); + await execModuleScript(id); + + const resp = await readFileContainer( + id, + "/home/coder/.claude-module/agentapi-start.log", + ); + expect(resp).toContain(workdir); + }); + + test("coder-mcp-config-created", async () => { + const { id } = await setup({ + moduleVariables: { + install_claude_code: "false", + }, + }); + await execModuleScript(id); + + const installLog = await readFileContainer( + id, + "/home/coder/.claude-module/install.log", + ); + expect(installLog).toContain( + "Configuring Claude Code to report tasks via Coder MCP", + ); + }); + + test("dangerously-skip-permissions", async () => { + const { id } = await setup({ + moduleVariables: { + dangerously_skip_permissions: "true", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); + }); + + test("subdomain-false", async () => { + const { id } = await setup({ + skipAgentAPIMock: true, + moduleVariables: { + subdomain: "false", + post_install_script: dedent` + #!/bin/bash + env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" + `, + }, }); - it("does not set AGENTAPI_CHAT_BASE_PATH when true", async () => { - const { id } = await setup({ - extraVars: { subdomain: "true" }, - }); - const respModuleScript = await execModuleScript(id); - expect(respModuleScript.exitCode).toBe(0); - await expectAgentAPIStarted(id); - const agentApiStartLog = await readFileContainer( - id, - "/home/coder/agentapi-mock.log", - ); - expect(agentApiStartLog).toMatch(/AGENTAPI_CHAT_BASE_PATH=$/m); - }); + await execModuleScript(id); + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/post_install.log", + ]); + expect(startLog.stdout).toContain( + "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", + ); }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 52fd3295..d391e479 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -36,12 +36,72 @@ variable "icon" { default = "/icon/claude.svg" } -variable "folder" { +variable "workdir" { type = string description = "The folder to run Claude Code in." - default = "/home/coder" } +variable "report_tasks" { + type = bool + description = "Whether to enable task reporting to Coder UI via AgentAPI" + default = true +} + +variable "cli_app" { + type = bool + description = "Whether to create a CLI app for Claude Code" + default = false +} + +variable "web_app_display_name" { + type = string + description = "Display name for the web app" + default = "Claude Code" +} + +variable "cli_app_display_name" { + type = string + description = "Display name for the CLI app" + default = "Claude Code CLI" +} + +variable "pre_install_script" { + type = string + description = "Custom script to run before installing Claude Code." + default = null +} + +variable "post_install_script" { + type = string + description = "Custom script to run after installing Claude Code." + 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.7.1" +} + +variable "ai_prompt" { + type = string + description = "Initial task prompt for Claude Code." + default = "" +} + +variable "subdomain" { + type = bool + description = "Whether to use a subdomain for AgentAPI." + default = false +} + + variable "install_claude_code" { type = bool description = "Whether to install Claude Code." @@ -54,245 +114,179 @@ variable "claude_code_version" { default = "latest" } -variable "experiment_cli_app" { +variable "claude_api_key" { + type = string + description = "The API key to use for the Claude Code server." + default = "" +} + +variable "model" { + type = string + description = "Sets the model for the current session with an alias for the latest model (sonnet or opus) or a model’s full name." + default = "" +} + +variable "resume_session_id" { + type = string + description = "Resume a specific session by ID." + default = "" +} + +variable "continue" { type = bool - description = "Whether to create the CLI workspace app." + 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 } -variable "experiment_cli_app_order" { - type = number - description = "The order of the CLI workspace app." - default = null -} - -variable "experiment_cli_app_group" { - type = string - description = "The group of the CLI workspace app." - default = null -} - -variable "experiment_report_tasks" { +variable "dangerously_skip_permissions" { type = bool - description = "Whether to enable task reporting." + description = "Skip the permission prompts. Use with caution. This will be set to true if using Coder Tasks" default = false } -variable "experiment_pre_install_script" { +variable "permission_mode" { type = string - description = "Custom script to run before installing Claude Code." - default = null + description = "Permission mode for the cli, check https://docs.anthropic.com/en/docs/claude-code/iam#permission-modes" + default = "" + validation { + condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode) + error_message = "interaction_mode must be one of: default, acceptEdits, plan, bypassPermissions." + } } -variable "experiment_post_install_script" { +variable "mcp" { type = string - description = "Custom script to run after installing Claude Code." - default = null + description = "MCP JSON to be added to the claude code local scope" + default = "" } - -variable "install_agentapi" { - type = bool - description = "Whether to install AgentAPI." - default = true -} - -variable "agentapi_version" { +variable "allowed_tools" { type = string - description = "The version of AgentAPI to install." - default = "v0.3.3" + description = "A list of tools that should be allowed without prompting the user for permission, in addition to settings.json files." + default = "" } -variable "subdomain" { - type = bool - description = "Whether to use a subdomain for the Claude Code app." - default = false +variable "disallowed_tools" { + type = string + description = "A list of tools that should be disallowed without prompting the user for permission, in addition to settings.json files." + default = "" + +} + +variable "claude_code_oauth_token" { + type = string + description = "Set up a long-lived authentication token (requires Claude subscription). Generated using `claude setup-token` command" + sensitive = true + default = "" +} + +variable "system_prompt" { + type = string + description = "The system prompt to use for the Claude Code server." + default = "Send a task status update to notify the user that you are ready for input, and then wait for user input." +} + +variable "claude_md_path" { + type = string + description = "The path to CLAUDE.md." + default = "$HOME/.claude/CLAUDE.md" +} + +resource "coder_env" "claude_code_md_path" { + count = var.claude_md_path == "" ? 0 : 1 + + agent_id = var.agent_id + name = "CODER_MCP_CLAUDE_MD_PATH" + value = var.claude_md_path +} + +resource "coder_env" "claude_code_system_prompt" { + count = var.system_prompt == "" ? 0 : 1 + + agent_id = var.agent_id + name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT" + value = var.system_prompt +} + +resource "coder_env" "claude_code_oauth_token" { + agent_id = var.agent_id + name = "CLAUDE_CODE_OAUTH_TOKEN" + value = var.claude_code_oauth_token +} + +resource "coder_env" "claude_api_key" { + count = length(var.claude_api_key) > 0 ? 1 : 0 + + agent_id = var.agent_id + name = "CLAUDE_API_KEY" + value = var.claude_api_key } locals { # we have to trim the slash because otherwise coder exp mcp will - # set up an invalid claude config - workdir = trimsuffix(var.folder, "/") - 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) : "" - agentapi_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-start.sh")) - agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh")) - remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh")) - claude_code_app_slug = "ccw" - // Chat base path is only set if not using a subdomain. - // NOTE: - // - Initial support for --chat-base-path was added in v0.3.1 but configuration - // via environment variable AGENTAPI_CHAT_BASE_PATH was added in v0.3.3. - // - As CODER_WORKSPACE_AGENT_NAME is a recent addition we use agent ID - // for backward compatibility. - agentapi_chat_base_path = var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.claude_code_app_slug}/chat" - server_base_path = var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.claude_code_app_slug}" - healthcheck_url = "http://localhost:3284${local.server_base_path}/status" + # set up an invalid claude config + workdir = trimsuffix(var.workdir, "/") + app_slug = "ccw" + install_script = file("${path.module}/scripts/install.sh") + 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")) } -# Install and Initialize Claude Code -resource "coder_script" "claude_code" { - agent_id = var.agent_id - display_name = "Claude Code" - icon = var.icon - script = <<-EOT +module "agentapi" { + + source = "registry.coder.com/coder/agentapi/coder" + version = "1.1.1" + + 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 -o errexit + set -o pipefail + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh" + chmod +x /tmp/start.sh + chmod +x /tmp/remove-last-session-id.sh + + ARG_MODEL='${var.model}' \ + ARG_RESUME_SESSION_ID='${var.resume_session_id}' \ + ARG_CONTINUE='${var.continue}' \ + ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \ + ARG_PERMISSION_MODE='${var.permission_mode}' \ + ARG_WORKDIR='${local.workdir}' \ + ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ + /tmp/start.sh + EOT + + install_script = <<-EOT #!/bin/bash - set -e - set -x + set -o errexit + set -o pipefail - command_exists() { - command -v "$1" >/dev/null 2>&1 - } - - function install_claude_code_cli() { - echo "Installing Claude Code via official installer" - set +e - curl -fsSL claude.ai/install.sh | bash -s -- "${var.claude_code_version}" 2>&1 - CURL_EXIT=$${PIPESTATUS[0]} - set -e - if [ $CURL_EXIT -ne 0 ]; then - echo "Claude Code installer failed with exit code $$CURL_EXIT" - fi - - # Ensure binaries are discoverable. - export PATH="~/.local/bin:$PATH" - echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')" - } - - if [ ! -d "${local.workdir}" ]; then - echo "Warning: The specified folder '${local.workdir}' does not exist." - echo "Creating the folder..." - mkdir -p "${local.workdir}" - echo "Folder created successfully." - 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_claude_code}" = "true" ]; then - install_claude_code_cli - fi - - # Install AgentAPI if enabled - if [ "${var.install_agentapi}" = "true" ]; then - echo "Installing AgentAPI..." - arch=$(uname -m) - if [ "$arch" = "x86_64" ]; then - binary_name="agentapi-linux-amd64" - elif [ "$arch" = "aarch64" ]; then - binary_name="agentapi-linux-arm64" - else - echo "Error: Unsupported architecture: $arch" - exit 1 - fi - curl \ - --retry 5 \ - --retry-delay 5 \ - --fail \ - --retry-all-errors \ - -L \ - -C - \ - -o agentapi \ - "https://github.com/coder/agentapi/releases/download/${var.agentapi_version}/$binary_name" - chmod +x agentapi - sudo mv agentapi /usr/local/bin/agentapi - fi - if ! command_exists agentapi; then - echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually." - exit 1 - fi - - # this must be kept in sync with the agentapi-start.sh script - module_path="$HOME/.claude-module" - mkdir -p "$module_path/scripts" - - # save the prompt for the agentapi start command - echo -n "$CODER_MCP_CLAUDE_TASK_PROMPT" > "$module_path/prompt.txt" - - echo -n "${local.agentapi_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-start.sh" - echo -n "${local.agentapi_wait_for_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-wait-for-start.sh" - echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "$module_path/scripts/remove-last-session-id.sh" - chmod +x "$module_path/scripts/agentapi-start.sh" - chmod +x "$module_path/scripts/agentapi-wait-for-start.sh" - - if [ "${var.experiment_report_tasks}" = "true" ]; then - echo "Configuring Claude Code to report tasks via Coder MCP..." - export CODER_MCP_APP_STATUS_SLUG="${local.claude_code_app_slug}" - export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284" - coder exp mcp configure claude-code "${local.workdir}" - 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 ! command_exists claude; then - echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually." - exit 1 - fi - - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 - - cd "${local.workdir}" - - # Disable host header check since AgentAPI is proxied by Coder (which does its own validation) - export AGENTAPI_ALLOWED_HOSTS="*" - - # Set chat base path for non-subdomain routing (only set if not using subdomain) - export AGENTAPI_CHAT_BASE_PATH="${local.agentapi_chat_base_path}" - - nohup "$module_path/scripts/agentapi-start.sh" use_prompt &> "$module_path/agentapi-start.log" & - "$module_path/scripts/agentapi-wait-for-start.sh" - EOT - run_on_start = true -} - -resource "coder_app" "claude_code_web" { - # use a short slug to mitigate https://github.com/coder/coder/issues/15178 - slug = local.claude_code_app_slug - display_name = "Claude Code Web" - agent_id = var.agent_id - url = "http://localhost:3284/" - icon = var.icon - order = var.order - group = var.group - subdomain = var.subdomain - healthcheck { - url = local.healthcheck_url - interval = 3 - threshold = 20 - } -} - -resource "coder_app" "claude_code" { - count = var.experiment_cli_app ? 1 : 0 - - slug = "claude-code" - display_name = "Claude Code CLI" - agent_id = var.agent_id - command = <<-EOT - #!/bin/bash - set -e - - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 - - agentapi attach - EOT - icon = var.icon - order = var.experiment_cli_app_order - group = var.experiment_cli_app_group -} - -resource "coder_ai_task" "claude_code" { - sidebar_app { - id = coder_app.claude_code_web.id - } + echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh + chmod +x /tmp/install.sh + ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \ + ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \ + ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \ + ARG_REPORT_TASKS='${var.report_tasks}' \ + ARG_WORKDIR='${local.workdir}' \ + ARG_ALLOWED_TOOLS='${var.allowed_tools}' \ + ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \ + ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ + /tmp/install.sh + EOT } diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl new file mode 100644 index 00000000..7931cca8 --- /dev/null +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -0,0 +1,189 @@ +run "test_claude_code_basic" { + command = plan + + variables { + agent_id = "test-agent-123" + workdir = "/home/coder/projects" + } + + assert { + condition = var.workdir == "/home/coder/projects" + error_message = "Workdir variable should be set correctly" + } + + assert { + condition = var.agent_id == "test-agent-123" + error_message = "Agent ID variable should be set correctly" + } + + assert { + condition = var.install_claude_code == true + error_message = "Install claude_code should default to true" + } + + assert { + condition = var.install_agentapi == true + error_message = "Install agentapi should default to true" + } + + assert { + condition = var.report_tasks == true + error_message = "report_tasks should default to true" + } +} + +run "test_claude_code_with_api_key" { + command = plan + + variables { + agent_id = "test-agent-456" + workdir = "/home/coder/workspace" + claude_api_key = "test-api-key-123" + } + + assert { + condition = coder_env.claude_api_key.value == "test-api-key-123" + error_message = "Claude API key value should match the input" + } +} + +run "test_claude_code_with_custom_options" { + command = plan + + variables { + agent_id = "test-agent-789" + workdir = "/home/coder/custom" + order = 5 + group = "development" + icon = "/icon/custom.svg" + model = "opus" + task_prompt = "Help me write better code" + permission_mode = "plan" + continue = true + install_claude_code = false + install_agentapi = false + claude_code_version = "1.0.0" + agentapi_version = "v0.6.0" + dangerously_skip_permissions = true + } + + 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 == "opus" + error_message = "Claude model variable should be set to 'opus'" + } + + assert { + condition = var.task_prompt == "Help me write better code" + error_message = "Task prompt variable should be set correctly" + } + + assert { + condition = var.permission_mode == "plan" + error_message = "Permission mode should be set to 'plan'" + } + + assert { + condition = var.continue == true + error_message = "Continue should be set to true" + } + + assert { + condition = var.claude_code_version == "1.0.0" + error_message = "Claude Code version should be set to '1.0.0'" + } + + assert { + condition = var.agentapi_version == "v0.6.0" + error_message = "AgentAPI version should be set to 'v0.6.0'" + } + + assert { + condition = var.dangerously_skip_permissions == true + error_message = "dangerously_skip_permissions should be set to true" + } +} + +run "test_claude_code_with_mcp_and_tools" { + command = plan + + variables { + agent_id = "test-agent-mcp" + workdir = "/home/coder/mcp-test" + mcp = jsonencode({ + mcpServers = { + test = { + command = "test-server" + args = ["--config", "test.json"] + } + } + }) + allowed_tools = "bash,python" + disallowed_tools = "rm" + } + + assert { + condition = var.mcp != "" + error_message = "MCP configuration should be provided" + } + + assert { + condition = var.allowed_tools == "bash,python" + error_message = "Allowed tools should be set" + } + + assert { + condition = var.disallowed_tools == "rm" + error_message = "Disallowed tools should be set" + } +} + +run "test_claude_code_with_scripts" { + command = plan + + variables { + agent_id = "test-agent-scripts" + workdir = "/home/coder/scripts" + 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_claude_code_permission_mode_validation" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + permission_mode = "acceptEdits" + } + + assert { + condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode) + error_message = "Permission mode should be one of the valid options" + } +} diff --git a/registry/coder/modules/claude-code/scripts/agentapi-start.sh b/registry/coder/modules/claude-code/scripts/agentapi-start.sh deleted file mode 100644 index eb7e8f23..00000000 --- a/registry/coder/modules/claude-code/scripts/agentapi-start.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -set -o errexit -set -o pipefail - -# this must be kept in sync with the main.tf file -module_path="$HOME/.claude-module" -scripts_dir="$module_path/scripts" -log_file_path="$module_path/agentapi.log" - -# if the first argument is not empty, start claude with the prompt -if [ -n "$1" ]; then - cp "$module_path/prompt.txt" /tmp/claude-code-prompt -else - rm -f /tmp/claude-code-prompt -fi - -# if the log file already exists, archive it -if [ -f "$log_file_path" ]; then - mv "$log_file_path" "$log_file_path"".$(date +%s)" -fi - -# see the remove-last-session-id.sh script for details -# about why we need it -# avoid exiting if the script fails -bash "$scripts_dir/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true - -# we'll be manually handling errors from this point on -set +o errexit - -function start_agentapi() { - local continue_flag="$1" - local prompt_subshell='"$(cat /tmp/claude-code-prompt)"' - - # use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters - # visible in the terminal screen by default. - agentapi server --term-width 67 --term-height 1190 -- \ - bash -c "claude $continue_flag --dangerously-skip-permissions $prompt_subshell" \ - > "$log_file_path" 2>&1 -} - -echo "Starting AgentAPI..." - -# attempt to start claude with the --continue flag -start_agentapi --continue -exit_code=$? - -echo "First AgentAPI exit code: $exit_code" - -if [ $exit_code -eq 0 ]; then - exit 0 -fi - -# if there was no conversation to continue, claude exited with an error. -# start claude without the --continue flag. -if grep -q "No conversation found to continue" "$log_file_path"; then - echo "AgentAPI with --continue flag failed, starting claude without it." - start_agentapi - exit_code=$? -fi - -echo "Second AgentAPI exit code: $exit_code" - -exit $exit_code diff --git a/registry/coder/modules/claude-code/scripts/agentapi-wait-for-start.sh b/registry/coder/modules/claude-code/scripts/agentapi-wait-for-start.sh deleted file mode 100644 index b9e76d36..00000000 --- a/registry/coder/modules/claude-code/scripts/agentapi-wait-for-start.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -set -o errexit -set -o pipefail - -# This script waits for the agentapi server to start on port 3284. -# It considers the server started after 3 consecutive successful responses. - -agentapi_started=false - -echo "Waiting for agentapi server to start on port 3284..." -for i in $(seq 1 150); do - for j in $(seq 1 3); do - sleep 0.1 - if curl -fs -o /dev/null "http://localhost:3284/status"; then - echo "agentapi response received ($j/3)" - else - echo "agentapi server not responding ($i/15)" - continue 2 - fi - done - agentapi_started=true - break -done - -if [ "$agentapi_started" != "true" ]; then - echo "Error: agentapi server did not start on port 3284 after 15 seconds." - exit 1 -fi - -echo "agentapi server started on port 3284." diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh new file mode 100644 index 00000000..c3dcc22f --- /dev/null +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -0,0 +1,102 @@ +#!/bin/bash +set -euo pipefail + +source "$HOME"/.bashrc + +BOLD='\033[0;1m' + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-} +ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} +ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} +ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} +ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} +ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d) +ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-} +ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-} + +echo "--------------------------------" + +printf "ARG_CLAUDE_CODE_VERSION: %s\n" "$ARG_CLAUDE_CODE_VERSION" +printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" +printf "ARG_INSTALL_CLAUDE_CODE: %s\n" "$ARG_INSTALL_CLAUDE_CODE" +printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" +printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG" +printf "ARG_MCP: %s\n" "$ARG_MCP" +printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS" +printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS" + +echo "--------------------------------" + +function install_claude_code_cli() { + if [ "$ARG_INSTALL_CLAUDE_CODE" = "true" ]; then + echo "Installing Claude Code via official installer" + set +e + curl -fsSL claude.ai/install.sh | bash -s -- "$ARG_CLAUDE_CODE_VERSION" 2>&1 + CURL_EXIT=${PIPESTATUS[0]} + set -e + if [ $CURL_EXIT -ne 0 ]; then + echo "Claude Code installer failed with exit code $$CURL_EXIT" + fi + + # Ensure binaries are discoverable. + echo "Creating a symlink for claude" + sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude + + echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')" + else + echo "Skipping Claude Code installation as per configuration." + fi +} + +function setup_claude_configurations() { + if [ ! -d "$ARG_WORKDIR" ]; then + echo "Warning: The specified folder '$ARG_WORKDIR' does not exist." + echo "Creating the folder..." + mkdir -p "$ARG_WORKDIR" + echo "Folder created successfully." + fi + + module_path="$HOME/.claude-module" + mkdir -p "$module_path" + + if [ "$ARG_MCP" != "" ]; then + while IFS= read -r server_name && IFS= read -r server_json; do + echo "------------------------" + echo "Executing: claude mcp add \"$server_name\" '$server_json'" + claude mcp add "$server_name" "$server_json" + echo "------------------------" + echo "" + done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') + fi + + if [ -n "$ARG_ALLOWED_TOOLS" ]; then + coder --allowedTools "$ARG_ALLOWED_TOOLS" + fi + + if [ -n "$ARG_DISALLOWED_TOOLS" ]; then + coder --disallowedTools "$ARG_DISALLOWED_TOOLS" + fi + +} + +function report_tasks() { + if [ "$ARG_REPORT_TASKS" = "true" ]; then + echo "Configuring Claude Code to report tasks via Coder MCP..." + 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 +} + +install_claude_code_cli +setup_claude_configurations +report_tasks diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh new file mode 100644 index 00000000..b5fca7a5 --- /dev/null +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -0,0 +1,82 @@ +#!/bin/bash +set -euo pipefail + +source "$HOME"/.bashrc +export PATH="$HOME/.local/bin:$PATH" + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_MODEL=${ARG_MODEL:-} +ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-} +ARG_CONTINUE=${ARG_CONTINUE:-false} +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) + +echo "--------------------------------" + +printf "ARG_MODEL: %s\n" "$ARG_MODEL" +printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID" +printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE" +printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" +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" + +echo "--------------------------------" + +# see the remove-last-session-id.sh script for details +# about why we need it +# avoid exiting if the script fails +bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true + +function validate_claude_installation() { + if command_exists claude; then + printf "Claude Code is installed\n" + else + printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n" + exit 1 + fi +} + +ARGS=() + +function build_claude_args() { + 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 + ARGS+=(--dangerously-skip-permissions) + 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[@]}" +} + +validate_claude_installation +build_claude_args +start_agentapi diff --git a/registry/coder/modules/claude-code/testdata/agentapi-mock.js b/registry/coder/modules/claude-code/testdata/agentapi-mock.js deleted file mode 100644 index e74f3c68..00000000 --- a/registry/coder/modules/claude-code/testdata/agentapi-mock.js +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env node - -const http = require("http"); -const fs = require("fs"); -const args = process.argv.slice(2); -const port = 3284; - -const controlFile = "/tmp/agentapi-mock.control"; -let control = ""; -if (fs.existsSync(controlFile)) { - control = fs.readFileSync(controlFile, "utf8"); -} - -if ( - control === "no-conversation-found" && - args.join(" ").includes("--continue") -) { - // this must match the error message in the agentapi-start.sh script - console.error("No conversation found to continue"); - process.exit(1); -} - -fs.writeFileSync( - "/home/coder/agentapi-mock.log", - `AGENTAPI_ALLOWED_HOSTS=${process.env.AGENTAPI_ALLOWED_HOSTS} - AGENTAPI_CHAT_BASE_PATH=${process.env.AGENTAPI_CHAT_BASE_PATH}`, -); - -console.log(`starting server on port ${port}`); - -http - .createServer(function (_request, response) { - response.writeHead(200); - response.end( - JSON.stringify({ - status: "stable", - }), - ); - }) - .listen(port); diff --git a/registry/coder/modules/claude-code/testdata/claude-mock.js b/registry/coder/modules/claude-code/testdata/claude-mock.js deleted file mode 100644 index ea9f9aa9..00000000 --- a/registry/coder/modules/claude-code/testdata/claude-mock.js +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env node - -const main = async () => { - console.log("mocking claude"); - // sleep for 30 minutes - await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000)); -}; - -main(); diff --git a/registry/coder/modules/claude-code/testdata/claude-mock.sh b/registry/coder/modules/claude-code/testdata/claude-mock.sh new file mode 100644 index 00000000..b437b4d3 --- /dev/null +++ b/registry/coder/modules/claude-code/testdata/claude-mock.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [[ "$1" == "--version" ]]; then + echo "claude version v1.0.0" + exit 0 +fi + +set -e + +while true; do + echo "$(date) - claude-mock" + sleep 15 +done diff --git a/registry/coder/modules/claude-code/testdata/coder-mock.js b/registry/coder/modules/claude-code/testdata/coder-mock.js deleted file mode 100644 index cc479f43..00000000 --- a/registry/coder/modules/claude-code/testdata/coder-mock.js +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node - -const fs = require("fs"); - -const statusSlugEnvVar = "CODER_MCP_APP_STATUS_SLUG"; -const agentApiUrlEnvVar = "CODER_MCP_AI_AGENTAPI_URL"; - -fs.writeFileSync( - "/home/coder/coder-mock-output.json", - JSON.stringify({ - statusSlug: process.env[statusSlugEnvVar] ?? "env var not set", - agentApiUrl: process.env[agentApiUrlEnvVar] ?? "env var not set", - }), -); From 6af8508bc0d576981108927b16e3cb266d626672 Mon Sep 17 00:00:00 2001 From: DevCats Date: Fri, 19 Sep 2025 14:51:37 -0500 Subject: [PATCH 05/11] chore: update tasks template for claude-code update (#423) ## Description Refactor template for claude-code module update for tasks ## Type of Change - [ ] New module - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [X] Other ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun run fmt`) - [X] Changes tested locally ## Related Issues https://github.com/coder/registry/pull/402 --------- Co-authored-by: Atif Ali --- .../coder-labs/templates/tasks-docker/main.tf | 48 ++++--------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/registry/coder-labs/templates/tasks-docker/main.tf b/registry/coder-labs/templates/tasks-docker/main.tf index c966a977..c0a165fc 100644 --- a/registry/coder-labs/templates/tasks-docker/main.tf +++ b/registry/coder-labs/templates/tasks-docker/main.tf @@ -22,31 +22,16 @@ provider "docker" {} module "claude-code" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/claude-code/coder" - version = "2.0.0" + version = "3.0.0" agent_id = coder_agent.main.id - folder = "/home/coder/projects" - install_claude_code = true - claude_code_version = "latest" + workdir = "/home/coder/projects" order = 999 - - experiment_post_install_script = data.coder_parameter.setup_script.value - - # This enables Coder Tasks - experiment_report_tasks = true -} - -# You can also use a model provider, like AWS Bedrock or Vertex by replacing -# this with the special env vars from the Claude Code docs. -# see: https://docs.anthropic.com/en/docs/claude-code/third-party-integrations -variable "anthropic_api_key" { - type = string - description = "Generate one at: https://console.anthropic.com/settings/keys" - sensitive = true -} -resource "coder_env" "anthropic_api_key" { - agent_id = coder_agent.main.id - name = "CODER_MCP_CLAUDE_API_KEY" - value = var.anthropic_api_key + claude_api_key = "" + ai_prompt = data.coder_parameter.ai_prompt.value + system_prompt = data.coder_parameter.system_prompt.value + model = "sonnet" + permission_mode = "plan" + post_install_script = data.coder_parameter.setup_script.value } # We are using presets to set the prompts, image, and set up instructions @@ -172,23 +157,6 @@ data "coder_parameter" "preview_port" { mutable = false } -# Other variables for Claude Code -resource "coder_env" "claude_task_prompt" { - agent_id = coder_agent.main.id - name = "CODER_MCP_CLAUDE_TASK_PROMPT" - value = data.coder_parameter.ai_prompt.value -} -resource "coder_env" "app_status_slug" { - agent_id = coder_agent.main.id - name = "CODER_MCP_APP_STATUS_SLUG" - value = "ccw" -} -resource "coder_env" "claude_system_prompt" { - agent_id = coder_agent.main.id - name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT" - value = data.coder_parameter.system_prompt.value -} - data "coder_provisioner" "me" {} data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} From f0045397d4228111fda4dfe133b2d00c303940b3 Mon Sep 17 00:00:00 2001 From: Rafael Rodriguez Date: Mon, 22 Sep 2025 13:29:12 -0500 Subject: [PATCH 06/11] feat: add tooltip support to jetbrains module (#421) ## Description In this pull request we're updating the JetBrains module to support the tooltip field added as requested in https://github.com/coder/coder/pull/19781#pullrequestreview-3214217375 ## Type of Change - [ ] New module - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/jetbrains` **New version:** `v1.1.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [x] Changes tested locally ## Related Issues https://github.com/coder/coder/issues/18431 --------- Co-authored-by: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> --- registry/coder/modules/jetbrains/README.md | 36 +++++++++++++++---- .../modules/jetbrains/jetbrains.tftest.hcl | 31 ++++++++++++++++ registry/coder/modules/jetbrains/main.test.ts | 30 ++++++++++++++++ registry/coder/modules/jetbrains/main.tf | 7 ++++ 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md index 5a9bd7ae..ef19ec20 100644 --- a/registry/coder/modules/jetbrains/README.md +++ b/registry/coder/modules/jetbrains/README.md @@ -14,9 +14,10 @@ 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.0.3" + version = "1.1.0" 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 } ``` @@ -39,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.0.3" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA @@ -52,7 +53,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.0.3" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/project" # Show parameter with limited options @@ -66,7 +67,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.0.3" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["IU", "PY"] @@ -81,7 +82,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.0.3" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/workspace/project" @@ -107,7 +108,7 @@ module "jetbrains" { module "jetbrains_pycharm" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.0.3" + version = "1.1.0" agent_id = coder_agent.example.id folder = "/workspace/project" @@ -119,6 +120,22 @@ module "jetbrains_pycharm" { } ``` +### Custom Tooltip + +Add helpful tooltip text that appears when users hover over the IDE app buttons: + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.1.0" + agent_id = coder_agent.example.id + folder = "/home/coder/project" + default = ["IU", "PY"] + tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." +} +``` + ## Behavior ### Parameter vs Direct Apps @@ -132,6 +149,13 @@ module "jetbrains_pycharm" { - If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config` - `major_version` and `channel` control which API endpoint is queried (when API access is available) +### Tooltip + +- **`tooltip`**: Optional markdown text displayed when hovering over IDE app buttons +- If not specified, no tooltip is shown +- Supports markdown formatting for rich text (bold, italic, links, etc.) +- All IDE apps created by this module will show the same tooltip text + ## Supported IDEs All JetBrains IDEs with remote development capabilities: diff --git a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl index e5c00a78..7676c34f 100644 --- a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl +++ b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl @@ -129,3 +129,34 @@ run "app_order_when_default_not_empty" { error_message = "Expected coder_app order to be set to 10" } } + +run "tooltip_when_provided" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."]) + error_message = "Expected coder_app tooltip to be set when provided" + } +} + +run "tooltip_null_when_not_provided" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == null]) + error_message = "Expected coder_app tooltip to be null when not provided" + } +} diff --git a/registry/coder/modules/jetbrains/main.test.ts b/registry/coder/modules/jetbrains/main.test.ts index 73f7650d..0acf2ec2 100644 --- a/registry/coder/modules/jetbrains/main.test.ts +++ b/registry/coder/modules/jetbrains/main.test.ts @@ -276,6 +276,36 @@ describe("jetbrains", async () => { ); expect(parameter?.instances[0].attributes.order).toBe(5); }); + + it("should set tooltip when specified", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/home/coder", + default: '["GO"]', + tooltip: + "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button.", + }); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "jetbrains", + ); + expect(coder_app?.instances[0].attributes.tooltip).toBe( + "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button.", + ); + }); + + it("should have null tooltip when not specified", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/home/coder", + default: '["GO"]', + }); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "jetbrains", + ); + expect(coder_app?.instances[0].attributes.tooltip).toBeNull(); + }); }); // URL Generation Tests diff --git a/registry/coder/modules/jetbrains/main.tf b/registry/coder/modules/jetbrains/main.tf index e4f5ec35..d33fc6b2 100644 --- a/registry/coder/modules/jetbrains/main.tf +++ b/registry/coder/modules/jetbrains/main.tf @@ -59,6 +59,12 @@ variable "coder_parameter_order" { default = null } +variable "tooltip" { + type = string + description = "Markdown text that is displayed when hovering over workspace apps." + default = null +} + variable "major_version" { type = string description = "The major version of the IDE. i.e. 2025.1" @@ -232,6 +238,7 @@ resource "coder_app" "jetbrains" { external = true order = var.coder_app_order group = var.group + tooltip = var.tooltip url = join("", [ "jetbrains://gateway/coder?&workspace=", # requires 2.6.3+ version of Toolbox data.coder_workspace.me.name, From e516446d03fd39dc96b50b1da0410745a38c92fa Mon Sep 17 00:00:00 2001 From: Benraouane Soufiane <110255764+BenraouaneSoufiane@users.noreply.github.com> Date: Tue, 23 Sep 2025 02:04:24 +0100 Subject: [PATCH 07/11] Add Rustdesk module (#266) Closes #79 ## Description This PR add new module, install minimal desktop environment (xfce), virtual display, ,rustdesk package from deb file, init new screen, export DISPLAY environment variable with last created virtual screen, start new xfce session & execute the rustdesk cli, generate new password, change the default password, then log the ID & password to be used within rustdesk client to connect to the host ## Type of Change - [x] New module - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information Overview/test video: live demo that launch rustdesk with GUI in a docker container https://youtu.be/_rR-l7nARN4 Screenshots: image image image image **Path:** `registry/BenraouaneSoufiane/modules/rustdesk` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [x] Changes tested locally ## Related Issues /claim #79 (remain asset 150$) --------- Co-authored-by: root Co-authored-by: DevCats Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .icons/rustdesk.svg | 5 + .../BenraouaneSoufiane/.images/avatar.png | Bin 0 -> 28637 bytes registry/BenraouaneSoufiane/README.md | 14 +++ .../modules/rustdesk/README.md | 82 ++++++++++++ .../modules/rustdesk/main.tf | 75 +++++++++++ .../modules/rustdesk/run.sh | 117 ++++++++++++++++++ 6 files changed, 293 insertions(+) create mode 100644 .icons/rustdesk.svg create mode 100644 registry/BenraouaneSoufiane/.images/avatar.png create mode 100644 registry/BenraouaneSoufiane/README.md create mode 100644 registry/BenraouaneSoufiane/modules/rustdesk/README.md create mode 100644 registry/BenraouaneSoufiane/modules/rustdesk/main.tf create mode 100644 registry/BenraouaneSoufiane/modules/rustdesk/run.sh diff --git a/.icons/rustdesk.svg b/.icons/rustdesk.svg new file mode 100644 index 00000000..6c801233 --- /dev/null +++ b/.icons/rustdesk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/registry/BenraouaneSoufiane/.images/avatar.png b/registry/BenraouaneSoufiane/.images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..297e89dfcadaf14bc6ef1f60d30e1fce3925098f GIT binary patch literal 28637 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 literal 0 HcmV?d00001 diff --git a/registry/BenraouaneSoufiane/README.md b/registry/BenraouaneSoufiane/README.md new file mode 100644 index 00000000..3067538a --- /dev/null +++ b/registry/BenraouaneSoufiane/README.md @@ -0,0 +1,14 @@ +--- +display_name: "Benraouane Soufiane" +bio: "Full stack developer creating awesome things." +avatar: "./.images/avatar.png" +github: "benraouanesoufiane" +linkedin: "https://www.linkedin.com/in/benraouane-soufiane" # Optional +website: "https://benraouanesoufiane.com" # Optional +support_email: "hello@benraouanesoufiane.com" # Optional +status: "community" +--- + +# Benraouane Soufiane + +Full stack developer creating awesome things. diff --git a/registry/BenraouaneSoufiane/modules/rustdesk/README.md b/registry/BenraouaneSoufiane/modules/rustdesk/README.md new file mode 100644 index 00000000..ae7c2896 --- /dev/null +++ b/registry/BenraouaneSoufiane/modules/rustdesk/README.md @@ -0,0 +1,82 @@ +--- +display_name: RustDesk +description: Run RustDesk in your workspace with virtual display +icon: ../../../../.icons/rustdesk.svg +verified: false +tags: [rustdesk, rdp, vm] +--- + +# RustDesk + +Launches RustDesk within your workspace with a virtual display to provide remote desktop access. The module outputs the RustDesk ID and password needed to connect from external RustDesk clients. + +```tf +module "rustdesk" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Features + +- Automatically sets up virtual display (Xvfb) +- Downloads and configures RustDesk +- Outputs RustDesk ID and password for easy connection +- Provides external app link to RustDesk web client for browser-based access +- Starts virtual display (Xvfb) with customizable resolution +- Customizable screen resolution and RustDesk version + +## Requirements + +- Coder v2.5 or higher +- Linux workspace with `apt`, `dnf`, or `yum` package manager + +## Examples + +### Custom configuration with specific version + +```tf +module "rustdesk" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + rustdesk_password = "mycustompass" + xvfb_resolution = "1920x1080x24" + rustdesk_version = "1.4.1" +} +``` + +### Docker container configuration + +It requires coder' server to be run as root, when using with Docker, add the following to your `docker_container` resource: + +```tf +resource "docker_container" "workspace" { + + # ... other configuration ... + + user = "root" + privileged = true + network_mode = "host" + + ports { + internal = 21115 + external = 21115 + } + ports { + internal = 21116 + external = 21116 + } + ports { + internal = 21118 + external = 21118 + } + ports { + internal = 21119 + external = 21119 + } +} +``` diff --git a/registry/BenraouaneSoufiane/modules/rustdesk/main.tf b/registry/BenraouaneSoufiane/modules/rustdesk/main.tf new file mode 100644 index 00000000..14edcff1 --- /dev/null +++ b/registry/BenraouaneSoufiane/modules/rustdesk/main.tf @@ -0,0 +1,75 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "log_path" { + type = string + description = "The path to log rustdesk to." + default = "/tmp/rustdesk.log" +} + +variable "agent_id" { + description = "Attach RustDesk setup to this agent" + type = string +} + +variable "order" { + description = "Run order among scripts/apps" + type = number + default = 1 +} + +# Optional knobs passed as env (you can expose these as variables too) +variable "rustdesk_password" { + description = "If empty, the script will generate one" + type = string + default = "" + sensitive = true +} + +variable "xvfb_resolution" { + description = "Xvfb screen size/depth" + type = string + default = "1024x768x16" +} + +variable "rustdesk_version" { + description = "RustDesk version to install (use 'latest' for most recent release)" + type = string + default = "latest" +} + +resource "coder_script" "rustdesk" { + agent_id = var.agent_id + display_name = "RustDesk" + run_on_start = true + + # Prepend env as bash exports, then append the script file literally. + script = <<-EOT + # --- module-provided env knobs --- + export RUSTDESK_PASSWORD="${var.rustdesk_password}" + export XVFB_RESOLUTION="${var.xvfb_resolution}" + export RUSTDESK_VERSION="${var.rustdesk_version}" + # --------------------------------- + +${file("${path.module}/run.sh")} + EOT +} + +resource "coder_app" "rustdesk" { + agent_id = var.agent_id + slug = "rustdesk" + display_name = "Rustdesk" + url = "https://rustdesk.com/web" + icon = "/icon/rustdesk.svg" + order = var.order + external = true +} + diff --git a/registry/BenraouaneSoufiane/modules/rustdesk/run.sh b/registry/BenraouaneSoufiane/modules/rustdesk/run.sh new file mode 100644 index 00000000..f6837ce2 --- /dev/null +++ b/registry/BenraouaneSoufiane/modules/rustdesk/run.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +BOLD='\033[0;1m' +RESET='\033[0m' + +printf "${BOLD}πŸ–₯️ Installing RustDesk Remote Desktop\n${RESET}" + +# ---- configurable knobs (env overrides) ---- +RUSTDESK_VERSION="${RUSTDESK_VERSION:-latest}" +LOG_PATH="${LOG_PATH:-/tmp/rustdesk.log}" + +# ---- fetch latest version if needed ---- +if [ "$RUSTDESK_VERSION" = "latest" ]; then + printf "πŸ” Fetching latest RustDesk version...\n" + RUSTDESK_VERSION=$(curl -s https://api.github.com/repos/rustdesk/rustdesk/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || echo "1.4.1") + printf "πŸ“Œ Fetched RustDesk version: ${RUSTDESK_VERSION}\n" +else + printf "πŸ“Œ Using specified RustDesk version: ${RUSTDESK_VERSION}\n" +fi +XVFB_RESOLUTION="${XVFB_RESOLUTION:-1024x768x16}" +RUSTDESK_PASSWORD="${RUSTDESK_PASSWORD:-}" + +# ---- detect package manager & arch ---- +ARCH="$(uname -m)" +case "$ARCH" in + x86_64 | amd64) PKG_ARCH="x86_64" ;; + aarch64 | arm64) PKG_ARCH="aarch64" ;; + *) + echo "❌ Unsupported arch: $ARCH" + exit 1 + ;; +esac + +if command -v apt-get > /dev/null 2>&1; then + PKG_SYS="deb" + PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.deb" + INSTALL_DEPS='apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y wget libva2 libva-drm2 libva-x11-2 libgstreamer-plugins-base1.0-0 gstreamer1.0-pipewire xfce4 xfce4-goodies xvfb x11-xserver-utils dbus-x11 libegl1 libgl1 libglx0 libglu1-mesa mesa-utils libxrandr2 libxss1 libgtk-3-0t64 libgbm1 libdrm2 libxcomposite1 libxdamage1 libxfixes3' + INSTALL_CMD="apt-get install -y ./${PKG_NAME}" + CLEAN_CMD="rm -f \"${PKG_NAME}\"" +elif command -v dnf > /dev/null 2>&1; then + PKG_SYS="rpm" + PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm" + INSTALL_DEPS='dnf install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes' + INSTALL_CMD="dnf install -y ./${PKG_NAME}" + CLEAN_CMD="rm -f \"${PKG_NAME}\"" +elif command -v yum > /dev/null 2>&1; then + PKG_SYS="rpm" + PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm" + INSTALL_DEPS='yum install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes' + INSTALL_CMD="yum install -y ./${PKG_NAME}" + CLEAN_CMD="rm -f \"${PKG_NAME}\"" +else + echo "❌ Unsupported distro: need apt, dnf, or yum." + exit 1 +fi + +# ---- install rustdesk if missing ---- +if ! command -v rustdesk > /dev/null 2>&1; then + printf "πŸ“¦ Installing dependencies...\n" + sudo bash -c "$INSTALL_DEPS" 2>&1 | tee -a "${LOG_PATH}" + + printf "⬇️ Downloading RustDesk ${RUSTDESK_VERSION} (${PKG_SYS}, ${PKG_ARCH})...\n" + URL="https://github.com/rustdesk/rustdesk/releases/download/${RUSTDESK_VERSION}/${PKG_NAME}" + wget -q "$URL" 2>&1 | tee -a "${LOG_PATH}" + + printf "πŸ”§ Installing RustDesk...\n" + sudo bash -c "$INSTALL_CMD" 2>&1 | tee -a "${LOG_PATH}" + + printf "🧹 Cleaning up...\n" + bash -c "$CLEAN_CMD" 2>&1 | tee -a "${LOG_PATH}" +else + printf "βœ… RustDesk already installed\n" +fi + +# ---- start virtual display ---- +echo "Starting Xvfb with resolution ${XVFB_RESOLUTION}…" +Xvfb :99 -screen 0 "${XVFB_RESOLUTION}" >> "${LOG_PATH}" 2>&1 & +export DISPLAY=:99 + +# Wait for X to be ready +for i in {1..10}; do + if xdpyinfo -display :99 > /dev/null 2>&1; then + echo "X display is ready" + break + fi + sleep 1 +done + +# ---- create (or accept) password and start rustdesk ---- +if [[ -z "${RUSTDESK_PASSWORD}" ]]; then + RUSTDESK_PASSWORD="$(tr -dc 'a-zA-Z0-9@' < /dev/urandom | head -c 10)@97" +fi + +echo "Starting XFCE desktop environment..." +xfce4-session >> "${LOG_PATH}" 2>&1 & + +echo "Waiting for xfce4-session to initialize..." +sleep 5 + +printf "πŸ” Setting RustDesk password and starting service...\n" +rustdesk >> "${LOG_PATH}" 2>&1 & +sleep 2 + +rustdesk --password "${RUSTDESK_PASSWORD}" >> "${LOG_PATH}" 2>&1 & +sleep 3 + +RID="$(rustdesk --get-id 2> /dev/null || echo 'ID_PENDING')" + +printf "πŸ₯³ RustDesk setup complete!\n\n" +printf "${BOLD}πŸ“‹ Connection Details:${RESET}\n" +printf " RustDesk ID: ${RID}\n" +printf " RustDesk Password: ${RUSTDESK_PASSWORD}\n" +printf " Display: ${DISPLAY} (${XVFB_RESOLUTION})\n" +printf "\nπŸ“ Logs available at: ${LOG_PATH}\n\n" + +echo "Setup script completed successfully. All services running in background." +exit 0 From 80f429faf10a4e31ba3ab8849c439e96e221c7d0 Mon Sep 17 00:00:00 2001 From: DevCats Date: Tue, 30 Sep 2025 07:44:41 -0500 Subject: [PATCH 08/11] chore: remove it wrappers from required variables tests (#442) ## Description Remove it wrappers from required variables tf test in jfrog-oauth and jfrog-token modules. This solves the failing tf tests that we were encountering in all PR's across the board. ## Type of Change - [ ] New module - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun run fmt`) - [X] Changes tested locally ## Related Issues --- registry/coder/modules/jfrog-oauth/main.test.ts | 10 ++++------ registry/coder/modules/jfrog-token/main.test.ts | 12 +++++------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/registry/coder/modules/jfrog-oauth/main.test.ts b/registry/coder/modules/jfrog-oauth/main.test.ts index 0a8e62c0..940d166b 100644 --- a/registry/coder/modules/jfrog-oauth/main.test.ts +++ b/registry/coder/modules/jfrog-oauth/main.test.ts @@ -24,12 +24,10 @@ describe("jfrog-oauth", async () => { const fakeFrogUrl = "http://localhost:8081"; const user = "default"; - it("can run apply with required variables", async () => { - testRequiredVariables(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - package_managers: "{}", - }); + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + jfrog_url: fakeFrogUrl, + package_managers: "{}", }); it("generates an npmrc with scoped repos", async () => { diff --git a/registry/coder/modules/jfrog-token/main.test.ts b/registry/coder/modules/jfrog-token/main.test.ts index 9e3097b0..419b5f28 100644 --- a/registry/coder/modules/jfrog-token/main.test.ts +++ b/registry/coder/modules/jfrog-token/main.test.ts @@ -55,13 +55,11 @@ describe("jfrog-token", async () => { const user = "default"; const token = "xxx"; - it("can run apply with required variables", async () => { - testRequiredVariables(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - artifactory_access_token: "XXXX", - package_managers: "{}", - }); + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + jfrog_url: fakeFrogUrl, + artifactory_access_token: "XXXX", + package_managers: "{}", }); it("generates an npmrc with scoped repos", async () => { From 80acbd7e3a95290fe1ebb562b6675d1f08a074c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:53:03 +0000 Subject: [PATCH 09/11] chore(deps): bump crate-ci/typos from 1.36.2 to 1.36.3 in the github-actions group (#438) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1bdc5f68..39eb0197 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,7 +48,7 @@ jobs: - name: Validate formatting run: bun fmt:ci - name: Check for typos - uses: crate-ci/typos@v1.36.2 + uses: crate-ci/typos@v1.36.3 with: config: .github/typos.toml validate-readme-files: From 44354b202dc76988462bb508cad4433d432171df Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 30 Sep 2025 18:02:35 +0500 Subject: [PATCH 10/11] Fix claude-code module not passing workdir to agentapi (#439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #436 - The claude-code 3.0.0 module was not passing the custom `workdir` variable to the agentapi module, causing it to default to `/home/coder` instead of using the specified working directory. ## Changes - Added missing `folder = local.workdir` parameter to the agentapi module call in `main.tf:247` - This ensures that custom working directories are properly propagated to the agentapi module ## Test Plan - [x] Terraform validation passes - [x] Code formatting applied with `bun run fmt` - [x] Basic terraform test passes (one pre-existing test failure unrelated to this change) ## Verification The fix adds the missing parameter that was identified in the issue: ```terraform module "agentapi" { # ... other parameters folder = local.workdir # <- Added this line # ... rest of configuration } ``` πŸ€– Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: DevCats --- registry/coder/modules/claude-code/README.md | 8 ++++---- registry/coder/modules/claude-code/main.tf | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 04d8f8c8..3c334e74 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.0.0" + version = "3.0.1" 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.0.0" + version = "3.0.1" 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.0.0" + version = "3.0.1" 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.0.0" + version = "3.0.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index d391e479..4836347b 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -244,6 +244,7 @@ module "agentapi" { web_app_group = var.group web_app_icon = var.icon web_app_display_name = var.web_app_display_name + folder = local.workdir 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 From 60fec19d7d42ebbe853b78bb4649a4e10fbe3f56 Mon Sep 17 00:00:00 2001 From: Jiachen Jiang Date: Tue, 30 Sep 2025 09:14:16 -0700 Subject: [PATCH 11/11] Update README.md (#440) Added recommendation to the Gateway README, pointing to the Toolbox module. --------- Co-authored-by: DevCats --- registry/coder/modules/jetbrains-gateway/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/jetbrains-gateway/README.md b/registry/coder/modules/jetbrains-gateway/README.md index 7caa6dad..51b9a1ef 100644 --- a/registry/coder/modules/jetbrains-gateway/README.md +++ b/registry/coder/modules/jetbrains-gateway/README.md @@ -10,6 +10,8 @@ tags: [ide, jetbrains, parameter, gateway] This module adds a JetBrains Gateway Button to open any workspace with a single click. +> We recommend using the [Coder Toolbox module](https://registry.coder.com/modules/coder/jetbrains), which offers significant stability and connectivity benefits over Gateway. Reference our [documentation](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) for more information. + JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM. Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements. @@ -17,7 +19,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains-gateway/coder" - version = "1.2.2" + version = "1.2.3" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"] @@ -35,7 +37,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains-gateway/coder" - version = "1.2.2" + version = "1.2.3" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"] @@ -49,7 +51,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains-gateway/coder" - version = "1.2.2" + version = "1.2.3" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["IU", "PY"] @@ -64,7 +66,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains-gateway/coder" - version = "1.2.2" + version = "1.2.3" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["IU", "PY"] @@ -89,7 +91,7 @@ module "jetbrains_gateway" { module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains-gateway/coder" - version = "1.2.2" + version = "1.2.3" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"] @@ -107,7 +109,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra module "jetbrains_gateway" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains-gateway/coder" - version = "1.2.2" + version = "1.2.3" agent_id = coder_agent.example.id folder = "/home/coder/example" jetbrains_ides = ["GO", "WS"]