diff --git a/.github/typos.toml b/.github/typos.toml
index fdb74748..7ebdacef 100644
--- a/.github/typos.toml
+++ b/.github/typos.toml
@@ -5,6 +5,8 @@ Hashi = "Hashi"
HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
mavrick = "mavrick" # Username
+inh = "inh" # Option in setpriv command
+exportfs = "exportfs" # nfs related binary
[files]
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
\ No newline at end of file
diff --git a/.icons/copyparty.svg b/.icons/copyparty.svg
new file mode 100644
index 00000000..2c4f0d04
--- /dev/null
+++ b/.icons/copyparty.svg
@@ -0,0 +1,210 @@
+
+
diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md
index 9c749229..98062326 100644
--- a/registry/coder-labs/modules/codex/README.md
+++ b/registry/coder-labs/modules/codex/README.md
@@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
- version = "3.0.0"
+ version = "3.1.0"
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
workdir = "/home/coder/project"
@@ -33,7 +33,7 @@ module "codex" {
module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
- version = "3.0.0"
+ version = "3.1.0"
agent_id = coder_agent.example.id
openai_api_key = "..."
workdir = "/home/coder/project"
@@ -61,7 +61,7 @@ module "coder-login" {
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
- version = "3.0.0"
+ version = "3.1.0"
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_parameter.ai_prompt.value
@@ -84,6 +84,7 @@ module "codex" {
- **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory
- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI
- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
+- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions.
## Configuration
@@ -107,7 +108,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
- version = "3.0.0"
+ version = "3.1.0"
# ... other variables ...
# Override default configuration
diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts
index 7d34a9c4..2041e36e 100644
--- a/registry/coder-labs/modules/codex/main.test.ts
+++ b/registry/coder-labs/modules/codex/main.test.ts
@@ -368,4 +368,90 @@ describe("codex", async () => {
expect(prompt.exitCode).not.toBe(0);
expect(prompt.stderr).toContain("No such file or directory");
});
+
+ test("codex-continue-capture-new-session", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ continue: "true",
+ ai_prompt: "test task",
+ },
+ });
+
+ const workdir = "/home/coder";
+ const expectedSessionId = "019a1234-5678-9abc-def0-123456789012";
+ const sessionsDir = "/home/coder/.codex/sessions";
+ const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`;
+
+ await execContainer(id, ["mkdir", "-p", sessionsDir]);
+ await execContainer(id, [
+ "bash",
+ "-c",
+ `echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`,
+ ]);
+
+ await execModuleScript(id);
+
+ await expectAgentAPIStarted(id);
+
+ const trackingFile = "/home/coder/.codex-module/.codex-task-session";
+ const maxAttempts = 30;
+ let trackingFileContents = "";
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const result = await execContainer(id, [
+ "bash",
+ "-c",
+ `cat ${trackingFile} 2>/dev/null || echo ""`,
+ ]);
+ if (result.stdout.trim().length > 0) {
+ trackingFileContents = result.stdout;
+ break;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ }
+
+ expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`);
+
+ const startLog = await readFileContainer(
+ id,
+ "/home/coder/.codex-module/agentapi-start.log",
+ );
+ expect(startLog).toContain("Capturing new session ID");
+ expect(startLog).toContain("Session tracked");
+ expect(startLog).toContain(expectedSessionId);
+ });
+
+ test("codex-continue-resume-existing-session", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ continue: "true",
+ ai_prompt: "test prompt",
+ },
+ });
+
+ const workdir = "/home/coder";
+ const mockSessionId = "019a1234-5678-9abc-def0-123456789012";
+ const trackingFile = "/home/coder/.codex-module/.codex-task-session";
+
+ await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]);
+ await execContainer(id, [
+ "bash",
+ "-c",
+ `echo "${workdir}|${mockSessionId}" > ${trackingFile}`,
+ ]);
+
+ await execModuleScript(id);
+
+ const startLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.codex-module/agentapi-start.log",
+ ]);
+ expect(startLog.stdout).toContain("Found existing task session");
+ expect(startLog.stdout).toContain(mockSessionId);
+ expect(startLog.stdout).toContain("Resuming existing session");
+ expect(startLog.stdout).toContain(
+ `Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`,
+ );
+ expect(startLog.stdout).not.toContain("test prompt");
+ });
});
diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf
index d181c10f..a68cd79f 100644
--- a/registry/coder-labs/modules/codex/main.tf
+++ b/registry/coder-labs/modules/codex/main.tf
@@ -137,6 +137,12 @@ variable "ai_prompt" {
default = ""
}
+variable "continue" {
+ type = bool
+ description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
+ default = true
+}
+
variable "codex_system_prompt" {
type = string
description = "System instructions written to AGENTS.md in the ~/.codex directory"
@@ -187,8 +193,9 @@ module "agentapi" {
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_CODEX_MODEL='${var.codex_model}' \
- ARG_CODEX_START_DIRECTORY='${var.workdir}' \
+ ARG_CODEX_START_DIRECTORY='${local.workdir}' \
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
+ ARG_CONTINUE='${var.continue}' \
/tmp/start.sh
EOT
@@ -206,7 +213,7 @@ module "agentapi" {
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
- ARG_CODEX_START_DIRECTORY='${var.workdir}' \
+ ARG_CODEX_START_DIRECTORY='${local.workdir}' \
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
/tmp/install.sh
EOT
diff --git a/registry/coder-labs/modules/codex/scripts/start.sh b/registry/coder-labs/modules/codex/scripts/start.sh
index be54d575..663e80e5 100644
--- a/registry/coder-labs/modules/codex/scripts/start.sh
+++ b/registry/coder-labs/modules/codex/scripts/start.sh
@@ -3,6 +3,7 @@
source "$HOME"/.bashrc
set -o errexit
set -o pipefail
+
command_exists() {
command -v "$1" > /dev/null 2>&1
}
@@ -16,6 +17,7 @@ fi
printf "Version: %s\n" "$(codex --version)"
set -o nounset
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
+ARG_CONTINUE=${ARG_CONTINUE:-true}
echo "=== Codex Launch Configuration ==="
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
@@ -23,53 +25,187 @@ printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
+printf "Continue Sessions: %s\n" "$ARG_CONTINUE"
echo "======================================"
set +o nounset
-CODEX_ARGS=()
-if command_exists codex; then
- printf "Codex is installed\n"
-else
- printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
- exit 1
-fi
+SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session"
-if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
- printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
- cd "${ARG_CODEX_START_DIRECTORY}" || {
- printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
- exit 1
- }
-else
- printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
- mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
- printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
- exit 1
- }
- cd "${ARG_CODEX_START_DIRECTORY}" || {
- printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
- exit 1
- }
-fi
+find_session_for_directory() {
+ local target_dir="$1"
-if [ -n "$ARG_CODEX_MODEL" ]; then
- CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
-fi
-
-if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
- printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT"
- if [ "${ARG_REPORT_TASKS}" == "true" ]; then
- PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
- else
- PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
+ if [ ! -f "$SESSION_TRACKING_FILE" ]; then
+ return 1
fi
- CODEX_ARGS+=("$PROMPT")
-else
- printf "No task prompt given.\n"
-fi
-# Terminal dimensions optimized for Coder Tasks UI sidebar:
-# - Width 67: fits comfortably in sidebar
-# - Height 1190: adjusted due to Codex terminal height bug
-printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
-agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}"
+ local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
+
+ if [ -n "$session_id" ]; then
+ echo "$session_id"
+ return 0
+ fi
+
+ return 1
+}
+
+store_session_mapping() {
+ local dir="$1"
+ local session_id="$2"
+
+ mkdir -p "$(dirname "$SESSION_TRACKING_FILE")"
+
+ if [ -f "$SESSION_TRACKING_FILE" ]; then
+ grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true
+ mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE"
+ fi
+
+ echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE"
+}
+
+find_recent_session_file() {
+ local target_dir="$1"
+ local sessions_dir="$HOME/.codex/sessions"
+
+ if [ ! -d "$sessions_dir" ]; then
+ return 1
+ fi
+
+ local latest_file=""
+ local latest_time=0
+
+ while IFS= read -r session_file; do
+ local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
+ local first_line=$(head -n 1 "$session_file" 2> /dev/null)
+ local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
+
+ if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then
+ latest_file="$session_file"
+ latest_time="$file_time"
+ fi
+ done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null)
+
+ if [ -n "$latest_file" ]; then
+ local first_line=$(head -n 1 "$latest_file")
+ local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
+ if [ -n "$session_id" ]; then
+ echo "$session_id"
+ return 0
+ fi
+ fi
+
+ return 1
+}
+
+wait_for_session_file() {
+ local target_dir="$1"
+ local max_attempts=20
+ local attempt=0
+
+ while [ $attempt -lt $max_attempts ]; do
+ local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
+ if [ -n "$session_id" ]; then
+ echo "$session_id"
+ return 0
+ fi
+ sleep 0.5
+ attempt=$((attempt + 1))
+ done
+
+ return 1
+}
+
+validate_codex_installation() {
+ if command_exists codex; then
+ printf "Codex is installed\n"
+ else
+ printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
+ exit 1
+ fi
+}
+
+setup_workdir() {
+ if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
+ printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ cd "${ARG_CODEX_START_DIRECTORY}" || {
+ printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ exit 1
+ }
+ else
+ printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
+ printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ exit 1
+ }
+ cd "${ARG_CODEX_START_DIRECTORY}" || {
+ printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ exit 1
+ }
+ fi
+}
+
+build_codex_args() {
+ CODEX_ARGS=()
+
+ if [ -n "$ARG_CODEX_MODEL" ]; then
+ CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
+ fi
+
+ if [ "$ARG_CONTINUE" = "true" ]; then
+ existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "")
+
+ if [ -n "$existing_session" ]; then
+ printf "Found existing task session for this directory: %s\n" "$existing_session"
+ printf "Resuming existing session...\n"
+ CODEX_ARGS+=("resume" "$existing_session")
+ else
+ printf "No existing task session found for this directory\n"
+ printf "Starting new task session...\n"
+
+ if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
+ if [ "${ARG_REPORT_TASKS}" == "true" ]; then
+ PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
+ else
+ PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
+ fi
+ CODEX_ARGS+=("$PROMPT")
+ fi
+ fi
+ else
+ printf "Continue disabled, starting fresh session\n"
+
+ if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
+ if [ "${ARG_REPORT_TASKS}" == "true" ]; then
+ PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
+ else
+ PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
+ fi
+ CODEX_ARGS+=("$PROMPT")
+ fi
+ fi
+}
+
+capture_session_id() {
+ if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then
+ printf "Capturing new session ID...\n"
+ new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "")
+
+ if [ -n "$new_session" ]; then
+ store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session"
+ printf "✓ Session tracked: %s\n" "$new_session"
+ printf "This session will be automatically resumed on next restart\n"
+ else
+ printf "⚠ Could not capture session ID after 10s timeout\n"
+ fi
+ fi
+}
+
+start_codex() {
+ printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
+ agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" &
+ capture_session_id
+}
+
+validate_codex_installation
+setup_workdir
+build_codex_args
+start_codex
diff --git a/registry/coder-labs/modules/codex/testdata/codex-mock.sh b/registry/coder-labs/modules/codex/testdata/codex-mock.sh
index 8c1c7366..fe8f3806 100644
--- a/registry/coder-labs/modules/codex/testdata/codex-mock.sh
+++ b/registry/coder-labs/modules/codex/testdata/codex-mock.sh
@@ -1,5 +1,6 @@
#!/bin/bash
+# Handle --version flag
if [[ "$1" == "--version" ]]; then
echo "HELLO: $(bash -c env)"
echo "codex version v1.0.0"
@@ -8,7 +9,30 @@ fi
set -e
+SESSION_ID=""
+IS_RESUME=false
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ resume)
+ IS_RESUME=true
+ SESSION_ID="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [ "$IS_RESUME" = false ]; then
+ SESSION_ID="019a1234-5678-9abc-def0-123456789012"
+ echo "Created new session: $SESSION_ID"
+else
+ echo "Resuming session: $SESSION_ID"
+fi
+
while true; do
- echo "$(date) - codex-mock"
+ echo "$(date) - codex-mock (session: $SESSION_ID)"
sleep 15
done
diff --git a/registry/coder-labs/modules/copilot/README.md b/registry/coder-labs/modules/copilot/README.md
index 83f59c7c..e0b520e0 100644
--- a/registry/coder-labs/modules/copilot/README.md
+++ b/registry/coder-labs/modules/copilot/README.md
@@ -13,7 +13,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
- version = "0.2.1"
+ version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
}
@@ -51,7 +51,7 @@ data "coder_parameter" "ai_prompt" {
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
- version = "0.2.1"
+ version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
@@ -71,12 +71,12 @@ Customize tool permissions, MCP servers, and Copilot settings:
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
- version = "0.2.1"
+ version = "0.2.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"
+ # Version pinning (defaults to "latest", use specific version if desired)
+ copilot_version = "0.0.334"
# Tool permissions
allow_tools = ["shell(git)", "shell(npm)", "write"]
@@ -142,7 +142,7 @@ variable "github_token" {
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
- version = "0.2.1"
+ version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
github_token = var.github_token
@@ -156,7 +156,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
- version = "0.2.1"
+ version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder"
report_tasks = false
diff --git a/registry/coder-labs/modules/copilot/main.tf b/registry/coder-labs/modules/copilot/main.tf
index eb9f78d4..41a83d53 100644
--- a/registry/coder-labs/modules/copilot/main.tf
+++ b/registry/coder-labs/modules/copilot/main.tf
@@ -104,7 +104,7 @@ variable "agentapi_version" {
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"
+ default = "latest"
}
variable "report_tasks" {
diff --git a/registry/coder-labs/modules/sourcegraph-amp/README.md b/registry/coder-labs/modules/sourcegraph-amp/README.md
index 5a5039f0..608defd6 100644
--- a/registry/coder-labs/modules/sourcegraph-amp/README.md
+++ b/registry/coder-labs/modules/sourcegraph-amp/README.md
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
```tf
module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
- version = "2.0.0"
+ version = "2.0.1"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
@@ -48,7 +48,7 @@ variable "amp_api_key" {
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
- amp_version = "2.0.0"
+ amp_version = "2.0.1"
agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key # recommended for tasks usage
workdir = "/home/coder/project"
diff --git a/registry/coder-labs/modules/sourcegraph-amp/main.tf b/registry/coder-labs/modules/sourcegraph-amp/main.tf
index ddaa475d..fc36ea8d 100644
--- a/registry/coder-labs/modules/sourcegraph-amp/main.tf
+++ b/registry/coder-labs/modules/sourcegraph-amp/main.tf
@@ -6,7 +6,12 @@ terraform {
source = "coder/coder"
version = ">= 2.7"
}
+ external = {
+ source = "hashicorp/external"
+ version = "2.3.5"
+ }
}
+
}
variable "agent_id" {
diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md
index d68af511..954db1ce 100644
--- a/registry/coder/modules/agentapi/README.md
+++ b/registry/coder/modules/agentapi/README.md
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
- version = "1.2.0"
+ version = "2.0.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
diff --git a/registry/coder/modules/agentapi/main.tf b/registry/coder/modules/agentapi/main.tf
index e73f45f6..5c3ab9c4 100644
--- a/registry/coder/modules/agentapi/main.tf
+++ b/registry/coder/modules/agentapi/main.tf
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
- version = ">= 2.7"
+ version = ">= 2.12"
}
}
}
@@ -239,8 +239,6 @@ resource "coder_app" "agentapi_cli" {
group = var.cli_app_group
}
-resource "coder_ai_task" "agentapi" {
- sidebar_app {
- id = coder_app.agentapi_web.id
- }
+output "task_app_id" {
+ value = coder_app.agentapi_web.id
}
diff --git a/registry/coder/modules/aider/README.md b/registry/coder/modules/aider/README.md
index c93fb89c..3ce9c88e 100644
--- a/registry/coder/modules/aider/README.md
+++ b/registry/coder/modules/aider/README.md
@@ -8,76 +8,58 @@ tags: [agent, ai, aider]
# Aider
-Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux.
+Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider with AgentAPI for seamless Coder Tasks Support.
```tf
-module "aider" {
- source = "registry.coder.com/coder/aider/coder"
- version = "1.1.2"
- agent_id = coder_agent.example.id
-}
-```
-
-## Features
-
-- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace
-- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter
-- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background
-- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding
-- **Project Integration**: Works with any project directory, including Git repositories
-- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal
-- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable
-
-## Module Parameters
-
-> [!NOTE]
-> The `use_screen` and `use_tmux` parameters cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`.
-
-## Usage Examples
-
-### Basic setup with API key
-
-```tf
-variable "anthropic_api_key" {
+variable "api_key" {
type = string
- description = "Anthropic API key"
+ description = "API key"
sensitive = true
}
module "aider" {
- count = data.coder_workspace.me.start_count
- source = "registry.coder.com/coder/aider/coder"
- version = "1.1.2"
- agent_id = coder_agent.example.id
- ai_api_key = var.anthropic_api_key
-}
-```
-
-This basic setup will:
-
-- Install Aider in the workspace
-- Create a persistent screen session named "aider"
-- Configure Aider to use Anthropic Claude 3.7 Sonnet model
-- Enable task reporting (configures Aider to report tasks to Coder MCP)
-
-### Using OpenAI with tmux
-
-```tf
-variable "openai_api_key" {
- type = string
- description = "OpenAI API key"
- sensitive = true
-}
-
-module "aider" {
- count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
- version = "1.1.2"
+ version = "2.0.0"
agent_id = coder_agent.example.id
- use_tmux = true
- ai_provider = "openai"
- ai_model = "4o" # Uses Aider's built-in alias for gpt-4o
- ai_api_key = var.openai_api_key
+ api_key = var.api_key
+ ai_provider = "google"
+ model = "gemini"
+}
+```
+
+## Prerequisites
+
+- pipx is automatically installed if not already available
+
+## Usage Example
+
+```tf
+data "coder_parameter" "ai_prompt" {
+ name = "AI Prompt"
+ description = "Write an initial prompt for Aider to work on."
+ type = "string"
+ default = ""
+ mutable = true
+}
+
+variable "gemini_api_key" {
+ type = string
+ description = "Gemini API key"
+ sensitive = true
+}
+
+module "aider" {
+ source = "registry.coder.com/coder/aider/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ api_key = var.gemini_api_key
+ install_aider = true
+ workdir = "/home/coder"
+ ai_provider = "google"
+ model = "gemini"
+ install_agentapi = true
+ ai_prompt = data.coder_parameter.ai_prompt.value
+ system_prompt = "..."
}
```
@@ -93,174 +75,16 @@ variable "custom_api_key" {
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
- version = "1.1.2"
+ version = "2.0.0"
agent_id = coder_agent.example.id
+ workdir = "/home/coder"
ai_provider = "custom"
custom_env_var_name = "MY_CUSTOM_API_KEY"
- ai_model = "custom-model"
- ai_api_key = var.custom_api_key
+ model = "custom-model"
+ api_key = var.custom_api_key
}
```
-### Adding Custom Extensions (Experimental)
-
-You can extend Aider's capabilities by adding custom extensions:
-
-```tf
-module "aider" {
- count = data.coder_workspace.me.start_count
- source = "registry.coder.com/coder/aider/coder"
- version = "1.1.2"
- agent_id = coder_agent.example.id
- ai_api_key = var.anthropic_api_key
-
- experiment_pre_install_script = <<-EOT
- pip install some-custom-dependency
- EOT
-
- experiment_additional_extensions = <<-EOT
- custom-extension:
- args: []
- cmd: custom-extension-command
- description: A custom extension for Aider
- enabled: true
- envs: {}
- name: custom-extension
- timeout: 300
- type: stdio
- EOT
-}
-```
-
-Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
-
-## Task Reporting (Experimental)
-
-> 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.
-
-Your workspace must have either `screen` or `tmux` installed to use this.
-
-Task reporting is **enabled by default** in this module, allowing you to:
-
-- Send an initial prompt to Aider during workspace creation
-- Monitor task progress in the Coder UI
-- Use the `coder_parameter` resource to collect prompts from users
-
-### Setting up Task Reporting
-
-To use task reporting effectively:
-
-1. Add the Coder Login module to your template
-2. Configure the necessary variables to pass the task prompt
-3. Optionally add a coder_parameter to collect prompts from users
-
-Here's a complete example:
-
-```tf
-module "coder-login" {
- count = data.coder_workspace.me.start_count
- source = "registry.coder.com/modules/coder-login/coder"
- version = "1.0.15"
- agent_id = coder_agent.example.id
-}
-
-variable "anthropic_api_key" {
- type = string
- description = "Anthropic API key"
- sensitive = true
-}
-
-data "coder_parameter" "ai_prompt" {
- type = "string"
- name = "AI Prompt"
- default = ""
- description = "Write a prompt for Aider"
- mutable = true
- ephemeral = true
-}
-
-module "aider" {
- count = data.coder_workspace.me.start_count
- source = "registry.coder.com/coder/aider/coder"
- version = "1.1.2"
- agent_id = coder_agent.example.id
- ai_api_key = var.anthropic_api_key
- task_prompt = data.coder_parameter.ai_prompt.value
-
- # Optionally customize the system prompt
- system_prompt = <<-EOT
-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 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
-}
-```
-
-When a task prompt is provided via the `task_prompt` variable, the module automatically:
-
-1. Combines the system prompt with the task prompt into a single message in the format:
-
-```
-SYSTEM PROMPT:
-[system_prompt content]
-
-This is your current task: [task_prompt]
-```
-
-2. Executes the task during workspace creation using the `--message` and `--yes-always` flags
-3. Logs task output to `$HOME/.aider.log` for reference
-
-If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration.
-
-## Using Aider in Your Workspace
-
-After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation.
-
-### Session Options
-
-You can run Aider in three different ways:
-
-1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button
-
-- Simple setup without persistent context
-- Suitable for quick coding sessions
-
-2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections
-
-- Session name: "aider" (or configured via `session_name`)
-
-3. **Tmux Mode**: Run Aider in a tmux session instead of screen
-
-- Set `use_tmux = true` to enable
-- Session name: "aider" (or configured via `session_name`)
-- Configures tmux with mouse support for shared sessions
-
-Persistent sessions (screen/tmux) allow you to:
-
-- Disconnect and reconnect without losing context
-- Run Aider in the background while doing other work
-- Switch between terminal and browser interfaces
-
### Available AI Providers and Models
Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases:
@@ -280,10 +104,12 @@ For a complete and up-to-date list of supported aliases and models, please refer
## Troubleshooting
-If you encounter issues:
+- If `aider` is not found, ensure `install_aider = true` and your API key is valid
+- Logs are written under `/home/coder/.aider-module/` (`install.log`, `agentapi-start.log`) for debugging
+- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
-1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions`
-2. **API key issues**: Ensure you've entered the correct API key for your selected provider
-3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace
+## References
-For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/).
+- [Aider Documentation](https://aider.chat/docs)
+- [AgentAPI Documentation](https://github.com/coder/agentapi)
+- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
diff --git a/registry/coder/modules/aider/main.test.ts b/registry/coder/modules/aider/main.test.ts
index c25513a5..c0aa51df 100644
--- a/registry/coder/modules/aider/main.test.ts
+++ b/registry/coder/modules/aider/main.test.ts
@@ -1,107 +1,138 @@
-import { describe, expect, it } from "bun:test";
import {
- findResourceInstance,
- runTerraformApply,
- runTerraformInit,
- testRequiredVariables,
-} from "~test";
+ test,
+ afterEach,
+ describe,
+ setDefaultTimeout,
+ beforeAll,
+ expect,
+} from "bun:test";
+import { execContainer, readFileContainer, runTerraformInit } from "~test";
+import {
+ loadTestFile,
+ writeExecutable,
+ setup as setupUtil,
+ execModuleScript,
+ expectAgentAPIStarted,
+} from "../../../coder/modules/agentapi/test-util";
-describe("aider", async () => {
- await runTerraformInit(import.meta.dir);
+let cleanupFunctions: (() => Promise)[] = [];
+const registerCleanup = (cleanup: () => Promise) => {
+ cleanupFunctions.push(cleanup);
+};
+afterEach(async () => {
+ const cleanupFnsCopy = cleanupFunctions.slice().reverse();
+ cleanupFunctions = [];
+ for (const cleanup of cleanupFnsCopy) {
+ try {
+ await cleanup();
+ } catch (error) {
+ console.error("Error during cleanup:", error);
+ }
+ }
+});
- testRequiredVariables(import.meta.dir, {
- agent_id: "foo",
+interface SetupProps {
+ skipAgentAPIMock?: boolean;
+ skipAiderMock?: boolean;
+ moduleVariables?: Record;
+ agentapiMockScript?: string;
+}
+
+const setup = async (props?: SetupProps): Promise<{ id: string }> => {
+ const projectDir = "/home/coder/project";
+ const { id } = await setupUtil({
+ moduleDir: import.meta.dir,
+ moduleVariables: {
+ install_aider: props?.skipAiderMock ? "true" : "false",
+ install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
+ aider_model: "test-model",
+ ...props?.moduleVariables,
+ },
+ registerCleanup,
+ projectDir,
+ skipAgentAPIMock: props?.skipAgentAPIMock,
+ agentapiMockScript: props?.agentapiMockScript,
});
- it("configures task prompt correctly", async () => {
- const testPrompt = "Add a hello world function";
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- task_prompt: testPrompt,
+ // Place the Aider mock CLI binary inside the container
+ if (!props?.skipAiderMock) {
+ await writeExecutable({
+ containerId: id,
+ filePath: "/usr/bin/aider",
+ content: await loadTestFile(`${import.meta.dir}`, "aider-mock.sh"),
});
+ }
- const instance = findResourceInstance(state, "coder_script");
- expect(instance.script).toContain(
- `This is your current task: ${testPrompt}`,
- );
- expect(instance.script).toContain("aider --architect --yes-always");
+ return { id };
+};
+
+setDefaultTimeout(60 * 1000);
+
+describe("Aider", async () => {
+ beforeAll(async () => {
+ await runTerraformInit(import.meta.dir);
});
- it("handles custom system prompt", async () => {
- const customPrompt = "Report all tasks with state: working";
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- system_prompt: customPrompt,
+ test("happy-path", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ model: "gemini",
+ },
});
-
- const instance = findResourceInstance(state, "coder_script");
- expect(instance.script).toContain(customPrompt);
+ await execModuleScript(id);
+ await expectAgentAPIStarted(id);
});
- it("handles pre and post install scripts", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- experiment_pre_install_script: "echo 'Pre-install script executed'",
- experiment_post_install_script: "echo 'Post-install script executed'",
+ test("api-key", async () => {
+ const apiKey = "test-api-key-123";
+ const { id } = await setup({
+ moduleVariables: {
+ api_key: apiKey,
+ model: "gemini",
+ },
});
-
- const instance = findResourceInstance(state, "coder_script");
-
- expect(instance.script).toContain("Running pre-install script");
- expect(instance.script).toContain("Running post-install script");
- expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
- expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
+ await execModuleScript(id);
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.aider-module/agentapi-start.log",
+ );
+ expect(resp).toContain("API key provided!");
});
- it("validates that use_screen and use_tmux cannot both be true", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- use_screen: true,
- use_tmux: true,
+ test("custom-folder", async () => {
+ const workdir = "/tmp/aider-test";
+ const { id } = await setup({
+ moduleVariables: {
+ workdir,
+ model: "gemini",
+ },
});
-
- const instance = findResourceInstance(state, "coder_script");
-
- expect(instance.script).toContain(
- "Error: Both use_screen and use_tmux cannot be enabled at the same time",
+ await execModuleScript(id);
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.aider-module/install.log",
);
- expect(instance.script).toContain("exit 1");
+ expect(resp).toContain(workdir);
});
- it("configures Aider with known provider and model", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- ai_provider: "anthropic",
- ai_model: "sonnet",
- ai_api_key: "test-anthropic-key",
+ test("pre-post-install-scripts", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
+ post_install_script: "#!/bin/bash\necho 'post-install-script'",
+ model: "gemini",
+ },
});
-
- const instance = findResourceInstance(state, "coder_script");
- expect(instance.script).toContain(
- 'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
+ await execModuleScript(id);
+ const preLog = await readFileContainer(
+ id,
+ "/home/coder/.aider-module/pre_install.log",
);
- expect(instance.script).toContain("--model sonnet");
- expect(instance.script).toContain(
- "Starting Aider using anthropic provider and model: sonnet",
- );
- });
-
- it("handles custom provider with custom env var and API key", async () => {
- const state = await runTerraformApply(import.meta.dir, {
- agent_id: "foo",
- ai_provider: "custom",
- custom_env_var_name: "MY_CUSTOM_API_KEY",
- ai_model: "custom-model",
- ai_api_key: "test-custom-key",
- });
-
- const instance = findResourceInstance(state, "coder_script");
- expect(instance.script).toContain(
- 'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
- );
- expect(instance.script).toContain("--model custom-model");
- expect(instance.script).toContain(
- "Starting Aider using custom provider and model: custom-model",
+ expect(preLog).toContain("pre-install-script");
+ const postLog = await readFileContainer(
+ id,
+ "/home/coder/.aider-module/post_install.log",
);
+ expect(postLog).toContain("post-install-script");
});
});
diff --git a/registry/coder/modules/aider/main.tf b/registry/coder/modules/aider/main.tf
index e1f2eccd..70274cb8 100644
--- a/registry/coder/modules/aider/main.tf
+++ b/registry/coder/modules/aider/main.tf
@@ -36,87 +36,84 @@ variable "icon" {
default = "/icon/aider.svg"
}
-variable "folder" {
+variable "workdir" {
type = string
description = "The folder to run Aider in."
default = "/home/coder"
}
+variable "report_tasks" {
+ type = bool
+ description = "Whether to enable task reporting to Coder UI via AgentAPI"
+ default = false
+}
+
+variable "subdomain" {
+ type = bool
+ description = "Whether to use a subdomain for AgentAPI."
+ default = false
+}
+
+variable "cli_app" {
+ type = bool
+ description = "Whether to create a CLI app for Aider"
+ default = false
+}
+
+variable "web_app_display_name" {
+ type = string
+ description = "Display name for the web app"
+ default = "Aider"
+}
+
+variable "cli_app_display_name" {
+ type = string
+ description = "Display name for the CLI app"
+ default = "Aider CLI"
+}
+
+variable "pre_install_script" {
+ type = string
+ description = "Custom script to run before installing Aider."
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Custom script to run after installing Aider."
+ default = null
+}
+
+variable "install_agentapi" {
+ type = bool
+ description = "Whether to install AgentAPI."
+ default = true
+}
+
+variable "agentapi_version" {
+ type = string
+ description = "The version of AgentAPI to install."
+ default = "v0.10.0"
+}
+
+variable "ai_prompt" {
+ type = string
+ description = "Initial task prompt for Aider."
+ default = ""
+}
+
+# ---------------------------------------------
+
variable "install_aider" {
type = bool
description = "Whether to install Aider."
default = true
}
-variable "aider_version" {
- type = string
- description = "The version of Aider to install."
- default = "latest"
-}
-
-variable "use_screen" {
- type = bool
- description = "Whether to use screen for running Aider in the background"
- default = true
-}
-
-variable "use_tmux" {
- type = bool
- description = "Whether to use tmux instead of screen for running Aider in the background"
- default = false
-}
-
-variable "session_name" {
- type = string
- description = "Name for the persistent session (screen or tmux)"
- default = "aider"
-}
-
-variable "experiment_report_tasks" {
- type = bool
- description = "Whether to enable task reporting."
- default = true
-}
-
variable "system_prompt" {
type = string
description = "System prompt for instructing Aider on task reporting and behavior"
- default = <<-EOT
-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 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 "task_prompt" {
- type = string
- description = "Task prompt to use with Aider"
- default = ""
-}
-
-variable "experiment_pre_install_script" {
- type = string
- description = "Custom script to run before installing Aider."
- default = null
-}
-
-variable "experiment_post_install_script" {
- type = string
- description = "Custom script to run after installing Aider."
- default = null
+ 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 "experiment_additional_extensions" {
@@ -128,20 +125,19 @@ variable "experiment_additional_extensions" {
variable "ai_provider" {
type = string
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
- default = "anthropic"
+ default = "google"
validation {
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
- error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
+ error_message = "provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
}
}
-variable "ai_model" {
+variable "model" {
type = string
description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc."
- default = "sonnet"
}
-variable "ai_api_key" {
+variable "api_key" {
type = string
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
default = ""
@@ -154,55 +150,66 @@ variable "custom_env_var_name" {
default = ""
}
+variable "base_aider_config" {
+ type = string
+ description = <<-EOT
+ Base Aider configuration in yaml format. Will be stored in .aider.conf.yml file.
+
+ options include:
+ read:
+ - CONVENTIONS.md
+ - anotherfile.txt
+ - thirdfile.py
+ model: xxx
+ ##Specify the OpenAI API key
+ openai-api-key: xxx
+ ## (deprecated, use --set-env OPENAI_API_TYPE=)
+ openai-api-type: xxx
+ ## (deprecated, use --set-env OPENAI_API_VERSION=)
+ openai-api-version: xxx
+ ## (deprecated, use --set-env OPENAI_API_DEPLOYMENT_ID=)
+ openai-api-deployment-id: xxx
+ ## Set an environment variable (to control API settings, can be used multiple times)
+ set-env: xxx
+ ## Specify multiple values like this:
+ set-env:
+ - xxx
+ - yyy
+ - zzz
+
+ Reference : https://aider.chat/docs/config/aider_conf.html
+ EOT
+ default = null
+}
+
+
locals {
- base_extensions = <<-EOT
-coder:
- args:
- - exp
- - mcp
- - server
- cmd: coder
- description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
- enabled: true
- envs:
- CODER_MCP_APP_STATUS_SLUG: aider
- name: Coder
- timeout: 3000
- type: stdio
-developer:
- display_name: Developer
- enabled: true
- name: developer
- timeout: 300
- type: builtin
-EOT
+ app_slug = "aider"
+ base_aider_config = var.base_aider_config != null ? "${replace(trimspace(var.base_aider_config), "\n", "\n ")}" : ""
+ task_reporting_prompt = <<-EOT
- formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
- additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
+-- 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
- combined_extensions = <<-EOT
-extensions:
-${local.formatted_base}${local.additional_extensions}
-EOT
- 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) : ""
-
- # Combine system prompt and task prompt for aider
- combined_prompt = trimspace(<<-EOT
-SYSTEM PROMPT:
-${var.system_prompt}
-
-This is your current task: ${var.task_prompt}
-EOT
- )
+ final_system_prompt = var.report_tasks ? "\n${var.system_prompt}${local.task_reporting_prompt}\n" : "\n${var.system_prompt}\n"
# Map providers to their environment variable names
provider_env_vars = {
openai = "OPENAI_API_KEY"
anthropic = "ANTHROPIC_API_KEY"
azure = "AZURE_OPENAI_API_KEY"
- google = "GOOGLE_API_KEY"
+ google = "GEMINI_API_KEY"
cohere = "COHERE_API_KEY"
mistral = "MISTRAL_API_KEY"
ollama = "OLLAMA_HOST"
@@ -214,296 +221,60 @@ EOT
# Model flag for aider command
model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model"
+
+ install_script = file("${path.module}/scripts/install.sh")
+ start_script = file("${path.module}/scripts/start.sh")
+ module_dir_name = ".aider-module"
}
-# Install and Initialize Aider
-resource "coder_script" "aider" {
- agent_id = var.agent_id
- display_name = "Aider"
- icon = var.icon
- script = <<-EOT
+module "agentapi" {
+ source = "registry.coder.com/coder/agentapi/coder"
+ version = "1.2.0"
+
+ 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
+ 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 -e
+ set -o errexit
+ set -o pipefail
- command_exists() {
- command -v "$1" >/dev/null 2>&1
- }
-
- echo "Setting up Aider AI pair programming..."
-
- if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
- echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
- exit 1
- fi
-
- mkdir -p "${var.folder}"
-
- if [ "$(uname)" = "Linux" ]; then
- echo "Checking dependencies for Linux..."
-
- if [ "${var.use_tmux}" = "true" ]; then
- if ! command_exists tmux; then
- echo "Installing tmux for persistent sessions..."
- if command -v apt-get >/dev/null 2>&1; then
- if command -v sudo >/dev/null 2>&1; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq tmux
- else
- apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
- apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges"
- fi
- elif command -v dnf >/dev/null 2>&1; then
- if command -v sudo >/dev/null 2>&1; then
- sudo dnf install -y -q tmux
- else
- dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges"
- fi
- else
- echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found."
- fi
- else
- echo "tmux is already installed, skipping installation."
- fi
- elif [ "${var.use_screen}" = "true" ]; then
- if ! command_exists screen; then
- echo "Installing screen for persistent sessions..."
- if command -v apt-get >/dev/null 2>&1; then
- if command -v sudo >/dev/null 2>&1; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq screen
- else
- apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
- apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges"
- fi
- elif command -v dnf >/dev/null 2>&1; then
- if command -v sudo >/dev/null 2>&1; then
- sudo dnf install -y -q screen
- else
- dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges"
- fi
- else
- echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found."
- fi
- else
- echo "screen is already installed, skipping installation."
- fi
- fi
- else
- echo "This module currently only supports Linux workspaces."
- exit 1
- 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_aider}" = "true" ]; then
- echo "Installing Aider..."
-
- if ! command_exists python3 || ! command_exists pip3; then
- echo "Installing Python dependencies required for Aider..."
- if command -v apt-get >/dev/null 2>&1; then
- if command -v sudo >/dev/null 2>&1; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq python3-pip python3-venv
- else
- apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
- apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
- fi
- elif command -v dnf >/dev/null 2>&1; then
- if command -v sudo >/dev/null 2>&1; then
- sudo dnf install -y -q python3-pip python3-virtualenv
- else
- dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
- fi
- else
- echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
- fi
- else
- echo "Python is already installed, skipping installation."
- fi
-
- if ! command_exists aider; then
- curl -LsSf https://aider.chat/install.sh | sh
- fi
-
- if [ -f "$HOME/.bashrc" ]; then
- if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
- echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
- fi
- fi
-
- if [ -f "$HOME/.zshrc" ]; then
- if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
- echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
- fi
- fi
-
- 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_report_tasks}" = "true" ]; then
- echo "Configuring Aider to report tasks via Coder MCP..."
-
- mkdir -p "$HOME/.config/aider"
-
- cat > "$HOME/.config/aider/config.yml" << EOL
-${trimspace(local.combined_extensions)}
-EOL
- echo "Added Coder MCP extension to Aider config.yml"
- fi
-
- echo "Starting persistent Aider session..."
-
- touch "$HOME/.aider.log"
-
- export LANG=en_US.UTF-8
- export LC_ALL=en_US.UTF-8
-
- export PATH="$HOME/bin:$PATH"
-
- if [ "${var.use_tmux}" = "true" ]; then
- if [ -n "${var.task_prompt}" ]; then
- echo "Running Aider with message in tmux session..."
-
- # Configure tmux for shared sessions
- if [ ! -f "$HOME/.tmux.conf" ]; then
- echo "Creating ~/.tmux.conf with shared session settings..."
- echo "set -g mouse on" > "$HOME/.tmux.conf"
- fi
-
- if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
- echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
- echo "set -g mouse on" >> "$HOME/.tmux.conf"
- fi
-
- echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
- tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
- echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
- else
- # Configure tmux for shared sessions
- if [ ! -f "$HOME/.tmux.conf" ]; then
- echo "Creating ~/.tmux.conf with shared session settings..."
- echo "set -g mouse on" > "$HOME/.tmux.conf"
- fi
-
- if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
- echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
- echo "set -g mouse on" >> "$HOME/.tmux.conf"
- fi
-
- echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
- tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
- echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
- fi
- else
- if [ -n "${var.task_prompt}" ]; then
- echo "Running Aider with message in screen session..."
-
- if [ ! -f "$HOME/.screenrc" ]; then
- echo "Creating ~/.screenrc and adding multiuser settings..."
- echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
- fi
-
- if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
- echo "Adding 'multiuser on' to ~/.screenrc..."
- echo "multiuser on" >> "$HOME/.screenrc"
- fi
-
- if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
- echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
- echo "acladd $(whoami)" >> "$HOME/.screenrc"
- fi
-
- echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
- screen -U -dmS ${var.session_name} bash -c "
- cd ${var.folder}
- export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
- export ${local.env_var_name}=\"${var.ai_api_key}\"
- aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
- /bin/bash
- "
-
- echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
- else
-
- if [ ! -f "$HOME/.screenrc" ]; then
- echo "Creating ~/.screenrc and adding multiuser settings..."
- echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
- fi
-
- if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
- echo "Adding 'multiuser on' to ~/.screenrc..."
- echo "multiuser on" >> "$HOME/.screenrc"
- fi
-
- if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
- echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
- echo "acladd $(whoami)" >> "$HOME/.screenrc"
- fi
-
- echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
- screen -U -dmS ${var.session_name} bash -c "
- cd ${var.folder}
- export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
- export ${local.env_var_name}=\"${var.ai_api_key}\"
- aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
- /bin/bash
- "
- echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
- fi
- fi
-
- echo "Aider setup complete!"
+ echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
+ chmod +x /tmp/start.sh
+ ARG_WORKDIR='${var.workdir}' \
+ ARG_API_KEY='${base64encode(var.api_key)}' \
+ ARG_MODEL='${var.model}' \
+ ARG_PROVIDER='${var.ai_provider}' \
+ ARG_ENV_API_NAME_HOLDER='${local.env_var_name}' \
+ ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
+ ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
+ /tmp/start.sh
EOT
- run_on_start = true
-}
-# Aider CLI app
-resource "coder_app" "aider_cli" {
- agent_id = var.agent_id
- slug = "aider"
- display_name = "Aider"
- icon = var.icon
- command = <<-EOT
+ install_script = <<-EOT
#!/bin/bash
- set -e
+ set -o errexit
+ set -o pipefail
- export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
-
- export LANG=en_US.UTF-8
- export LC_ALL=en_US.UTF-8
-
- if [ "${var.use_tmux}" = "true" ]; then
- if tmux has-session -t ${var.session_name} 2>/dev/null; then
- echo "Attaching to existing Aider tmux session..."
- tmux attach-session -t ${var.session_name}
- else
- echo "Starting new Aider tmux session..."
- tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
- fi
- elif [ "${var.use_screen}" = "true" ]; then
- if ! screen -list | grep -q "${var.session_name}"; then
- echo "Error: No existing Aider session found. Please wait for the script to start it."
- exit 1
- fi
- screen -xRR ${var.session_name}
- else
- cd "${var.folder}"
- echo "Starting Aider directly..."
- export ${local.env_var_name}="${var.ai_api_key}"
- aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}"
- fi
+ echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
+ chmod +x /tmp/install.sh
+ ARG_WORKDIR='${var.workdir}' \
+ ARG_INSTALL_AIDER='${var.install_aider}' \
+ ARG_REPORT_TASKS='${var.report_tasks}' \
+ ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.base_aider_config)}' | base64 -d)" \
+ /tmp/install.sh
EOT
- order = var.order
- group = var.group
}
+
diff --git a/registry/coder/modules/aider/main.tftest.hcl b/registry/coder/modules/aider/main.tftest.hcl
new file mode 100644
index 00000000..281bde86
--- /dev/null
+++ b/registry/coder/modules/aider/main.tftest.hcl
@@ -0,0 +1,149 @@
+run "test_aider_basic" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-123"
+ workdir = "/home/coder"
+ model = "gemini"
+ }
+
+ assert {
+ condition = var.workdir == "/home/coder"
+ error_message = "Workdir variable should default to /home/coder"
+ }
+
+ assert {
+ condition = var.agent_id == "test-agent-123"
+ error_message = "Agent ID variable should be set correctly"
+ }
+
+ assert {
+ condition = var.install_aider == true
+ error_message = "install_aider should default to true"
+ }
+
+ assert {
+ condition = var.install_agentapi == true
+ error_message = "install_agentapi should default to true"
+ }
+
+ assert {
+ condition = var.report_tasks == false
+ error_message = "report_tasks should default to false"
+ }
+}
+
+run "test_with_api_key" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-456"
+ workdir = "/home/coder/workspace"
+ api_key = "test-api-key-123"
+ model = "gemini"
+ }
+
+ assert {
+ condition = var.api_key == "test-api-key-123"
+ error_message = "API key value should match the input"
+ }
+}
+
+run "test_custom_options" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-789"
+ workdir = "/home/coder/custom"
+ order = 5
+ group = "development"
+ icon = "/icon/custom.svg"
+ model = "4o"
+ ai_prompt = "Help me write better code"
+ install_aider = false
+ install_agentapi = false
+ agentapi_version = "v0.10.0"
+ api_key = ""
+ base_aider_config = "read:\n - CONVENTIONS.md"
+ }
+
+ 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 == "4o"
+ error_message = "Model variable should be set to '4o'"
+ }
+
+ assert {
+ condition = var.ai_prompt == "Help me write better code"
+ error_message = "AI prompt variable should be set correctly"
+ }
+
+ assert {
+ condition = var.install_aider == false
+ error_message = "install_aider should be set to false"
+ }
+
+ assert {
+ condition = var.install_agentapi == false
+ error_message = "install_agentapi should be set to false"
+ }
+
+ assert {
+ condition = var.agentapi_version == "v0.10.0"
+ error_message = "AgentAPI version should be set to 'v0.10.0'"
+ }
+}
+
+run "test_with_scripts" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-scripts"
+ workdir = "/home/coder/scripts"
+ model = "gemini"
+ 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_ai_provider_env_mapping" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-provider"
+ workdir = "/home/coder/test"
+ ai_provider = "google"
+ model = "gemini"
+ custom_env_var_name = ""
+ }
+
+ # Ensure provider -> env var mapping works as expected (based on locals.provider_env_vars)
+ assert {
+ condition = var.ai_provider == "google"
+ error_message = "AI provider should be set to 'google' for this test"
+ }
+}
diff --git a/registry/coder/modules/aider/scripts/install.sh b/registry/coder/modules/aider/scripts/install.sh
new file mode 100644
index 00000000..b2244aa0
--- /dev/null
+++ b/registry/coder/modules/aider/scripts/install.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+set -euo pipefail
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+# Inputs
+ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
+ARG_INSTALL_AIDER=${ARG_INSTALL_AIDER:-true}
+ARG_AIDER_CONFIG=${ARG_AIDER_CONFIG:-}
+
+echo "--------------------------------"
+echo "Install flag: $ARG_INSTALL_AIDER"
+echo "Workspace: $ARG_WORKDIR"
+echo "--------------------------------"
+
+function install_aider() {
+ echo "pipx installing..."
+ sudo apt-get install -y pipx
+ echo "pipx installed!"
+ pipx ensurepath
+ mkdir -p "$ARG_WORKDIR/.local/bin"
+ export PATH="$HOME/.local/bin:$ARG_WORKDIR/.local/bin:$PATH"
+
+ if ! command_exists aider; then
+ echo "Installing Aider via pipx..."
+ pipx install --force aider-install
+ aider-install
+ fi
+ echo "Aider installed: $(aider --version || echo 'Aider installation check failed')"
+}
+
+function configure_aider_settings() {
+ if [ -n "${ARG_AIDER_CONFIG}" ]; then
+ echo "Configuring Aider environment variables and model"
+
+ mkdir -p "$HOME/.config/aider"
+
+ echo "$ARG_AIDER_CONFIG" > "$HOME/.config/aider/.aider.conf.yml"
+ echo "Aider config created at $HOME/.config/aider/.aider.conf.yml"
+ else
+ printf "No Aider environment variables or model configured\n"
+ fi
+}
+
+install_aider
+configure_aider_settings
diff --git a/registry/coder/modules/aider/scripts/start.sh b/registry/coder/modules/aider/scripts/start.sh
new file mode 100644
index 00000000..1bd18ffa
--- /dev/null
+++ b/registry/coder/modules/aider/scripts/start.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+set -euo pipefail
+
+# Ensure pipx-installed apps are in PATH
+export PATH="$HOME/.local/bin:$PATH"
+
+ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
+ARG_API_KEY=$(echo -n "${ARG_API_KEY:-}" | base64 -d)
+ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
+ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
+ARG_MODEL=${ARG_MODEL:-}
+ARG_PROVIDER=${ARG_PROVIDER:-}
+ARG_ENV_API_NAME_HOLDER=${ARG_ENV_API_NAME_HOLDER:-}
+
+echo "--------------------------------"
+echo "Provider: $ARG_PROVIDER"
+echo "Model: $ARG_MODEL"
+echo "--------------------------------"
+
+if [ -n "$ARG_API_KEY" ]; then
+ printf "API key provided!\n"
+ export $ARG_ENV_API_NAME_HOLDER=$ARG_API_KEY
+else
+ printf "API key not provided.\n"
+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"
+}
+
+start_agentapi() {
+ echo "Starting in directory: $ARG_WORKDIR"
+ cd "$ARG_WORKDIR"
+
+ local initial_prompt
+ initial_prompt=$(build_initial_prompt)
+ if [ -n "$initial_prompt" ]; then
+ echo "Starting agentapi with initial prompt"
+ agentapi server -I="$initial_prompt" --type aider --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
+ else
+ agentapi server --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
+ fi
+}
+
+# TODO: Implement MCP server for coder when Aider support MCP servers.
+
+start_agentapi
diff --git a/registry/coder/modules/aider/testdata/aider-mock.sh b/registry/coder/modules/aider/testdata/aider-mock.sh
new file mode 100644
index 00000000..e021b2d2
--- /dev/null
+++ b/registry/coder/modules/aider/testdata/aider-mock.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+if [[ "$1" == "--version" ]]; then
+ echo "HELLO: $(bash -c env)"
+ echo "aider version v0.86.0"
+ exit 0
+fi
+
+set -e
+
+while true; do
+ echo "$(date) - aider-agent-mock"
+ sleep 15
+done
diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md
index ed10e2a5..3a0ec420 100644
--- a/registry/coder/modules/claude-code/README.md
+++ b/registry/coder/modules/claude-code/README.md
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.1.1"
+ version = "3.4.3"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -32,8 +32,29 @@ module "claude-code" {
- 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)
+### Session Resumption Behavior
+
+By default, Claude Code automatically resumes existing conversations when your workspace restarts. Sessions are tracked per workspace directory, so conversations continue where you left off. If no session exists (first start), your `ai_prompt` will run normally. To disable this behavior and always start fresh, set `continue = false`
+
## Examples
+### Usage with Agent Boundaries
+
+This example shows how to configure the Claude Code module to run the agent behind a process-level boundary that restricts its network access.
+
+```tf
+module "claude-code" {
+ source = "dev.registry.coder.com/coder/claude-code/coder"
+ enable_boundary = true
+ boundary_version = "main"
+ boundary_log_dir = "/tmp/boundary_logs"
+ boundary_log_level = "WARN"
+ boundary_additional_allowed_urls = ["GET *google.com"]
+ boundary_proxy_port = "8087"
+ version = "3.4.3"
+}
+```
+
### Usage with Tasks and Advanced Configuration
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.
@@ -49,7 +70,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.1.1"
+ version = "3.4.3"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
@@ -85,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.1.1"
+ version = "3.4.3"
agent_id = coder_agent.example.id
workdir = "/home/coder"
install_claude_code = true
@@ -108,7 +129,7 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.1.1"
+ version = "3.4.3"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
@@ -181,7 +202,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.1.1"
+ version = "3.4.3"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@@ -238,7 +259,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
- version = "3.1.1"
+ version = "3.4.3"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts
index 9c132f1a..a7c2dd14 100644
--- a/registry/coder/modules/claude-code/main.test.ts
+++ b/registry/coder/modules/claude-code/main.test.ts
@@ -167,7 +167,7 @@ describe("claude-code", async () => {
const { id } = await setup({
moduleVariables: {
permission_mode: mode,
- task_prompt: "test prompt",
+ ai_prompt: "test prompt",
},
});
await execModuleScript(id);
@@ -185,7 +185,7 @@ describe("claude-code", async () => {
const { id } = await setup({
moduleVariables: {
model: model,
- task_prompt: "test prompt",
+ ai_prompt: "test prompt",
},
});
await execModuleScript(id);
@@ -198,13 +198,24 @@ describe("claude-code", async () => {
expect(startLog.stdout).toContain(`--model ${model}`);
});
- test("claude-continue-previous-conversation", async () => {
+ test("claude-continue-resume-existing-session", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
- task_prompt: "test prompt",
+ ai_prompt: "test prompt",
},
});
+
+ // Create a mock session file with the predefined task session ID
+ const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
+ const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
+ await execContainer(id, ["mkdir", "-p", sessionDir]);
+ await execContainer(id, [
+ "bash",
+ "-c",
+ `touch ${sessionDir}/session-${taskSessionId}.jsonl`,
+ ]);
+
await execModuleScript(id);
const startLog = await execContainer(id, [
@@ -212,7 +223,9 @@ describe("claude-code", async () => {
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
- expect(startLog.stdout).toContain("--continue");
+ expect(startLog.stdout).toContain("--resume");
+ expect(startLog.stdout).toContain(taskSessionId);
+ expect(startLog.stdout).toContain("Resuming existing task session");
});
test("pre-post-install-scripts", async () => {
diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf
index 8909a024..93b3761b 100644
--- a/registry/coder/modules/claude-code/main.tf
+++ b/registry/coder/modules/claude-code/main.tf
@@ -134,8 +134,8 @@ variable "resume_session_id" {
variable "continue" {
type = bool
- 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
+ description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
+ default = true
}
variable "dangerously_skip_permissions" {
@@ -192,6 +192,54 @@ variable "claude_md_path" {
default = "$HOME/.claude/CLAUDE.md"
}
+variable "enable_boundary" {
+ type = bool
+ description = "Whether to enable coder boundary for network filtering"
+ default = false
+}
+
+variable "boundary_version" {
+ type = string
+ description = "Boundary version, valid git reference should be provided (tag, commit, branch)"
+ default = "main"
+}
+
+variable "boundary_log_dir" {
+ type = string
+ description = "Directory for boundary logs"
+ default = "/tmp/boundary_logs"
+}
+
+variable "boundary_log_level" {
+ type = string
+ description = "Log level for boundary process"
+ default = "WARN"
+}
+
+variable "boundary_additional_allowed_urls" {
+ type = list(string)
+ description = "Additional URLs to allow through boundary (in addition to default allowed URLs)"
+ default = []
+}
+
+variable "boundary_proxy_port" {
+ type = string
+ description = "Port for HTTP Proxy used by Boundary"
+ default = "8087"
+}
+
+variable "enable_boundary_pprof" {
+ type = bool
+ description = "Whether to enable coder boundary pprof server"
+ default = false
+}
+
+variable "boundary_pprof_port" {
+ type = string
+ description = "Port for pprof server used by Boundary"
+ default = "6067"
+}
+
resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
@@ -229,6 +277,8 @@ locals {
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"))
+ # Extract hostname from access_url for boundary --allow flag
+ coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
# Required prompts for the module to properly report task status to Coder
report_tasks_system_prompt = <<-EOT
@@ -299,6 +349,15 @@ module "agentapi" {
ARG_PERMISSION_MODE='${var.permission_mode}' \
ARG_WORKDIR='${local.workdir}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
+ ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \
+ ARG_BOUNDARY_VERSION='${var.boundary_version}' \
+ ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \
+ ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \
+ ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join("|", var.boundary_additional_allowed_urls)}' \
+ ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \
+ ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \
+ ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \
+ ARG_CODER_HOST='${local.coder_host}' \
/tmp/start.sh
EOT
diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl
index 9999c1b1..6994caf2 100644
--- a/registry/coder/modules/claude-code/main.tftest.hcl
+++ b/registry/coder/modules/claude-code/main.tftest.hcl
@@ -188,6 +188,32 @@ run "test_claude_code_permission_mode_validation" {
}
}
+run "test_claude_code_with_boundary" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-boundary"
+ workdir = "/home/coder/boundary-test"
+ enable_boundary = true
+ boundary_log_dir = "/tmp/test-boundary-logs"
+ }
+
+ assert {
+ condition = var.enable_boundary == true
+ error_message = "Boundary should be enabled"
+ }
+
+ assert {
+ condition = var.boundary_log_dir == "/tmp/test-boundary-logs"
+ error_message = "Boundary log dir should be set correctly"
+ }
+
+ assert {
+ condition = local.coder_host != ""
+ error_message = "Coder host should be extracted from access URL"
+ }
+}
+
run "test_claude_code_system_prompt" {
command = plan
@@ -267,4 +293,4 @@ run "test_claude_report_tasks_disabled" {
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "")
error_message = "System prompt should end with "
}
-}
\ No newline at end of file
+}
diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh
index 1285df90..21133384 100644
--- a/registry/coder/modules/claude-code/scripts/install.sh
+++ b/registry/coder/modules/claude-code/scripts/install.sh
@@ -91,11 +91,6 @@ function report_tasks() {
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
}
diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh
index 6eeb411b..783e908d 100644
--- a/registry/coder/modules/claude-code/scripts/start.sh
+++ b/registry/coder/modules/claude-code/scripts/start.sh
@@ -17,6 +17,14 @@ 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)
+ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false}
+ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"}
+ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"}
+ARG_BOUNDARY_LOG_LEVEL=${ARG_BOUNDARY_LOG_LEVEL:-"WARN"}
+ARG_BOUNDARY_PROXY_PORT=${ARG_BOUNDARY_PROXY_PORT:-"8087"}
+ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false}
+ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"}
+ARG_CODER_HOST=${ARG_CODER_HOST:-}
echo "--------------------------------"
@@ -27,6 +35,12 @@ printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIO
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"
+printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY"
+printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION"
+printf "ARG_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR"
+printf "ARG_BOUNDARY_LOG_LEVEL: %s\n" "$ARG_BOUNDARY_LOG_LEVEL"
+printf "ARG_BOUNDARY_PROXY_PORT: %s\n" "$ARG_BOUNDARY_PROXY_PORT"
+printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
echo "--------------------------------"
@@ -35,6 +49,14 @@ echo "--------------------------------"
# avoid exiting if the script fails
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true
+function install_boundary() {
+ # Install boundary from public github repo
+ git clone https://github.com/coder/boundary
+ cd boundary
+ git checkout $ARG_BOUNDARY_VERSION
+ go install ./cmd/...
+}
+
function validate_claude_installation() {
if command_exists claude; then
printf "Claude Code is installed\n"
@@ -44,41 +66,115 @@ function validate_claude_installation() {
fi
}
+TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
+
+task_session_exists() {
+ if find "$HOME/.claude" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
+ return 0
+ else
+ return 1
+ fi
+}
+
ARGS=()
-function build_claude_args() {
+function start_agentapi() {
+ # For Task reporting
+ export CODER_MCP_ALLOWED_TOOLS="coder_report_task"
+
+ mkdir -p "$ARG_WORKDIR"
+ cd "$ARG_WORKDIR"
+
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
+ if [ -n "$ARG_RESUME_SESSION_ID" ]; then
+ echo "Using explicit resume_session_id: $ARG_RESUME_SESSION_ID"
+ ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
+ if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
+ elif [ "$ARG_CONTINUE" = "true" ]; then
+ if task_session_exists; then
+ echo "Task session detected (ID: $TASK_SESSION_ID)"
+ ARGS+=(--resume "$TASK_SESSION_ID")
+ if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
+ ARGS+=(--dangerously-skip-permissions)
+ fi
+ echo "Resuming existing task session"
+ else
+ echo "No existing task session found"
+ ARGS+=(--session-id "$TASK_SESSION_ID")
+ if [ -n "$ARG_AI_PROMPT" ]; then
+ ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
+ echo "Starting new task session with prompt"
+ else
+ if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
+ ARGS+=(--dangerously-skip-permissions)
+ fi
+ echo "Starting new task session"
+ fi
+ fi
+ else
+ echo "Continue disabled, starting fresh session"
+ if [ -n "$ARG_AI_PROMPT" ]; then
+ ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
+ echo "Starting new session with prompt"
+ else
+ if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
+ ARGS+=(--dangerously-skip-permissions)
+ fi
+ echo "Starting claude code session"
+ 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[@]}"
+
+ if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then
+ install_boundary
+
+ mkdir -p "$ARG_BOUNDARY_LOG_DIR"
+ printf "Starting with coder boundary enabled\n"
+
+ # Build boundary args with conditional --unprivileged flag
+ BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR")
+ # Add default allowed URLs
+ BOUNDARY_ARGS+=(--allow "domain=anthropic.com" --allow "domain=registry.npmjs.org" --allow "domain=sentry.io" --allow "domain=claude.ai" --allow "domain=$ARG_CODER_HOST")
+
+ # Add any additional allowed URLs from the variable
+ if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then
+ IFS='|' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS"
+ for url in "${ADDITIONAL_URLS[@]}"; do
+ # Quote the URL to preserve spaces within the allow rule
+ BOUNDARY_ARGS+=(--allow "$url")
+ done
+ fi
+
+ # Set HTTP Proxy port used by Boundary
+ BOUNDARY_ARGS+=(--proxy-port $ARG_BOUNDARY_PROXY_PORT)
+
+ # Set log level for boundary
+ BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL)
+
+ if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then
+ # Enable boundary pprof server on specified port
+ BOUNDARY_ARGS+=(--pprof)
+ BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT})
+ fi
+
+ agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \
+ sudo -E env PATH=$PATH setpriv --reuid=$(id -u) --regid=$(id -g) --clear-groups \
+ --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \
+ claude "${ARGS[@]}"
+ else
+ agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
+ fi
}
validate_claude_installation
-build_claude_args
start_agentapi
diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md
index ef19ec20..9d08e645 100644
--- a/registry/coder/modules/jetbrains/README.md
+++ b/registry/coder/modules/jetbrains/README.md
@@ -14,7 +14,7 @@ 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.1.0"
+ version = "1.1.1"
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
@@ -40,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.1.0"
+ version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
@@ -53,7 +53,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.1.0"
+ version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
# Show parameter with limited options
@@ -67,7 +67,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.1.0"
+ version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
default = ["IU", "PY"]
@@ -82,7 +82,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.1.0"
+ version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/workspace/project"
@@ -108,7 +108,7 @@ module "jetbrains" {
module "jetbrains_pycharm" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.1.0"
+ version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/workspace/project"
@@ -128,7 +128,7 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons:
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
- version = "1.1.0"
+ version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
default = ["IU", "PY"]
diff --git a/registry/coder/modules/jetbrains/main.tf b/registry/coder/modules/jetbrains/main.tf
index d33fc6b2..8f0e0ac7 100644
--- a/registry/coder/modules/jetbrains/main.tf
+++ b/registry/coder/modules/jetbrains/main.tf
@@ -1,5 +1,5 @@
terraform {
- required_version = ">= 1.0"
+ required_version = ">= 1.9"
required_providers {
coder = {
@@ -163,7 +163,8 @@ variable "ide_config" {
condition = length(var.ide_config) > 0
error_message = "The ide_config must not be empty."
}
- # ide_config must be a superset of var.. options
+ # ide_config must be a superset of var.options
+ # Requires Terraform 1.9+ for cross-variable validation references
validation {
condition = alltrue([
for code in var.options : contains(keys(var.ide_config), code)
diff --git a/registry/coder/modules/kasmvnc/README.md b/registry/coder/modules/kasmvnc/README.md
index 2bc862d4..7f01b45b 100644
--- a/registry/coder/modules/kasmvnc/README.md
+++ b/registry/coder/modules/kasmvnc/README.md
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
- version = "1.2.4"
+ version = "1.2.5"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
diff --git a/registry/coder/modules/kasmvnc/main.tf b/registry/coder/modules/kasmvnc/main.tf
index ca7315ec..5a5b449b 100644
--- a/registry/coder/modules/kasmvnc/main.tf
+++ b/registry/coder/modules/kasmvnc/main.tf
@@ -23,7 +23,7 @@ variable "port" {
variable "kasm_version" {
type = string
description = "Version of KasmVNC to install."
- default = "1.3.2"
+ default = "1.4.0"
}
variable "desktop_environment" {
diff --git a/registry/coder/modules/kasmvnc/run.sh b/registry/coder/modules/kasmvnc/run.sh
index 04b8b9ee..089dce3e 100644
--- a/registry/coder/modules/kasmvnc/run.sh
+++ b/registry/coder/modules/kasmvnc/run.sh
@@ -8,10 +8,10 @@ error() {
exit 1
}
-# Function to check if vncserver is already installed
+# Function to check if KasmVNC is already installed
check_installed() {
- if command -v vncserver &> /dev/null; then
- echo "vncserver is already installed."
+ if command -v kasmvncserver &> /dev/null; then
+ echo "KasmVNC is already installed."
return 0 # Don't exit, just indicate it's installed
else
return 1 # Indicates not installed
@@ -158,7 +158,7 @@ case "$arch" in
;;
esac
-# Check if vncserver is installed, and install if not
+# Check if KasmVNC is installed, and install if not
if ! check_installed; then
# Check for NOPASSWD sudo (required)
if ! command -v sudo &> /dev/null || ! sudo -n true 2> /dev/null; then
@@ -188,7 +188,7 @@ if ! check_installed; then
;;
esac
else
- echo "vncserver already installed. Skipping installation."
+ echo "KasmVNC already installed. Skipping installation."
fi
if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
@@ -227,7 +227,7 @@ EOF
# This password is not used since we start the server without auth.
# The server is protected via the Coder session token / tunnel
# and does not listen publicly
-echo -e "password\npassword\n" | vncpasswd -wo -u "$USER"
+echo -e "password\npassword\n" | kasmvncpasswd -wo -u "$USER"
get_http_dir() {
# determine the served file path
@@ -290,7 +290,7 @@ VNC_LOG="/tmp/kasmvncserver.log"
printf "🚀 Starting KasmVNC server...\n"
set +e
-vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
+kasmvncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
RETVAL=$?
set -e
diff --git a/registry/coder/modules/vault-token/README.md b/registry/coder/modules/vault-token/README.md
index e30abdc5..4561a170 100644
--- a/registry/coder/modules/vault-token/README.md
+++ b/registry/coder/modules/vault-token/README.md
@@ -19,7 +19,7 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/coder/vault-token/coder"
- version = "1.2.1"
+ version = "1.2.2"
agent_id = coder_agent.example.id
vault_token = var.token # optional
vault_addr = "https://vault.example.com"
@@ -73,7 +73,7 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/coder/vault-token/coder"
- version = "1.2.1"
+ version = "1.2.2"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_token = var.token
diff --git a/registry/coder/modules/vault-token/run.sh b/registry/coder/modules/vault-token/run.sh
index e1da6ee8..9b83f32f 100644
--- a/registry/coder/modules/vault-token/run.sh
+++ b/registry/coder/modules/vault-token/run.sh
@@ -68,7 +68,7 @@ install() {
else
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${INSTALL_VERSION}"
fi
- fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_amd64.zip"
+ fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_$${ARCH}.zip"
if [ $? -ne 0 ]; then
printf "Failed to download Vault.\n"
return 1
diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md
index f19afc47..92c5ac17 100644
--- a/registry/coder/modules/windows-rdp/README.md
+++ b/registry/coder/modules/windows-rdp/README.md
@@ -15,7 +15,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
- version = "1.2.3"
+ version = "1.3.0"
agent_id = resource.coder_agent.main.id
}
```
@@ -32,7 +32,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
- version = "1.2.3"
+ version = "1.3.0"
agent_id = resource.coder_agent.main.id
}
```
@@ -43,7 +43,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
- version = "1.2.3"
+ version = "1.3.0"
agent_id = resource.coder_agent.main.id
}
```
@@ -54,7 +54,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
- version = "1.2.3"
+ version = "1.3.0"
agent_id = resource.coder_agent.main.id
devolutions_gateway_version = "2025.2.2" # Specify a specific version
}
diff --git a/registry/coder/modules/windows-rdp/devolutions-patch.js b/registry/coder/modules/windows-rdp/devolutions-patch.js
index ef736452..1f231ca3 100644
--- a/registry/coder/modules/windows-rdp/devolutions-patch.js
+++ b/registry/coder/modules/windows-rdp/devolutions-patch.js
@@ -25,401 +25,426 @@
* @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry
* @typedef {Readonly>} FormFieldEntries
*/
+(function () {
+ /**
+ * The communication protocol to set Devolutions to.
+ */
+ const PROTOCOL = "RDP";
-/**
- * The communication protocol to set Devolutions to.
- */
-const PROTOCOL = "RDP";
+ /**
+ * The hostname to use with Devolutions.
+ */
+ const HOSTNAME = "localhost";
-/**
- * The hostname to use with Devolutions.
- */
-const HOSTNAME = "localhost";
+ /**
+ * How often to poll the screen for the main Devolutions form.
+ */
+ const POLL_INTERVAL_MS = 500;
-/**
- * How often to poll the screen for the main Devolutions form.
- */
-const SCREEN_POLL_INTERVAL_MS = 500;
-
-/**
- * The fields in the Devolutions sign-in form that should be populated with
- * values from the Coder workspace.
- *
- * All properties should be defined as placeholder templates in the form
- * VALUE_NAME. The Coder module, when spun up, should then run some logic to
- * replace the template slots with actual values. These values should never
- * change from within JavaScript itself.
- *
- * @satisfies {FormFieldEntries}
- */
-const formFieldEntries = {
- /** @readonly */
- username: {
+ /**
+ * The fields in the Devolutions sign-in form that should be populated with
+ * values from the Coder workspace.
+ *
+ * All properties should be defined as placeholder templates in the form
+ * VALUE_NAME. The Coder module, when spun up, should then run some logic to
+ * replace the template slots with actual values. These values should never
+ * change from within JavaScript itself.
+ *
+ * @satisfies {FormFieldEntries}
+ */
+ const formFieldEntries = {
/** @readonly */
- querySelector: "web-client-username-control input",
+ username: {
+ /** @readonly */
+ querySelector: "web-client-username-control input",
+ /** @readonly */
+ value: "${CODER_USERNAME}",
+ },
/** @readonly */
- value: "${CODER_USERNAME}",
- },
+ password: {
+ /** @readonly */
+ querySelector: "web-client-password-control input",
- /** @readonly */
- password: {
- /** @readonly */
- querySelector: "web-client-password-control input",
-
- /** @readonly */
- value: "${CODER_PASSWORD}",
- },
-};
-
-/**
- * Handles typing in the values for the input form. All values are written
- * immediately, even though that would be physically impossible with a real
- * keyboard.
- *
- * Note: this code will never break, but you might get warnings in the console
- * from Angular about unexpected value changes. Angular patches over a lot of
- * the built-in browser APIs to support its component change detection system.
- * As part of that, it has validations for checking whether an input it
- * previously had control over changed without it doing anything.
- *
- * But the only way to simulate a keyboard input is by setting the input's
- * .value property, and then firing an input event. So basically, the inner
- * value will change, which Angular won't be happy about, but then the input
- * event will fire and sync everything back together.
- *
- * @param {HTMLInputElement} inputField
- * @param {string} inputText
- * @returns {Promise}
- */
-function setInputValue(inputField, inputText) {
- return new Promise((resolve, reject) => {
- // Adding timeout for input event, even though we'll be dispatching it
- // immediately, just in the off chance that something in the Angular app
- // intercepts it or stops it from propagating properly
- const timeoutId = window.setTimeout(() => {
- reject(new Error("Input event did not get processed correctly in time."));
- }, 3_000);
-
- const handleSuccessfulDispatch = () => {
- window.clearTimeout(timeoutId);
- inputField.removeEventListener("input", handleSuccessfulDispatch);
- resolve();
- };
-
- inputField.addEventListener("input", handleSuccessfulDispatch);
-
- // Code assumes that Angular will have an event handler in place to handle
- // the new event
- const inputEvent = new Event("input", {
- bubbles: true,
- cancelable: true,
- });
-
- inputField.value = inputText;
- inputField.dispatchEvent(inputEvent);
- });
-}
-
-/**
- * Takes a Devolutions remote session form, auto-fills it with data, and then
- * submits it.
- *
- * The logic here is more convoluted than it should be for two main reasons:
- * 1. Devolutions' HTML markup has errors. There are labels, but they aren't
- * bound to the inputs they're supposed to describe. This means no easy hooks
- * for selecting the elements, unfortunately.
- * 2. Trying to modify the .value properties on some of the inputs doesn't
- * work. Probably some combo of Angular data-binding and some inputs having
- * the readonly attribute. Have to simulate user input to get around this.
- *
- * @param {HTMLFormElement} myForm
- * @returns {Promise}
- */
-async function autoSubmitForm(myForm) {
- const setProtocolValue = () => {
- /** @type {HTMLDivElement | null} */
- const protocolDropdownTrigger = myForm.querySelector('div[role="button"]');
- if (protocolDropdownTrigger === null) {
- throw new Error("No clickable trigger for setting protocol value");
- }
-
- protocolDropdownTrigger.click();
-
- // Can't use form as container for querying the list of dropdown options,
- // because the elements don't actually exist inside the form. They're placed
- // in the top level of the HTML doc, and repositioned to make it look like
- // they're part of the form. Avoids CSS stacking context issues, maybe?
- /** @type {HTMLLIElement | null} */
- const protocolOption = document.querySelector(
- // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation
- 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li',
- );
-
- if (protocolOption === null) {
- throw new Error(
- "Unable to find protocol option on screen that matches desired protocol",
- );
- }
-
- protocolOption.click();
+ /** @readonly */
+ value: "${CODER_PASSWORD}",
+ },
};
- const setHostname = () => {
- /** @type {HTMLInputElement | null} */
- const hostnameInput = myForm.querySelector("p-autocomplete#hostname input");
+ /**
+ * This ensures that the Devolutions login form (which by default, always shows
+ * up on screen when the app first launches) stays visually hidden from the user
+ * when they open Devolutions via the Coder module.
+ *
+ * The form will still be filled out automatically and submitted in the
+ * background via the rest of the logic in this file, so this function is mainly
+ * to help avoid screen flickering and make the overall experience feel a little
+ * more polished (even though it's just one giant hack).
+ *
+ * @returns {void}
+ */
+ function hideFormForInitialSubmission() {
+ const styleId = "coder-patch--styles-initial-submission";
+ const cssOpacityVariableName = "--coder-opacity-multiplier";
- if (hostnameInput === null) {
- throw new Error("Unable to find field for adding hostname");
+ /** @type {HTMLStyleElement | null} */
+ // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation
+ let styleContainer = document.querySelector("#" + styleId);
+ if (!styleContainer) {
+ styleContainer = document.createElement("style");
+ styleContainer.id = styleId;
+ styleContainer.innerHTML = `
+ /*
+ Have to use opacity instead of visibility, because the element still
+ needs to be interactive via the script so that it can be auto-filled.
+ */
+ :root {
+ /*
+ Can be 0 or 1. Start off invisible to avoid risks of UI flickering,
+ but the rest of the function should be in charge of making the form
+ container visible again if something goes wrong during setup.
+
+ Double dollar sign needed to avoid Terraform script false positives
+ */
+ $${cssOpacityVariableName}: 0;
+ }
+
+ /*
+ web-client-form is the container for the main session form, while
+ the div is for the dropdown that is used for selecting the protocol.
+ The dropdown is not inside of the form for CSS styling reasons, so we
+ need to select both.
+ */
+ web-client-form,
+ body > div.p-overlay {
+ /*
+ Double dollar sign needed to avoid Terraform script false positives
+ */
+ opacity: calc(100% * var($${cssOpacityVariableName})) !important;
+ }
+ `;
+
+ document.head.appendChild(styleContainer);
}
- return setInputValue(hostnameInput, HOSTNAME);
- };
-
- const setCoderFormFieldValues = async () => {
- // The RDP form will not appear on screen unless the dropdown is set to use
- // the RDP protocol
- const rdpSubsection = myForm.querySelector("rdp-form");
- if (rdpSubsection === null) {
- throw new Error(
- "Unable to find RDP subsection. Is the value of the protocol set to RDP?",
- );
- }
-
- for (const { value, querySelector } of Object.values(formFieldEntries)) {
- /** @type {HTMLInputElement | null} */
- const input = document.querySelector(querySelector);
-
- if (input === null) {
- throw new Error(
- // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation
- 'Unable to element that matches query "' + querySelector + '"',
- );
- }
-
- await setInputValue(input, value);
- }
- };
-
- const triggerSubmission = () => {
- /** @type {HTMLButtonElement | null} */
- const submitButton = myForm.querySelector(
- 'p-button[ng-reflect-type="submit"] button',
- );
-
- if (submitButton === null) {
- throw new Error("Unable to find submission button");
- }
-
- if (submitButton.disabled) {
- throw new Error(
- "Unable to submit form because submit button is disabled. Are all fields filled out correctly?",
- );
- }
-
- submitButton.click();
- };
-
- setProtocolValue();
- await setHostname();
- await setCoderFormFieldValues();
- triggerSubmission();
-}
-
-/**
- * Sets up logic for auto-populating the form data when the form appears on
- * screen.
- *
- * @returns {void}
- */
-function setupFormDetection() {
- /** @type {HTMLFormElement | null} */
- let formValueFromLastMutation = null;
-
- /** @returns {void} */
- const onDynamicTabMutation = () => {
- /** @type {HTMLFormElement | null} */
- const latestForm = document.querySelector("web-client-form > form");
-
- // Only try to auto-fill if we went from having no form on screen to
- // having a form on screen. That way, we don't accidentally override the
- // form if the user is trying to customize values, and this essentially
- // makes the script values function as default values
- const mounted = formValueFromLastMutation === null && latestForm !== null;
- if (mounted) {
- autoSubmitForm(latestForm);
- }
-
- formValueFromLastMutation = latestForm;
- };
-
- /** @type {number | undefined} */
- let pollingId = undefined;
-
- /** @returns {void} */
- const checkScreenForDynamicTab = () => {
- const dynamicTab = document.querySelector("web-client-dynamic-tab");
-
- // Keep polling until the main content container is on screen
- if (dynamicTab === null) {
+ // The root node being undefined should be physically impossible (if it's
+ // undefined, the browser itself is busted), but we need to do a type check
+ // here so that the rest of the function doesn't need to do type checks over
+ // and over.
+ const rootNode = document.querySelector(":root");
+ if (!(rootNode instanceof HTMLHtmlElement)) {
+ // Remove the container entirely because if the browser is busted, who knows
+ // if the CSS variables can be applied correctly. Better to have something
+ // be a bit more ugly/painful to use, than have it be impossible to use
+ styleContainer.remove();
return;
}
- window.clearInterval(pollingId);
+ // It's safe to make the form visible preemptively because Devolutions
+ // outputs the Windows view through an HTML canvas that it overlays on top
+ // of the rest of the app. Even if the form isn't hidden at the style level,
+ // it will still be covered up.
+ const restoreOpacity = () => {
+ rootNode.style.setProperty(cssOpacityVariableName, "1");
+ };
- // Call the mutation callback manually, to ensure it runs at least once
- onDynamicTabMutation();
+ // If this file gets more complicated, it might make sense to set up the
+ // timeout and event listener so that if one triggers, it cancels the other,
+ // but having restoreOpacity run more than once is a no-op for right now.
+ // Not a big deal if these don't get cleaned up.
- // Having the mutation observer is kind of an extra safety net that isn't
- // really expected to run that often. Most of the content in the dynamic
- // tab is being rendered through Canvas, which won't trigger any mutations
- // that the observer can detect
- const dynamicTabObserver = new MutationObserver(onDynamicTabMutation);
- dynamicTabObserver.observe(dynamicTab, {
- subtree: true,
- childList: true,
- });
- };
+ // Have the form automatically reappear no matter what, so that if something
+ // does break, the user isn't left out to dry
+ window.setTimeout(restoreOpacity, 5_000);
- pollingId = window.setInterval(
- checkScreenForDynamicTab,
- SCREEN_POLL_INTERVAL_MS,
- );
-}
-
-/**
- * Sets up custom styles for hiding default Devolutions elements that Coder
- * users shouldn't need to care about.
- *
- * @returns {void}
- */
-function setupAlwaysOnStyles() {
- const styleId = "coder-patch--styles-always-on";
- // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation
- const existingContainer = document.querySelector("#" + styleId);
- if (existingContainer) {
- return;
+ /** @type {HTMLFormElement | null} */
+ const form = document.querySelector("web-client-form > form");
+ form?.addEventListener(
+ "submit",
+ () => {
+ // Not restoring opacity right away just to give the HTML canvas a little
+ // bit of time to get spun up and cover up the main form
+ window.setTimeout(restoreOpacity, 1_000);
+ },
+ { once: true },
+ );
}
- const styleContainer = document.createElement("style");
- styleContainer.id = styleId;
- styleContainer.innerHTML = `
- /* app-menu corresponds to the sidebar of the default view. */
- app-menu {
- display: none !important;
+ /**
+ * Sets up custom styles for hiding default Devolutions elements that Coder
+ * users shouldn't need to care about.
+ *
+ * @returns {void}
+ */
+ function setupAlwaysOnStyles() {
+ const styleId = "coder-patch--styles-always-on";
+ // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation
+ const existingContainer = document.querySelector("#" + styleId);
+ if (existingContainer) {
+ return;
}
- `;
- document.head.appendChild(styleContainer);
-}
-
-/**
- * This ensures that the Devolutions login form (which by default, always shows
- * up on screen when the app first launches) stays visually hidden from the user
- * when they open Devolutions via the Coder module.
- *
- * The form will still be filled out automatically and submitted in the
- * background via the rest of the logic in this file, so this function is mainly
- * to help avoid screen flickering and make the overall experience feel a little
- * more polished (even though it's just one giant hack).
- *
- * @returns {void}
- */
-function hideFormForInitialSubmission() {
- const styleId = "coder-patch--styles-initial-submission";
- const cssOpacityVariableName = "--coder-opacity-multiplier";
-
- /** @type {HTMLStyleElement | null} */
- // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation
- let styleContainer = document.querySelector("#" + styleId);
- if (!styleContainer) {
- styleContainer = document.createElement("style");
+ const styleContainer = document.createElement("style");
styleContainer.id = styleId;
styleContainer.innerHTML = `
- /*
- Have to use opacity instead of visibility, because the element still
- needs to be interactive via the script so that it can be auto-filled.
- */
- :root {
- /*
- Can be 0 or 1. Start off invisible to avoid risks of UI flickering,
- but the rest of the function should be in charge of making the form
- container visible again if something goes wrong during setup.
-
- Double dollar sign needed to avoid Terraform script false positives
- */
- $${cssOpacityVariableName}: 0;
+ /* app-menu corresponds to the sidebar of the default view. */
+ app-menu {
+ display: none !important;
}
- /*
- web-client-form is the container for the main session form, while
- the div is for the dropdown that is used for selecting the protocol.
- The dropdown is not inside of the form for CSS styling reasons, so we
- need to select both.
- */
- web-client-form,
- body > div.p-overlay {
- /*
- Double dollar sign needed to avoid Terraform script false positives
- */
- opacity: calc(100% * var($${cssOpacityVariableName})) !important;
+ /* app-net-scan corresponds to the auto-discovery feature. */
+ app-net-scan {
+ display: none !important;
}
`;
document.head.appendChild(styleContainer);
}
- // The root node being undefined should be physically impossible (if it's
- // undefined, the browser itself is busted), but we need to do a type check
- // here so that the rest of the function doesn't need to do type checks over
- // and over.
- const rootNode = document.querySelector(":root");
- if (!(rootNode instanceof HTMLHtmlElement)) {
- // Remove the container entirely because if the browser is busted, who knows
- // if the CSS variables can be applied correctly. Better to have something
- // be a bit more ugly/painful to use, than have it be impossible to use
- styleContainer.remove();
- return;
+ /**
+ * Handles typing in the values for the input form. All values are written
+ * immediately, even though that would be physically impossible with a real
+ * keyboard.
+ *
+ * Note: this code will never break, but you might get warnings in the console
+ * from Angular about unexpected value changes. Angular patches over a lot of
+ * the built-in browser APIs to support its component change detection system.
+ * As part of that, it has validations for checking whether an input it
+ * previously had control over changed without it doing anything.
+ *
+ * But the only way to simulate a keyboard input is by setting the input's
+ * .value property, and then firing an input event. So basically, the inner
+ * value will change, which Angular won't be happy about, but then the input
+ * event will fire and sync everything back together.
+ *
+ * @param {HTMLInputElement} inputField
+ * @param {string} inputText
+ * @returns {Promise}
+ */
+ function setInputValue(inputField, inputText) {
+ return new Promise((resolve, reject) => {
+ // Adding timeout for input event, even though we'll be dispatching it
+ // immediately, just in the off chance that something in the Angular app
+ // intercepts it or stops it from propagating properly
+ const timeoutId = window.setTimeout(() => {
+ reject(
+ new Error("Input event did not get processed correctly in time."),
+ );
+ }, 3_000);
+
+ const handleSuccessfulDispatch = () => {
+ window.clearTimeout(timeoutId);
+ inputField.removeEventListener("input", handleSuccessfulDispatch);
+ resolve();
+ };
+
+ inputField.addEventListener("input", handleSuccessfulDispatch);
+
+ // Code assumes that Angular will have an event handler in place to handle
+ // the new event
+ const inputEvent = new Event("input", {
+ bubbles: true,
+ cancelable: true,
+ });
+
+ inputField.value = inputText;
+ inputField.dispatchEvent(inputEvent);
+ });
}
- // It's safe to make the form visible preemptively because Devolutions
- // outputs the Windows view through an HTML canvas that it overlays on top
- // of the rest of the app. Even if the form isn't hidden at the style level,
- // it will still be covered up.
- const restoreOpacity = () => {
- rootNode.style.setProperty(cssOpacityVariableName, "1");
- };
+ /**
+ * Takes a Devolutions remote session form, auto-fills it with data, and then
+ * submits it.
+ *
+ * The logic here is more convoluted than it should be for two main reasons:
+ * 1. Devolutions' HTML markup has errors. There are labels, but they aren't
+ * bound to the inputs they're supposed to describe. This means no easy hooks
+ * for selecting the elements, unfortunately.
+ * 2. Trying to modify the .value properties on some of the inputs doesn't
+ * work. Probably some combo of Angular data-binding and some inputs having
+ * the readonly attribute. Have to simulate user input to get around this.
+ *
+ * @param {HTMLFormElement} form
+ */
+ async function fillForm(form) {
+ try {
+ log("Form detected. Starting auto-fill...");
- // If this file gets more complicated, it might make sense to set up the
- // timeout and event listener so that if one triggers, it cancels the other,
- // but having restoreOpacity run more than once is a no-op for right now.
- // Not a big deal if these don't get cleaned up.
+ // By default, RDP is selected. Leaving this here if needed
+ // in the future.
+ const protocolTrigger = form.querySelector('p-dropdown[id="protocol"]');
+ if (protocolTrigger) {
+ protocolTrigger.click();
+ const protocolOption = document.querySelector(
+ `li[aria-label="$${PROTOCOL}"]`,
+ );
+ if (protocolOption) {
+ protocolOption.click();
+ log(`Protocol set to $${PROTOCOL}`);
+ } else {
+ log("Protocol option not found.");
+ }
+ } else {
+ log("Protocol dropdown trigger not found.");
+ }
- // Have the form automatically reappear no matter what, so that if something
- // does break, the user isn't left out to dry
- window.setTimeout(restoreOpacity, 5_000);
+ const hostnameInput = form.querySelector("p-autocomplete#hostname input");
+ if (hostnameInput) {
+ await setInputValue(hostnameInput, HOSTNAME);
+ log(`Hostname set to $${HOSTNAME}`);
+ } else {
+ log("Hostname input not found.");
+ }
- /** @type {HTMLFormElement | null} */
- const form = document.querySelector("web-client-form > form");
- form?.addEventListener(
- "submit",
- () => {
- // Not restoring opacity right away just to give the HTML canvas a little
- // bit of time to get spun up and cover up the main form
- window.setTimeout(restoreOpacity, 1_000);
- },
- { once: true },
- );
-}
+ for (const [key, { querySelector, value }] of Object.entries(
+ formFieldEntries,
+ )) {
+ const input = document.querySelector(querySelector);
+ if (input) {
+ await setInputValue(input, value);
+ log(`Set $${key} to $${value}`);
+ } else {
+ log(`Input for $${key} not found with selector: $${querySelector}`);
+ }
+ }
-// Always safe to call these immediately because even if the Angular app isn't
-// loaded by the time the function gets called, the CSS will always be globally
-// available for when Angular is finally ready
-setupAlwaysOnStyles();
-hideFormForInitialSubmission();
+ const submitButton = form.querySelector(
+ 'p-button[class="p-element"] button',
+ );
+ if (submitButton && !submitButton.disabled) {
+ submitButton.click();
+ log("Form submitted.");
+ } else {
+ log("Submit button not found or disabled.");
+ }
+ } catch (err) {
+ console.error("[Devolutions Patch] Error during form fill:", err);
+ }
+ }
-if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", setupFormDetection);
-} else {
- setupFormDetection();
-}
+ /**
+ * Attaches a click event listener to the "Close Session" button within the provided top bar element.
+ * When clicked, the listener triggers the window to close.
+ * Logs a message indicating whether the listener was successfully attached or if the button was not found.
+ *
+ * @param {HTMLElement} topBar - The container element that includes the "Close Session" button.
+ * @returns {void}
+ */
+ function attachCloseListener(topBar) {
+ const buttons = topBar.querySelectorAll("button");
+
+ const closeButton = Array.from(buttons).find((button) => {
+ const labelSpan = button.querySelector(".p-button-label");
+ return labelSpan && labelSpan.textContent.trim() === "Close Session";
+ });
+
+ if (closeButton) {
+ closeButton.parentElement.addEventListener("click", () => {
+ window.close();
+ });
+ log("Close listener attached.");
+ } else {
+ log("Close button not found in top bar.");
+ }
+ }
+
+ /**
+ * Sets the checked state of a checkbox based on its label text.
+ * Searches all components in the document and identifies the one
+ * whose label matches the provided `filterText`. Once found, it sets the checkbox
+ * to the specified `checked` state (true or false) and dispatches a change event
+ * to ensure any bound listeners (e.g., Angular change detection) are triggered.
+ * Logs the outcome of the operation for debugging or audit purposes.
+ *
+ * @param {string} filterText - The exact label text of the checkbox to target.
+ * @param {boolean} checked - The desired checked state (true to check, false to uncheck).
+ * @returns {void}
+ */
+ function setCheckbox(filterText, checked) {
+ const checkboxes = document.querySelectorAll("p-checkbox");
+
+ const targetCheckbox = Array.from(checkboxes).find((checkbox) => {
+ const label = checkbox.querySelector(".p-checkbox-label");
+ return label && label.textContent.trim() === filterText;
+ });
+
+ if (targetCheckbox) {
+ const input = targetCheckbox.querySelector('input[type="checkbox"]');
+ if (input) {
+ input.checked = checked;
+ input.dispatchEvent(new Event("change", { bubbles: true }));
+ }
+ log(`$${filterText} set to $${checked}.`);
+ } else {
+ log(`$${filterText} checkbox not found in top bar.`);
+ }
+ }
+
+ /**
+ * Continuously polls the DOM for a specific form element.
+ * - Searches for a