mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 16015559e2 | |||
| f1010ee7a6 | |||
| 17734c073a | |||
| 6813e0b5b8 |
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.35.5
|
||||
uses: crate-ci/typos@v1.37.2
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.23.2"
|
||||
- name: Validate contributors
|
||||
|
||||
@@ -30,12 +30,12 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
- name: Authenticate with Google Cloud
|
||||
uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
|
||||
with:
|
||||
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
|
||||
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@26f734c2779b00b7dda794207734c511110a4368
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
|
||||
- name: Deploy to dev.registry.coder.com
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
|
||||
- name: Deploy to registry.coder.com
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: golangci-lint
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -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
|
||||
@@ -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.1.2"
|
||||
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.1.2"
|
||||
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.1.2"
|
||||
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.1.2"
|
||||
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.1.2"
|
||||
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.1.1"
|
||||
|
||||
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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 976 KiB |
@@ -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
|
||||
@@ -28,7 +28,6 @@ module "agentapi" {
|
||||
cli_app_display_name = "Goose CLI"
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
enable_tasks = var.enable_tasks # Set to false to disable AI tasks
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = local.start_script
|
||||
|
||||
@@ -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" {
|
||||
@@ -156,11 +156,6 @@ variable "module_dir_name" {
|
||||
description = "Name of the subdirectory in the home directory for module files."
|
||||
}
|
||||
|
||||
variable "enable_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable AI tasks. When false, the coder_ai_task resource will not be created, avoiding the requirement for an 'AI Prompt' parameter."
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
# we always trim the slash for consistency
|
||||
@@ -245,9 +240,7 @@ resource "coder_app" "agentapi_cli" {
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "agentapi" {
|
||||
count = var.enable_tasks ? 1 : 0
|
||||
|
||||
sidebar_app {
|
||||
id = coder_app.agentapi_web.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
---
|
||||
display_name: Amazon Q
|
||||
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant.
|
||||
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant with MCP integration and task reporting.
|
||||
icon: ../../../../.icons/amazon-q.svg
|
||||
verified: true
|
||||
tags: [agent, ai, aws, amazon-q]
|
||||
tags: [agent, ai, aws, amazon-q, tasks]
|
||||
---
|
||||
|
||||
# Amazon Q
|
||||
|
||||
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module installs and launches Amazon Q, with support for background operation, task reporting, and custom pre/post install scripts.
|
||||
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts.
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.2"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
|
||||
# Required: see below for how to generate
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
# Required: Authentication tarball (see below for generation)
|
||||
auth_tarball = <<-EOF
|
||||
base64encoded-tarball
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,97 +28,370 @@ module "amazon-q" {
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must generate an authenticated Amazon Q tarball on another machine:
|
||||
```sh
|
||||
cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
Paste the result into the `experiment_auth_tarball` variable.
|
||||
- To run in the background, your workspace must have `screen` or `tmux` installed.
|
||||
- **zstd** - Required for compressing the authentication tarball
|
||||
- **Ubuntu/Debian**: `sudo apt-get install zstd`
|
||||
- **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd`
|
||||
- **auth_tarball** - Required for installation and authentication
|
||||
|
||||
<details>
|
||||
<summary><strong>How to generate the Amazon Q auth tarball (step-by-step)</strong></summary>
|
||||
### Authentication Tarball
|
||||
|
||||
**1. Install and authenticate Amazon Q on your local machine:**
|
||||
You must generate an authenticated Amazon Q tarball on another machine where you have successfully logged in:
|
||||
|
||||
- Download and install Amazon Q from the [official site](https://aws.amazon.com/q/developer/).
|
||||
- Run `q login` and complete the authentication process in your terminal.
|
||||
```bash
|
||||
# 1. Install Amazon Q and login on your local machine
|
||||
q login
|
||||
|
||||
**2. Locate your Amazon Q config directory:**
|
||||
# 2. Generate the authentication tarball
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
|
||||
- The config is typically stored at `~/.local/share/amazon-q`.
|
||||
Copy the output and use it as the `auth_tarball` variable.
|
||||
|
||||
**3. Generate the tarball:**
|
||||
## Detailed Authentication Setup
|
||||
|
||||
- Run the following command in your terminal:
|
||||
```sh
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
**Step 1: Install Amazon Q locally**
|
||||
|
||||
**4. Copy the output:**
|
||||
- Download from [AWS Amazon Q Developer](https://aws.amazon.com/q/developer/)
|
||||
- Follow the installation instructions for your platform
|
||||
|
||||
- The command will output a long string. Copy this entire string.
|
||||
**Step 2: Authenticate**
|
||||
|
||||
**5. Paste into your Terraform variable:**
|
||||
```bash
|
||||
q login
|
||||
```
|
||||
|
||||
- Assign the string to the `experiment_auth_tarball` variable in your Terraform configuration, for example:
|
||||
```tf
|
||||
variable "amazon_q_auth_tarball" {
|
||||
type = string
|
||||
default = "PASTE_LONG_STRING_HERE"
|
||||
}
|
||||
```
|
||||
Complete the authentication process in your browser.
|
||||
|
||||
**Note:**
|
||||
**Step 3: Generate tarball**
|
||||
|
||||
- You must re-generate the tarball if you log out or re-authenticate Amazon Q on your local machine.
|
||||
- This process is required for each user who wants to use Amazon Q in their workspace.
|
||||
```bash
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0 > /tmp/amazon-q-auth.txt
|
||||
```
|
||||
|
||||
[Reference: Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/generate-docs.html)
|
||||
|
||||
</details>
|
||||
|
||||
## Examples
|
||||
|
||||
### Run Amazon Q in the background with tmux
|
||||
**Step 4: Use in Terraform**
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_use_tmux = true
|
||||
variable "amazon_q_auth_tarball" {
|
||||
type = string
|
||||
sensitive = true
|
||||
default = "PASTE_YOUR_TARBALL_HERE"
|
||||
}
|
||||
```
|
||||
|
||||
### Enable task reporting (experimental)
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - Regenerate the tarball if you logout or re-authenticate
|
||||
> - Each user needs their own authentication tarball
|
||||
> - Keep the tarball secure as it contains authentication credentials
|
||||
|
||||
### Coder Tasks Integration
|
||||
|
||||
A `coder_parameter` named **'AI Prompt'** is required to enable integration with [Coder Tasks](https://coder.com/docs/ai-coder/tasks).
|
||||
|
||||
```tf
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
name = "AI Prompt"
|
||||
display_name = "AI Prompt"
|
||||
description = "Prompt for the AI task to execute"
|
||||
type = "string"
|
||||
mutable = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_report_tasks = true
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
trust_all_tools = true
|
||||
|
||||
# Task reporting configuration
|
||||
report_tasks = true
|
||||
|
||||
# Enable CLI app alongside web app
|
||||
cli_app = true
|
||||
web_app_display_name = "Amazon Q"
|
||||
cli_app_display_name = "Q CLI"
|
||||
}
|
||||
```
|
||||
|
||||
### Run custom scripts before/after install
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - The parameter name must be exactly **'AI Prompt'** (case-sensitive)
|
||||
> - This parameter enables the AI task workflow integration
|
||||
> - The parameter value is passed to the Amazon Q module via the `ai_prompt` variable
|
||||
> - Without this parameter, `coder_ai_task` resources will not function properly
|
||||
>
|
||||
> **_Security Notice_**
|
||||
> In order to allow the tasks flow non-interactively all the tools are trusted
|
||||
> This flag bypasses standard permission checks and allows Amazon Q broader access to your system than normally permitted.
|
||||
> While this enables more functionality, it also means Amazon Q 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.
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.2"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_pre_install_script = "echo Pre-install!"
|
||||
experiment_post_install_script = "echo Post-install!"
|
||||
### Default System Prompt
|
||||
|
||||
The module includes a simple system prompt that instructs Amazon Q:
|
||||
|
||||
```
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
```
|
||||
|
||||
You can customize this behavior by providing your own system prompt via the `system_prompt` variable.
|
||||
|
||||
### Default Coder MCP Instructions
|
||||
|
||||
The module includes specific instructions for the Coder MCP server integration that are separate from the system prompt:
|
||||
|
||||
```
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input
|
||||
- Use "state": "complete" only when finished with a task
|
||||
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing
|
||||
- Include clear and actionable steps for the user
|
||||
- Be less than 160 characters in length
|
||||
```
|
||||
|
||||
You can customize these instructions by providing your own via the `coder_mcp_instructions` variable.
|
||||
|
||||
## Default Agent Configuration
|
||||
|
||||
The module includes a default agent configuration template that provides a comprehensive setup for Amazon Q integration:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "agent",
|
||||
"description": "This is an default agent config",
|
||||
"prompt": "${system_prompt}",
|
||||
"mcpServers": {},
|
||||
"tools": [
|
||||
"fs_read",
|
||||
"fs_write",
|
||||
"execute_bash",
|
||||
"use_aws",
|
||||
"@coder",
|
||||
"knowledge"
|
||||
],
|
||||
"toolAliases": {},
|
||||
"allowedTools": ["fs_read", "@coder"],
|
||||
"resources": [
|
||||
"file://AmazonQ.md",
|
||||
"file://README.md",
|
||||
"file://.amazonq/rules/**/*.md"
|
||||
],
|
||||
"hooks": {},
|
||||
"toolsSettings": {},
|
||||
"useLegacyMcpJson": true
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
### Configuration Details:
|
||||
|
||||
- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
|
||||
- If neither is set, Amazon Q runs in the foreground.
|
||||
- For more details, see the [main.tf](./main.tf) source.
|
||||
- **Tools Available:** File operations, bash execution, AWS CLI, Coder MCP integration, and knowledge base access
|
||||
- **@coder Tool:** Enables Coder MCP integration for task reporting (`coder_report_task` and related tools)
|
||||
- **Allowed Tools:** By default, only `fs_read` and `@coder` are allowed (can be customized for security)
|
||||
- **Resources:** Access to documentation and rule files in the workspace
|
||||
- **MCP Servers:** Empty by default, can be configured via `agent_config` variable
|
||||
- **System Prompt:** Dynamically populated from the `system_prompt` variable
|
||||
- **Legacy MCP:** Uses legacy MCP JSON format for compatibility
|
||||
|
||||
You can override this configuration by providing your own JSON via the `agent_config` variable.
|
||||
|
||||
### Agent Name Configuration
|
||||
|
||||
The module automatically extracts the agent name from the `"name"` field in the `agent_config` JSON and uses it for:
|
||||
|
||||
- **Configuration File:** Saves the agent config as `~/.aws/amazonq/cli-agents/{agent_name}.json`
|
||||
- **Default Agent:** Sets the agent as the default using `q settings chat.defaultAgent {agent_name}`
|
||||
- **MCP Integration:** Associates the Coder MCP server with the specified agent name
|
||||
|
||||
If no custom `agent_config` is provided, the default agent name "agent" is used.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
}
|
||||
```
|
||||
|
||||
This example will:
|
||||
|
||||
1. Download and install Amazon Q CLI v1.14.1
|
||||
2. Extract authentication tarball to ~/.local/share/amazon-q
|
||||
3. Configure Coder MCP integration for task reporting
|
||||
4. Create default agent configuration file
|
||||
5. Start Amazon Q in /home/coder directory
|
||||
6. Provide web interface through AgentAPI
|
||||
|
||||
> [!IMPORTANT]
|
||||
> By default `fs_write` tool is not allowed, which will pause the task execution
|
||||
> an will wait for the prompt to approve it usage.
|
||||
> To avoid this, and allow the normal task flow, user has two options:
|
||||
>
|
||||
> - Change the parameter `trust_all_tools` value to `true` (default to `false`)
|
||||
> OR
|
||||
> - Provide you own agent configuration with the tools of your choice allowed
|
||||
|
||||
### With Custom AI Prompt
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
|
||||
trust_all_tools = true
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **_Security Notice_**
|
||||
> In order to allow the tasks flow non-interactively all the tools are trusted
|
||||
> This flag bypasses standard permission checks and allows Amazon Q broader access to your system than normally permitted.
|
||||
> While this enables more functionality, it also means Amazon Q 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.
|
||||
|
||||
### With Custom Pre/Post Install Scripts
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
pre_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
echo "Setting up custom environment..."
|
||||
# Install additional dependencies
|
||||
sudo apt-get update && sudo apt-get install -y zstd
|
||||
EOT
|
||||
|
||||
post_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
echo "Configuring Amazon Q settings..."
|
||||
# Custom configuration commands
|
||||
q settings chat.model claude-3-sonnet
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### Specific Version Installation
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
amazon_q_version = "1.14.0" # Specific version
|
||||
install_amazon_q = true
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Agent Configuration
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
agent_config = <<-EOT
|
||||
{
|
||||
"name": "custom-agent",
|
||||
"description": "Custom Amazon Q agent for my workspace",
|
||||
"prompt": "You are a specialized DevOps assistant...",
|
||||
"tools": ["fs_read", "fs_write", "execute_bash", "use_aws"]
|
||||
}
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom AgentAPI Configuration
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.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"
|
||||
}
|
||||
```
|
||||
|
||||
### Air-Gapped Installation
|
||||
|
||||
For environments without direct internet access, you can host Amazon Q installation files internally and configure the module to use your internal repository:
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.amazon_q_auth_tarball
|
||||
|
||||
# Point to internal artifact repository
|
||||
q_install_url = "https://artifacts.internal.corp/amazon-q-releases"
|
||||
|
||||
# Use specific version available in your repository
|
||||
amazon_q_version = "1.14.1"
|
||||
}
|
||||
```
|
||||
|
||||
**Prerequisites for Air-Gapped Setup:**
|
||||
|
||||
1. Download Amazon Q installation files from AWS and host them internally
|
||||
2. Maintain the same directory structure: `{base_url}/{version}/q-{arch}-linux.zip`
|
||||
3. Ensure both architectures are available:
|
||||
- `q-x86_64-linux.zip` for Intel/AMD systems
|
||||
- `q-aarch64-linux.zip` for ARM systems
|
||||
4. Configure network access from Coder workspaces to your internal repository
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Authentication issues:**
|
||||
|
||||
- Regenerate the auth tarball on your local machine
|
||||
- Ensure the tarball is properly base64 encoded
|
||||
- Check that the original authentication is still valid
|
||||
|
||||
**MCP integration not working:**
|
||||
|
||||
- Verify that AgentAPI is installed (`install_agentapi = true`)
|
||||
- Check that the Coder agent is properly configured
|
||||
- Review the system prompt configuration
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
run "required_variables" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
}
|
||||
}
|
||||
|
||||
run "minimal_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdA==" # base64 "test"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable not configured correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should be 'amazonq'"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
# Using vanilla Kubernetes Deployment Template configuration
|
||||
run "test_case_1_basic_usage" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
}
|
||||
|
||||
# Q is installed and authenticated
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable should be configured for basic usage"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should be 'amazonq' for basic usage"
|
||||
}
|
||||
|
||||
# AgentAPI is installed and configured (default behavior)
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created for authentication"
|
||||
}
|
||||
|
||||
# Foundational configuration applied
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated with foundational configuration"
|
||||
}
|
||||
|
||||
# No additional parameters required (using defaults)
|
||||
assert {
|
||||
condition = local.agent_name == "agent"
|
||||
error_message = "Default agent name should be 'agent' when no custom config provided"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 2: Autonomous Usage – Autonomous Use of Q
|
||||
# AI prompt passed through from external source (Tasks interface or Issue Tracker CI)
|
||||
run "test_case_2_autonomous_usage" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
|
||||
}
|
||||
|
||||
# Q is installed and authenticated
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable should be configured for autonomous usage"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should be 'amazonq' for autonomous usage"
|
||||
}
|
||||
|
||||
# AgentAPI is installed and configured
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created for autonomous usage"
|
||||
}
|
||||
|
||||
# Foundational configuration for all components applied
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated for autonomous usage"
|
||||
}
|
||||
|
||||
# AI prompt is configured
|
||||
assert {
|
||||
condition = local.full_prompt == "Help me set up a Python FastAPI project with proper testing structure"
|
||||
error_message = "AI prompt should be configured correctly for autonomous usage"
|
||||
}
|
||||
|
||||
# Default agent name when no custom config
|
||||
assert {
|
||||
condition = local.agent_name == "agent"
|
||||
error_message = "Default agent name should be 'agent' for autonomous usage"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 3: Extended Configuration – Parameter Validation and File Rendering
|
||||
# Validates extended configuration options and parameter application
|
||||
run "test_case_3_extended_configuration" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
amazon_q_version = "1.14.1"
|
||||
q_install_url = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
install_amazon_q = true
|
||||
install_agentapi = true
|
||||
agentapi_version = "v0.6.0"
|
||||
trust_all_tools = true
|
||||
ai_prompt = "Help me create a production-grade TypeScript monorepo with testing and deployment"
|
||||
system_prompt = "You are a helpful software assistant working in a secure enterprise environment"
|
||||
pre_install_script = "echo 'Pre-install setup'"
|
||||
post_install_script = "echo 'Post-install cleanup'"
|
||||
agent_config = jsonencode({
|
||||
name = "production-agent"
|
||||
description = "Production Amazon Q agent for enterprise environment"
|
||||
prompt = "You are a helpful software assistant working in a secure enterprise environment"
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read"]
|
||||
resources = ["file://AmazonQ.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
# All installation parameters are applied correctly
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug should be configured correctly with extended parameters"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
|
||||
error_message = "Auth tarball should be configured correctly with extended parameters"
|
||||
}
|
||||
|
||||
# Custom agent configuration is loaded and referenced correctly
|
||||
assert {
|
||||
condition = local.agent_name == "production-agent"
|
||||
error_message = "Agent name should be extracted from custom agent config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Custom agent config should be processed correctly"
|
||||
}
|
||||
|
||||
# AI prompt and system prompt are configured
|
||||
assert {
|
||||
condition = local.full_prompt == "Help me create a production-grade TypeScript monorepo with testing and deployment"
|
||||
error_message = "AI prompt should be configured correctly in extended configuration"
|
||||
}
|
||||
|
||||
# Pre-install and post-install scripts are provided
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated correctly for extended configuration"
|
||||
}
|
||||
}
|
||||
|
||||
run "full_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
install_amazon_q = true
|
||||
install_agentapi = true
|
||||
agentapi_version = "v0.5.0"
|
||||
amazon_q_version = "latest"
|
||||
trust_all_tools = true
|
||||
ai_prompt = "Build a web application"
|
||||
auth_tarball = "dGVzdA=="
|
||||
order = 1
|
||||
group = "AI Tools"
|
||||
icon = "/icon/custom-amazon-q.svg"
|
||||
pre_install_script = "echo 'pre-install'"
|
||||
post_install_script = "echo 'post-install'"
|
||||
agent_config = jsonencode({
|
||||
name = "test-agent"
|
||||
description = "Test agent configuration"
|
||||
prompt = "You are a helpful AI assistant for testing."
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read"]
|
||||
resources = ["file://AmazonQ.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable not configured correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should be 'amazonq'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "auth_tarball_environment" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].name == "AMAZON_Q_AUTH_TARBALL"
|
||||
error_message = "Auth tarball environment variable name should be 'AMAZON_Q_AUTH_TARBALL'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
|
||||
error_message = "Auth tarball environment variable value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "empty_auth_tarball" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 0
|
||||
error_message = "Auth tarball environment variable should not be created when empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_system_prompt" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
system_prompt = "Custom system prompt for testing"
|
||||
}
|
||||
|
||||
# Test that the system prompt is used in the agent config template
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated with custom system prompt"
|
||||
}
|
||||
}
|
||||
|
||||
run "install_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
install_amazon_q = false
|
||||
install_agentapi = false
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug should still be configured even when install options are disabled"
|
||||
}
|
||||
}
|
||||
|
||||
run "version_configuration" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
amazon_q_version = "2.15.0"
|
||||
agentapi_version = "v0.4.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug.value == "amazonq"
|
||||
error_message = "Status slug value should remain 'amazonq' regardless of version"
|
||||
}
|
||||
}
|
||||
|
||||
# Additional test for agent name extraction
|
||||
run "agent_name_extraction" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
agent_config = jsonencode({
|
||||
name = "custom-enterprise-agent"
|
||||
description = "Custom enterprise agent configuration"
|
||||
prompt = "You are a custom enterprise AI assistant."
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read", "fs_write"]
|
||||
resources = ["file://README.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.agent_name == "custom-enterprise-agent"
|
||||
error_message = "Agent name should be extracted correctly from custom agent config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be processed correctly"
|
||||
}
|
||||
}
|
||||
|
||||
# Test for JSON encoding validation
|
||||
run "json_encoding_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
system_prompt = "Multi-line\nsystem prompt\nwith newlines"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.system_prompt) > 0
|
||||
error_message = "System prompt should be JSON encoded correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated correctly with multi-line system prompt"
|
||||
}
|
||||
}
|
||||
@@ -2,40 +2,530 @@ import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
findResourceInstance,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
|
||||
const moduleDir = path.resolve(__dirname);
|
||||
|
||||
// Always provide agent_config to bypass template parsing issues
|
||||
const baseAgentConfig = JSON.stringify({
|
||||
name: "test-agent",
|
||||
description: "Test agent configuration",
|
||||
prompt: "You are a helpful AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md", "file://.amazonq/rules/**/*.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const requiredVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
agent_config: baseAgentConfig,
|
||||
workdir: "/tmp/test-workdir",
|
||||
};
|
||||
|
||||
describe("amazon-q module", async () => {
|
||||
const fullConfigVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
install_amazon_q: true,
|
||||
install_agentapi: true,
|
||||
agentapi_version: "v0.6.0",
|
||||
amazon_q_version: "1.14.1",
|
||||
q_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
|
||||
trust_all_tools: false,
|
||||
ai_prompt: "Build a comprehensive test suite",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
order: 1,
|
||||
group: "AI Tools",
|
||||
icon: "/icon/custom-amazon-q.svg",
|
||||
pre_install_script: "echo 'Starting pre-install'",
|
||||
post_install_script: "echo 'Completed post-install'",
|
||||
agent_config: baseAgentConfig,
|
||||
};
|
||||
|
||||
describe("amazon-q module v2.0.0", async () => {
|
||||
await runTerraformInit(moduleDir);
|
||||
|
||||
// 1. Required variables
|
||||
testRequiredVariables(moduleDir, requiredVars);
|
||||
// Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case #1: Basic Usage
|
||||
it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => {
|
||||
const basicUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
};
|
||||
|
||||
// 2. coder_script resource is created
|
||||
it("creates coder_script resource", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
const scriptResource = findResourceInstance(state, "coder_script");
|
||||
expect(scriptResource).toBeDefined();
|
||||
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
|
||||
// Optionally, check that the script contains expected lines
|
||||
expect(scriptResource.script).toContain("Installing Amazon Q");
|
||||
const state = await runTerraformApply(moduleDir, basicUsageVars);
|
||||
|
||||
// Q is installed and authenticated
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
|
||||
// AgentAPI is installed and configured (default behavior)
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
|
||||
// Foundational configuration for all components is applied
|
||||
// No additional parameters are required for the module to work
|
||||
// Using the terminal application and Q chat returns a functional interface
|
||||
});
|
||||
|
||||
// 3. coder_app resource is created
|
||||
it("creates coder_app resource", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
const appResource = findResourceInstance(state, "coder_app", "amazon_q");
|
||||
expect(appResource).toBeDefined();
|
||||
expect(appResource.agent_id).toBe(requiredVars.agent_id);
|
||||
// Test Case 2: Autonomous Usage – Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case 2: Autonomous Usage
|
||||
it("Test Case 2: Autonomous Usage - Autonomous Use of Q", async () => {
|
||||
const autonomousUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
ai_prompt:
|
||||
"Help me set up a Python FastAPI project with proper testing structure",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, autonomousUsageVars);
|
||||
|
||||
// Q is installed and authenticated
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
|
||||
// AgentAPI is installed and configured
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
|
||||
|
||||
// AI prompt is passed through from external source
|
||||
// The Chat interface functions as required
|
||||
// The Tasks interface functions as required
|
||||
// The template can be invoked from GitHub integration as expected
|
||||
});
|
||||
|
||||
// Add more state-based tests as needed
|
||||
// Test Case 3: Extended Configuration – Parameter Validation and File Rendering
|
||||
// Matches CDES-203 Test Case 3: Extended Configuration
|
||||
it("Test Case 3: Extended Configuration - Parameter Validation and File Rendering", async () => {
|
||||
const extendedConfigVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
amazon_q_version: "1.14.1",
|
||||
q_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
|
||||
install_amazon_q: true,
|
||||
install_agentapi: true,
|
||||
agentapi_version: "v0.6.0",
|
||||
trust_all_tools: true,
|
||||
ai_prompt:
|
||||
"Help me create a production-grade TypeScript monorepo with testing and deployment",
|
||||
system_prompt:
|
||||
"You are a helpful software assistant working in a secure enterprise environment",
|
||||
pre_install_script: "echo 'Pre-install setup'",
|
||||
post_install_script: "echo 'Post-install cleanup'",
|
||||
agent_config: JSON.stringify({
|
||||
name: "production-agent",
|
||||
description: "Production Amazon Q agent for enterprise environment",
|
||||
prompt:
|
||||
"You are a helpful software assistant working in a secure enterprise environment",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: [
|
||||
"file://AmazonQ.md",
|
||||
"file://README.md",
|
||||
"file://.amazonq/rules/**/*.md",
|
||||
],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
}),
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, extendedConfigVars);
|
||||
|
||||
// All installation steps execute in the correct order
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
|
||||
// auth_tarball is unpacked and used as expected
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
|
||||
// agent_config is rendered correctly, and the name field is used as the agent's name
|
||||
// The specified ai_prompt and system_prompt are respected by the Q agent
|
||||
// Tools are trusted globally if trust_all_tools = true
|
||||
// Files and scripts execute in proper sequence
|
||||
});
|
||||
|
||||
// 1. Basic functionality test (replaces testRequiredVariables)
|
||||
it("works with required variables", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
|
||||
// 2. Environment variables are created correctly
|
||||
it("creates required environment variables", async () => {
|
||||
const state = await runTerraformApply(moduleDir, fullConfigVars);
|
||||
|
||||
// Check status slug environment variable
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
|
||||
// Check auth tarball environment variable
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
});
|
||||
|
||||
// 3. Empty auth tarball handling
|
||||
it("handles empty auth tarball correctly", async () => {
|
||||
const noAuthVars = {
|
||||
...requiredVars,
|
||||
auth_tarball: "",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, noAuthVars);
|
||||
|
||||
// Auth tarball environment variable should not be created when empty
|
||||
const authTarballEnv = state.resources?.find(
|
||||
(r) => r.type === "coder_env" && r.name === "auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeUndefined();
|
||||
});
|
||||
|
||||
// 4. Status slug is always created
|
||||
it("creates status slug environment variable", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
|
||||
// Status slug should always be configured
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
|
||||
// 5. Install options configuration
|
||||
it("respects install option flags", async () => {
|
||||
const noInstallVars = {
|
||||
...requiredVars,
|
||||
install_amazon_q: false,
|
||||
install_agentapi: false,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, noInstallVars);
|
||||
|
||||
// Status slug should still be configured even when install options are disabled
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
|
||||
// 6. Configurable installation URL
|
||||
it("uses configurable q_install_url parameter", async () => {
|
||||
const customUrlVars = {
|
||||
...requiredVars,
|
||||
q_install_url: "https://internal-mirror.company.com/amazon-q",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, customUrlVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 7. Version configuration
|
||||
it("uses specified versions", async () => {
|
||||
const versionVars = {
|
||||
...requiredVars,
|
||||
amazon_q_version: "1.14.1",
|
||||
agentapi_version: "v0.6.0",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, versionVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 8. UI configuration options
|
||||
it("supports UI customization options", async () => {
|
||||
const uiCustomVars = {
|
||||
...requiredVars,
|
||||
order: 5,
|
||||
group: "Custom AI Tools",
|
||||
icon: "/icon/custom-amazon-q-icon.svg",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, uiCustomVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 9. Pre and post install scripts
|
||||
it("supports pre and post install scripts", async () => {
|
||||
const scriptVars = {
|
||||
...requiredVars,
|
||||
pre_install_script: "echo 'Pre-install setup'",
|
||||
post_install_script: "echo 'Post-install cleanup'",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, scriptVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 10. Valid agent_config JSON with different agent name
|
||||
it("handles valid agent_config JSON with custom agent name", async () => {
|
||||
const customAgentConfig = JSON.stringify({
|
||||
name: "production-agent",
|
||||
description: "Production Amazon Q agent",
|
||||
prompt: "You are a production AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const validAgentConfigVars = {
|
||||
...requiredVars,
|
||||
agent_config: customAgentConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, validAgentConfigVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 11. Air-gapped installation support
|
||||
it("supports air-gapped installation with custom URL", async () => {
|
||||
const airGappedVars = {
|
||||
...requiredVars,
|
||||
q_install_url: "https://artifacts.internal.corp/amazon-q-releases",
|
||||
amazon_q_version: "1.14.1",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, airGappedVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 12. Trust all tools configuration
|
||||
it("handles trust_all_tools configuration", async () => {
|
||||
const trustVars = {
|
||||
...requiredVars,
|
||||
trust_all_tools: true,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, trustVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 13. AI prompt configuration
|
||||
it("handles AI prompt configuration", async () => {
|
||||
const promptVars = {
|
||||
...requiredVars,
|
||||
ai_prompt: "Create a comprehensive test suite for the application",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, promptVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 14. Agent config with minimal structure
|
||||
it("handles minimal agent config structure", async () => {
|
||||
const minimalAgentConfig = JSON.stringify({
|
||||
name: "minimal-agent",
|
||||
description: "Minimal agent config",
|
||||
prompt: "You are a minimal AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const minimalVars = {
|
||||
...requiredVars,
|
||||
agent_config: minimalAgentConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, minimalVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 15. JSON encoding validation for system prompts with newlines
|
||||
it("handles system prompts with newlines correctly", async () => {
|
||||
const multilinePromptVars = {
|
||||
...requiredVars,
|
||||
system_prompt: "Multi-line\nsystem prompt\nwith newlines",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, multilinePromptVars);
|
||||
|
||||
// Should create the basic resources without JSON parsing errors
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
|
||||
// 16. Agent name extraction from custom config
|
||||
it("extracts agent name from custom configuration correctly", async () => {
|
||||
const customNameConfig = JSON.stringify({
|
||||
name: "enterprise-production-agent",
|
||||
description: "Enterprise production agent configuration",
|
||||
prompt: "You are an enterprise production AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read", "fs_write", "execute_bash"],
|
||||
resources: ["file://README.md", "file://.amazonq/rules/**/*.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const customNameVars = {
|
||||
...requiredVars,
|
||||
agent_config: customNameConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, customNameVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("amazonq");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Improved amazon-q module main.tf
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +17,6 @@ variable "agent_id" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
@@ -36,12 +37,67 @@ variable "icon" {
|
||||
default = "/icon/amazon-q.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
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 Amazon Q"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "AmazonQ"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "AmazonQ CLI"
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "The initial task prompt to send to Amazon Q."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run before installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run after installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.6.1"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Amazon Q in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
variable "install_amazon_q" {
|
||||
type = bool
|
||||
description = "Whether to install Amazon Q."
|
||||
@@ -51,43 +107,19 @@ variable "install_amazon_q" {
|
||||
variable "amazon_q_version" {
|
||||
type = string
|
||||
description = "The version of Amazon Q to install."
|
||||
default = "latest"
|
||||
default = "1.14.1"
|
||||
}
|
||||
|
||||
variable "experiment_use_screen" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Amazon Q in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Amazon Q in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
variable "q_install_url" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Amazon Q."
|
||||
default = null
|
||||
description = "Base URL for Amazon Q installation downloads."
|
||||
default = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_auth_tarball" {
|
||||
type = string
|
||||
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory. After running `q login` on another machine, you may generate it with: `cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0`"
|
||||
default = "tarball"
|
||||
variable "trust_all_tools" {
|
||||
type = bool
|
||||
description = "Whether to trust all tools in Amazon Q."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
@@ -98,222 +130,141 @@ variable "system_prompt" {
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
variable "coder_mcp_instructions" {
|
||||
type = string
|
||||
description = "The initial task prompt to send to Amazon Q."
|
||||
default = "Please help me with my coding tasks. I'll provide specific instructions as needed."
|
||||
description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder."
|
||||
default = <<-EOT
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input
|
||||
- Use "state": "complete" only when finished with a task
|
||||
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing
|
||||
- Include clear and actionable steps for the user
|
||||
- Be less than 160 characters in length
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "auth_tarball" {
|
||||
type = string
|
||||
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "agent_config" {
|
||||
type = string
|
||||
description = "Optional Agent configuration JSON for Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_chat_based_path" {
|
||||
type = bool
|
||||
description = "Whether to use chat-based path for AgentAPI.Required if CODER_WILDCARD_ACCESS_URL is not defined in coder deployment"
|
||||
default = false
|
||||
}
|
||||
|
||||
# Expose status slug to the agent environment
|
||||
resource "coder_env" "status_slug" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = local.app_slug
|
||||
}
|
||||
|
||||
# Expose auth tarball as environment variable for install script
|
||||
resource "coder_env" "auth_tarball" {
|
||||
count = var.auth_tarball != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "AMAZON_Q_AUTH_TARBALL"
|
||||
value = var.auth_tarball
|
||||
}
|
||||
|
||||
locals {
|
||||
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) : ""
|
||||
full_prompt = <<-EOT
|
||||
${var.system_prompt}
|
||||
app_slug = "amazonq"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".amazonq-module"
|
||||
system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", ""))
|
||||
coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", ""))
|
||||
|
||||
Your first task is:
|
||||
# Create default agent config structure
|
||||
default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", {
|
||||
system_prompt = local.system_prompt
|
||||
})
|
||||
|
||||
${var.ai_prompt}
|
||||
EOT
|
||||
# Choose the JSON string: use var.agent_config if provided, otherwise encode default
|
||||
agent_config = var.agent_config != null ? var.agent_config : local.default_agent_config
|
||||
|
||||
# Extract agent name from the selected config
|
||||
agent_name = try(jsondecode(local.agent_config).name, "agent")
|
||||
|
||||
full_prompt = var.ai_prompt != null ? "${var.ai_prompt}" : ""
|
||||
|
||||
server_chat_parameters = var.agentapi_chat_based_path ? "--chat-base-path /@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" : ""
|
||||
}
|
||||
|
||||
resource "coder_script" "amazon_q" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Amazon Q"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.1"
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
|
||||
ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
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_amazon_q}" = "true" ]; then
|
||||
echo "Installing Amazon Q..."
|
||||
PREV_DIR="$PWD"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cd "$TMP_DIR"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
"x86_64")
|
||||
Q_URL="https://desktop-release.q.us-east-1.amazonaws.com/${var.amazon_q_version}/q-x86_64-linux.zip"
|
||||
;;
|
||||
"aarch64"|"arm64")
|
||||
Q_URL="https://desktop-release.codewhisperer.us-east-1.amazonaws.com/${var.amazon_q_version}/q-aarch64-linux.zip"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Downloading Amazon Q for $ARCH..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
|
||||
unzip q.zip
|
||||
./q/install.sh --no-confirm
|
||||
cd "$PREV_DIR"
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
echo "Installed Amazon Q version: $(q --version)"
|
||||
fi
|
||||
|
||||
echo "Extracting auth tarball..."
|
||||
PREV_DIR="$PWD"
|
||||
echo "${var.experiment_auth_tarball}" | base64 -d > /tmp/auth.tar.zst
|
||||
rm -rf ~/.local/share/amazon-q
|
||||
mkdir -p ~/.local/share/amazon-q
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -I zstd -xf /tmp/auth.tar.zst
|
||||
rm /tmp/auth.tar.zst
|
||||
cd "$PREV_DIR"
|
||||
echo "Extracted auth tarball"
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Amazon Q to report tasks via Coder MCP..."
|
||||
q mcp add --name coder --command "coder" --args "exp,mcp,server,--allowed-tools,coder_report_task" --env "CODER_MCP_APP_STATUS_SLUG=amazon-q" --scope global --force
|
||||
echo "Added Coder MCP server to Amazon Q configuration"
|
||||
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 [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
|
||||
echo "Please set only one of them to true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Amazon Q in the background with tmux..."
|
||||
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.amazon-q.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
|
||||
|
||||
tmux send-keys -t amazon-q "${local.full_prompt}"
|
||||
sleep 5
|
||||
tmux send-keys -t amazon-q Enter
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Amazon Q in the background..."
|
||||
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.amazon-q.log"
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS amazon-q bash -c '
|
||||
cd ${var.folder}
|
||||
q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
|
||||
exec bash
|
||||
'
|
||||
# Extremely hacky way to send the prompt to the screen session
|
||||
# This will be fixed in the future, but `amazon-q` was not sending MCP
|
||||
# tasks when an initial prompt is provided.
|
||||
screen -S amazon-q -X stuff "${local.full_prompt}"
|
||||
sleep 5
|
||||
screen -S amazon-q -X stuff "^M"
|
||||
else
|
||||
if ! command_exists q; then
|
||||
echo "Error: Amazon Q is not installed. Please enable install_amazon_q or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "amazon_q" {
|
||||
slug = "amazon-q"
|
||||
display_name = "Amazon Q"
|
||||
agent_id = var.agent_id
|
||||
command = <<-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
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t amazon-q 2>/dev/null; then
|
||||
echo "Attaching to existing Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
|
||||
tmux attach-session -t amazon-q
|
||||
else
|
||||
echo "Starting a new Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
|
||||
tmux new-session -s amazon-q -c ${var.folder} "q chat --trust-all-tools | tee -a \"$HOME/.amazon-q.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "amazon-q"; then
|
||||
echo "Attaching to existing Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
|
||||
screen -xRR amazon-q
|
||||
else
|
||||
echo "Starting a new Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
|
||||
screen -S amazon-q bash -c 'q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log"; exec bash'
|
||||
fi
|
||||
else
|
||||
cd ${var.folder}
|
||||
q chat --trust-all-tools
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_INSTALL='${var.install_amazon_q}' \
|
||||
ARG_VERSION='${var.amazon_q_version}' \
|
||||
ARG_Q_INSTALL_URL='${var.q_install_url}' \
|
||||
ARG_AUTH_TARBALL='${var.auth_tarball}' \
|
||||
ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \
|
||||
ARG_AGENT_NAME='${local.agent_name}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_CODER_MCP_INSTRUCTIONS='${base64encode(local.coder_mcp_instructions)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/bin/bash
|
||||
# Install script for amazon-q module
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Inputs
|
||||
ARG_INSTALL=${ARG_INSTALL:-true}
|
||||
ARG_VERSION=${ARG_VERSION:-latest}
|
||||
ARG_Q_INSTALL_URL=${ARG_Q_INSTALL_URL:-https://desktop-release.q.us-east-1.amazonaws.com}
|
||||
ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-}
|
||||
ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-}
|
||||
ARG_AGENT_NAME=${ARG_AGENT_NAME:-default-agent}
|
||||
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/.amazonq}
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_CODER_MCP_INSTRUCTIONS=${ARG_CODER_MCP_INSTRUCTIONS:-}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
|
||||
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
|
||||
|
||||
# Decode base64 inputs
|
||||
ARG_AGENT_CONFIG_DECODED=""
|
||||
if [ -n "$ARG_AGENT_CONFIG" ]; then
|
||||
ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d)
|
||||
fi
|
||||
|
||||
ARG_CODER_MCP_INSTRUCTIONS_DECODED=""
|
||||
if [ -n "$ARG_CODER_MCP_INSTRUCTIONS" ]; then
|
||||
ARG_CODER_MCP_INSTRUCTIONS_DECODED=$(echo -n "$ARG_CODER_MCP_INSTRUCTIONS" | base64 -d)
|
||||
fi
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "install: $ARG_INSTALL"
|
||||
echo "version: $ARG_VERSION"
|
||||
echo "q_install_url: $ARG_Q_INSTALL_URL"
|
||||
echo "agent_name: $ARG_AGENT_NAME"
|
||||
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
|
||||
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
|
||||
echo "auth_tarball_provided: ${ARG_AUTH_TARBALL}"
|
||||
echo "report_tasks: ${ARG_REPORT_TASKS}"
|
||||
echo "--------------------------------"
|
||||
|
||||
# Install Amazon Q if requested
|
||||
function install_amazon_q() {
|
||||
if [ "$ARG_INSTALL" = "true" ]; then
|
||||
echo "Installing Amazon Q..."
|
||||
PREV_DIR="$PWD"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cd "$TMP_DIR"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
"x86_64")
|
||||
Q_URL="${ARG_Q_INSTALL_URL}/${ARG_VERSION}/q-x86_64-linux.zip"
|
||||
;;
|
||||
"aarch64" | "arm64")
|
||||
Q_URL="${ARG_Q_INSTALL_URL}/${ARG_VERSION}/q-aarch64-linux.zip"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Downloading Amazon Q for $ARCH from $Q_URL..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
|
||||
unzip q.zip
|
||||
./q/install.sh --no-confirm
|
||||
cd "$PREV_DIR"
|
||||
rm -rf "$TMP_DIR"
|
||||
|
||||
# Ensure binaries are discoverable; create stable symlink to q
|
||||
CANDIDATES=(
|
||||
"$(command -v q || true)"
|
||||
"$HOME/.local/bin/q"
|
||||
)
|
||||
FOUND_BIN=""
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
if [ -n "$c" ] && [ -x "$c" ]; then
|
||||
FOUND_BIN="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
echo "Installed Amazon Q at: $(command -v q || true) (resolved: $FOUND_BIN)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract authentication tarball
|
||||
function extract_auth_tarball() {
|
||||
if [ -n "$ARG_AUTH_TARBALL" ]; then
|
||||
echo "Extracting auth tarball..."
|
||||
PREV_DIR="$PWD"
|
||||
echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst
|
||||
rm -rf ~/.local/share/amazon-q
|
||||
mkdir -p ~/.local/share/amazon-q
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -I zstd -xf /tmp/auth.tar.zst
|
||||
rm /tmp/auth.tar.zst
|
||||
cd "$PREV_DIR"
|
||||
echo "Extracted auth tarball to ~/.local/share/amazon-q"
|
||||
else
|
||||
echo "Warning: No auth tarball provided. Amazon Q may require manual authentication."
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure MCP integration and create agent
|
||||
function configure_agent() {
|
||||
# Create Amazon Q agent configuration directory
|
||||
AGENT_CONFIG_DIR="$HOME/.aws/amazonq/cli-agents"
|
||||
mkdir -p "$AGENT_CONFIG_DIR"
|
||||
ALLOWED_TOOLS="coder_get_workspace\,coder_create_workspace\,coder_list_workspaces\,coder_list_templates\,coder_template_version_parameters\,coder_get_authenticated_user\,coder_create_workspace_build\,coder_create_template_version\,coder_get_workspace_agent_logs\,coder_get_workspace_build_logs\,coder_get_template_version_logs\,coder_update_template_active_version\,coder_upload_tar_file\,coder_create_template\,coder_delete_template\,coder_workspace_bash"
|
||||
if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then
|
||||
echo "Applying custom MCP configuration..."
|
||||
# Use agent name as filename for the configuration
|
||||
echo "$ARG_AGENT_CONFIG_DECODED" > "$AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
|
||||
echo "Custom configuration saved to $AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
|
||||
fi
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Amazon Q to report tasks via Coder MCP..."
|
||||
q mcp add --name coder \
|
||||
--command "coder" \
|
||||
--agent "$ARG_AGENT_NAME" \
|
||||
--args "exp,mcp,server,--allowed-tools,coder_report_task,--instructions,'$ARG_CODER_MCP_INSTRUCTIONS_DECODED'" \
|
||||
--env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \
|
||||
--env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \
|
||||
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
|
||||
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
|
||||
--force || echo "Warning: Failed to add Coder MCP server"
|
||||
else
|
||||
q mcp add --name coder \
|
||||
--command "coder" \
|
||||
--agent "$ARG_AGENT_NAME" \
|
||||
--args "exp,mcp,server,--allowed-tools,coder_report_task" \
|
||||
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
|
||||
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
|
||||
--force || echo "Warning: Failed to add Coder MCP server"
|
||||
fi
|
||||
echo "Added Coder MCP server into $ARG_AGENT_NAME in Amazon Q configuration"
|
||||
q settings chat.defaultAgent "$ARG_AGENT_NAME"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
install_amazon_q
|
||||
extract_auth_tarball
|
||||
configure_agent
|
||||
|
||||
echo "Amazon Q installation and configuration complete!"
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# Start script for amazon-q module
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Decode inputs
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
|
||||
ARG_TRUST_ALL_TOOLS=${ARG_TRUST_ALL_TOOLS:-true}
|
||||
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/amazonq}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_SERVER_PARAMETERS=${ARG_SERVER_PARAMETERS:-""}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "ai_prompt: $ARG_AI_PROMPT"
|
||||
echo "trust_all_tools: $ARG_TRUST_ALL_TOOLS"
|
||||
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
|
||||
echo "workdir: $ARG_WORKDIR"
|
||||
echo "report_tasks: ${ARG_REPORT_TASKS}"
|
||||
echo "--------------------------------"
|
||||
|
||||
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
|
||||
|
||||
# Find Amazon Q CLI
|
||||
if command_exists q; then
|
||||
Q_CMD=q
|
||||
elif [ -x "$HOME/.local/bin/q" ]; then
|
||||
Q_CMD="$HOME/.local/bin/q"
|
||||
else
|
||||
echo "Error: Amazon Q CLI not found. Install it or set install_amazon_q=true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
# Set up environment
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Build command arguments
|
||||
ARGS=(chat)
|
||||
|
||||
if [ "$ARG_TRUST_ALL_TOOLS" = "true" ]; then
|
||||
ARGS+=(--trust-all-tools)
|
||||
fi
|
||||
|
||||
# Log and run with agentapi integration
|
||||
printf "Running: %q %s\n" "$Q_CMD" "$(printf '%q ' "${ARGS[@]}")"
|
||||
|
||||
# If we have an AI prompt, we need to handle it specially
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT"
|
||||
else
|
||||
PROMPT="$ARG_AI_PROMPT"
|
||||
fi
|
||||
ARGS+=("$PROMPT")
|
||||
fi
|
||||
|
||||
# Use agentapi to manage the interactive session with initial prompt
|
||||
agentapi server ${ARG_SERVER_PARAMETERS} --term-width 67 --term-height 1190 -- "$Q_CMD" "${ARGS[@]}"
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "agent",
|
||||
"description": "This is an default agent config",
|
||||
"prompt": ${system_prompt},
|
||||
"mcpServers": {},
|
||||
"tools": [
|
||||
"fs_read",
|
||||
"fs_write",
|
||||
"execute_bash",
|
||||
"use_aws",
|
||||
"@coder",
|
||||
"knowledge"
|
||||
],
|
||||
"toolAliases": {},
|
||||
"allowedTools": [
|
||||
"fs_read",
|
||||
"@coder"
|
||||
],
|
||||
"resources": [
|
||||
"file://AmazonQ.md",
|
||||
"file://README.md",
|
||||
"file://.amazonq/rules/**/*.md"
|
||||
],
|
||||
"hooks": {},
|
||||
"toolsSettings": {},
|
||||
"useLegacyMcpJson": true
|
||||
}
|
||||
@@ -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.0.3"
|
||||
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.0.3"
|
||||
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.6.1"
|
||||
|
||||
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.0.3"
|
||||
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.7.1"
|
||||
}
|
||||
|
||||
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.1.1"
|
||||
|
||||
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.1.2"
|
||||
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.1.2"
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ variable "agentapi_version" {
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = true
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "goose_provider" {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||
@@ -27,6 +27,7 @@ module "jfrog" {
|
||||
pypi = ["pypi", "extra-index-pypi"]
|
||||
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
|
||||
conda = ["conda", "conda-local"]
|
||||
maven = ["maven", "maven-local"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -46,7 +47,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "email"
|
||||
@@ -75,7 +76,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -150,4 +148,38 @@ EOF`;
|
||||
'if [ -z "YES" ]; then\n not_configured conda',
|
||||
);
|
||||
});
|
||||
it("generates a maven settings.xml with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
maven: ["central", "snapshots", "local"],
|
||||
}),
|
||||
});
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
'jf mvnc --global --repo-resolve "central"',
|
||||
);
|
||||
|
||||
expect(coderScript.script).toContain("<servers>");
|
||||
expect(coderScript.script).toContain("<id>central</id>");
|
||||
expect(coderScript.script).toContain("<id>snapshots</id>");
|
||||
expect(coderScript.script).toContain("<id>local</id>");
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/central</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/snapshots</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/local</url>",
|
||||
);
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured maven',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ variable "package_managers" {
|
||||
pypi = optional(list(string), [])
|
||||
docker = optional(list(string), [])
|
||||
conda = optional(list(string), [])
|
||||
maven = optional(list(string), [])
|
||||
})
|
||||
description = <<-EOF
|
||||
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
|
||||
@@ -69,6 +70,7 @@ variable "package_managers" {
|
||||
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
|
||||
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
|
||||
conda = ["YOUR_CONDA_REPO_KEY", "ANOTHER_CONDA_REPO_KEY"]
|
||||
maven = ["YOUR_MAVEN_REPO_KEY", "ANOTHER_MAVEN_REPO_KEY"]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
@@ -103,6 +105,9 @@ locals {
|
||||
conda_conf = templatefile(
|
||||
"${path.module}/conda.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.conda })
|
||||
)
|
||||
maven_settings = templatefile(
|
||||
"${path.module}/settings.xml.tftpl", merge(local.common_values, { REPOS = var.package_managers.maven })
|
||||
)
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
@@ -133,6 +138,9 @@ resource "coder_script" "jfrog" {
|
||||
HAS_CONDA = length(var.package_managers.conda) == 0 ? "" : "YES"
|
||||
CONDA_CONF = local.conda_conf
|
||||
REPOSITORY_CONDA = try(element(var.package_managers.conda, 0), "")
|
||||
HAS_MAVEN = length(var.package_managers.maven) == 0 ? "" : "YES"
|
||||
MAVEN_SETTINGS = local.maven_settings
|
||||
REPOSITORY_MAVEN = try(element(var.package_managers.maven, 0), "")
|
||||
}
|
||||
))
|
||||
run_on_start = true
|
||||
|
||||
@@ -94,6 +94,20 @@ EOF
|
||||
config_complete
|
||||
fi
|
||||
|
||||
# Configure Maven to use the Artifactory "maven" repository.
|
||||
if [ -z "${HAS_MAVEN}" ]; then
|
||||
not_configured maven
|
||||
else
|
||||
echo "☕ Configuring maven..."
|
||||
jf mvnc --global --repo-resolve "${REPOSITORY_MAVEN}"
|
||||
# Create Maven config directory if it doesn't exist
|
||||
mkdir -p ~/.m2
|
||||
cat << EOF > ~/.m2/settings.xml
|
||||
${MAVEN_SETTINGS}
|
||||
EOF
|
||||
config_complete
|
||||
fi
|
||||
|
||||
# Install the JFrog vscode extension for code-server.
|
||||
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
|
||||
while ! [ -x /tmp/code-server/bin/code-server ]; do
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
|
||||
http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
|
||||
<servers>
|
||||
%{ for REPO in REPOS ~}
|
||||
<server>
|
||||
<id>${REPO}</id>
|
||||
<username>${ARTIFACTORY_USERNAME}</username>
|
||||
<password>${ARTIFACTORY_ACCESS_TOKEN}</password>
|
||||
</server>
|
||||
%{ endfor ~}
|
||||
</servers>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>artifactory</id>
|
||||
<repositories>
|
||||
%{ for REPO in REPOS ~}
|
||||
<repository>
|
||||
<id>${REPO}</id>
|
||||
<url>${JFROG_URL}/artifactory/${REPO}</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
%{ endfor ~}
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
%{ for REPO in REPOS ~}
|
||||
<pluginRepository>
|
||||
<id>${REPO}</id>
|
||||
<url>${JFROG_URL}/artifactory/${REPO}</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
%{ endfor ~}
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<activeProfiles>
|
||||
<activeProfile>artifactory</activeProfile>
|
||||
</activeProfiles>
|
||||
|
||||
</settings>
|
||||
@@ -13,7 +13,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/coder/jfrog-token/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
artifactory_access_token = var.artifactory_access_token
|
||||
@@ -23,6 +23,7 @@ module "jfrog" {
|
||||
pypi = ["pypi", "extra-index-pypi"]
|
||||
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
|
||||
conda = ["conda", "conda-local"]
|
||||
maven = ["maven", "maven-local"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -41,7 +42,7 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/coder/jfrog-token/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://YYYY.jfrog.io"
|
||||
artifactory_access_token = var.artifactory_access_token # An admin access token
|
||||
@@ -50,17 +51,19 @@ module "jfrog" {
|
||||
go = ["go-local"]
|
||||
pypi = ["pypi-local"]
|
||||
conda = ["conda-local"]
|
||||
maven = ["maven-local"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip`, `conda` commands.
|
||||
You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip`, `conda`, `maven` commands.
|
||||
|
||||
```shell
|
||||
jf npm install prettier
|
||||
jf go get github.com/golang/example/hello
|
||||
jf pip install requests
|
||||
conda install numpy
|
||||
mvn clean install
|
||||
```
|
||||
|
||||
```shell
|
||||
@@ -68,6 +71,7 @@ npm install prettier
|
||||
go get github.com/golang/example/hello
|
||||
pip install requests
|
||||
conda install numpy
|
||||
mvn clean install
|
||||
```
|
||||
|
||||
### Configure code-server with JFrog extension
|
||||
@@ -77,7 +81,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/coder/jfrog-token/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
artifactory_access_token = var.artifactory_access_token
|
||||
@@ -97,7 +101,7 @@ data "coder_workspace" "me" {}
|
||||
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/coder/jfrog-token/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
artifactory_access_token = var.artifactory_access_token
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -187,4 +185,39 @@ EOF`;
|
||||
'if [ -z "YES" ]; then\n not_configured conda',
|
||||
);
|
||||
});
|
||||
it("generates a maven settings.xml with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
artifactory_access_token: "XXXX",
|
||||
package_managers: JSON.stringify({
|
||||
maven: ["central", "snapshots", "local"],
|
||||
}),
|
||||
});
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
'jf mvnc --global --repo-resolve "central"',
|
||||
);
|
||||
|
||||
expect(coderScript.script).toContain("<servers>");
|
||||
expect(coderScript.script).toContain("<id>central</id>");
|
||||
expect(coderScript.script).toContain("<id>snapshots</id>");
|
||||
expect(coderScript.script).toContain("<id>local</id>");
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
`<url>${fakeFrogUrl}/artifactory/central</url>`,
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
`<url>${fakeFrogUrl}/artifactory/snapshots</url>`,
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
`<url>${fakeFrogUrl}/artifactory/local</url>`,
|
||||
);
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured maven',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,6 +92,7 @@ variable "package_managers" {
|
||||
pypi = optional(list(string), [])
|
||||
docker = optional(list(string), [])
|
||||
conda = optional(list(string), [])
|
||||
maven = optional(list(string), [])
|
||||
})
|
||||
description = <<-EOF
|
||||
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
|
||||
@@ -102,6 +103,7 @@ variable "package_managers" {
|
||||
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
|
||||
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
|
||||
conda = ["YOUR_CONDA_REPO_KEY", "ANOTHER_CONDA_REPO_KEY"]
|
||||
maven = ["YOUR_MAVEN_REPO_KEY", "ANOTHER_MAVEN_REPO_KEY"]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
@@ -136,6 +138,9 @@ locals {
|
||||
conda_conf = templatefile(
|
||||
"${path.module}/conda.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.conda })
|
||||
)
|
||||
maven_settings = templatefile(
|
||||
"${path.module}/settings.xml.tftpl", merge(local.common_values, { REPOS = var.package_managers.maven })
|
||||
)
|
||||
}
|
||||
|
||||
# Configure the Artifactory provider
|
||||
@@ -179,6 +184,9 @@ resource "coder_script" "jfrog" {
|
||||
HAS_CONDA = length(var.package_managers.conda) == 0 ? "" : "YES"
|
||||
CONDA_CONF = local.conda_conf
|
||||
REPOSITORY_CONDA = try(element(var.package_managers.conda, 0), "")
|
||||
HAS_MAVEN = length(var.package_managers.maven) == 0 ? "" : "YES"
|
||||
MAVEN_SETTINGS = local.maven_settings
|
||||
REPOSITORY_MAVEN = try(element(var.package_managers.maven, 0), "")
|
||||
}
|
||||
))
|
||||
run_on_start = true
|
||||
|
||||
@@ -93,6 +93,20 @@ EOF
|
||||
config_complete
|
||||
fi
|
||||
|
||||
# Configure Maven to use the Artifactory "maven" repository.
|
||||
if [ -z "${HAS_MAVEN}" ]; then
|
||||
not_configured maven
|
||||
else
|
||||
echo "☕ Configuring maven..."
|
||||
jf mvnc --global --repo-resolve "${REPOSITORY_MAVEN}"
|
||||
# Create Maven config directory if it doesn't exist
|
||||
mkdir -p ~/.m2
|
||||
cat << EOF > ~/.m2/settings.xml
|
||||
${MAVEN_SETTINGS}
|
||||
EOF
|
||||
config_complete
|
||||
fi
|
||||
|
||||
# Install the JFrog vscode extension for code-server.
|
||||
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
|
||||
while ! [ -x /tmp/code-server/bin/code-server ]; do
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
|
||||
http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
|
||||
<servers>
|
||||
%{ for REPO in REPOS ~}
|
||||
<server>
|
||||
<id>${REPO}</id>
|
||||
<username>${ARTIFACTORY_USERNAME}</username>
|
||||
<password>${ARTIFACTORY_ACCESS_TOKEN}</password>
|
||||
</server>
|
||||
%{ endfor ~}
|
||||
</servers>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>artifactory</id>
|
||||
<repositories>
|
||||
%{ for REPO in REPOS ~}
|
||||
<repository>
|
||||
<id>${REPO}</id>
|
||||
<url>${JFROG_URL}/artifactory/${REPO}</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
%{ endfor ~}
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
%{ for REPO in REPOS ~}
|
||||
<pluginRepository>
|
||||
<id>${REPO}</id>
|
||||
<url>${JFROG_URL}/artifactory/${REPO}</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
%{ endfor ~}
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<activeProfiles>
|
||||
<activeProfile>artifactory</activeProfile>
|
||||
</activeProfiles>
|
||||
|
||||
</settings>
|
||||
@@ -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.
|
||||
|
||||
@@ -40,7 +40,7 @@ variable "use_kubeconfig" {
|
||||
|
||||
variable "namespace" {
|
||||
type = string
|
||||
default = "default"
|
||||
default = "coder"
|
||||
description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace."
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ data "coder_parameter" "cpu" {
|
||||
display_name = "CPU"
|
||||
description = "CPU limit (cores)."
|
||||
default = "2"
|
||||
icon = "/emojis/1f5a5.png"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = true
|
||||
validation {
|
||||
min = 1
|
||||
@@ -161,6 +161,8 @@ locals {
|
||||
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
|
||||
# if the cache repo is enabled.
|
||||
"ENVBUILDER_GIT_URL" : var.cache_repo == "" ? local.repo_url : "",
|
||||
# Used for when SSH is an available authentication mechanism for git providers
|
||||
"ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64" : base64encode(try(data.coder_workspace_owner.me.ssh_private_key, "")),
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
"ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
|
||||
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
|
||||
@@ -263,8 +265,9 @@ resource "kubernetes_deployment" "main" {
|
||||
name = "dev"
|
||||
image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
|
||||
image_pull_policy = "Always"
|
||||
security_context {}
|
||||
|
||||
security_context {
|
||||
privileged = true
|
||||
}
|
||||
# Set the environment using cached_image.cached.0.env if the cache repo is enabled.
|
||||
# Otherwise, use the local.envbuilder_env.
|
||||
# You could alternatively write the environment variables to a ConfigMap or Secret
|
||||
@@ -352,21 +355,22 @@ resource "coder_agent" "main" {
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
# Note: May not work on AWS Linux Nodes See: https://github.com/coder/clistat/issues/17
|
||||
# metadata {
|
||||
# display_name = "CPU Usage"
|
||||
# key = "0_cpu_usage"
|
||||
# script = "coder stat cpu"
|
||||
# interval = 10
|
||||
# timeout = 1
|
||||
# }
|
||||
# Note: May not work on AWS Linux Nodes
|
||||
# metadata {
|
||||
# display_name = "RAM Usage"
|
||||
# key = "1_ram_usage"
|
||||
# script = "coder stat mem"
|
||||
# interval = 10
|
||||
# timeout = 1
|
||||
# }
|
||||
|
||||
metadata {
|
||||
display_name = "Workspaces Disk"
|
||||
|
||||
Reference in New Issue
Block a user