diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 1ca7c9c5..b4a895de 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id openai_api_key = var.openai_api_key workdir = "/home/coder/project" @@ -32,7 +32,7 @@ module "codex" { module "codex" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" @@ -51,7 +51,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" enable_aibridge = true @@ -94,7 +94,7 @@ data "coder_task" "me" {} module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id openai_api_key = "..." ai_prompt = data.coder_task.me.prompt @@ -112,7 +112,7 @@ This example shows additional configuration options for custom models, MCP serve ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 2f65df86..cc07ce2f 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -131,7 +131,7 @@ variable "install_agentapi" { variable "agentapi_version" { type = string description = "The version of AgentAPI to install." - default = "v0.11.6" + default = "v0.11.8" } variable "codex_model" { diff --git a/registry/coder-labs/modules/nextflow/README.md b/registry/coder-labs/modules/nextflow/README.md index 7e6911dc..adefa645 100644 --- a/registry/coder-labs/modules/nextflow/README.md +++ b/registry/coder-labs/modules/nextflow/README.md @@ -10,8 +10,6 @@ tags: [nextflow, workflow, hpc, bioinformatics] A module that adds Nextflow to your Coder template. -![Nextflow](../../.images/nextflow.png) - ```tf module "nextflow" { count = data.coder_workspace.me.start_count diff --git a/registry/coder/modules/agent-helper/README.md b/registry/coder/modules/agent-helper/README.md new file mode 100644 index 00000000..62eb3573 --- /dev/null +++ b/registry/coder/modules/agent-helper/README.md @@ -0,0 +1,65 @@ +--- +display_name: Agent Helper +description: Building block for modules that need orchestrated script execution +icon: ../../../../.icons/coder.svg +verified: false +tags: [internal, library] +--- + +# Agent Helper + +> [!CAUTION] +> We do not recommend using this module directly. It is intended primarily for internal use by Coder to create modules with orchestrated script execution. + +The Agent Helper module is a building block for modules that need to run multiple scripts in a specific order. It uses `coder exp sync` for dependency management and is designed for orchestrating pre-install, install, post-install, and start scripts. + +> [!NOTE] +> +> - The `agent_name` should be the same as that of the agentapi module's `agent_name` if used together. + +```tf +module "agent_helper" { + source = "registry.coder.com/coder/agent-helper/coder" + version = "1.0.0" + + agent_id = coder_agent.main.id + agent_name = "myagent" + module_dir_name = ".my-module" + + pre_install_script = <<-EOT + #!/bin/bash + echo "Running pre-install tasks..." + # Your pre-install logic here + EOT + + install_script = <<-EOT + #!/bin/bash + echo "Installing dependencies..." + # Your install logic here + EOT + + post_install_script = <<-EOT + #!/bin/bash + echo "Running post-install configuration..." + # Your post-install logic here + EOT + + start_script = <<-EOT + #!/bin/bash + echo "Starting the application..." + # Your start logic here + EOT +} +``` + +## Execution Order + +The module orchestrates scripts in the following order: + +1. **Log File Creation** - Creates module directory and log files +2. **Pre-Install Script** (optional) - Runs before installation +3. **Install Script** - Main installation +4. **Post-Install Script** (optional) - Runs after installation +5. **Start Script** - Starts the application + +Each script waits for its prerequisites to complete before running using `coder exp sync` dependency management. diff --git a/registry/coder/modules/agent-helper/main.test.ts b/registry/coder/modules/agent-helper/main.test.ts new file mode 100644 index 00000000..6c132589 --- /dev/null +++ b/registry/coder/modules/agent-helper/main.test.ts @@ -0,0 +1,13 @@ +import { describe } from "bun:test"; +import { runTerraformInit, testRequiredVariables } from "~test"; + +describe("agent-helper", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "test-agent", + module_dir_name: ".test-module", + start_script: "echo 'start'", + }); +}); diff --git a/registry/coder/modules/agent-helper/main.tf b/registry/coder/modules/agent-helper/main.tf new file mode 100644 index 00000000..cfb8b778 --- /dev/null +++ b/registry/coder/modules/agent-helper/main.tf @@ -0,0 +1,190 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +data "coder_task" "me" {} + +variable "pre_install_script" { + type = string + description = "Custom script to run before installing the agent used by AgentAPI." + default = null +} + +variable "install_script" { + type = string + description = "Script to install the agent used by AgentAPI." + default = null +} + +variable "post_install_script" { + type = string + description = "Custom script to run after installing the agent used by AgentAPI." + default = null +} + +variable "start_script" { + type = string + description = "Script that starts AgentAPI." +} + +variable "agent_name" { + type = string + description = "The name of the agent. This is used to construct unique script names for the experiment sync." + +} + +variable "module_dir_name" { + type = string + description = "The name of the module directory." +} + +locals { + encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : "" + encoded_install_script = var.install_script != null ? base64encode(var.install_script) : "" + encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : "" + encoded_start_script = base64encode(var.start_script) + + pre_install_script_name = "${var.agent_name}-pre_install_script" + install_script_name = "${var.agent_name}-install_script" + post_install_script_name = "${var.agent_name}-post_install_script" + start_script_name = "${var.agent_name}-start_script" + + module_dir_path = "$HOME/${var.module_dir_name}" + + pre_install_path = "${local.module_dir_path}/pre_install.sh" + install_path = "${local.module_dir_path}/install.sh" + post_install_path = "${local.module_dir_path}/post_install.sh" + start_path = "${local.module_dir_path}/start.sh" + + pre_install_log_path = "${local.module_dir_path}/pre_install.log" + install_log_path = "${local.module_dir_path}/install.log" + post_install_log_path = "${local.module_dir_path}/post_install.log" + start_log_path = "${local.module_dir_path}/start.log" +} + +resource "coder_script" "pre_install_script" { + count = var.pre_install_script == null ? 0 : 1 + agent_id = var.agent_id + display_name = "Pre-Install Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + mkdir -p ${local.module_dir_path} + + trap 'coder exp sync complete ${local.pre_install_script_name}' EXIT + coder exp sync start ${local.pre_install_script_name} + + echo -n '${local.encoded_pre_install_script}' | base64 -d > ${local.pre_install_path} + chmod +x ${local.pre_install_path} + + ${local.pre_install_path} > ${local.pre_install_log_path} 2>&1 + EOT +} + +resource "coder_script" "install_script" { + agent_id = var.agent_id + display_name = "Install Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + mkdir -p ${local.module_dir_path} + + trap 'coder exp sync complete ${local.install_script_name}' EXIT + %{if var.pre_install_script != null~} + coder exp sync want ${local.install_script_name} ${local.pre_install_script_name} + %{endif~} + coder exp sync start ${local.install_script_name} + echo -n '${local.encoded_install_script}' | base64 -d > ${local.install_path} + chmod +x ${local.install_path} + + ${local.install_path} > ${local.install_log_path} 2>&1 + EOT +} + +resource "coder_script" "post_install_script" { + count = var.post_install_script != null ? 1 : 0 + agent_id = var.agent_id + display_name = "Post-Install Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + trap 'coder exp sync complete ${local.post_install_script_name}' EXIT + coder exp sync want ${local.post_install_script_name} ${local.install_script_name} + coder exp sync start ${local.post_install_script_name} + + echo -n '${local.encoded_post_install_script}' | base64 -d > ${local.post_install_path} + chmod +x ${local.post_install_path} + + ${local.post_install_path} > ${local.post_install_log_path} 2>&1 + EOT +} + +resource "coder_script" "start_script" { + agent_id = var.agent_id + display_name = "Start Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + trap 'coder exp sync complete ${local.start_script_name}' EXIT + + %{if var.post_install_script != null~} + coder exp sync want ${local.start_script_name} ${local.install_script_name} ${local.post_install_script_name} + %{else~} + coder exp sync want ${local.start_script_name} ${local.install_script_name} + %{endif~} + coder exp sync start ${local.start_script_name} + + echo -n '${local.encoded_start_script}' | base64 -d > ${local.start_path} + chmod +x ${local.start_path} + + ${local.start_path} > ${local.start_log_path} 2>&1 + EOT +} + +output "pre_install_script_name" { + description = "The name of the pre-install script for sync." + value = local.pre_install_script_name +} + +output "install_script_name" { + description = "The name of the install script for sync." + value = local.install_script_name +} + +output "post_install_script_name" { + description = "The name of the post-install script for sync." + value = local.post_install_script_name +} + +output "start_script_name" { + description = "The name of the start script for sync." + value = local.start_script_name +} \ No newline at end of file diff --git a/registry/coder/modules/agent-helper/main.tftest.hcl b/registry/coder/modules/agent-helper/main.tftest.hcl new file mode 100644 index 00000000..91546fb0 --- /dev/null +++ b/registry/coder/modules/agent-helper/main.tftest.hcl @@ -0,0 +1,271 @@ +# Test for agent-helper module + +# Test with all scripts provided +run "test_with_all_scripts" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_dir_name = ".test-module" + pre_install_script = "echo 'pre-install'" + install_script = "echo 'install'" + post_install_script = "echo 'post-install'" + start_script = "echo 'start'" + } + + # Verify pre_install_script is created when provided + assert { + condition = length(coder_script.pre_install_script) == 1 + error_message = "Pre-install script should be created when pre_install_script is provided" + } + + assert { + condition = coder_script.pre_install_script[0].agent_id == "test-agent-id" + error_message = "Pre-install script agent ID should match input" + } + + assert { + condition = coder_script.pre_install_script[0].display_name == "Pre-Install Script" + error_message = "Pre-install script should have correct display name" + } + + assert { + condition = coder_script.pre_install_script[0].run_on_start == true + error_message = "Pre-install script should run on start" + } + + # Verify install_script is created + assert { + condition = coder_script.install_script.agent_id == "test-agent-id" + error_message = "Install script agent ID should match input" + } + + assert { + condition = coder_script.install_script.display_name == "Install Script" + error_message = "Install script should have correct display name" + } + + assert { + condition = coder_script.install_script.run_on_start == true + error_message = "Install script should run on start" + } + + # Verify post_install_script is created when provided + assert { + condition = length(coder_script.post_install_script) == 1 + error_message = "Post-install script should be created when post_install_script is provided" + } + + assert { + condition = coder_script.post_install_script[0].agent_id == "test-agent-id" + error_message = "Post-install script agent ID should match input" + } + + assert { + condition = coder_script.post_install_script[0].display_name == "Post-Install Script" + error_message = "Post-install script should have correct display name" + } + + assert { + condition = coder_script.post_install_script[0].run_on_start == true + error_message = "Post-install script should run on start" + } + + # Verify start_script is created + assert { + condition = coder_script.start_script.agent_id == "test-agent-id" + error_message = "Start script agent ID should match input" + } + + assert { + condition = coder_script.start_script.display_name == "Start Script" + error_message = "Start script should have correct display name" + } + + assert { + condition = coder_script.start_script.run_on_start == true + error_message = "Start script should run on start" + } + + # Verify outputs for script names + assert { + condition = output.pre_install_script_name == "test-agent-pre_install_script" + error_message = "Pre-install script name output should be correctly formatted" + } + + assert { + condition = output.install_script_name == "test-agent-install_script" + error_message = "Install script name output should be correctly formatted" + } + + assert { + condition = output.post_install_script_name == "test-agent-post_install_script" + error_message = "Post-install script name output should be correctly formatted" + } + + assert { + condition = output.start_script_name == "test-agent-start_script" + error_message = "Start script name output should be correctly formatted" + } +} + +# Test with only required scripts (no pre/post install) +run "test_without_optional_scripts" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_dir_name = ".test-module" + install_script = "echo 'install'" + start_script = "echo 'start'" + } + + # Verify pre_install_script is NOT created when not provided + assert { + condition = length(coder_script.pre_install_script) == 0 + error_message = "Pre-install script should not be created when pre_install_script is null" + } + + # Verify post_install_script is NOT created when not provided + assert { + condition = length(coder_script.post_install_script) == 0 + error_message = "Post-install script should not be created when post_install_script is null" + } + + # Verify required scripts are still created + assert { + condition = coder_script.install_script.agent_id == "test-agent-id" + error_message = "Install script should be created" + } + + assert { + condition = coder_script.start_script.agent_id == "test-agent-id" + error_message = "Start script should be created" + } + + # Verify outputs + assert { + condition = output.pre_install_script_name == "test-agent-pre_install_script" + error_message = "Pre-install script name output should be generated even when script is not created" + } + + assert { + condition = output.install_script_name == "test-agent-install_script" + error_message = "Install script name output should be correctly formatted" + } + + assert { + condition = output.post_install_script_name == "test-agent-post_install_script" + error_message = "Post-install script name output should be generated even when script is not created" + } + + assert { + condition = output.start_script_name == "test-agent-start_script" + error_message = "Start script name output should be correctly formatted" + } +} + +# Test with mock data sources +run "test_with_mock_data" { + command = plan + + variables { + agent_id = "mock-agent" + agent_name = "mock-agent" + module_dir_name = ".mock-module" + install_script = "echo 'install'" + start_script = "echo 'start'" + } + + # Mock the data sources for testing + override_data { + target = data.coder_workspace.me + values = { + id = "test-workspace-id" + name = "test-workspace" + owner = "test-owner" + owner_id = "test-owner-id" + template_id = "test-template-id" + template_name = "test-template" + access_url = "https://coder.example.com" + start_count = 1 + transition = "start" + } + } + + override_data { + target = data.coder_workspace_owner.me + values = { + id = "test-owner-id" + email = "test@example.com" + name = "Test User" + session_token = "mock-token" + } + } + + override_data { + target = data.coder_task.me + values = { + id = "test-task-id" + } + } + + # Verify scripts are created with mocked data + assert { + condition = coder_script.install_script.agent_id == "mock-agent" + error_message = "Install script should use the mocked agent ID" + } + + assert { + condition = coder_script.start_script.agent_id == "mock-agent" + error_message = "Start script should use the mocked agent ID" + } +} + +# Test script naming with custom agent_name +run "test_script_naming" { + command = plan + + variables { + agent_id = "test-agent" + agent_name = "custom-name" + module_dir_name = ".test-module" + install_script = "echo 'install'" + start_script = "echo 'start'" + } + + # Verify script names are constructed correctly + # The script should contain references to custom-name-* in the sync commands + assert { + condition = can(regex("custom-name-install_script", coder_script.install_script.script)) + error_message = "Install script should use custom agent_name in sync commands" + } + + assert { + condition = can(regex("custom-name-start_script", coder_script.start_script.script)) + error_message = "Start script should use custom agent_name in sync commands" + } + + # Verify outputs use custom agent_name + assert { + condition = output.pre_install_script_name == "custom-name-pre_install_script" + error_message = "Pre-install script name output should use custom agent_name" + } + + assert { + condition = output.install_script_name == "custom-name-install_script" + error_message = "Install script name output should use custom agent_name" + } + + assert { + condition = output.post_install_script_name == "custom-name-post_install_script" + error_message = "Post-install script name output should use custom agent_name" + } + + assert { + condition = output.start_script_name == "custom-name-start_script" + error_message = "Start script name output should use custom agent_name" + } +} diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md index 06897d6b..e7a9869f 100644 --- a/registry/coder/modules/agentapi/README.md +++ b/registry/coder/modules/agentapi/README.md @@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI ```tf module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "2.1.0" + version = "2.1.1" agent_id = var.agent_id web_app_slug = local.app_slug diff --git a/registry/coder/modules/agentapi/scripts/agentapi-wait-for-start.sh b/registry/coder/modules/agentapi/scripts/agentapi-wait-for-start.sh index 6e18332f..6ae5b14a 100644 --- a/registry/coder/modules/agentapi/scripts/agentapi-wait-for-start.sh +++ b/registry/coder/modules/agentapi/scripts/agentapi-wait-for-start.sh @@ -3,20 +3,22 @@ set -o errexit set -o pipefail port=${1:-3284} +max_attempts=150 -# This script waits for the agentapi server to start on port 3284. +# This script waits for the agentapi server to start on the given port. +# Each attempt sleeps 0.1s, so 150 attempts ≈ 15 seconds. # It considers the server started after 3 consecutive successful responses. agentapi_started=false echo "Waiting for agentapi server to start on port $port..." -for i in $(seq 1 150); do +for i in $(seq 1 "$max_attempts"); do for j in $(seq 1 3); do sleep 0.1 if curl -fs -o /dev/null "http://localhost:$port/status"; then echo "agentapi response received ($j/3)" else - echo "agentapi server not responding ($i/15)" + echo "agentapi server not responding ($i/$max_attempts)" continue 2 fi done @@ -25,7 +27,7 @@ for i in $(seq 1 150); do done if [ "$agentapi_started" != "true" ]; then - echo "Error: agentapi server did not start on port $port after 15 seconds." + echo "Error: agentapi server did not start on port $port after $max_attempts attempts." exit 1 fi diff --git a/registry/coder/modules/antigravity/README.md b/registry/coder/modules/antigravity/README.md index ed5882b2..734cbef4 100644 --- a/registry/coder/modules/antigravity/README.md +++ b/registry/coder/modules/antigravity/README.md @@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) module "antigravity" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/antigravity/coder" - version = "1.0.0" + version = "1.0.1" agent_id = coder_agent.example.id } ``` @@ -29,7 +29,7 @@ module "antigravity" { module "antigravity" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/antigravity/coder" - version = "1.0.0" + version = "1.0.1" agent_id = coder_agent.example.id folder = "/home/coder/project" } @@ -45,7 +45,7 @@ The following example configures Antigravity to use the GitHub MCP server with a module "antigravity" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/antigravity/coder" - version = "1.0.0" + version = "1.0.1" agent_id = coder_agent.example.id folder = "/home/coder/project" mcp = jsonencode({ diff --git a/registry/coder/modules/antigravity/main.tf b/registry/coder/modules/antigravity/main.tf index 27c6166d..81ccd1c8 100644 --- a/registry/coder/modules/antigravity/main.tf +++ b/registry/coder/modules/antigravity/main.tf @@ -66,15 +66,15 @@ locals { module "vscode-desktop-core" { source = "registry.coder.com/coder/vscode-desktop-core/coder" - version = "1.0.1" + version = "1.0.2" agent_id = var.agent_id - web_app_icon = "/icon/antigravity.svg" - web_app_slug = var.slug - web_app_display_name = var.display_name - web_app_order = var.order - web_app_group = var.group + coder_app_icon = "/icon/antigravity.svg" + coder_app_slug = var.slug + coder_app_display_name = var.display_name + coder_app_order = var.order + coder_app_group = var.group folder = var.folder open_recent = var.open_recent diff --git a/registry/coder/modules/cursor/README.md b/registry/coder/modules/cursor/README.md index 7a870ac0..628950f5 100644 --- a/registry/coder/modules/cursor/README.md +++ b/registry/coder/modules/cursor/README.md @@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) module "cursor" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cursor/coder" - version = "1.4.0" + version = "1.4.1" agent_id = coder_agent.main.id } ``` @@ -29,7 +29,7 @@ module "cursor" { module "cursor" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cursor/coder" - version = "1.4.0" + version = "1.4.1" agent_id = coder_agent.main.id folder = "/home/coder/project" } @@ -45,7 +45,7 @@ The following example configures Cursor to use the GitHub MCP server with authen module "cursor" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cursor/coder" - version = "1.4.0" + version = "1.4.1" agent_id = coder_agent.main.id folder = "/home/coder/project" mcp = jsonencode({ diff --git a/registry/coder/modules/cursor/main.tf b/registry/coder/modules/cursor/main.tf index 0c0f8aa2..a33a2cc3 100644 --- a/registry/coder/modules/cursor/main.tf +++ b/registry/coder/modules/cursor/main.tf @@ -66,7 +66,7 @@ locals { module "vscode-desktop-core" { source = "registry.coder.com/coder/vscode-desktop-core/coder" - version = "1.0.0" + version = "1.0.2" agent_id = var.agent_id diff --git a/registry/coder/modules/dotfiles/README.md b/registry/coder/modules/dotfiles/README.md index 7d994c1e..9cb6a45d 100644 --- a/registry/coder/modules/dotfiles/README.md +++ b/registry/coder/modules/dotfiles/README.md @@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/ module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.4" + version = "1.3.0" agent_id = coder_agent.example.id } ``` @@ -31,7 +31,7 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.4" + version = "1.3.0" agent_id = coder_agent.example.id } ``` @@ -42,7 +42,7 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.4" + version = "1.3.0" agent_id = coder_agent.example.id user = "root" } @@ -54,14 +54,14 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.4" + version = "1.3.0" agent_id = coder_agent.example.id } module "dotfiles-root" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.4" + version = "1.3.0" agent_id = coder_agent.example.id user = "root" dotfiles_uri = module.dotfiles.dotfiles_uri @@ -76,7 +76,7 @@ You can set a default dotfiles repository for all users by setting the `default_ module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.4" + version = "1.3.0" agent_id = coder_agent.example.id default_dotfiles_uri = "https://github.com/coder/dotfiles" } diff --git a/registry/coder/modules/dotfiles/main.tf b/registry/coder/modules/dotfiles/main.tf index 760f4181..40b1a4e0 100644 --- a/registry/coder/modules/dotfiles/main.tf +++ b/registry/coder/modules/dotfiles/main.tf @@ -84,6 +84,12 @@ variable "manual_update" { default = false } +variable "post_clone_script" { + description = "Custom script to run after applying dotfiles. Runs every time, even if dotfiles were already applied." + type = string + default = null +} + data "coder_parameter" "dotfiles_uri" { count = var.dotfiles_uri == null ? 1 : 0 type = "string" @@ -102,15 +108,17 @@ data "coder_parameter" "dotfiles_uri" { } locals { - dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value - user = var.user != null ? var.user : "" + dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value + user = var.user != null ? var.user : "" + encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : "" } resource "coder_script" "dotfiles" { agent_id = var.agent_id script = templatefile("${path.module}/run.sh", { DOTFILES_URI : local.dotfiles_uri, - DOTFILES_USER : local.user + DOTFILES_USER : local.user, + POST_CLONE_SCRIPT : local.encoded_post_clone_script }) display_name = "Dotfiles" icon = "/icon/dotfiles.svg" @@ -127,7 +135,8 @@ resource "coder_app" "dotfiles" { group = var.group command = templatefile("${path.module}/run.sh", { DOTFILES_URI : local.dotfiles_uri, - DOTFILES_USER : local.user + DOTFILES_USER : local.user, + POST_CLONE_SCRIPT : local.encoded_post_clone_script }) } diff --git a/registry/coder/modules/dotfiles/run.sh b/registry/coder/modules/dotfiles/run.sh index a068aca7..49ab3ec5 100644 --- a/registry/coder/modules/dotfiles/run.sh +++ b/registry/coder/modules/dotfiles/run.sh @@ -43,3 +43,14 @@ if [ -n "$${DOTFILES_URI// }" ]; then sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log" fi fi + +POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}" + +if [ -n "$POST_CLONE_SCRIPT" ]; then + echo "Running post-clone script..." + POST_CLONE_TMP=$(mktemp) + echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP" + chmod +x "$POST_CLONE_TMP" + $POST_CLONE_TMP + rm "$POST_CLONE_TMP" +fi diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md index cf97d127..7fa8f674 100644 --- a/registry/coder/modules/jetbrains/README.md +++ b/registry/coder/modules/jetbrains/README.md @@ -42,7 +42,7 @@ module "jetbrains" { version = "1.3.0" agent_id = coder_agent.main.id folder = "/home/coder/project" - default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA + default = ["PY", "IU"] # Pre-configure PyCharm and IntelliJ IDEA } ``` diff --git a/registry/coder/modules/kasmvnc/README.md b/registry/coder/modules/kasmvnc/README.md index 7fcc7fb0..2f9fff7a 100644 --- a/registry/coder/modules/kasmvnc/README.md +++ b/registry/coder/modules/kasmvnc/README.md @@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and module "kasmvnc" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/kasmvnc/coder" - version = "1.2.7" + version = "1.3.0" agent_id = coder_agent.example.id desktop_environment = "xfce" subdomain = true diff --git a/registry/coder/modules/kasmvnc/main.tf b/registry/coder/modules/kasmvnc/main.tf index 4635f612..66324b37 100644 --- a/registry/coder/modules/kasmvnc/main.tf +++ b/registry/coder/modules/kasmvnc/main.tf @@ -54,6 +54,15 @@ variable "subdomain" { description = "Is subdomain sharing enabled in your cluster?" } +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'." + } +} + resource "coder_script" "kasm_vnc" { agent_id = var.agent_id display_name = "KasmVNC" @@ -75,7 +84,7 @@ resource "coder_app" "kasm_vnc" { url = "http://localhost:${var.port}" icon = "/icon/kasmvnc.svg" subdomain = var.subdomain - share = "owner" + share = var.share order = var.order group = var.group diff --git a/registry/coder/modules/kiro/README.md b/registry/coder/modules/kiro/README.md index 23c17885..51fbe9ae 100644 --- a/registry/coder/modules/kiro/README.md +++ b/registry/coder/modules/kiro/README.md @@ -18,7 +18,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) module "kiro" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/kiro/coder" - version = "1.2.0" + version = "1.2.1" agent_id = coder_agent.main.id } ``` @@ -31,7 +31,7 @@ module "kiro" { module "kiro" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/kiro/coder" - version = "1.2.0" + version = "1.2.1" agent_id = coder_agent.main.id folder = "/home/coder/project" } @@ -47,7 +47,7 @@ The following example configures Kiro to use the GitHub MCP server with authenti module "kiro" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/kiro/coder" - version = "1.2.0" + version = "1.2.1" agent_id = coder_agent.main.id folder = "/home/coder/project" mcp = jsonencode({ diff --git a/registry/coder/modules/kiro/main.tf b/registry/coder/modules/kiro/main.tf index c48364bc..84b44b34 100644 --- a/registry/coder/modules/kiro/main.tf +++ b/registry/coder/modules/kiro/main.tf @@ -53,7 +53,7 @@ locals { module "vscode-desktop-core" { source = "registry.coder.com/coder/vscode-desktop-core/coder" - version = "1.0.0" + version = "1.0.2" agent_id = var.agent_id diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 00a9de65..b9cfafc0 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -14,7 +14,7 @@ Automatically install and run [Mux](https://github.com/coder/mux) in a Coder wor module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.8" + version = "1.1.0" agent_id = coder_agent.main.id } ``` @@ -37,7 +37,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.8" + version = "1.1.0" agent_id = coder_agent.main.id } ``` @@ -48,7 +48,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.8" + version = "1.1.0" agent_id = coder_agent.main.id # Default is "latest"; set to a specific version to pin install_version = "0.4.0" @@ -63,7 +63,7 @@ Start Mux with `mux server --add-project /path/to/project`: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.8" + version = "1.1.0" agent_id = coder_agent.main.id add-project = "/path/to/project" } @@ -75,7 +75,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.8" + version = "1.1.0" agent_id = coder_agent.main.id port = 8080 } @@ -89,7 +89,7 @@ Run an existing copy of Mux if found, otherwise install from npm: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.8" + version = "1.1.0" agent_id = coder_agent.main.id use_cached = true } @@ -103,7 +103,7 @@ Run without installing from the network (requires Mux to be pre-installed): module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.8" + version = "1.1.0" agent_id = coder_agent.main.id install = false } diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index 870beae2..1eeddecf 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -7,6 +7,10 @@ terraform { source = "coder/coder" version = ">= 2.5" } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } } } @@ -113,6 +117,22 @@ variable "open_in" { } } +# Per-module auth token for cross-site request protection. +# We pass this token into each mux process at launch time (process-scoped env) +# and include it in the app URL query string (?token=...). +# +# Why process-scoped env instead of a shared coder_env value: +# multiple mux module instances can target the same agent (different slug/port). +# A single global MUX_SERVER_AUTH_TOKEN env key would cause collisions. +resource "random_password" "mux_auth_token" { + length = 64 + special = false +} + +locals { + mux_auth_token = random_password.mux_auth_token.result +} + resource "coder_script" "mux" { agent_id = var.agent_id display_name = var.display_name @@ -125,6 +145,7 @@ resource "coder_script" "mux" { INSTALL_PREFIX : var.install_prefix, OFFLINE : !var.install, USE_CACHED : var.use_cached, + AUTH_TOKEN : local.mux_auth_token, }) run_on_start = true @@ -140,7 +161,7 @@ resource "coder_app" "mux" { agent_id = var.agent_id slug = var.slug display_name = var.display_name - url = "http://localhost:${var.port}" + url = "http://localhost:${var.port}?token=${local.mux_auth_token}" icon = "/icon/mux.svg" subdomain = var.subdomain share = var.share @@ -154,5 +175,3 @@ resource "coder_app" "mux" { threshold = 6 } } - - diff --git a/registry/coder/modules/mux/mux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl index c403d377..af103ae2 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -20,8 +20,10 @@ run "install_false_and_use_cached_conflict" { ] } +# Needs command = apply because the URL contains random_password.result, +# which is unknown during plan. run "custom_port" { - command = plan + command = apply variables { agent_id = "foo" @@ -29,8 +31,51 @@ run "custom_port" { } assert { - condition = resource.coder_app.mux.url == "http://localhost:8080" - error_message = "coder_app URL must use the configured port" + condition = startswith(resource.coder_app.mux.url, "http://localhost:8080?token=") + error_message = "coder_app URL must use the configured port and include auth token" + } + + assert { + condition = trimprefix(resource.coder_app.mux.url, "http://localhost:8080?token=") == random_password.mux_auth_token.result + error_message = "URL token must match the generated auth token" + } +} + +# Needs command = apply because random_password.result is unknown during plan. +run "auth_token_in_server_script" { + command = apply + + variables { + agent_id = "foo" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "MUX_SERVER_AUTH_TOKEN=") + error_message = "mux launch script must set MUX_SERVER_AUTH_TOKEN" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, random_password.mux_auth_token.result) + error_message = "mux launch script must use the generated auth token" + } +} + +# Needs command = apply because random_password.result is unknown during plan. +run "auth_token_in_url" { + command = apply + + variables { + agent_id = "foo" + } + + assert { + condition = startswith(resource.coder_app.mux.url, "http://localhost:4000?token=") + error_message = "coder_app URL must include auth token query parameter" + } + + assert { + condition = trimprefix(resource.coder_app.mux.url, "http://localhost:4000?token=") == random_password.mux_auth_token.result + error_message = "URL token must match the generated auth token" } } @@ -62,5 +107,3 @@ run "use_cached_only_success" { use_cached = true } } - - diff --git a/registry/coder/modules/mux/run.sh b/registry/coder/modules/mux/run.sh index 2409f19d..0d0c6520 100644 --- a/registry/coder/modules/mux/run.sh +++ b/registry/coder/modules/mux/run.sh @@ -9,7 +9,9 @@ function run_mux() { rm -f "$HOME/.mux/server.lock" local port_value + local auth_token_value port_value="${PORT}" + auth_token_value="${AUTH_TOKEN}" if [ -z "$port_value" ]; then port_value="4000" fi @@ -20,7 +22,7 @@ function run_mux() { fi echo "🚀 Starting mux server on port $port_value..." echo "Check logs at ${LOG_PATH}!" - PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 & + MUX_SERVER_AUTH_TOKEN="$auth_token_value" PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 & } # Check if mux is already installed for offline mode diff --git a/registry/coder/modules/vscode-desktop-core/README.md b/registry/coder/modules/vscode-desktop-core/README.md index d95e2da2..1b86783a 100644 --- a/registry/coder/modules/vscode-desktop-core/README.md +++ b/registry/coder/modules/vscode-desktop-core/README.md @@ -16,15 +16,15 @@ The VSCode Desktop Core module is a building block for modules that need to expo ```tf module "vscode-desktop-core" { source = "registry.coder.com/coder/vscode-desktop-core/coder" - version = "1.0.1" + version = "1.0.2" agent_id = var.agent_id - web_app_icon = "/icon/code.svg" - web_app_slug = "vscode" - web_app_display_name = "VS Code Desktop" - web_app_order = var.order - web_app_group = var.group + coder_app_icon = "/icon/code.svg" + coder_app_slug = "vscode" + coder_app_display_name = "VS Code Desktop" + coder_app_order = var.order + coder_app_group = var.group folder = var.folder open_recent = var.open_recent diff --git a/registry/coder/modules/vscode-desktop-core/main.test.ts b/registry/coder/modules/vscode-desktop-core/main.test.ts index 46c51227..87674f32 100644 --- a/registry/coder/modules/vscode-desktop-core/main.test.ts +++ b/registry/coder/modules/vscode-desktop-core/main.test.ts @@ -11,9 +11,9 @@ const appName = "vscode-desktop"; const defaultVariables = { agent_id: "foo", - web_app_icon: "/icon/code.svg", - web_app_slug: "vscode", - web_app_display_name: "VS Code Desktop", + coder_app_icon: "/icon/code.svg", + coder_app_slug: "vscode", + coder_app_display_name: "VS Code Desktop", protocol: "vscode", }; @@ -99,16 +99,16 @@ describe("vscode-desktop-core", async () => { ); expect(coder_app?.instances[0].attributes.slug).toBe( - defaultVariables.web_app_slug, + defaultVariables.coder_app_slug, ); expect(coder_app?.instances[0].attributes.display_name).toBe( - defaultVariables.web_app_display_name, + defaultVariables.coder_app_display_name, ); }); it("sets order", async () => { const state = await runTerraformApply(import.meta.dir, { - web_app_order: "5", + coder_app_order: "5", ...defaultVariables, }); @@ -122,7 +122,7 @@ describe("vscode-desktop-core", async () => { it("sets group", async () => { const state = await runTerraformApply(import.meta.dir, { - web_app_group: "web-app-group", + coder_app_group: "web-app-group", ...defaultVariables, }); diff --git a/registry/coder/modules/vscode-desktop-core/main.tf b/registry/coder/modules/vscode-desktop-core/main.tf index 7e675712..9a7da34c 100644 --- a/registry/coder/modules/vscode-desktop-core/main.tf +++ b/registry/coder/modules/vscode-desktop-core/main.tf @@ -31,28 +31,28 @@ variable "protocol" { description = "The URI protocol the IDE." } -variable "web_app_icon" { +variable "coder_app_icon" { type = string description = "The icon of the coder_app." } -variable "web_app_slug" { +variable "coder_app_slug" { type = string description = "The slug of the coder_app." } -variable "web_app_display_name" { +variable "coder_app_display_name" { type = string description = "The display name of the coder_app." } -variable "web_app_order" { +variable "coder_app_order" { type = number description = "The order of the coder_app." default = null } -variable "web_app_group" { +variable "coder_app_group" { type = string description = "The group of the coder_app." default = null @@ -65,12 +65,12 @@ resource "coder_app" "vscode-desktop" { agent_id = var.agent_id external = true - icon = var.web_app_icon - slug = var.web_app_slug - display_name = var.web_app_display_name + icon = var.coder_app_icon + slug = var.coder_app_slug + display_name = var.coder_app_display_name - order = var.web_app_order - group = var.web_app_group + order = var.coder_app_order + group = var.coder_app_group url = join("", [ var.protocol, diff --git a/registry/coder/modules/vscode-desktop/README.md b/registry/coder/modules/vscode-desktop/README.md index 56f39bf7..7252361d 100644 --- a/registry/coder/modules/vscode-desktop/README.md +++ b/registry/coder/modules/vscode-desktop/README.md @@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) module "vscode" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-desktop/coder" - version = "1.2.0" + version = "1.2.1" agent_id = coder_agent.main.id } ``` @@ -29,7 +29,7 @@ module "vscode" { module "vscode" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-desktop/coder" - version = "1.2.0" + version = "1.2.1" agent_id = coder_agent.main.id folder = "/home/coder/project" } diff --git a/registry/coder/modules/vscode-desktop/main.tf b/registry/coder/modules/vscode-desktop/main.tf index c9e6dd35..8d98a1a7 100644 --- a/registry/coder/modules/vscode-desktop/main.tf +++ b/registry/coder/modules/vscode-desktop/main.tf @@ -40,7 +40,7 @@ variable "group" { module "vscode-desktop-core" { source = "registry.coder.com/coder/vscode-desktop-core/coder" - version = "1.0.0" + version = "1.0.2" agent_id = var.agent_id diff --git a/registry/coder/modules/windsurf/README.md b/registry/coder/modules/windsurf/README.md index 77c57d40..4463552f 100644 --- a/registry/coder/modules/windsurf/README.md +++ b/registry/coder/modules/windsurf/README.md @@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) module "windsurf" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windsurf/coder" - version = "1.3.0" + version = "1.3.1" agent_id = coder_agent.main.id } ``` @@ -29,7 +29,7 @@ module "windsurf" { module "windsurf" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windsurf/coder" - version = "1.3.0" + version = "1.3.1" agent_id = coder_agent.main.id folder = "/home/coder/project" } @@ -45,7 +45,7 @@ The following example configures Windsurf to use the GitHub MCP server with auth module "windsurf" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windsurf/coder" - version = "1.3.0" + version = "1.3.1" agent_id = coder_agent.main.id folder = "/home/coder/project" mcp = jsonencode({ diff --git a/registry/coder/modules/windsurf/main.tf b/registry/coder/modules/windsurf/main.tf index 3ec29d5b..90521fa6 100644 --- a/registry/coder/modules/windsurf/main.tf +++ b/registry/coder/modules/windsurf/main.tf @@ -65,7 +65,7 @@ locals { module "vscode-desktop-core" { source = "registry.coder.com/coder/vscode-desktop-core/coder" - version = "1.0.0" + version = "1.0.2" agent_id = var.agent_id diff --git a/registry/coder/templates/azure-linux/README.md b/registry/coder/templates/azure-linux/README.md index 33d771ed..ddae36db 100644 --- a/registry/coder/templates/azure-linux/README.md +++ b/registry/coder/templates/azure-linux/README.md @@ -27,8 +27,21 @@ This template provisions the following resources: - Azure VM (ephemeral, deleted on stop) - Managed disk (persistent, mounted to `/home/coder`) +- Resource group, virtual network, subnet, and network interface (persistent, required by the managed disk and VM) -This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles. +### What happens on stop + +When a workspace is **stopped**, only the VM is destroyed. The managed disk, resource group, virtual network, subnet, and network interface all persist. This is by design — the managed disk retains your `/home/coder` data across workspace restarts, and the other resources remain because the disk depends on them. + +This means you will see these Azure resources in your subscription even when a workspace is stopped. This is expected behavior. + +### What happens on delete + +When a workspace is **deleted**, all resources are destroyed, including the resource group, networking resources, and managed disk. + +### Workspace restarts + +Since the VM is ephemeral, any tools or files outside of the home directory are not persisted across restarts. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles. > [!NOTE] > This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. diff --git a/registry/cytoshahar/modules/positron/README.md b/registry/cytoshahar/modules/positron/README.md index 139b560c..21d4e543 100644 --- a/registry/cytoshahar/modules/positron/README.md +++ b/registry/cytoshahar/modules/positron/README.md @@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) module "positron" { count = data.coder_workspace.me.start_count source = "registry.coder.com/cytoshahar/positron/coder" - version = "1.0.1" + version = "1.0.2" agent_id = coder_agent.main.id } ``` @@ -29,7 +29,7 @@ module "positron" { module "positron" { count = data.coder_workspace.me.start_count source = "registry.coder.com/cytoshahar/positron/coder" - version = "1.0.1" + version = "1.0.2" agent_id = coder_agent.main.id folder = "/home/coder/project" } diff --git a/registry/cytoshahar/modules/positron/main.tf b/registry/cytoshahar/modules/positron/main.tf index 9365b444..1d391296 100644 --- a/registry/cytoshahar/modules/positron/main.tf +++ b/registry/cytoshahar/modules/positron/main.tf @@ -41,13 +41,13 @@ variable "group" { variable "slug" { type = string description = "The slug of the app." - default = "cursor" + default = "positron" } variable "display_name" { type = string description = "The display name of the app." - default = "Cursor Desktop" + default = "Positron Desktop" } data "coder_workspace" "me" {} @@ -55,7 +55,7 @@ data "coder_workspace_owner" "me" {} module "vscode-desktop-core" { source = "registry.coder.com/coder/vscode-desktop-core/coder" - version = "1.0.0" + version = "1.0.2" agent_id = var.agent_id