mirror of
https://github.com/coder/registry.git
synced 2026-06-05 14:08:15 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d0504aef9 | |||
| c1c0dec90f | |||
| 59b67c2c98 | |||
| 7abe422e0a | |||
| db8217e4e5 | |||
| f75afeb0c8 | |||
| 182e5548e2 | |||
| d057a820c1 | |||
| b4e9545c35 | |||
| 50ac3b31f6 | |||
| 056937a758 | |||
| af8b4f02fd | |||
| 2de6a57a3f | |||
| 60fec19d7d | |||
| 44354b202d | |||
| 80acbd7e3a | |||
| 80f429faf1 | |||
| e516446d03 | |||
| f0045397d4 | |||
| 6af8508bc0 | |||
| d212de47ed | |||
| 54b9bf3038 | |||
| cb990bbee0 | |||
| 213aabb3b0 | |||
| 2937286712 | |||
| 8d556a8ab7 |
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.36.2
|
||||
uses: crate-ci/typos@v1.37.2
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg width="251" height="251" viewBox="0 0 251 251" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 47.0195C39.45 49.6394 71.06 81.3272 73.54 120.815H119.61C117.05 55.8589 64.93 3.64245 0 0.942627V47.0195Z" fill="#0DC09D"/>
|
||||
<path d="M73.8 131.324C71.18 170.771 39.49 202.379 0 204.859V250.926C64.96 248.366 117.18 196.249 119.88 131.324H73.8Z" fill="#0DC09D"/>
|
||||
<path d="M176.201 120.545C178.821 81.0972 210.511 49.4894 250.001 47.0095V0.942627C185.041 3.50245 132.821 55.619 130.121 120.545H176.201Z" fill="#0DC09D"/>
|
||||
<path d="M250.001 204.849C210.551 202.229 178.941 170.542 176.461 131.054H130.391C132.951 196.01 185.071 248.226 250.001 250.926V204.849Z" fill="#0DC09D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 693 B |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="32" viewBox="0 0 375 375" width="32" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect fill="#0071ff" height="375.001088" rx="58.59392" stroke-width=".91553" width="375.001088" x=".0009759" y="-.0066962"/>
|
||||
<path d="m150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -495,4 +495,8 @@ When reporting bugs, include:
|
||||
4. **Breaking changes** without defaults
|
||||
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
|
||||
|
||||
## For Maintainers
|
||||
|
||||
Guidelines for reviewing PRs, managing releases, and maintaining the registry. [See the maintainer guide for detailed information.](./MAINTAINER.md)
|
||||
|
||||
Happy contributing! 🚀
|
||||
|
||||
+3
-1
@@ -23,6 +23,7 @@ Check that PRs have:
|
||||
- [ ] Working tests (`terraform test`)
|
||||
- [ ] Formatted code (`bun run fmt`)
|
||||
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
|
||||
- [ ] Version label: `version:patch`, `version:minor`, or `version:major`
|
||||
|
||||
### Version Guidelines
|
||||
|
||||
@@ -32,7 +33,8 @@ When reviewing PRs, ensure the version change follows semantic versioning:
|
||||
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
|
||||
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
|
||||
|
||||
PRs should clearly indicate the version change (e.g., `v1.2.3 → v1.2.4`).
|
||||
PRs should clearly indicate the intended version change (e.g., `v1.2.3 → v1.2.4`) and include the appropriate label: `version:patch`, `version:minor`, or `version:major`.
|
||||
The “Version Bump” CI uses this label to validate required updates (README version refs, etc.).
|
||||
|
||||
### Validate READMEs
|
||||
|
||||
|
||||
@@ -48,3 +48,7 @@ Simply include that snippet inside your Coder template, defining any data depend
|
||||
## Contributing
|
||||
|
||||
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
|
||||
|
||||
## For Maintainers
|
||||
|
||||
Guidelines for maintainers reviewing PRs and managing releases. [See the maintainer guide for more information.](./MAINTAINER.md)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,14 @@
|
||||
---
|
||||
display_name: "Benraouane Soufiane"
|
||||
bio: "Full stack developer creating awesome things."
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "benraouanesoufiane"
|
||||
linkedin: "https://www.linkedin.com/in/benraouane-soufiane" # Optional
|
||||
website: "https://benraouanesoufiane.com" # Optional
|
||||
support_email: "hello@benraouanesoufiane.com" # Optional
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Benraouane Soufiane
|
||||
|
||||
Full stack developer creating awesome things.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
display_name: RustDesk
|
||||
description: Run RustDesk in your workspace with virtual display
|
||||
icon: ../../../../.icons/rustdesk.svg
|
||||
verified: false
|
||||
tags: [rustdesk, rdp, vm]
|
||||
---
|
||||
|
||||
# RustDesk
|
||||
|
||||
Launches RustDesk within your workspace with a virtual display to provide remote desktop access. The module outputs the RustDesk ID and password needed to connect from external RustDesk clients.
|
||||
|
||||
```tf
|
||||
module "rustdesk" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically sets up virtual display (Xvfb)
|
||||
- Downloads and configures RustDesk
|
||||
- Outputs RustDesk ID and password for easy connection
|
||||
- Provides external app link to RustDesk web client for browser-based access
|
||||
- Starts virtual display (Xvfb) with customizable resolution
|
||||
- Customizable screen resolution and RustDesk version
|
||||
|
||||
## Requirements
|
||||
|
||||
- Coder v2.5 or higher
|
||||
- Linux workspace with `apt`, `dnf`, or `yum` package manager
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom configuration with specific version
|
||||
|
||||
```tf
|
||||
module "rustdesk" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
rustdesk_password = "mycustompass"
|
||||
xvfb_resolution = "1920x1080x24"
|
||||
rustdesk_version = "1.4.1"
|
||||
}
|
||||
```
|
||||
|
||||
### Docker container configuration
|
||||
|
||||
It requires coder' server to be run as root, when using with Docker, add the following to your `docker_container` resource:
|
||||
|
||||
```tf
|
||||
resource "docker_container" "workspace" {
|
||||
|
||||
# ... other configuration ...
|
||||
|
||||
user = "root"
|
||||
privileged = true
|
||||
network_mode = "host"
|
||||
|
||||
ports {
|
||||
internal = 21115
|
||||
external = 21115
|
||||
}
|
||||
ports {
|
||||
internal = 21116
|
||||
external = 21116
|
||||
}
|
||||
ports {
|
||||
internal = 21118
|
||||
external = 21118
|
||||
}
|
||||
ports {
|
||||
internal = 21119
|
||||
external = 21119
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "log_path" {
|
||||
type = string
|
||||
description = "The path to log rustdesk to."
|
||||
default = "/tmp/rustdesk.log"
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
description = "Attach RustDesk setup to this agent"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
description = "Run order among scripts/apps"
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
# Optional knobs passed as env (you can expose these as variables too)
|
||||
variable "rustdesk_password" {
|
||||
description = "If empty, the script will generate one"
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "xvfb_resolution" {
|
||||
description = "Xvfb screen size/depth"
|
||||
type = string
|
||||
default = "1024x768x16"
|
||||
}
|
||||
|
||||
variable "rustdesk_version" {
|
||||
description = "RustDesk version to install (use 'latest' for most recent release)"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
resource "coder_script" "rustdesk" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "RustDesk"
|
||||
run_on_start = true
|
||||
|
||||
# Prepend env as bash exports, then append the script file literally.
|
||||
script = <<-EOT
|
||||
# --- module-provided env knobs ---
|
||||
export RUSTDESK_PASSWORD="${var.rustdesk_password}"
|
||||
export XVFB_RESOLUTION="${var.xvfb_resolution}"
|
||||
export RUSTDESK_VERSION="${var.rustdesk_version}"
|
||||
# ---------------------------------
|
||||
|
||||
${file("${path.module}/run.sh")}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "coder_app" "rustdesk" {
|
||||
agent_id = var.agent_id
|
||||
slug = "rustdesk"
|
||||
display_name = "Rustdesk"
|
||||
url = "https://rustdesk.com/web"
|
||||
icon = "/icon/rustdesk.svg"
|
||||
order = var.order
|
||||
external = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "${BOLD}🖥️ Installing RustDesk Remote Desktop\n${RESET}"
|
||||
|
||||
# ---- configurable knobs (env overrides) ----
|
||||
RUSTDESK_VERSION="${RUSTDESK_VERSION:-latest}"
|
||||
LOG_PATH="${LOG_PATH:-/tmp/rustdesk.log}"
|
||||
|
||||
# ---- fetch latest version if needed ----
|
||||
if [ "$RUSTDESK_VERSION" = "latest" ]; then
|
||||
printf "🔍 Fetching latest RustDesk version...\n"
|
||||
RUSTDESK_VERSION=$(curl -s https://api.github.com/repos/rustdesk/rustdesk/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || echo "1.4.1")
|
||||
printf "📌 Fetched RustDesk version: ${RUSTDESK_VERSION}\n"
|
||||
else
|
||||
printf "📌 Using specified RustDesk version: ${RUSTDESK_VERSION}\n"
|
||||
fi
|
||||
XVFB_RESOLUTION="${XVFB_RESOLUTION:-1024x768x16}"
|
||||
RUSTDESK_PASSWORD="${RUSTDESK_PASSWORD:-}"
|
||||
|
||||
# ---- detect package manager & arch ----
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
x86_64 | amd64) PKG_ARCH="x86_64" ;;
|
||||
aarch64 | arm64) PKG_ARCH="aarch64" ;;
|
||||
*)
|
||||
echo "❌ Unsupported arch: $ARCH"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v apt-get > /dev/null 2>&1; then
|
||||
PKG_SYS="deb"
|
||||
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.deb"
|
||||
INSTALL_DEPS='apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y wget libva2 libva-drm2 libva-x11-2 libgstreamer-plugins-base1.0-0 gstreamer1.0-pipewire xfce4 xfce4-goodies xvfb x11-xserver-utils dbus-x11 libegl1 libgl1 libglx0 libglu1-mesa mesa-utils libxrandr2 libxss1 libgtk-3-0t64 libgbm1 libdrm2 libxcomposite1 libxdamage1 libxfixes3'
|
||||
INSTALL_CMD="apt-get install -y ./${PKG_NAME}"
|
||||
CLEAN_CMD="rm -f \"${PKG_NAME}\""
|
||||
elif command -v dnf > /dev/null 2>&1; then
|
||||
PKG_SYS="rpm"
|
||||
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
|
||||
INSTALL_DEPS='dnf install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
|
||||
INSTALL_CMD="dnf install -y ./${PKG_NAME}"
|
||||
CLEAN_CMD="rm -f \"${PKG_NAME}\""
|
||||
elif command -v yum > /dev/null 2>&1; then
|
||||
PKG_SYS="rpm"
|
||||
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
|
||||
INSTALL_DEPS='yum install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
|
||||
INSTALL_CMD="yum install -y ./${PKG_NAME}"
|
||||
CLEAN_CMD="rm -f \"${PKG_NAME}\""
|
||||
else
|
||||
echo "❌ Unsupported distro: need apt, dnf, or yum."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- install rustdesk if missing ----
|
||||
if ! command -v rustdesk > /dev/null 2>&1; then
|
||||
printf "📦 Installing dependencies...\n"
|
||||
sudo bash -c "$INSTALL_DEPS" 2>&1 | tee -a "${LOG_PATH}"
|
||||
|
||||
printf "⬇️ Downloading RustDesk ${RUSTDESK_VERSION} (${PKG_SYS}, ${PKG_ARCH})...\n"
|
||||
URL="https://github.com/rustdesk/rustdesk/releases/download/${RUSTDESK_VERSION}/${PKG_NAME}"
|
||||
wget -q "$URL" 2>&1 | tee -a "${LOG_PATH}"
|
||||
|
||||
printf "🔧 Installing RustDesk...\n"
|
||||
sudo bash -c "$INSTALL_CMD" 2>&1 | tee -a "${LOG_PATH}"
|
||||
|
||||
printf "🧹 Cleaning up...\n"
|
||||
bash -c "$CLEAN_CMD" 2>&1 | tee -a "${LOG_PATH}"
|
||||
else
|
||||
printf "✅ RustDesk already installed\n"
|
||||
fi
|
||||
|
||||
# ---- start virtual display ----
|
||||
echo "Starting Xvfb with resolution ${XVFB_RESOLUTION}…"
|
||||
Xvfb :99 -screen 0 "${XVFB_RESOLUTION}" >> "${LOG_PATH}" 2>&1 &
|
||||
export DISPLAY=:99
|
||||
|
||||
# Wait for X to be ready
|
||||
for i in {1..10}; do
|
||||
if xdpyinfo -display :99 > /dev/null 2>&1; then
|
||||
echo "X display is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# ---- create (or accept) password and start rustdesk ----
|
||||
if [[ -z "${RUSTDESK_PASSWORD}" ]]; then
|
||||
RUSTDESK_PASSWORD="$(tr -dc 'a-zA-Z0-9@' < /dev/urandom | head -c 10)@97"
|
||||
fi
|
||||
|
||||
echo "Starting XFCE desktop environment..."
|
||||
xfce4-session >> "${LOG_PATH}" 2>&1 &
|
||||
|
||||
echo "Waiting for xfce4-session to initialize..."
|
||||
sleep 5
|
||||
|
||||
printf "🔐 Setting RustDesk password and starting service...\n"
|
||||
rustdesk >> "${LOG_PATH}" 2>&1 &
|
||||
sleep 2
|
||||
|
||||
rustdesk --password "${RUSTDESK_PASSWORD}" >> "${LOG_PATH}" 2>&1 &
|
||||
sleep 3
|
||||
|
||||
RID="$(rustdesk --get-id 2> /dev/null || echo 'ID_PENDING')"
|
||||
|
||||
printf "🥳 RustDesk setup complete!\n\n"
|
||||
printf "${BOLD}📋 Connection Details:${RESET}\n"
|
||||
printf " RustDesk ID: ${RID}\n"
|
||||
printf " RustDesk Password: ${RUSTDESK_PASSWORD}\n"
|
||||
printf " Display: ${DISPLAY} (${XVFB_RESOLUTION})\n"
|
||||
printf "\n📝 Logs available at: ${LOG_PATH}\n\n"
|
||||
|
||||
echo "Setup script completed successfully. All services running in background."
|
||||
exit 0
|
||||
@@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ module "coder-login" {
|
||||
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -103,7 +103,7 @@ EOF
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.6.0"
|
||||
default = "v0.10.0"
|
||||
validation {
|
||||
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+", var.agentapi_version))
|
||||
error_message = "agentapi_version must be a valid semantic version starting with 'v', like 'v0.3.3'."
|
||||
@@ -178,7 +178,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -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 = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = var.openai_api_key
|
||||
folder = "/home/coder/project"
|
||||
@@ -33,7 +33,7 @@ module "codex" {
|
||||
module "codex" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
folder = "/home/coder/project"
|
||||
@@ -60,7 +60,7 @@ module "coder-login" {
|
||||
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
@@ -106,7 +106,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
# ... other variables ...
|
||||
|
||||
# Override default configuration
|
||||
|
||||
@@ -80,7 +80,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.5.0"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "codex_model" {
|
||||
@@ -128,7 +128,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
display_name: Copilot CLI
|
||||
description: GitHub Copilot CLI agent for AI-powered terminal assistance
|
||||
icon: ../../../../.icons/github.svg
|
||||
verified: false
|
||||
tags: [agent, copilot, ai, github, tasks]
|
||||
---
|
||||
|
||||
# Copilot
|
||||
|
||||
Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) in your workspace for AI-powered coding assistance directly from the terminal. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This example assumes you have [Coder external authentication](https://coder.com/docs/admin/external-auth) configured with `id = "github"`. If not, you can provide a direct token using the `github_token` variable or provide the correct external authentication id for GitHub by setting `external_auth_id = "my-github"`.
|
||||
|
||||
> [!NOTE]
|
||||
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js v22+** and **npm v10+**
|
||||
- **[Active Copilot subscription](https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot)** (GitHub Copilot Pro, Pro+, Business, or Enterprise)
|
||||
- **GitHub authentication** via one of:
|
||||
- [Coder external authentication](https://coder.com/docs/admin/external-auth) (recommended)
|
||||
- Direct token via `github_token` variable
|
||||
- Interactive login in Copilot
|
||||
|
||||
## Examples
|
||||
|
||||
### Usage with Tasks
|
||||
|
||||
For development environments where you want Copilot to have full access to tools and automatically resume sessions:
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Initial task prompt for Copilot."
|
||||
mutable = true
|
||||
}
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
copilot_model = "claude-sonnet-4.5"
|
||||
allow_all_tools = true
|
||||
resume_session = true
|
||||
|
||||
trusted_directories = ["/home/coder/projects", "/tmp"]
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
Customize tool permissions, MCP servers, and Copilot settings:
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
# Version pinning (defaults to "0.0.334", use "latest" for newest version)
|
||||
copilot_version = "latest"
|
||||
|
||||
# Tool permissions
|
||||
allow_tools = ["shell(git)", "shell(npm)", "write"]
|
||||
trusted_directories = ["/home/coder/projects", "/tmp"]
|
||||
|
||||
# Custom Copilot configuration
|
||||
copilot_config = jsonencode({
|
||||
banner = "never"
|
||||
theme = "dark"
|
||||
})
|
||||
|
||||
# MCP server configuration
|
||||
mcp_config = jsonencode({
|
||||
mcpServers = {
|
||||
filesystem = {
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
|
||||
description = "Provides file system access to the workspace"
|
||||
name = "Filesystem"
|
||||
timeout = 3000
|
||||
type = "local"
|
||||
tools = ["*"]
|
||||
trust = true
|
||||
}
|
||||
playwright = {
|
||||
command = "npx"
|
||||
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
|
||||
description = "Browser automation for testing and previewing changes"
|
||||
name = "Playwright"
|
||||
timeout = 5000
|
||||
type = "local"
|
||||
tools = ["*"]
|
||||
trust = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Pre-install Node.js if needed
|
||||
pre_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> GitHub Copilot CLI does not automatically install MCP servers. You have two options:
|
||||
>
|
||||
> - Use `npx -y` in the MCP config (shown above) to auto-install on each run
|
||||
> - Pre-install MCP servers in `pre_install_script` for faster startup (e.g., `npm install -g @modelcontextprotocol/server-filesystem`)
|
||||
|
||||
### Direct Token Authentication
|
||||
|
||||
Use this example when you want to provide a GitHub Personal Access Token instead of using Coder external auth:
|
||||
|
||||
```tf
|
||||
variable "github_token" {
|
||||
type = string
|
||||
description = "GitHub Personal Access Token"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
github_token = var.github_token
|
||||
}
|
||||
```
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Run Copilot as a command-line tool without task reporting or web interface. This installs and configures Copilot, making it available as a CLI app in the Coder agent bar that you can launch to interact with Copilot directly from your terminal. Set `report_tasks = false` to disable integration with Coder Tasks.
|
||||
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
The module supports multiple authentication methods (in priority order):
|
||||
|
||||
1. **[Coder External Auth](https://coder.com/docs/admin/external-auth) (Recommended)** - Automatic if GitHub external auth is configured in Coder
|
||||
2. **Direct Token** - Pass `github_token` variable (OAuth or Personal Access Token)
|
||||
3. **Interactive** - Copilot prompts for login via `/login` command if no auth found
|
||||
|
||||
> [!NOTE]
|
||||
> OAuth tokens work best with Copilot. Personal Access Tokens may have limited functionality.
|
||||
|
||||
## Session Resumption
|
||||
|
||||
By default, the module resumes the latest Copilot session when the workspace restarts. Set `resume_session = false` to always start fresh sessions.
|
||||
|
||||
> [!NOTE]
|
||||
> Session resumption requires persistent storage for the home directory or workspace volume. Without persistent storage, sessions will not resume across workspace restarts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues, check the log files in the `~/.copilot-module` directory within your workspace for detailed information.
|
||||
|
||||
```bash
|
||||
# Installation logs
|
||||
cat ~/.copilot-module/install.log
|
||||
|
||||
# Startup logs
|
||||
cat ~/.copilot-module/agentapi-start.log
|
||||
|
||||
# Pre/post install script logs
|
||||
cat ~/.copilot-module/pre_install.log
|
||||
cat ~/.copilot-module/post_install.log
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To use tasks with Copilot, you must have an active GitHub Copilot subscription.
|
||||
> The `workdir` variable is required and specifies the directory where Copilot will run.
|
||||
|
||||
## References
|
||||
|
||||
- [GitHub Copilot CLI Documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
|
||||
- [Installing GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
@@ -0,0 +1,236 @@
|
||||
run "defaults_are_correct" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.copilot_model == "claude-sonnet-4.5"
|
||||
error_message = "Default model should be 'claude-sonnet-4.5'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.report_tasks == true
|
||||
error_message = "Task reporting should be enabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.resume_session == true
|
||||
error_message = "Session resumption should be enabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.allow_all_tools == false
|
||||
error_message = "allow_all_tools should be disabled by default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.mcp_app_status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug env var should be created"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.mcp_app_status_slug.value == "copilot"
|
||||
error_message = "Status slug value should be 'copilot'"
|
||||
}
|
||||
}
|
||||
|
||||
run "github_token_creates_env_var" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
github_token = "test_github_token_abc123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.github_token) == 1
|
||||
error_message = "github_token env var should be created when token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.github_token[0].name == "GITHUB_TOKEN"
|
||||
error_message = "github_token env var name should be 'GITHUB_TOKEN'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.github_token[0].value == "test_github_token_abc123"
|
||||
error_message = "github_token env var value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "github_token_not_created_when_empty" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
github_token = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.github_token) == 0
|
||||
error_message = "github_token env var should not be created when empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_model_env_var_for_non_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "claude-sonnet-4"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.copilot_model) == 1
|
||||
error_message = "copilot_model env var should be created for non-default model"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.copilot_model[0].name == "COPILOT_MODEL"
|
||||
error_message = "copilot_model env var name should be 'COPILOT_MODEL'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.copilot_model[0].value == "claude-sonnet-4"
|
||||
error_message = "copilot_model env var value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_model_not_created_for_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "claude-sonnet-4.5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.copilot_model) == 0
|
||||
error_message = "copilot_model env var should not be created for default model"
|
||||
}
|
||||
}
|
||||
|
||||
run "model_validation_accepts_valid_models" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_model = "gpt-5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
|
||||
error_message = "Model should be one of the valid options"
|
||||
}
|
||||
}
|
||||
|
||||
run "copilot_config_merges_with_trusted_directories" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder/project"
|
||||
trusted_directories = ["/workspace", "/data"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.final_copilot_config) > 0
|
||||
error_message = "final_copilot_config should be computed"
|
||||
}
|
||||
|
||||
# Verify workdir is trimmed of trailing slash
|
||||
assert {
|
||||
condition = local.workdir == "/home/coder/project"
|
||||
error_message = "workdir should be trimmed of trailing slash"
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_copilot_config_overrides_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
copilot_config = jsonencode({
|
||||
banner = "always"
|
||||
theme = "dark"
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.copilot_config != ""
|
||||
error_message = "Custom copilot config should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = jsondecode(local.final_copilot_config).banner == "always"
|
||||
error_message = "Custom banner setting should be applied"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = jsondecode(local.final_copilot_config).theme == "dark"
|
||||
error_message = "Custom theme setting should be applied"
|
||||
}
|
||||
}
|
||||
|
||||
run "trusted_directories_merged_with_custom_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder/project"
|
||||
copilot_config = jsonencode({
|
||||
banner = "always"
|
||||
theme = "dark"
|
||||
trusted_folders = ["/custom"]
|
||||
})
|
||||
trusted_directories = ["/workspace", "/data"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/custom")
|
||||
error_message = "Custom trusted folder should be included"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/home/coder/project")
|
||||
error_message = "Workdir should be included in trusted folders"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/workspace")
|
||||
error_message = "trusted_directories should be merged into config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/data")
|
||||
error_message = "All trusted_directories should be merged into config"
|
||||
}
|
||||
}
|
||||
|
||||
run "app_slug_is_consistent" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workdir = "/home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.app_slug == "copilot"
|
||||
error_message = "app_slug should be 'copilot'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.module_dir_name == ".copilot-module"
|
||||
error_message = "module_dir_name should be '.copilot-module'"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("copilot", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
it("creates mcp_app_status_slug env var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"mcp_app_status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("copilot");
|
||||
});
|
||||
|
||||
it("creates github_token env var with correct value", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
github_token: "test_token_12345",
|
||||
});
|
||||
|
||||
const githubTokenEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"github_token",
|
||||
);
|
||||
expect(githubTokenEnv).toBeDefined();
|
||||
expect(githubTokenEnv.name).toBe("GITHUB_TOKEN");
|
||||
expect(githubTokenEnv.value).toBe("test_token_12345");
|
||||
});
|
||||
|
||||
it("does not create github_token env var when empty", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
github_token: "",
|
||||
});
|
||||
|
||||
const githubTokenEnvs = state.resources.filter(
|
||||
(r) => r.type === "coder_env" && r.name === "github_token",
|
||||
);
|
||||
expect(githubTokenEnvs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("creates copilot_model env var for non-default models", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4",
|
||||
});
|
||||
|
||||
const modelEnv = findResourceInstance(state, "coder_env", "copilot_model");
|
||||
expect(modelEnv).toBeDefined();
|
||||
expect(modelEnv.name).toBe("COPILOT_MODEL");
|
||||
expect(modelEnv.value).toBe("claude-sonnet-4");
|
||||
});
|
||||
|
||||
it("does not create copilot_model env var for default model", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4.5",
|
||||
});
|
||||
|
||||
const modelEnvs = state.resources.filter(
|
||||
(r) => r.type === "coder_env" && r.name === "copilot_model",
|
||||
);
|
||||
expect(modelEnvs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("creates coder_script resources via agentapi module", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
});
|
||||
|
||||
// The agentapi module should create coder_script resources for install and start
|
||||
const scripts = state.resources.filter((r) => r.type === "coder_script");
|
||||
expect(scripts.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("validates copilot_model accepts valid values", async () => {
|
||||
// Test valid models don't throw errors
|
||||
await expect(
|
||||
runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "gpt-5",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
await expect(
|
||||
runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder",
|
||||
copilot_model: "claude-sonnet-4.5",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("merges trusted_directories with custom copilot_config", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
workdir: "/home/coder/project",
|
||||
trusted_directories: JSON.stringify(["/workspace", "/data"]),
|
||||
copilot_config: JSON.stringify({
|
||||
banner: "always",
|
||||
theme: "dark",
|
||||
trusted_folders: ["/custom"],
|
||||
}),
|
||||
});
|
||||
|
||||
// Verify that the state was created successfully with the merged config
|
||||
// The actual merging logic is tested in the .tftest.hcl file
|
||||
expect(state).toBeDefined();
|
||||
expect(state.resources).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,301 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Copilot in."
|
||||
}
|
||||
|
||||
variable "external_auth_id" {
|
||||
type = string
|
||||
description = "ID of the GitHub external auth provider configured in Coder."
|
||||
default = "github"
|
||||
}
|
||||
|
||||
variable "github_token" {
|
||||
type = string
|
||||
description = "GitHub OAuth token or Personal Access Token. If provided, this will be used instead of auto-detecting authentication."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "copilot_model" {
|
||||
type = string
|
||||
description = "Model to use. Supported values: claude-sonnet-4, claude-sonnet-4.5 (default), gpt-5."
|
||||
default = "claude-sonnet-4.5"
|
||||
validation {
|
||||
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
|
||||
error_message = "copilot_model must be one of: claude-sonnet-4, claude-sonnet-4.5, gpt-5."
|
||||
}
|
||||
}
|
||||
|
||||
variable "copilot_config" {
|
||||
type = string
|
||||
description = "Custom Copilot configuration as JSON string. Leave empty to use default configuration with banner disabled, theme set to auto, and workdir as trusted folder."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for programmatic mode."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "The system prompt to use for the Copilot server. Task reporting instructions are automatically added when report_tasks is enabled."
|
||||
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
|
||||
}
|
||||
|
||||
variable "trusted_directories" {
|
||||
type = list(string)
|
||||
description = "Additional directories to trust for Copilot operations."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "allow_all_tools" {
|
||||
type = bool
|
||||
description = "Allow all tools without prompting (equivalent to --allow-all-tools)."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "allow_tools" {
|
||||
type = list(string)
|
||||
description = "Specific tools to allow: shell(command), write, or MCP_SERVER_NAME."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "deny_tools" {
|
||||
type = list(string)
|
||||
description = "Specific tools to deny: shell(command), write, or MCP_SERVER_NAME."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "mcp_config" {
|
||||
type = string
|
||||
description = "Custom MCP server configuration as JSON string."
|
||||
default = ""
|
||||
}
|
||||
|
||||
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 "copilot_version" {
|
||||
type = string
|
||||
description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'."
|
||||
default = "0.0.334"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation."
|
||||
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/github.svg"
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app."
|
||||
default = "Copilot"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Copilot."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app."
|
||||
default = "Copilot"
|
||||
}
|
||||
|
||||
variable "resume_session" {
|
||||
type = bool
|
||||
description = "Whether to automatically resume the latest Copilot session on workspace restart."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before configuring Copilot."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after configuring Copilot."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "copilot"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".copilot-module"
|
||||
|
||||
all_trusted_folders = concat([local.workdir], var.trusted_directories)
|
||||
|
||||
parsed_custom_config = try(jsondecode(var.copilot_config), {})
|
||||
|
||||
existing_trusted_folders = try(local.parsed_custom_config.trusted_folders, [])
|
||||
|
||||
merged_copilot_config = merge(
|
||||
{
|
||||
banner = "never"
|
||||
theme = "auto"
|
||||
},
|
||||
local.parsed_custom_config,
|
||||
{
|
||||
trusted_folders = concat(local.existing_trusted_folders, local.all_trusted_folders)
|
||||
}
|
||||
)
|
||||
|
||||
final_copilot_config = jsonencode(local.merged_copilot_config)
|
||||
|
||||
task_reporting_prompt = <<-EOT
|
||||
|
||||
-- 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
|
||||
EOT
|
||||
|
||||
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
|
||||
}
|
||||
|
||||
resource "coder_env" "mcp_app_status_slug" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = local.app_slug
|
||||
}
|
||||
|
||||
resource "coder_env" "copilot_model" {
|
||||
count = var.copilot_model != "claude-sonnet-4.5" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "COPILOT_MODEL"
|
||||
value = var.copilot_model
|
||||
}
|
||||
|
||||
resource "coder_env" "github_token" {
|
||||
count = var.github_token != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "GITHUB_TOKEN"
|
||||
value = var.github_token
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
agentapi_subdomain = var.subdomain
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
ARG_ALLOW_ALL_TOOLS='${var.allow_all_tools}' \
|
||||
ARG_ALLOW_TOOLS='${join(",", var.allow_tools)}' \
|
||||
ARG_DENY_TOOLS='${join(",", var.deny_tools)}' \
|
||||
ARG_TRUSTED_DIRECTORIES='${join(",", var.trusted_directories)}' \
|
||||
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
|
||||
ARG_RESUME_SESSION='${var.resume_session}' \
|
||||
/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_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_MCP_CONFIG='${var.mcp_config != "" ? base64encode(var.mcp_config) : ""}' \
|
||||
ARG_COPILOT_CONFIG='${base64encode(local.final_copilot_config)}' \
|
||||
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
|
||||
ARG_COPILOT_VERSION='${var.copilot_version}' \
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_MCP_CONFIG=$(echo -n "${ARG_MCP_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_COPILOT_CONFIG=$(echo -n "${ARG_COPILOT_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
|
||||
ARG_COPILOT_VERSION=${ARG_COPILOT_VERSION:-0.0.334}
|
||||
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-claude-sonnet-4.5}
|
||||
|
||||
validate_prerequisites() {
|
||||
if ! command_exists node; then
|
||||
echo "ERROR: Node.js not found. Copilot requires Node.js v22+."
|
||||
echo "Install with: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command_exists npm; then
|
||||
echo "ERROR: npm not found. Copilot requires npm v10+."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$node_version" -lt 22 ]; then
|
||||
echo "WARNING: Node.js v$node_version detected. Copilot requires v22+."
|
||||
fi
|
||||
}
|
||||
|
||||
install_copilot() {
|
||||
if ! command_exists copilot; then
|
||||
echo "Installing GitHub Copilot CLI (version: ${ARG_COPILOT_VERSION})..."
|
||||
if [ "$ARG_COPILOT_VERSION" = "latest" ]; then
|
||||
npm install -g @github/copilot
|
||||
else
|
||||
npm install -g "@github/copilot@${ARG_COPILOT_VERSION}"
|
||||
fi
|
||||
|
||||
if ! command_exists copilot; then
|
||||
echo "ERROR: Failed to install Copilot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GitHub Copilot CLI installed successfully"
|
||||
else
|
||||
echo "GitHub Copilot CLI already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
check_github_authentication() {
|
||||
echo "Checking GitHub authentication..."
|
||||
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
echo "✓ GitHub token provided via module configuration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command_exists coder; then
|
||||
if coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" > /dev/null 2>&1; then
|
||||
echo "✓ GitHub OAuth authentication via Coder external auth"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command_exists gh && gh auth status > /dev/null 2>&1; then
|
||||
echo "✓ GitHub OAuth authentication via GitHub CLI"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠ No GitHub authentication detected"
|
||||
echo " Copilot will prompt for authentication when started"
|
||||
echo " For seamless experience, configure GitHub external auth in Coder or run 'gh auth login'"
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_copilot_configurations() {
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
|
||||
local module_path="$HOME/.copilot-module"
|
||||
mkdir -p "$module_path"
|
||||
|
||||
setup_copilot_config
|
||||
|
||||
echo "$ARG_WORKDIR" > "$module_path/trusted_directories"
|
||||
}
|
||||
|
||||
setup_copilot_config() {
|
||||
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
local copilot_config_dir="$XDG_CONFIG_HOME/.copilot"
|
||||
local copilot_config_file="$copilot_config_dir/config.json"
|
||||
local mcp_config_file="$copilot_config_dir/mcp-config.json"
|
||||
|
||||
mkdir -p "$copilot_config_dir"
|
||||
|
||||
if [ -n "$ARG_COPILOT_CONFIG" ]; then
|
||||
echo "Setting up Copilot configuration..."
|
||||
|
||||
if command_exists jq; then
|
||||
echo "$ARG_COPILOT_CONFIG" | jq 'del(.mcpServers)' > "$copilot_config_file"
|
||||
else
|
||||
echo "$ARG_COPILOT_CONFIG" > "$copilot_config_file"
|
||||
fi
|
||||
|
||||
echo "Setting up MCP server configuration..."
|
||||
setup_mcp_config "$mcp_config_file"
|
||||
else
|
||||
echo "ERROR: No Copilot configuration provided"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
setup_mcp_config() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
echo '{"mcpServers": {}}' > "$mcp_config_file"
|
||||
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
|
||||
echo "Adding Coder MCP server for task reporting..."
|
||||
setup_coder_mcp_server "$mcp_config_file"
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_MCP_CONFIG" ]; then
|
||||
echo "Adding custom MCP servers..."
|
||||
add_custom_mcp_servers "$mcp_config_file"
|
||||
fi
|
||||
|
||||
echo "MCP configuration completed: $mcp_config_file"
|
||||
}
|
||||
|
||||
setup_coder_mcp_server() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
local coder_mcp_wrapper_script
|
||||
coder_mcp_wrapper_script=$(
|
||||
cat << EOF
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
export CODER_MCP_APP_STATUS_SLUG="${ARG_MCP_APP_STATUS_SLUG}"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
export CODER_AGENT_URL="${CODER_AGENT_URL}"
|
||||
export CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN}"
|
||||
|
||||
exec coder exp mcp server
|
||||
EOF
|
||||
)
|
||||
echo "$coder_mcp_wrapper_script" > "/tmp/coder-mcp-server.sh"
|
||||
chmod +x /tmp/coder-mcp-server.sh
|
||||
|
||||
local coder_mcp_config
|
||||
coder_mcp_config=$(
|
||||
cat << EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"coder": {
|
||||
"command": "/tmp/coder-mcp-server.sh",
|
||||
"args": [],
|
||||
"description": "CRITICAL: Call this tool immediately after receiving ANY user message to report task status. Rules: (1) Call FIRST before doing work - report what you will do with state='working'. (2) Be granular - report each step separately. (3) State 'working' = actively processing without needing user input. (4) State 'complete' = task 100% finished. (5) State 'failure' = need user input, missing info, or blocked. Example: User says 'fix the bug' -> call with state='working', description='Investigating authentication bug'. When done -> call with state='complete', description='Fixed token validation'. You MUST report on every interaction.",
|
||||
"name": "Coder",
|
||||
"timeout": 3000,
|
||||
"type": "local",
|
||||
"tools": ["*"],
|
||||
"trust": true
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$coder_mcp_config" > "$mcp_config_file"
|
||||
}
|
||||
|
||||
add_custom_mcp_servers() {
|
||||
local mcp_config_file="$1"
|
||||
|
||||
if command_exists jq; then
|
||||
local custom_servers
|
||||
custom_servers=$(echo "$ARG_MCP_CONFIG" | jq '.mcpServers // {}')
|
||||
|
||||
local updated_config
|
||||
updated_config=$(jq --argjson custom "$custom_servers" '.mcpServers += $custom' "$mcp_config_file")
|
||||
echo "$updated_config" > "$mcp_config_file"
|
||||
elif command_exists node; then
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const existing = JSON.parse(fs.readFileSync('$mcp_config_file', 'utf8'));
|
||||
const input = JSON.parse(\`$ARG_MCP_CONFIG\`);
|
||||
const custom = input.mcpServers || {};
|
||||
existing.mcpServers = {...existing.mcpServers, ...custom};
|
||||
fs.writeFileSync('$mcp_config_file', JSON.stringify(existing, null, 2));
|
||||
"
|
||||
else
|
||||
echo "WARNING: jq and node not available, cannot merge custom MCP servers"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_copilot_model() {
|
||||
if [ -n "$ARG_COPILOT_MODEL" ] && [ "$ARG_COPILOT_MODEL" != "claude-sonnet-4.5" ]; then
|
||||
echo "Setting Copilot model to: $ARG_COPILOT_MODEL"
|
||||
copilot config model "$ARG_COPILOT_MODEL" || {
|
||||
echo "WARNING: Failed to set model via copilot config, will use environment variable fallback"
|
||||
export COPILOT_MODEL="$ARG_COPILOT_MODEL"
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
configure_coder_integration() {
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
|
||||
echo "Configuring Copilot task reporting..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
echo "✓ Coder MCP server configured for task reporting"
|
||||
else
|
||||
echo "Task reporting disabled or no app status slug provided."
|
||||
export CODER_MCP_APP_STATUS_SLUG=""
|
||||
export CODER_MCP_AI_AGENTAPI_URL=""
|
||||
fi
|
||||
}
|
||||
|
||||
validate_prerequisites
|
||||
install_copilot
|
||||
check_github_authentication
|
||||
setup_copilot_configurations
|
||||
configure_copilot_model
|
||||
configure_coder_integration
|
||||
|
||||
echo "Copilot module setup completed."
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$HOME"/.bashrc
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
|
||||
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-}
|
||||
ARG_ALLOW_ALL_TOOLS=${ARG_ALLOW_ALL_TOOLS:-false}
|
||||
ARG_ALLOW_TOOLS=${ARG_ALLOW_TOOLS:-}
|
||||
ARG_DENY_TOOLS=${ARG_DENY_TOOLS:-}
|
||||
ARG_TRUSTED_DIRECTORIES=${ARG_TRUSTED_DIRECTORIES:-}
|
||||
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
|
||||
ARG_RESUME_SESSION=${ARG_RESUME_SESSION:-true}
|
||||
|
||||
validate_copilot_installation() {
|
||||
if ! command_exists copilot; then
|
||||
echo "ERROR: Copilot not installed. Run: npm install -g @github/copilot"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
build_initial_prompt() {
|
||||
local initial_prompt=""
|
||||
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
|
||||
initial_prompt="$ARG_SYSTEM_PROMPT
|
||||
|
||||
$ARG_AI_PROMPT"
|
||||
else
|
||||
initial_prompt="$ARG_AI_PROMPT"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$initial_prompt"
|
||||
}
|
||||
|
||||
build_copilot_args() {
|
||||
COPILOT_ARGS=()
|
||||
|
||||
if [ "$ARG_ALLOW_ALL_TOOLS" = "true" ]; then
|
||||
COPILOT_ARGS+=(--allow-all-tools)
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_ALLOW_TOOLS" ]; then
|
||||
IFS=',' read -ra ALLOW_ARRAY <<< "$ARG_ALLOW_TOOLS"
|
||||
for tool in "${ALLOW_ARRAY[@]}"; do
|
||||
if [ -n "$tool" ]; then
|
||||
COPILOT_ARGS+=(--allow-tool "$tool")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_DENY_TOOLS" ]; then
|
||||
IFS=',' read -ra DENY_ARRAY <<< "$ARG_DENY_TOOLS"
|
||||
for tool in "${DENY_ARRAY[@]}"; do
|
||||
if [ -n "$tool" ]; then
|
||||
COPILOT_ARGS+=(--deny-tool "$tool")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
check_existing_session() {
|
||||
if [ "$ARG_RESUME_SESSION" = "true" ]; then
|
||||
if copilot --help > /dev/null 2>&1; then
|
||||
local session_dir="$HOME/.copilot/history-session-state"
|
||||
if [ -d "$session_dir" ] && [ -n "$(ls "$session_dir"/session_*_*.json 2> /dev/null)" ]; then
|
||||
echo "Found existing Copilot session. Will continue latest session." >&2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
setup_github_authentication() {
|
||||
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
echo "Setting up GitHub authentication..."
|
||||
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
export GH_TOKEN="$GITHUB_TOKEN"
|
||||
echo "✓ Using GitHub token from module configuration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command_exists coder; then
|
||||
local github_token
|
||||
if github_token=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null); then
|
||||
if [ -n "$github_token" ] && [ "$github_token" != "null" ]; then
|
||||
export GITHUB_TOKEN="$github_token"
|
||||
export GH_TOKEN="$github_token"
|
||||
echo "✓ Using Coder external auth OAuth token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if command_exists gh && gh auth status > /dev/null 2>&1; then
|
||||
echo "✓ Using GitHub CLI OAuth authentication"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠ No GitHub authentication available"
|
||||
echo " Copilot will prompt for login during first use"
|
||||
echo " Use the '/login' command in Copilot to authenticate"
|
||||
return 0
|
||||
}
|
||||
|
||||
start_agentapi() {
|
||||
echo "Starting in directory: $ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
build_copilot_args
|
||||
|
||||
if check_existing_session; then
|
||||
echo "Continuing latest Copilot session..."
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue
|
||||
fi
|
||||
else
|
||||
echo "Starting new Copilot session..."
|
||||
local initial_prompt
|
||||
initial_prompt=$(build_initial_prompt)
|
||||
|
||||
if [ -n "$initial_prompt" ]; then
|
||||
echo "Using initial prompt with system context"
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot
|
||||
fi
|
||||
else
|
||||
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
|
||||
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
|
||||
else
|
||||
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
setup_github_authentication
|
||||
validate_copilot_installation
|
||||
start_agentapi
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "GitHub Copilot CLI v1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while true; do
|
||||
echo "$(date) - Copilot mock running..."
|
||||
sleep 15
|
||||
done
|
||||
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
|
||||
```tf
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "coder-login" {
|
||||
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.5.0"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "force" {
|
||||
@@ -131,7 +131,7 @@ resource "coder_env" "cursor_api_key" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
|
||||
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
module "gemini" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
gemini_model = "gemini-2.5-flash"
|
||||
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -81,7 +81,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.2.3"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "gemini_model" {
|
||||
@@ -176,7 +176,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
display_name: Nextflow
|
||||
description: A module that adds Nextflow to your Coder template.
|
||||
icon: ../../../../.icons/nextflow.svg
|
||||
verified: true
|
||||
tags: [nextflow, workflow, hpc, bioinformatics]
|
||||
---
|
||||
|
||||
# Nextflow
|
||||
|
||||
A module that adds Nextflow to your Coder template.
|
||||
|
||||

|
||||
|
||||
```tf
|
||||
module "nextflow" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/nextflow/coder"
|
||||
version = "0.9.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Add required variables for your modules and remove any unneeded variables
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "nextflow_version" {
|
||||
type = string
|
||||
description = "Nextflow version"
|
||||
default = "25.04.7"
|
||||
}
|
||||
|
||||
variable "project_path" {
|
||||
type = string
|
||||
description = "The path to Nextflow project, it will be mounted in the container."
|
||||
}
|
||||
|
||||
variable "http_server_port" {
|
||||
type = number
|
||||
description = "The port to run HTTP server on."
|
||||
default = 9876
|
||||
}
|
||||
|
||||
variable "http_server_reports_dir" {
|
||||
type = string
|
||||
description = "Subdirectory for HTTP server reports, relative to the project path."
|
||||
default = "reports"
|
||||
}
|
||||
|
||||
variable "http_server_log_path" {
|
||||
type = string
|
||||
description = "HTTP server logs"
|
||||
default = "/tmp/nextflow_reports.log"
|
||||
}
|
||||
|
||||
variable "stub_run" {
|
||||
type = bool
|
||||
description = "Execute a stub run?"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "stub_run_command" {
|
||||
type = string
|
||||
description = "Nextflow command to be executed in the stub run."
|
||||
default = "run rnaseq-nf -with-report reports/report.html -with-trace reports/trace.txt -with-timeline reports/timeline.html -with-dag reports/flowchart.png"
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "nextflow" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "nextflow"
|
||||
icon = "/icon/nextflow.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
NEXTFLOW_VERSION : var.nextflow_version,
|
||||
PROJECT_PATH : var.project_path,
|
||||
HTTP_SERVER_PORT : var.http_server_port,
|
||||
HTTP_SERVER_REPORTS_DIR : var.http_server_reports_dir,
|
||||
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
|
||||
STUB_RUN : var.stub_run,
|
||||
STUB_RUN_COMMAND : var.stub_run_command,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "nextflow" {
|
||||
agent_id = var.agent_id
|
||||
slug = "nextflow-reports"
|
||||
display_name = "Nextflow Reports"
|
||||
url = "http://localhost:${var.http_server_port}"
|
||||
icon = "/icon/nextflow.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "$${BOLD}Starting Nextflow...$${RESET}\n"
|
||||
|
||||
if ! command -v nextflow > /dev/null 2>&1; then
|
||||
# Update system dependencies
|
||||
sudo apt update
|
||||
sudo apt install openjdk-21-jdk graphviz salmon fastqc multiqc -y
|
||||
|
||||
# Install nextflow
|
||||
export NXF_VER=${NEXTFLOW_VERSION}
|
||||
curl -s https://get.nextflow.io | bash
|
||||
sudo mv nextflow /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/nextflow
|
||||
|
||||
# Verify installation
|
||||
tmp_verify=$(mktemp -d coder-nextflow-XXXXXX)
|
||||
nextflow run hello \
|
||||
-with-report "$${tmp_verify}/report.html" \
|
||||
-with-trace "$${tmp_verify}/trace.txt" \
|
||||
-with-timeline "$${tmp_verify}/timeline.html" \
|
||||
-with-dag "$${tmp_verify}/flowchart.png"
|
||||
rm -r "$${tmp_verify}"
|
||||
else
|
||||
echo "Nextflow is already installed\n\n"
|
||||
fi
|
||||
|
||||
if [ ! -z ${PROJECT_PATH} ]; then
|
||||
# Project is located at PROJECT_PATH
|
||||
echo "Change directory: ${PROJECT_PATH}"
|
||||
cd ${PROJECT_PATH}
|
||||
fi
|
||||
|
||||
# Start a web server to preview reports
|
||||
mkdir -p ${HTTP_SERVER_REPORTS_DIR}
|
||||
echo "Starting HTTP server in background, check logs: ${HTTP_SERVER_LOG_PATH}"
|
||||
python3 -m http.server --directory ${HTTP_SERVER_REPORTS_DIR} ${HTTP_SERVER_PORT} > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
|
||||
|
||||
# Stub run?
|
||||
if [ "${STUB_RUN}" = "true" ]; then
|
||||
nextflow ${STUB_RUN_COMMAND} -stub-run
|
||||
fi
|
||||
|
||||
printf "\n$${BOLD}Nextflow ${NEXTFLOW_VERSION} is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}$${RESET}\n"
|
||||
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
|
||||
```tf
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
|
||||
install_sourcegraph_amp = true
|
||||
@@ -60,7 +60,7 @@ variable "sourcegraph_amp_api_key" {
|
||||
module "amp-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
|
||||
install_sourcegraph_amp = true
|
||||
|
||||
@@ -69,7 +69,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.0"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
@@ -151,7 +151,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -22,33 +22,16 @@ provider "docker" {}
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.0"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
folder = "/home/coder/projects"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
workdir = "/home/coder/projects"
|
||||
order = 999
|
||||
|
||||
experiment_post_install_script = data.coder_parameter.setup_script.value
|
||||
|
||||
# This enables Coder Tasks
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
|
||||
# You can also use a model provider, like AWS Bedrock or Vertex by replacing
|
||||
# this with the special env vars from the Claude Code docs.
|
||||
# see: https://docs.anthropic.com/en/docs/claude-code/third-party-integrations
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Generate one at: https://console.anthropic.com/settings/keys"
|
||||
sensitive = true
|
||||
}
|
||||
resource "coder_env" "anthropic_api_key" {
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
name = "CODER_MCP_CLAUDE_API_KEY"
|
||||
value = var.anthropic_api_key
|
||||
claude_api_key = ""
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
system_prompt = data.coder_parameter.system_prompt.value
|
||||
model = "sonnet"
|
||||
permission_mode = "plan"
|
||||
post_install_script = data.coder_parameter.setup_script.value
|
||||
}
|
||||
|
||||
# We are using presets to set the prompts, image, and set up instructions
|
||||
@@ -174,26 +157,6 @@ data "coder_parameter" "preview_port" {
|
||||
mutable = false
|
||||
}
|
||||
|
||||
# Other variables for Claude Code
|
||||
resource "coder_env" "claude_task_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
value = data.coder_parameter.ai_prompt.value
|
||||
}
|
||||
resource "coder_env" "app_status_slug" {
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = "ccw"
|
||||
}
|
||||
resource "coder_env" "claude_system_prompt" {
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
||||
value = data.coder_parameter.system_prompt.value
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
@@ -301,38 +264,27 @@ module "code-server" {
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
order = 1
|
||||
}
|
||||
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder/jetbrains/coder"
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
@@ -368,7 +320,6 @@ resource "docker_volume" "home_volume" {
|
||||
|
||||
resource "coder_app" "preview" {
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
slug = "preview"
|
||||
display_name = "Preview your app"
|
||||
icon = "${data.coder_workspace.me.access_url}/emojis/1f50e.png"
|
||||
|
||||
@@ -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 = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -117,7 +117,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.3"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "agentapi_port" {
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's A
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
|
||||
@@ -102,7 +102,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
@@ -228,7 +228,7 @@ If no custom `agent_config` is provided, the default agent name "agent" is used.
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
@@ -258,7 +258,7 @@ This example will:
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
@@ -279,7 +279,7 @@ module "amazon-q" {
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
@@ -305,7 +305,7 @@ module "amazon-q" {
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
@@ -319,7 +319,7 @@ module "amazon-q" {
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
@@ -340,14 +340,14 @@ module "amazon-q" {
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
# AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url
|
||||
agentapi_chat_based_path = true
|
||||
agentapi_version = "v0.6.1"
|
||||
agentapi_version = "v0.10.0"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -358,7 +358,7 @@ For environments without direct internet access, you can host Amazon Q installat
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
@@ -88,7 +88,7 @@ variable "post_install_script" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.6.1"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
@@ -215,7 +215,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -1,117 +1,142 @@
|
||||
---
|
||||
display_name: Claude Code
|
||||
description: Run Claude Code in your workspace
|
||||
description: Run the Claude Code agent in your workspace.
|
||||
icon: ../../../../.icons/claude.svg
|
||||
verified: true
|
||||
tags: [agent, claude-code, ai, tasks]
|
||||
tags: [agent, claude-code, ai, tasks, anthropic]
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
|
||||
Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks.
|
||||
Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
}
|
||||
```
|
||||
|
||||
> **Security Notice**: This module uses the [`--dangerously-skip-permissions`](https://docs.anthropic.com/en/docs/claude-code/cli-usage#cli-flags) flag when running Claude Code. This flag
|
||||
> bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While
|
||||
> this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as
|
||||
> the user running it. Use this module _only_ in trusted environments and be aware of the security implications.
|
||||
> [!WARNING]
|
||||
> **Security Notice**: This module uses the `--dangerously-skip-permissions` flag when running Claude Code tasks. This flag bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as the user running it. Use this module _only_ in trusted environments and be aware of the security implications.
|
||||
|
||||
> [!NOTE]
|
||||
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
|
||||
- An **Anthropic API key** or a _Claude Session Token_ is required for tasks.
|
||||
- You can get the API key from the [Anthropic Console](https://console.anthropic.com/dashboard).
|
||||
- You can get the Session Token using the `claude setup-token` command. This is a long-lived authentication token (requires Claude subscription)
|
||||
|
||||
## Examples
|
||||
|
||||
### Run in the background and report tasks (Experimental)
|
||||
### Usage with Tasks and Advanced Configuration
|
||||
|
||||
> This functionality is in early access as of Coder v2.21 and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
|
||||
>
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
This example shows how to configure the Claude Code module with an AI prompt, API key shared by all users of the template, and other custom settings.
|
||||
|
||||
```tf
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "The Anthropic API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Claude Code"
|
||||
description = "Initial task prompt for Claude Code."
|
||||
mutable = true
|
||||
}
|
||||
|
||||
# Set the prompt and system prompt for Claude Code via environment variables
|
||||
resource "coder_agent" "main" {
|
||||
# ...
|
||||
env = {
|
||||
CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
CODER_MCP_APP_STATUS_SLUG = "claude-code"
|
||||
CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT
|
||||
You are a helpful assistant that can help with code.
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "1.0.40"
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
# Enable experimental features
|
||||
experiment_report_tasks = true
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
# OR
|
||||
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
|
||||
|
||||
claude_code_version = "1.0.82" # Pin to a specific version
|
||||
agentapi_version = "v0.10.0"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
model = "sonnet"
|
||||
|
||||
permission_mode = "plan"
|
||||
|
||||
mcp = <<-EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-custom-tool": {
|
||||
"command": "my-tool-server"
|
||||
"args": ["--port", "8080"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
## Run standalone
|
||||
### Standalone Mode
|
||||
|
||||
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it without any task reporting to the Coder UI.
|
||||
Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.2.0"
|
||||
version = "3.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
workdir = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
}
|
||||
```
|
||||
|
||||
# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
|
||||
icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png"
|
||||
### Usage with Claude Code Subscription
|
||||
|
||||
```tf
|
||||
|
||||
variable "claude_code_oauth_token" {
|
||||
type = string
|
||||
description = "Generate one using `claude setup-token` command"
|
||||
sensitive = true
|
||||
value = "xxxx-xxx-xxxx"
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The module will create log files in the workspace's `~/.claude-module` directory. If you run into any issues, look at them for more information.
|
||||
If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information.
|
||||
|
||||
```bash
|
||||
# Installation logs
|
||||
cat ~/.claude-module/install.log
|
||||
|
||||
# Startup logs
|
||||
cat ~/.claude-module/agentapi-start.log
|
||||
|
||||
# Pre/post install script logs
|
||||
cat ~/.claude-module/pre_install.log
|
||||
cat ~/.claude-module/post_install.log
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To use tasks with Claude Code, you must provide an `anthropic_api_key` or `claude_code_oauth_token`.
|
||||
> The `workdir` variable is required and specifies the directory where Claude Code will run.
|
||||
|
||||
## References
|
||||
|
||||
- [Claude Code Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
import {
|
||||
test,
|
||||
afterEach,
|
||||
expect,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
expect,
|
||||
} from "bun:test";
|
||||
import path from "path";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
writeCoder,
|
||||
writeFileContainer,
|
||||
} from "~test";
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "../agentapi/test-util";
|
||||
import dedent from "dedent";
|
||||
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
|
||||
// Cleanup logic depends on the fact that bun's built-in test runner
|
||||
// runs tests sequentially.
|
||||
// https://bun.sh/docs/test/discovery#execution-order
|
||||
// Weird things would happen if tried to run tests in parallel.
|
||||
// One test could clean up resources that another test was still using.
|
||||
afterEach(async () => {
|
||||
// reverse the cleanup functions so that they are run in the correct order
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
@@ -43,298 +32,281 @@ afterEach(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const setupContainer = async ({
|
||||
image,
|
||||
vars,
|
||||
}: {
|
||||
image?: string;
|
||||
vars?: Record<string, string>;
|
||||
} = {}) => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
registerCleanup(() => removeContainer(id));
|
||||
return { id, coderScript };
|
||||
};
|
||||
|
||||
const loadTestFile = async (...relativePath: string[]) => {
|
||||
return await Bun.file(
|
||||
path.join(import.meta.dir, "testdata", ...relativePath),
|
||||
).text();
|
||||
};
|
||||
|
||||
const writeExecutable = async ({
|
||||
containerId,
|
||||
filePath,
|
||||
content,
|
||||
}: {
|
||||
containerId: string;
|
||||
filePath: string;
|
||||
content: string;
|
||||
}) => {
|
||||
await writeFileContainer(containerId, filePath, content, {
|
||||
user: "root",
|
||||
});
|
||||
await execContainer(
|
||||
containerId,
|
||||
["bash", "-c", `chmod 755 ${filePath}`],
|
||||
["--user", "root"],
|
||||
);
|
||||
};
|
||||
|
||||
const writeAgentAPIMockControl = async ({
|
||||
containerId,
|
||||
content,
|
||||
}: {
|
||||
containerId: string;
|
||||
content: string;
|
||||
}) => {
|
||||
await writeFileContainer(containerId, "/tmp/agentapi-mock.control", content, {
|
||||
user: "coder",
|
||||
});
|
||||
};
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
skipClaudeMock?: boolean;
|
||||
moduleVariables?: Record<string, string>;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const projectDir = "/home/coder/project";
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const { id, coderScript } = await setupContainer({
|
||||
vars: {
|
||||
experiment_report_tasks: "true",
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_claude_code: props?.skipClaudeMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
install_claude_code: "false",
|
||||
agentapi_version: "preview",
|
||||
folder: projectDir,
|
||||
workdir: projectDir,
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
|
||||
// the module script assumes that there is a coder executable in the PATH
|
||||
await writeCoder(id, await loadTestFile("coder-mock.js"));
|
||||
if (!props?.skipAgentAPIMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/agentapi",
|
||||
content: await loadTestFile("agentapi-mock.js"),
|
||||
});
|
||||
}
|
||||
if (!props?.skipClaudeMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/claude",
|
||||
content: await loadTestFile("claude-mock.js"),
|
||||
content: await loadTestFile(import.meta.dir, "claude-mock.sh"),
|
||||
});
|
||||
}
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/home/coder/script.sh",
|
||||
content: coderScript.script,
|
||||
});
|
||||
return { id };
|
||||
};
|
||||
|
||||
const expectAgentAPIStarted = async (id: string) => {
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`curl -fs -o /dev/null "http://localhost:3284/status"`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log("agentapi not started");
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
const execModuleScript = async (id: string) => {
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
// increase the default timeout to 60 seconds
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
// we don't run these tests in CI because they take too long and make network
|
||||
// calls. they are dedicated for local development.
|
||||
describe("claude-code", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
// test that the script runs successfully if claude starts without any errors
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("install-claude-code-version", async () => {
|
||||
const version_to_install = "1.0.40";
|
||||
const { id } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
claude_code_version: version_to_install,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/install.log",
|
||||
]);
|
||||
expect(resp.stdout).toContain(version_to_install);
|
||||
});
|
||||
|
||||
test("check-latest-claude-code-version-works", async () => {
|
||||
const { id } = await setup({
|
||||
skipClaudeMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("claude-api-key", async () => {
|
||||
const apiKey = "test-api-key-123";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
claude_api_key: apiKey,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const envCheck = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"',
|
||||
]);
|
||||
expect(envCheck.stdout).toContain("CLAUDE_API_KEY");
|
||||
});
|
||||
|
||||
test("claude-mcp-config", async () => {
|
||||
const mcpConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
test: {
|
||||
command: "test-cmd",
|
||||
type: "stdio",
|
||||
},
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
mcp: mcpConfig,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(id, "/home/coder/.claude.json");
|
||||
expect(resp).toContain("test-cmd");
|
||||
});
|
||||
|
||||
test("claude-task-prompt", async () => {
|
||||
const prompt = "This is a task prompt for Claude.";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
ai_prompt: prompt,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"sudo /home/coder/script.sh",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(resp.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
expect(resp.stdout).toContain(prompt);
|
||||
});
|
||||
|
||||
// test that the script removes lastSessionId from the .claude.json file
|
||||
test("last-session-id-removed", async () => {
|
||||
const { id } = await setup();
|
||||
test("claude-permission-mode", async () => {
|
||||
const mode = "plan";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
permission_mode: mode,
|
||||
task_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
await writeFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude.json",
|
||||
JSON.stringify({
|
||||
projects: {
|
||||
[projectDir]: {
|
||||
lastSessionId: "123",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const catResp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude.json",
|
||||
]);
|
||||
expect(catResp.exitCode).toBe(0);
|
||||
expect(catResp.stdout).toContain("lastSessionId");
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const catResp2 = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude.json",
|
||||
]);
|
||||
expect(catResp2.exitCode).toBe(0);
|
||||
expect(catResp2.stdout).not.toContain("lastSessionId");
|
||||
});
|
||||
|
||||
// test that the script handles a .claude.json file that doesn't contain
|
||||
// a lastSessionId field
|
||||
test("last-session-id-not-found", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
await writeFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude.json",
|
||||
JSON.stringify({
|
||||
projects: {
|
||||
"/home/coder": {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const catResp = await execContainer(id, [
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(catResp.exitCode).toBe(0);
|
||||
expect(catResp.stdout).toContain(
|
||||
"No lastSessionId found in .claude.json - nothing to do",
|
||||
);
|
||||
expect(startLog.stdout).toContain(`--permission-mode ${mode}`);
|
||||
});
|
||||
|
||||
// test that if claude fails to run with the --continue flag and returns a
|
||||
// no conversation found error, then the module script retries without the flag
|
||||
test("no-conversation-found", async () => {
|
||||
const { id } = await setup();
|
||||
await writeAgentAPIMockControl({
|
||||
containerId: id,
|
||||
content: "no-conversation-found",
|
||||
test("claude-model", async () => {
|
||||
const model = "opus";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
model: model,
|
||||
task_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
// check that mocking works
|
||||
const respAgentAPI = await execContainer(id, [
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"agentapi --continue",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(respAgentAPI.exitCode).toBe(1);
|
||||
expect(respAgentAPI.stderr).toContain("No conversation found to continue");
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
});
|
||||
|
||||
test("install-agentapi", async () => {
|
||||
const { id } = await setup({ skipAgentAPIMock: true });
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
const respAgentAPI = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"agentapi --version",
|
||||
]);
|
||||
expect(respAgentAPI.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
// the coder binary should be executed with specific env vars
|
||||
// that are set by the module script
|
||||
test("coder-env-vars", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
const respCoderMock = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/coder-mock-output.json",
|
||||
]);
|
||||
if (respCoderMock.exitCode !== 0) {
|
||||
console.log(respCoderMock.stdout);
|
||||
console.log(respCoderMock.stderr);
|
||||
}
|
||||
expect(respCoderMock.exitCode).toBe(0);
|
||||
expect(JSON.parse(respCoderMock.stdout)).toEqual({
|
||||
statusSlug: "ccw",
|
||||
agentApiUrl: "http://localhost:3284",
|
||||
test("claude-continue-previous-conversation", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
task_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
});
|
||||
|
||||
// verify that the agentapi binary has access to the AGENTAPI_ALLOWED_HOSTS environment variable
|
||||
// set in main.tf
|
||||
test("agentapi-allowed-hosts", async () => {
|
||||
const { id } = await setup();
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'",
|
||||
post_install_script: "#!/bin/bash\necho 'claude-post-install-script'",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const agentApiStartLog = await readFileContainer(
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/agentapi-mock.log",
|
||||
"/home/coder/.claude-module/pre_install.log",
|
||||
);
|
||||
expect(preInstallLog).toContain("claude-pre-install-script");
|
||||
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude-module/post_install.log",
|
||||
);
|
||||
expect(postInstallLog).toContain("claude-post-install-script");
|
||||
});
|
||||
|
||||
test("workdir-variable", async () => {
|
||||
const workdir = "/home/coder/claude-test-folder";
|
||||
const { id } = await setup({
|
||||
skipClaudeMock: false,
|
||||
moduleVariables: {
|
||||
workdir,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude-module/agentapi-start.log",
|
||||
);
|
||||
expect(resp).toContain(workdir);
|
||||
});
|
||||
|
||||
test("coder-mcp-config-created", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
install_claude_code: "false",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const installLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.claude-module/install.log",
|
||||
);
|
||||
expect(installLog).toContain(
|
||||
"Configuring Claude Code to report tasks via Coder MCP",
|
||||
);
|
||||
});
|
||||
|
||||
test("dangerously-skip-permissions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
dangerously_skip_permissions: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`--dangerously-skip-permissions`);
|
||||
});
|
||||
|
||||
test("subdomain-false", async () => {
|
||||
const { id } = await setup({
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
subdomain: "false",
|
||||
post_install_script: dedent`
|
||||
#!/bin/bash
|
||||
env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found"
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/post_install.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(
|
||||
"ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
|
||||
);
|
||||
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,12 +36,72 @@ variable "icon" {
|
||||
default = "/icon/claude.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Claude Code in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Claude Code"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Claude Code"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Claude Code CLI"
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "Initial task prompt for Claude Code."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
|
||||
variable "install_claude_code" {
|
||||
type = bool
|
||||
description = "Whether to install Claude Code."
|
||||
@@ -54,245 +114,209 @@ variable "claude_code_version" {
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "experiment_cli_app" {
|
||||
variable "claude_api_key" {
|
||||
type = string
|
||||
description = "The API key to use for the Claude Code server."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "model" {
|
||||
type = string
|
||||
description = "Sets the model for the current session with an alias for the latest model (sonnet or opus) or a model’s full name."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "resume_session_id" {
|
||||
type = string
|
||||
description = "Resume a specific session by ID."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "continue" {
|
||||
type = bool
|
||||
description = "Whether to create the CLI workspace app."
|
||||
description = "Load the most recent conversation in the current directory. Task will fail in a new workspace with no conversation/session to continue"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_cli_app_order" {
|
||||
type = number
|
||||
description = "The order of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_cli_app_group" {
|
||||
type = string
|
||||
description = "The group of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
variable "dangerously_skip_permissions" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
description = "Skip the permission prompts. Use with caution. This will be set to true if using Coder Tasks"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
variable "permission_mode" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Claude Code."
|
||||
default = null
|
||||
description = "Permission mode for the cli, check https://docs.anthropic.com/en/docs/claude-code/iam#permission-modes"
|
||||
default = ""
|
||||
validation {
|
||||
condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode)
|
||||
error_message = "interaction_mode must be one of: default, acceptEdits, plan, bypassPermissions."
|
||||
}
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Claude Code."
|
||||
default = null
|
||||
description = "MCP JSON to be added to the claude code local scope"
|
||||
default = ""
|
||||
}
|
||||
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
variable "allowed_tools" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.3"
|
||||
description = "A list of tools that should be allowed without prompting the user for permission, in addition to settings.json files."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for the Claude Code app."
|
||||
default = true
|
||||
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"
|
||||
}
|
||||
|
||||
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 = length(var.claude_api_key) > 0 ? 1 : 0
|
||||
|
||||
agent_id = var.agent_id
|
||||
name = "CLAUDE_API_KEY"
|
||||
value = var.claude_api_key
|
||||
}
|
||||
|
||||
locals {
|
||||
# we have to trim the slash because otherwise coder exp mcp will
|
||||
# set up an invalid claude config
|
||||
workdir = trimsuffix(var.folder, "/")
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
agentapi_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-start.sh"))
|
||||
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
|
||||
claude_code_app_slug = "ccw"
|
||||
// Chat base path is only set if not using a subdomain.
|
||||
// NOTE:
|
||||
// - Initial support for --chat-base-path was added in v0.3.1 but configuration
|
||||
// via environment variable AGENTAPI_CHAT_BASE_PATH was added in v0.3.3.
|
||||
// - As CODER_WORKSPACE_AGENT_NAME is a recent addition we use agent ID
|
||||
// for backward compatibility.
|
||||
agentapi_chat_base_path = var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.claude_code_app_slug}/chat"
|
||||
server_base_path = var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.claude_code_app_slug}"
|
||||
healthcheck_url = "http://localhost:3284${local.server_base_path}/status"
|
||||
}
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "ccw"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
|
||||
|
||||
# Install and Initialize Claude Code
|
||||
resource "coder_script" "claude_code" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Claude Code"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
# 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.
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
-- 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
|
||||
|
||||
function install_claude_code_cli() {
|
||||
echo "Installing Claude Code via official installer"
|
||||
set +e
|
||||
curl -fsSL claude.ai/install.sh | bash -s -- "${var.claude_code_version}" 2>&1
|
||||
CURL_EXIT=$${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ $CURL_EXIT -ne 0 ]; then
|
||||
echo "Claude Code installer failed with exit code $$CURL_EXIT"
|
||||
fi
|
||||
|
||||
# Ensure binaries are discoverable.
|
||||
export PATH="~/.local/bin:$PATH"
|
||||
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
|
||||
}
|
||||
|
||||
if [ ! -d "${local.workdir}" ]; then
|
||||
echo "Warning: The specified folder '${local.workdir}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "${local.workdir}"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.install_claude_code}" = "true" ]; then
|
||||
install_claude_code_cli
|
||||
fi
|
||||
|
||||
# Install AgentAPI if enabled
|
||||
if [ "${var.install_agentapi}" = "true" ]; then
|
||||
echo "Installing AgentAPI..."
|
||||
arch=$(uname -m)
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
binary_name="agentapi-linux-amd64"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
binary_name="agentapi-linux-arm64"
|
||||
else
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"https://github.com/coder/agentapi/releases/download/${var.agentapi_version}/$binary_name"
|
||||
chmod +x agentapi
|
||||
sudo mv agentapi /usr/local/bin/agentapi
|
||||
fi
|
||||
if ! command_exists agentapi; then
|
||||
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# this must be kept in sync with the agentapi-start.sh script
|
||||
module_path="$HOME/.claude-module"
|
||||
mkdir -p "$module_path/scripts"
|
||||
|
||||
# save the prompt for the agentapi start command
|
||||
echo -n "$CODER_MCP_CLAUDE_TASK_PROMPT" > "$module_path/prompt.txt"
|
||||
|
||||
echo -n "${local.agentapi_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-start.sh"
|
||||
echo -n "${local.agentapi_wait_for_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "$module_path/scripts/remove-last-session-id.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="${local.claude_code_app_slug}"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "${local.workdir}"
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if ! command_exists claude; then
|
||||
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
cd "${local.workdir}"
|
||||
|
||||
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
|
||||
export AGENTAPI_ALLOWED_HOSTS="*"
|
||||
|
||||
# Set chat base path for non-subdomain routing (only set if not using subdomain)
|
||||
export AGENTAPI_CHAT_BASE_PATH="${local.agentapi_chat_base_path}"
|
||||
|
||||
nohup "$module_path/scripts/agentapi-start.sh" use_prompt &> "$module_path/agentapi-start.log" &
|
||||
"$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
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
|
||||
run_on_start = true
|
||||
|
||||
# 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) : ""
|
||||
)
|
||||
}
|
||||
|
||||
resource "coder_app" "claude_code_web" {
|
||||
# use a short slug to mitigate https://github.com/coder/coder/issues/15178
|
||||
slug = local.claude_code_app_slug
|
||||
display_name = "Claude Code Web"
|
||||
agent_id = var.agent_id
|
||||
url = "http://localhost:3284/"
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
subdomain = var.subdomain
|
||||
healthcheck {
|
||||
url = local.healthcheck_url
|
||||
interval = 3
|
||||
threshold = 20
|
||||
}
|
||||
}
|
||||
module "agentapi" {
|
||||
|
||||
resource "coder_app" "claude_code" {
|
||||
count = var.experiment_cli_app ? 1 : 0
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
|
||||
slug = "claude-code"
|
||||
display_name = "Claude Code CLI"
|
||||
agent_id = var.agent_id
|
||||
command = <<-EOT
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
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
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh"
|
||||
chmod +x /tmp/start.sh
|
||||
chmod +x /tmp/remove-last-session-id.sh
|
||||
|
||||
ARG_MODEL='${var.model}' \
|
||||
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
|
||||
ARG_CONTINUE='${var.continue}' \
|
||||
ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \
|
||||
ARG_PERMISSION_MODE='${var.permission_mode}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
agentapi attach
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.experiment_cli_app_order
|
||||
group = var.experiment_cli_app_group
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \
|
||||
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_ALLOWED_TOOLS='${var.allowed_tools}' \
|
||||
ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \
|
||||
ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "claude_code" {
|
||||
sidebar_app {
|
||||
id = coder_app.claude_code_web.id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
run "test_claude_code_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
workdir = "/home/coder/projects"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.workdir == "/home/coder/projects"
|
||||
error_message = "Workdir variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agent_id == "test-agent-123"
|
||||
error_message = "Agent ID variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_claude_code == true
|
||||
error_message = "Install claude_code should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.install_agentapi == true
|
||||
error_message = "Install agentapi should default to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.report_tasks == true
|
||||
error_message = "report_tasks should default to true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
workdir = "/home/coder/workspace"
|
||||
claude_api_key = "test-api-key-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_env.claude_api_key[0].value == "test-api-key-123"
|
||||
error_message = "Claude API key value should match the input"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_custom_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
workdir = "/home/coder/custom"
|
||||
order = 5
|
||||
group = "development"
|
||||
icon = "/icon/custom.svg"
|
||||
model = "opus"
|
||||
task_prompt = "Help me write better code"
|
||||
permission_mode = "plan"
|
||||
continue = true
|
||||
install_claude_code = false
|
||||
install_agentapi = false
|
||||
claude_code_version = "1.0.0"
|
||||
agentapi_version = "v0.6.0"
|
||||
dangerously_skip_permissions = true
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.order == 5
|
||||
error_message = "Order variable should be set to 5"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.group == "development"
|
||||
error_message = "Group variable should be set to 'development'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.icon == "/icon/custom.svg"
|
||||
error_message = "Icon variable should be set to custom icon"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.model == "opus"
|
||||
error_message = "Claude model variable should be set to 'opus'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.task_prompt == "Help me write better code"
|
||||
error_message = "Task prompt variable should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.permission_mode == "plan"
|
||||
error_message = "Permission mode should be set to 'plan'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.continue == true
|
||||
error_message = "Continue should be set to true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.claude_code_version == "1.0.0"
|
||||
error_message = "Claude Code version should be set to '1.0.0'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.agentapi_version == "v0.6.0"
|
||||
error_message = "AgentAPI version should be set to 'v0.6.0'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.dangerously_skip_permissions == true
|
||||
error_message = "dangerously_skip_permissions should be set to true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_mcp_and_tools" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-mcp"
|
||||
workdir = "/home/coder/mcp-test"
|
||||
mcp = jsonencode({
|
||||
mcpServers = {
|
||||
test = {
|
||||
command = "test-server"
|
||||
args = ["--config", "test.json"]
|
||||
}
|
||||
}
|
||||
})
|
||||
allowed_tools = "bash,python"
|
||||
disallowed_tools = "rm"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.mcp != ""
|
||||
error_message = "MCP configuration should be provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.allowed_tools == "bash,python"
|
||||
error_message = "Allowed tools should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.disallowed_tools == "rm"
|
||||
error_message = "Disallowed tools should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_scripts" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-scripts"
|
||||
workdir = "/home/coder/scripts"
|
||||
pre_install_script = "echo 'Pre-install script'"
|
||||
post_install_script = "echo 'Post-install script'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.pre_install_script == "echo 'Pre-install script'"
|
||||
error_message = "Pre-install script should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.post_install_script == "echo 'Post-install script'"
|
||||
error_message = "Post-install script should be set correctly"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_permission_mode_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-validation"
|
||||
workdir = "/home/coder/test"
|
||||
permission_mode = "acceptEdits"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode)
|
||||
error_message = "Permission mode should be one of the valid options"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_system_prompt" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-system-prompt"
|
||||
workdir = "/home/coder/test"
|
||||
system_prompt = "Custom addition"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
|
||||
error_message = "System prompt should not be empty"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(regexall("Custom addition", coder_env.claude_code_system_prompt.value)) > 0
|
||||
error_message = "System prompt should have system_prompt variable value"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_report_tasks_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-report-tasks"
|
||||
workdir = "/home/coder/test"
|
||||
# report_tasks: default is true
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
|
||||
error_message = "System prompt should not be empty"
|
||||
}
|
||||
|
||||
# Ensure system prompt is wrapped by <system>
|
||||
assert {
|
||||
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
|
||||
error_message = "System prompt should start with <system>"
|
||||
}
|
||||
assert {
|
||||
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
|
||||
error_message = "System prompt should end with </system>"
|
||||
}
|
||||
|
||||
# Ensure Coder sections are injected when report_tasks=true (default)
|
||||
assert {
|
||||
condition = length(regexall("-- Tool Selection --", coder_env.claude_code_system_prompt.value)) > 0
|
||||
error_message = "System prompt should have Tool Selection section"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(regexall("-- Task Reporting --", coder_env.claude_code_system_prompt.value)) > 0
|
||||
error_message = "System prompt should have Task Reporting section"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_report_tasks_disabled" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-report-tasks"
|
||||
workdir = "/home/coder/test"
|
||||
report_tasks = false
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
|
||||
error_message = "System prompt should not be empty"
|
||||
}
|
||||
|
||||
# Ensure system prompt is wrapped by <system>
|
||||
assert {
|
||||
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
|
||||
error_message = "System prompt should start with <system>"
|
||||
}
|
||||
assert {
|
||||
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
|
||||
error_message = "System prompt should end with </system>"
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# this must be kept in sync with the main.tf file
|
||||
module_path="$HOME/.claude-module"
|
||||
scripts_dir="$module_path/scripts"
|
||||
log_file_path="$module_path/agentapi.log"
|
||||
|
||||
# if the first argument is not empty, start claude with the prompt
|
||||
if [ -n "$1" ]; then
|
||||
cp "$module_path/prompt.txt" /tmp/claude-code-prompt
|
||||
else
|
||||
rm -f /tmp/claude-code-prompt
|
||||
fi
|
||||
|
||||
# if the log file already exists, archive it
|
||||
if [ -f "$log_file_path" ]; then
|
||||
mv "$log_file_path" "$log_file_path"".$(date +%s)"
|
||||
fi
|
||||
|
||||
# see the remove-last-session-id.sh script for details
|
||||
# about why we need it
|
||||
# avoid exiting if the script fails
|
||||
bash "$scripts_dir/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true
|
||||
|
||||
# we'll be manually handling errors from this point on
|
||||
set +o errexit
|
||||
|
||||
function start_agentapi() {
|
||||
local continue_flag="$1"
|
||||
local prompt_subshell='"$(cat /tmp/claude-code-prompt)"'
|
||||
|
||||
# use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters
|
||||
# visible in the terminal screen by default.
|
||||
agentapi server --term-width 67 --term-height 1190 -- \
|
||||
bash -c "claude $continue_flag --dangerously-skip-permissions $prompt_subshell" \
|
||||
> "$log_file_path" 2>&1
|
||||
}
|
||||
|
||||
echo "Starting AgentAPI..."
|
||||
|
||||
# attempt to start claude with the --continue flag
|
||||
start_agentapi --continue
|
||||
exit_code=$?
|
||||
|
||||
echo "First AgentAPI exit code: $exit_code"
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# if there was no conversation to continue, claude exited with an error.
|
||||
# start claude without the --continue flag.
|
||||
if grep -q "No conversation found to continue" "$log_file_path"; then
|
||||
echo "AgentAPI with --continue flag failed, starting claude without it."
|
||||
start_agentapi
|
||||
exit_code=$?
|
||||
fi
|
||||
|
||||
echo "Second AgentAPI exit code: $exit_code"
|
||||
|
||||
exit $exit_code
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# This script waits for the agentapi server to start on port 3284.
|
||||
# It considers the server started after 3 consecutive successful responses.
|
||||
|
||||
agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port 3284..."
|
||||
for i in $(seq 1 150); do
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:3284/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
agentapi_started=true
|
||||
break
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
echo "Error: agentapi server did not start on port 3284 after 15 seconds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "agentapi server started on port 3284."
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d)
|
||||
ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-}
|
||||
ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
printf "ARG_CLAUDE_CODE_VERSION: %s\n" "$ARG_CLAUDE_CODE_VERSION"
|
||||
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
|
||||
printf "ARG_INSTALL_CLAUDE_CODE: %s\n" "$ARG_INSTALL_CLAUDE_CODE"
|
||||
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
|
||||
printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
|
||||
printf "ARG_MCP: %s\n" "$ARG_MCP"
|
||||
printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS"
|
||||
printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
function install_claude_code_cli() {
|
||||
if [ "$ARG_INSTALL_CLAUDE_CODE" = "true" ]; then
|
||||
echo "Installing Claude Code via official installer"
|
||||
set +e
|
||||
curl -fsSL claude.ai/install.sh | bash -s -- "$ARG_CLAUDE_CODE_VERSION" 2>&1
|
||||
CURL_EXIT=${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ $CURL_EXIT -ne 0 ]; then
|
||||
echo "Claude Code installer failed with exit code $$CURL_EXIT"
|
||||
fi
|
||||
|
||||
# Ensure binaries are discoverable.
|
||||
echo "Creating a symlink for claude"
|
||||
sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude
|
||||
|
||||
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
|
||||
else
|
||||
echo "Skipping Claude Code installation as per configuration."
|
||||
fi
|
||||
}
|
||||
|
||||
function setup_claude_configurations() {
|
||||
if [ ! -d "$ARG_WORKDIR" ]; then
|
||||
echo "Warning: The specified folder '$ARG_WORKDIR' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
|
||||
module_path="$HOME/.claude-module"
|
||||
mkdir -p "$module_path"
|
||||
|
||||
if [ "$ARG_MCP" != "" ]; then
|
||||
while IFS= read -r server_name && IFS= read -r server_json; do
|
||||
echo "------------------------"
|
||||
echo "Executing: claude mcp add \"$server_name\" '$server_json'"
|
||||
claude mcp add "$server_name" "$server_json"
|
||||
echo "------------------------"
|
||||
echo ""
|
||||
done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)')
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_ALLOWED_TOOLS" ]; then
|
||||
coder --allowedTools "$ARG_ALLOWED_TOOLS"
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_DISALLOWED_TOOLS" ]; then
|
||||
coder --disallowedTools "$ARG_DISALLOWED_TOOLS"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function report_tasks() {
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
else
|
||||
export CODER_MCP_APP_STATUS_SLUG=""
|
||||
export CODER_MCP_AI_AGENTAPI_URL=""
|
||||
echo "Configuring Claude Code with Coder MCP..."
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
fi
|
||||
}
|
||||
|
||||
install_claude_code_cli
|
||||
setup_claude_configurations
|
||||
report_tasks
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ARG_MODEL=${ARG_MODEL:-}
|
||||
ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-}
|
||||
ARG_CONTINUE=${ARG_CONTINUE:-false}
|
||||
ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-}
|
||||
ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
printf "ARG_MODEL: %s\n" "$ARG_MODEL"
|
||||
printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID"
|
||||
printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE"
|
||||
printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS"
|
||||
printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE"
|
||||
printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT"
|
||||
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
# see the remove-last-session-id.sh script for details
|
||||
# about why we need it
|
||||
# avoid exiting if the script fails
|
||||
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true
|
||||
|
||||
function validate_claude_installation() {
|
||||
if command_exists claude; then
|
||||
printf "Claude Code is installed\n"
|
||||
else
|
||||
printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ARGS=()
|
||||
|
||||
function build_claude_args() {
|
||||
if [ -n "$ARG_MODEL" ]; then
|
||||
ARGS+=(--model "$ARG_MODEL")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
fi
|
||||
|
||||
if [ "$ARG_CONTINUE" = "true" ]; then
|
||||
ARGS+=(--continue)
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_PERMISSION_MODE" ]; then
|
||||
ARGS+=(--permission-mode "$ARG_PERMISSION_MODE")
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function start_agentapi() {
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ -n "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
fi
|
||||
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
|
||||
}
|
||||
|
||||
validate_claude_installation
|
||||
build_claude_args
|
||||
start_agentapi
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const args = process.argv.slice(2);
|
||||
const port = 3284;
|
||||
|
||||
const controlFile = "/tmp/agentapi-mock.control";
|
||||
let control = "";
|
||||
if (fs.existsSync(controlFile)) {
|
||||
control = fs.readFileSync(controlFile, "utf8");
|
||||
}
|
||||
|
||||
if (
|
||||
control === "no-conversation-found" &&
|
||||
args.join(" ").includes("--continue")
|
||||
) {
|
||||
// this must match the error message in the agentapi-start.sh script
|
||||
console.error("No conversation found to continue");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
"/home/coder/agentapi-mock.log",
|
||||
`AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`,
|
||||
);
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
|
||||
http
|
||||
.createServer(function (_request, response) {
|
||||
response.writeHead(200);
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
status: "stable",
|
||||
}),
|
||||
);
|
||||
})
|
||||
.listen(port);
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const main = async () => {
|
||||
console.log("mocking claude");
|
||||
// sleep for 30 minutes
|
||||
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "claude version v1.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - claude-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const statusSlugEnvVar = "CODER_MCP_APP_STATUS_SLUG";
|
||||
const agentApiUrlEnvVar = "CODER_MCP_AI_AGENTAPI_URL";
|
||||
|
||||
fs.writeFileSync(
|
||||
"/home/coder/coder-mock-output.json",
|
||||
JSON.stringify({
|
||||
statusSlug: process.env[statusSlugEnvVar] ?? "env var not set",
|
||||
agentApiUrl: process.env[agentApiUrlEnvVar] ?? "env var not set",
|
||||
}),
|
||||
);
|
||||
@@ -13,7 +13,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -79,7 +79,7 @@ resource "coder_agent" "main" {
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -123,4 +123,6 @@ Note: The indentation in the heredoc is preserved, so you can write the YAML nat
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
|
||||
|
||||
The module will create log files in the workspace's `~/.goose-module` directory. If you run into any issues, look at them for more information.
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
test,
|
||||
afterEach,
|
||||
describe,
|
||||
it,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
expect,
|
||||
@@ -253,22 +254,41 @@ describe("goose", async () => {
|
||||
expect(prompt.stderr).toContain("No such file or directory");
|
||||
});
|
||||
|
||||
test("subdomain-false", async () => {
|
||||
const { id } = await setup({
|
||||
agentapiMockScript: await loadTestFile(
|
||||
import.meta.dir,
|
||||
"agentapi-mock-print-args.js",
|
||||
),
|
||||
moduleVariables: {
|
||||
subdomain: "false",
|
||||
},
|
||||
describe("subdomain", async () => {
|
||||
it("sets AGENTAPI_CHAT_BASE_PATH when false", async () => {
|
||||
const { id } = await setup({
|
||||
agentapiMockScript: await loadTestFile(
|
||||
import.meta.dir,
|
||||
"agentapi-mock-print-args.js",
|
||||
),
|
||||
moduleVariables: {
|
||||
subdomain: "false",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
|
||||
expect(agentapiMockOutput).toContain(
|
||||
"AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/goose/chat",
|
||||
);
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
it("does not set AGENTAPI_CHAT_BASE_PATH when true", async () => {
|
||||
const { id } = await setup({
|
||||
agentapiMockScript: await loadTestFile(
|
||||
import.meta.dir,
|
||||
"agentapi-mock-print-args.js",
|
||||
),
|
||||
moduleVariables: {
|
||||
subdomain: "true",
|
||||
},
|
||||
});
|
||||
|
||||
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
|
||||
expect(agentapiMockOutput).toContain(
|
||||
"AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/goose/chat",
|
||||
);
|
||||
await execModuleScript(id);
|
||||
|
||||
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
|
||||
expect(agentapiMockOutput).toMatch(/AGENTAPI_CHAT_BASE_PATH=$/m);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,13 +63,13 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.3"
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = true
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "goose_provider" {
|
||||
@@ -139,7 +139,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -10,6 +10,8 @@ tags: [ide, jetbrains, parameter, gateway]
|
||||
|
||||
This module adds a JetBrains Gateway Button to open any workspace with a single click.
|
||||
|
||||
> We recommend using the [Coder Toolbox module](https://registry.coder.com/modules/coder/jetbrains), which offers significant stability and connectivity benefits over Gateway. Reference our [documentation](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) for more information.
|
||||
|
||||
JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM.
|
||||
Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements.
|
||||
|
||||
@@ -17,7 +19,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
|
||||
@@ -35,7 +37,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -49,7 +51,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -64,7 +66,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -89,7 +91,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -107,7 +109,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("jetbrains-gateway", async () => {
|
||||
folder: "/home/coder",
|
||||
});
|
||||
expect(state.outputs.url.value).toBe(
|
||||
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent_id=foo",
|
||||
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent=",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
@@ -40,4 +40,28 @@ describe("jetbrains-gateway", async () => {
|
||||
});
|
||||
expect(state.outputs.identifier.value).toBe("IU");
|
||||
});
|
||||
|
||||
it("optionally includes agent when an agent name is provided", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: "main",
|
||||
folder: "/home/coder",
|
||||
});
|
||||
|
||||
expect(state.outputs.url.value).toBe(
|
||||
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent=main",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes the agent parameter even when the provided value is blank", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: " ",
|
||||
folder: "/home/coder",
|
||||
});
|
||||
|
||||
expect(state.outputs.url.value).toBe(
|
||||
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent= ",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,15 +30,14 @@ variable "agent_id" {
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug for the coder_app. Allows resuing the module with the same template."
|
||||
description = "The slug for the coder_app. Allows reusing the module with the same template."
|
||||
default = "gateway"
|
||||
}
|
||||
|
||||
variable "agent_name" {
|
||||
type = string
|
||||
description = "Agent name. (unused). Will be removed in a future version"
|
||||
|
||||
default = ""
|
||||
description = "Agent name."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
@@ -348,8 +347,8 @@ resource "coder_app" "gateway" {
|
||||
local.build_number,
|
||||
"&ide_download_link=",
|
||||
local.download_link,
|
||||
"&agent_id=",
|
||||
var.agent_id,
|
||||
"&agent=",
|
||||
var.agent_name,
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
# tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,7 +40,7 @@ When `default` contains IDE codes, those IDEs are created directly without user
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
|
||||
@@ -52,7 +53,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
# Show parameter with limited options
|
||||
@@ -66,7 +67,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
@@ -81,7 +82,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -107,7 +108,7 @@ module "jetbrains" {
|
||||
module "jetbrains_pycharm" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -119,6 +120,22 @@ module "jetbrains_pycharm" {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Tooltip
|
||||
|
||||
Add helpful tooltip text that appears when users hover over the IDE app buttons:
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
|
||||
}
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
### Parameter vs Direct Apps
|
||||
@@ -132,6 +149,13 @@ module "jetbrains_pycharm" {
|
||||
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config`
|
||||
- `major_version` and `channel` control which API endpoint is queried (when API access is available)
|
||||
|
||||
### Tooltip
|
||||
|
||||
- **`tooltip`**: Optional markdown text displayed when hovering over IDE app buttons
|
||||
- If not specified, no tooltip is shown
|
||||
- Supports markdown formatting for rich text (bold, italic, links, etc.)
|
||||
- All IDE apps created by this module will show the same tooltip text
|
||||
|
||||
## Supported IDEs
|
||||
|
||||
All JetBrains IDEs with remote development capabilities:
|
||||
|
||||
@@ -129,3 +129,34 @@ run "app_order_when_default_not_empty" {
|
||||
error_message = "Expected coder_app order to be set to 10"
|
||||
}
|
||||
}
|
||||
|
||||
run "tooltip_when_provided" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."])
|
||||
error_message = "Expected coder_app tooltip to be set when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "tooltip_null_when_not_provided" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == null])
|
||||
error_message = "Expected coder_app tooltip to be null when not provided"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +276,36 @@ describe("jetbrains", async () => {
|
||||
);
|
||||
expect(parameter?.instances[0].attributes.order).toBe(5);
|
||||
});
|
||||
|
||||
it("should set tooltip when specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
tooltip:
|
||||
"You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button.",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.tooltip).toBe(
|
||||
"You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should have null tooltip when not specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.tooltip).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// URL Generation Tests
|
||||
|
||||
@@ -59,6 +59,12 @@ variable "coder_parameter_order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "tooltip" {
|
||||
type = string
|
||||
description = "Markdown text that is displayed when hovering over workspace apps."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "major_version" {
|
||||
type = string
|
||||
description = "The major version of the IDE. i.e. 2025.1"
|
||||
@@ -232,6 +238,7 @@ resource "coder_app" "jetbrains" {
|
||||
external = true
|
||||
order = var.coder_app_order
|
||||
group = var.group
|
||||
tooltip = var.tooltip
|
||||
url = join("", [
|
||||
"jetbrains://gateway/coder?&workspace=", # requires 2.6.3+ version of Toolbox
|
||||
data.coder_workspace.me.name,
|
||||
|
||||
@@ -24,12 +24,10 @@ describe("jfrog-oauth", async () => {
|
||||
const fakeFrogUrl = "http://localhost:8081";
|
||||
const user = "default";
|
||||
|
||||
it("can run apply with required variables", async () => {
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: "{}",
|
||||
});
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: "{}",
|
||||
});
|
||||
|
||||
it("generates an npmrc with scoped repos", async () => {
|
||||
|
||||
@@ -55,13 +55,11 @@ describe("jfrog-token", async () => {
|
||||
const user = "default";
|
||||
const token = "xxx";
|
||||
|
||||
it("can run apply with required variables", async () => {
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
artifactory_access_token: "XXXX",
|
||||
package_managers: "{}",
|
||||
});
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
artifactory_access_token: "XXXX",
|
||||
package_managers: "{}",
|
||||
});
|
||||
|
||||
it("generates an npmrc with scoped repos", async () => {
|
||||
|
||||
@@ -14,11 +14,12 @@ 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.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** This module only works on workspaces with a pre-installed desktop environment. As an example base image you can use `codercom/enterprise-desktop` image.
|
||||
> [!IMPORTANT]
|
||||
> This module only works on workspaces with a pre-installed desktop environment. As an example base image you can use [`codercom/example-desktop`](https://hub.docker.com/r/codercom/example-desktop) image.
|
||||
|
||||
Reference in New Issue
Block a user