Adds a `web_app` variable (default: `true`) to both the `claude-code`
and `agentapi` modules. When set to `false`, AgentAPI still runs but the
web UI app icon is not shown in the Coder dashboard.
This mirrors the existing `cli_app` toggle pattern.
## Changes
### `agentapi` module
- New `web_app` variable (bool, default `true`)
- `coder_app.agentapi_web` now has `count = local.web_app ? 1 : 0`
- **Task-safe:** `local.web_app` is computed as `var.web_app ||
local.is_task`, where `is_task = try(data.coder_task.me.enabled,
false)`. This means the web app is always created when the workspace is
a Task, regardless of the `web_app` variable.
- `task_app_id` output returns `""` when `local.web_app` is `false`
### `claude-code` module
- New `web_app` variable (bool, default `true`)
- `TODO` comment to wire `web_app` through to agentapi once published
## Usage (once fully wired)
```hcl
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
...
web_app = false # hides the Claude Code web UI from the dashboard
}
```
Setting `web_app = false` is safe even in templates that use
`coder_ai_task` — the module detects Tasks via
`data.coder_task.me.enabled` and automatically enables the web app.
## Merge strategy
This needs to land in two steps:
1. **Merge this PR** — publishes the agentapi module with `web_app`
support, and adds the `web_app` variable to claude-code (not yet wired
through)
2. **Follow-up PR** — bump the agentapi version in claude-code and
replace the `TODO` with `web_app = var.web_app`
---------
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
359 lines
12 KiB
HCL
359 lines
12 KiB
HCL
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 "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)."
|
|
default = null
|
|
}
|
|
|
|
variable "web_app_group" {
|
|
type = string
|
|
description = "The name of a group that this app belongs to."
|
|
default = null
|
|
}
|
|
|
|
variable "web_app_icon" {
|
|
type = string
|
|
description = "The icon to use for the app."
|
|
}
|
|
|
|
variable "web_app_display_name" {
|
|
type = string
|
|
description = "The display name of the web app."
|
|
}
|
|
|
|
variable "web_app_slug" {
|
|
type = string
|
|
description = "The slug of the web app."
|
|
}
|
|
|
|
variable "folder" {
|
|
type = string
|
|
description = "The folder to run AgentAPI in."
|
|
default = "/home/coder"
|
|
}
|
|
|
|
variable "web_app" {
|
|
type = bool
|
|
description = "Whether to create the web workspace app. This is automatically enabled when using Coder Tasks, regardless of this setting."
|
|
default = true
|
|
}
|
|
|
|
variable "cli_app" {
|
|
type = bool
|
|
description = "Whether to create the CLI workspace app."
|
|
default = false
|
|
}
|
|
|
|
variable "cli_app_order" {
|
|
type = number
|
|
description = "The order of the CLI workspace app."
|
|
default = null
|
|
}
|
|
|
|
variable "cli_app_group" {
|
|
type = string
|
|
description = "The group of the CLI workspace app."
|
|
default = null
|
|
}
|
|
|
|
variable "cli_app_icon" {
|
|
type = string
|
|
description = "The icon to use for the app."
|
|
default = "/icon/claude.svg"
|
|
}
|
|
|
|
variable "cli_app_display_name" {
|
|
type = string
|
|
description = "The display name of the CLI workspace app."
|
|
}
|
|
|
|
variable "cli_app_slug" {
|
|
type = string
|
|
description = "The slug of the CLI workspace app."
|
|
}
|
|
|
|
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 = ""
|
|
}
|
|
|
|
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 "install_agentapi" {
|
|
type = bool
|
|
description = "Whether to install AgentAPI."
|
|
default = true
|
|
}
|
|
|
|
variable "agentapi_version" {
|
|
type = string
|
|
description = "The version of AgentAPI to install."
|
|
default = "v0.10.0"
|
|
}
|
|
|
|
variable "agentapi_port" {
|
|
type = number
|
|
description = "The port used by AgentAPI."
|
|
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
|
|
# was added in v0.3.3.
|
|
# This is unfortunately a regex because there is no builtin way to compare semantic versions in Terraform.
|
|
# See: https://regex101.com/r/oHPyRa/1
|
|
agentapi_subdomain_false_min_version_expr = "^v(0\\.(3\\.[3-9]|3.[1-9]\\d+|[4-9]\\.\\d+|[1-9]\\d+\\.\\d+)|[1-9]\\d*\\.\\d+\\.\\d+)$"
|
|
}
|
|
|
|
variable "agentapi_subdomain" {
|
|
type = bool
|
|
description = "Whether to use a subdomain for AgentAPI."
|
|
default = true
|
|
validation {
|
|
condition = var.agentapi_subdomain || (
|
|
# If version doesn't look like a valid semantic version, just allow it.
|
|
# Note that boolean operators do not short-circuit in Terraform.
|
|
can(regex("^v\\d+\\.\\d+\\.\\d+$", var.agentapi_version)) ?
|
|
can(regex(local.agentapi_subdomain_false_min_version_expr, var.agentapi_version)) :
|
|
true
|
|
)
|
|
error_message = "Running with subdomain = false is only supported by agentapi >= v0.3.3."
|
|
}
|
|
}
|
|
|
|
variable "module_dir_name" {
|
|
type = string
|
|
description = "Name of the subdirectory in the home directory for module files."
|
|
}
|
|
|
|
variable "enable_boundary" {
|
|
type = bool
|
|
description = "Enable coder boundary for network filtering. Requires boundary_config to be set."
|
|
default = false
|
|
}
|
|
|
|
variable "boundary_config_path" {
|
|
type = string
|
|
description = "Path to boundary config.yaml inside the workspace. If provided, exposed as BOUNDARY_CONFIG env var."
|
|
default = ""
|
|
}
|
|
|
|
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_state_persistence" {
|
|
type = bool
|
|
description = "Enable AgentAPI conversation state persistence across restarts."
|
|
default = false
|
|
}
|
|
|
|
variable "state_file_path" {
|
|
type = string
|
|
description = "Path to the AgentAPI state file. Defaults to $HOME/<module_dir_name>/agentapi-state.json."
|
|
default = ""
|
|
}
|
|
|
|
variable "pid_file_path" {
|
|
type = string
|
|
description = "Path to the AgentAPI PID file. Defaults to $HOME/<module_dir_name>/agentapi.pid."
|
|
default = ""
|
|
}
|
|
|
|
resource "coder_env" "boundary_config" {
|
|
count = var.enable_boundary && var.boundary_config_path != "" ? 1 : 0
|
|
agent_id = var.agent_id
|
|
name = "BOUNDARY_CONFIG"
|
|
value = var.boundary_config_path
|
|
}
|
|
|
|
locals {
|
|
# If this is a Task, always create the web app regardless of var.web_app
|
|
# since coder_ai_task requires the app to function.
|
|
is_task = try(data.coder_task.me.enabled, false)
|
|
web_app = var.web_app || local.is_task
|
|
|
|
# we always trim the slash for consistency
|
|
workdir = trimsuffix(var.folder, "/")
|
|
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) : ""
|
|
agentapi_start_script_b64 = base64encode(var.start_script)
|
|
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
|
|
// Chat base path is only set if not using a subdomain.
|
|
// NOTE:
|
|
// - Initial support for --chat-base-path was added in v0.3.1 but configuration
|
|
// via environment variable AGENTAPI_CHAT_BASE_PATH was added in v0.3.3.
|
|
// - As CODER_WORKSPACE_AGENT_NAME is a recent addition we use agent ID
|
|
// for backward compatibility.
|
|
agentapi_chat_base_path = var.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")
|
|
lib_script = file("${path.module}/scripts/lib.sh")
|
|
boundary_script = file("${path.module}/scripts/boundary.sh")
|
|
}
|
|
|
|
resource "coder_script" "agentapi" {
|
|
agent_id = var.agent_id
|
|
display_name = "Install and start AgentAPI"
|
|
icon = var.web_app_icon
|
|
script = <<-EOT
|
|
#!/bin/bash
|
|
set -o errexit
|
|
set -o pipefail
|
|
|
|
echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh
|
|
chmod +x /tmp/main.sh
|
|
echo -n '${base64encode(local.lib_script)}' | base64 -d > /tmp/agentapi-lib.sh
|
|
|
|
echo -n '${base64encode(local.boundary_script)}' | base64 -d > /tmp/agentapi-boundary.sh
|
|
chmod +x /tmp/agentapi-boundary.sh
|
|
|
|
ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
|
|
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \
|
|
ARG_PRE_INSTALL_SCRIPT="$(echo -n '${local.encoded_pre_install_script}' | base64 -d)" \
|
|
ARG_INSTALL_SCRIPT="$(echo -n '${local.encoded_install_script}' | base64 -d)" \
|
|
ARG_INSTALL_AGENTAPI='${var.install_agentapi}' \
|
|
ARG_AGENTAPI_VERSION='${var.agentapi_version}' \
|
|
ARG_START_SCRIPT="$(echo -n '${local.agentapi_start_script_b64}' | base64 -d)" \
|
|
ARG_WAIT_FOR_START_SCRIPT="$(echo -n '${local.agentapi_wait_for_start_script_b64}' | base64 -d)" \
|
|
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}' \
|
|
ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \
|
|
ARG_BOUNDARY_VERSION='${var.boundary_version}' \
|
|
ARG_COMPILE_BOUNDARY_FROM_SOURCE='${var.compile_boundary_from_source}' \
|
|
ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \
|
|
ARG_ENABLE_STATE_PERSISTENCE='${var.enable_state_persistence}' \
|
|
ARG_STATE_FILE_PATH='${var.state_file_path}' \
|
|
ARG_PID_FILE_PATH='${var.pid_file_path}' \
|
|
/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
|
|
echo -n '${base64encode(local.lib_script)}' | base64 -d > /tmp/agentapi-lib.sh
|
|
|
|
ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \
|
|
ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \
|
|
ARG_AGENTAPI_PORT='${var.agentapi_port}' \
|
|
ARG_ENABLE_STATE_PERSISTENCE='${var.enable_state_persistence}' \
|
|
ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
|
|
ARG_PID_FILE_PATH='${var.pid_file_path}' \
|
|
/tmp/agentapi-shutdown.sh
|
|
EOT
|
|
}
|
|
|
|
resource "coder_app" "agentapi_web" {
|
|
count = local.web_app ? 1 : 0
|
|
|
|
slug = var.web_app_slug
|
|
display_name = var.web_app_display_name
|
|
agent_id = var.agent_id
|
|
url = "http://localhost:${var.agentapi_port}/"
|
|
icon = var.web_app_icon
|
|
order = var.web_app_order
|
|
group = var.web_app_group
|
|
subdomain = var.agentapi_subdomain
|
|
healthcheck {
|
|
url = "http://localhost:${var.agentapi_port}/status"
|
|
interval = 3
|
|
threshold = 20
|
|
}
|
|
}
|
|
|
|
resource "coder_app" "agentapi_cli" {
|
|
count = var.cli_app ? 1 : 0
|
|
|
|
slug = var.cli_app_slug
|
|
display_name = var.cli_app_display_name
|
|
agent_id = var.agent_id
|
|
command = <<-EOT
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
export LANG=en_US.UTF-8
|
|
export LC_ALL=en_US.UTF-8
|
|
|
|
agentapi attach
|
|
EOT
|
|
icon = var.cli_app_icon
|
|
order = var.cli_app_order
|
|
group = var.cli_app_group
|
|
}
|
|
|
|
output "task_app_id" {
|
|
value = local.web_app ? coder_app.agentapi_web[0].id : ""
|
|
}
|