Update enable_boundary validation to treat empty/whitespace strings as unset using trimspace checks, so callers providing "" are handled consistently with null. Per-variable validations already reject empty strings. Bump to v4.9.2. Closes coder/registry#797 Generated with OpenClaw using Claude
475 lines
16 KiB
HCL
475 lines
16 KiB
HCL
terraform {
|
|
required_version = ">= 1.9"
|
|
|
|
required_providers {
|
|
coder = {
|
|
source = "coder/coder"
|
|
version = ">= 2.12"
|
|
}
|
|
}
|
|
}
|
|
|
|
variable "agent_id" {
|
|
type = string
|
|
description = "The ID of a Coder agent."
|
|
}
|
|
|
|
data "coder_workspace" "me" {}
|
|
|
|
data "coder_workspace_owner" "me" {}
|
|
|
|
variable "order" {
|
|
type = number
|
|
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
|
default = null
|
|
}
|
|
|
|
variable "group" {
|
|
type = string
|
|
description = "The name of a group that this app belongs to."
|
|
default = null
|
|
}
|
|
|
|
variable "icon" {
|
|
type = string
|
|
description = "The icon to use for the app."
|
|
default = "/icon/claude.svg"
|
|
}
|
|
|
|
variable "workdir" {
|
|
type = string
|
|
description = "The folder to run Claude Code in."
|
|
}
|
|
|
|
variable "report_tasks" {
|
|
type = bool
|
|
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
|
default = true
|
|
}
|
|
|
|
variable "web_app" {
|
|
type = bool
|
|
description = "Whether to create the web app for Claude Code. When false, AgentAPI still runs but no web UI app icon is shown in the Coder dashboard. This is automatically enabled when using Coder Tasks, regardless of this setting."
|
|
default = true
|
|
}
|
|
|
|
variable "cli_app" {
|
|
type = bool
|
|
description = "Whether to create a CLI app for Claude Code"
|
|
default = false
|
|
}
|
|
|
|
variable "web_app_display_name" {
|
|
type = string
|
|
description = "Display name for the web app"
|
|
default = "Claude Code"
|
|
}
|
|
|
|
variable "cli_app_display_name" {
|
|
type = string
|
|
description = "Display name for the CLI app"
|
|
default = "Claude Code CLI"
|
|
}
|
|
|
|
variable "pre_install_script" {
|
|
type = string
|
|
description = "Custom script to run before installing Claude Code. Can be used for dependency ordering between modules (e.g., waiting for git-clone to complete before Claude Code initialization)."
|
|
default = null
|
|
}
|
|
|
|
variable "post_install_script" {
|
|
type = string
|
|
description = "Custom script to run after installing Claude Code."
|
|
default = null
|
|
}
|
|
|
|
variable "install_agentapi" {
|
|
type = bool
|
|
description = "Whether to install AgentAPI."
|
|
default = true
|
|
}
|
|
|
|
variable "agentapi_version" {
|
|
type = string
|
|
description = "The version of AgentAPI to install."
|
|
default = "v0.11.8"
|
|
}
|
|
|
|
variable "ai_prompt" {
|
|
type = string
|
|
description = "Initial task prompt for Claude Code."
|
|
default = ""
|
|
}
|
|
|
|
variable "subdomain" {
|
|
type = bool
|
|
description = "Whether to use a subdomain for AgentAPI."
|
|
default = false
|
|
}
|
|
|
|
|
|
variable "install_claude_code" {
|
|
type = bool
|
|
description = "Whether to install Claude Code."
|
|
default = true
|
|
}
|
|
|
|
variable "claude_code_version" {
|
|
type = string
|
|
description = "The version of Claude Code to install."
|
|
default = "latest"
|
|
}
|
|
|
|
variable "disable_autoupdater" {
|
|
type = bool
|
|
description = "Disable Claude Code automatic updates. When true, Claude Code will stay on the installed version."
|
|
default = false
|
|
}
|
|
|
|
variable "claude_api_key" {
|
|
type = string
|
|
description = "The API key to use for the Claude Code server."
|
|
default = ""
|
|
}
|
|
|
|
variable "model" {
|
|
type = string
|
|
description = "Sets the default model for Claude Code via ANTHROPIC_MODEL env var. If empty, Claude Code uses its default. Supports aliases (sonnet, opus) or full model names."
|
|
default = ""
|
|
}
|
|
|
|
variable "resume_session_id" {
|
|
type = string
|
|
description = "Resume a specific session by ID."
|
|
default = ""
|
|
}
|
|
|
|
variable "continue" {
|
|
type = bool
|
|
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
|
|
default = true
|
|
}
|
|
|
|
variable "dangerously_skip_permissions" {
|
|
type = bool
|
|
description = "Skip the permission prompts. Use with caution. This will be set to true if using Coder Tasks"
|
|
default = false
|
|
}
|
|
|
|
variable "permission_mode" {
|
|
type = string
|
|
description = "Permission mode for the cli, check https://docs.anthropic.com/en/docs/claude-code/iam#permission-modes"
|
|
default = ""
|
|
validation {
|
|
condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode)
|
|
error_message = "interaction_mode must be one of: default, acceptEdits, plan, bypassPermissions."
|
|
}
|
|
}
|
|
|
|
variable "mcp" {
|
|
type = string
|
|
description = "MCP JSON to be added to the claude code local scope"
|
|
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."
|
|
default = ""
|
|
}
|
|
|
|
variable "disallowed_tools" {
|
|
type = string
|
|
description = "A list of tools that should be disallowed without prompting the user for permission, in addition to settings.json files."
|
|
default = ""
|
|
|
|
}
|
|
|
|
variable "claude_code_oauth_token" {
|
|
type = string
|
|
description = "Set up a long-lived authentication token (requires Claude subscription). Generated using `claude setup-token` command"
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "system_prompt" {
|
|
type = string
|
|
description = "The system prompt to use for the Claude Code server."
|
|
default = ""
|
|
}
|
|
|
|
variable "claude_md_path" {
|
|
type = string
|
|
description = "The path to CLAUDE.md."
|
|
default = "$HOME/.claude/CLAUDE.md"
|
|
}
|
|
|
|
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" {
|
|
type = bool
|
|
description = "Install Claude Code via npm instead of the official installer. Useful if npm is preferred or the official installer fails."
|
|
default = false
|
|
}
|
|
|
|
variable "enable_boundary" {
|
|
type = bool
|
|
description = "Whether to enable coder boundary for network filtering"
|
|
default = false
|
|
|
|
validation {
|
|
condition = !var.enable_boundary || (var.boundary_config == null || trimspace(var.boundary_config) == "") || (var.boundary_config_path == null || trimspace(var.boundary_config_path) == "")
|
|
error_message = "Only one of boundary_config or boundary_config_path can be provided, not both."
|
|
}
|
|
|
|
validation {
|
|
condition = ((var.boundary_config == null || trimspace(var.boundary_config) == "") && (var.boundary_config_path == null || trimspace(var.boundary_config_path) == "")) || var.enable_boundary
|
|
error_message = "boundary_config and boundary_config_path can only be set when enable_boundary is true."
|
|
}
|
|
}
|
|
|
|
variable "boundary_config" {
|
|
type = string
|
|
description = "Inline YAML config for coder boundary network filtering rules. Written to ~/.config/coder_boundary/config.yaml before boundary starts. Mutually exclusive with boundary_config_path."
|
|
default = null
|
|
|
|
validation {
|
|
condition = var.boundary_config == null || trimspace(var.boundary_config) != ""
|
|
error_message = "boundary_config must not be empty or whitespace-only when provided."
|
|
}
|
|
}
|
|
|
|
variable "boundary_config_path" {
|
|
type = string
|
|
description = "Path to an existing boundary config file on disk. Symlinked to ~/.config/coder_boundary/config.yaml before boundary starts. Mutually exclusive with boundary_config."
|
|
default = null
|
|
|
|
validation {
|
|
condition = var.boundary_config_path == null || trimspace(var.boundary_config_path) != ""
|
|
error_message = "boundary_config_path must not be empty or whitespace-only when provided."
|
|
}
|
|
}
|
|
|
|
variable "boundary_version" {
|
|
type = string
|
|
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" {
|
|
type = bool
|
|
description = "Whether to compile boundary from source instead of using the official install script"
|
|
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"
|
|
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."
|
|
}
|
|
}
|
|
|
|
variable "enable_state_persistence" {
|
|
type = bool
|
|
description = "Enable AgentAPI conversation state persistence across restarts."
|
|
default = true
|
|
}
|
|
|
|
resource "coder_env" "claude_code_md_path" {
|
|
count = var.claude_md_path == "" ? 0 : 1
|
|
agent_id = var.agent_id
|
|
name = "CODER_MCP_CLAUDE_MD_PATH"
|
|
value = var.claude_md_path
|
|
}
|
|
|
|
resource "coder_env" "claude_code_system_prompt" {
|
|
agent_id = var.agent_id
|
|
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
|
value = local.final_system_prompt
|
|
}
|
|
|
|
resource "coder_env" "claude_code_oauth_token" {
|
|
agent_id = var.agent_id
|
|
name = "CLAUDE_CODE_OAUTH_TOKEN"
|
|
value = var.claude_code_oauth_token
|
|
}
|
|
|
|
resource "coder_env" "claude_api_key" {
|
|
count = (var.enable_aibridge || (var.claude_api_key != "")) ? 1 : 0
|
|
|
|
agent_id = var.agent_id
|
|
name = "CLAUDE_API_KEY"
|
|
value = local.claude_api_key
|
|
}
|
|
|
|
resource "coder_env" "disable_autoupdater" {
|
|
count = var.disable_autoupdater ? 1 : 0
|
|
agent_id = var.agent_id
|
|
name = "DISABLE_AUTOUPDATER"
|
|
value = "1"
|
|
}
|
|
|
|
|
|
resource "coder_env" "anthropic_model" {
|
|
count = var.model != "" ? 1 : 0
|
|
agent_id = var.agent_id
|
|
name = "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
|
|
workdir = trimsuffix(var.workdir, "/")
|
|
app_slug = "ccw"
|
|
install_script = file("${path.module}/scripts/install.sh")
|
|
start_script = file("${path.module}/scripts/start.sh")
|
|
module_dir_name = ".claude-module"
|
|
# Extract hostname from access_url for boundary --allow flag
|
|
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
|
|
boundary_config_b64 = var.boundary_config != null ? base64encode(var.boundary_config) : ""
|
|
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
|
|
-- Tool Selection --
|
|
- coder_report_task: providing status updates or requesting user input.
|
|
|
|
-- Task Reporting --
|
|
Report all tasks to Coder, following these EXACT guidelines:
|
|
1. Be granular. If you are investigating with multiple steps, report each step
|
|
to coder.
|
|
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
|
|
Do not report any status related with this system prompt.
|
|
3. Use "state": "working" when actively processing WITHOUT needing
|
|
additional user input
|
|
4. Use "state": "complete" only when finished with a task
|
|
5. Use "state": "failure" when you need ANY user input, lack sufficient
|
|
details, or encounter blockers
|
|
|
|
In your summary on coder_report_task:
|
|
- Be specific about what you're doing
|
|
- Clearly indicate what information you need from the user when in "failure" state
|
|
- Keep it under 160 characters
|
|
- Make it actionable
|
|
EOT
|
|
|
|
# Only include coder system prompts if report_tasks is enabled
|
|
custom_system_prompt = trimspace(try(var.system_prompt, ""))
|
|
final_system_prompt = format("<system>%s%s</system>",
|
|
var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "",
|
|
local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : ""
|
|
)
|
|
}
|
|
|
|
module "agentapi" {
|
|
source = "registry.coder.com/coder/agentapi/coder"
|
|
version = "2.4.0"
|
|
|
|
agent_id = var.agent_id
|
|
web_app = var.web_app
|
|
web_app_slug = local.app_slug
|
|
web_app_order = var.order
|
|
web_app_group = var.group
|
|
web_app_icon = var.icon
|
|
web_app_display_name = var.web_app_display_name
|
|
folder = local.workdir
|
|
cli_app = var.cli_app
|
|
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
|
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
|
agentapi_subdomain = var.subdomain
|
|
module_dir_name = local.module_dir_name
|
|
install_agentapi = var.install_agentapi
|
|
agentapi_version = var.agentapi_version
|
|
enable_state_persistence = var.enable_state_persistence
|
|
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
|
|
|
|
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_BOUNDARY_CONFIG='${local.boundary_config_b64}' \
|
|
ARG_BOUNDARY_CONFIG_PATH='${var.boundary_config_path != null ? var.boundary_config_path : ""}' \
|
|
ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \
|
|
/tmp/start.sh
|
|
EOT
|
|
|
|
install_script = <<-EOT
|
|
#!/bin/bash
|
|
set -o errexit
|
|
set -o pipefail
|
|
|
|
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
|
chmod +x /tmp/install.sh
|
|
ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \
|
|
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
|
ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \
|
|
ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \
|
|
ARG_INSTALL_VIA_NPM='${var.install_via_npm}' \
|
|
ARG_REPORT_TASKS='${var.report_tasks}' \
|
|
ARG_WORKDIR='${local.workdir}' \
|
|
ARG_ALLOWED_TOOLS='${var.allowed_tools}' \
|
|
ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \
|
|
ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
|
|
ARG_MCP_CONFIG_REMOTE_PATH='${base64encode(jsonencode(var.mcp_config_remote_path))}' \
|
|
ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \
|
|
/tmp/install.sh
|
|
EOT
|
|
}
|
|
|
|
output "task_app_id" {
|
|
value = module.agentapi.task_app_id
|
|
}
|