Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
dd96e8c
feat: conditional agentapi
35C4n0r Nov 28, 2025
c5d8357
wip
35C4n0r Nov 28, 2025
2c00575
wip
35C4n0r Nov 28, 2025
a91c884
wip
35C4n0r Dec 3, 2025
f3bfa9c
wip
35C4n0r Dec 3, 2025
0d03fa4
wip
35C4n0r Dec 3, 2025
05c5724
wip
35C4n0r Dec 3, 2025
65a73a8
wip
35C4n0r Dec 3, 2025
c6a7d04
wip
35C4n0r Dec 4, 2025
bdc8aea
wip
35C4n0r Dec 4, 2025
50eb191
wip
35C4n0r Dec 4, 2025
09386a4
wip
35C4n0r Dec 4, 2025
e12cd61
wip
35C4n0r Dec 4, 2025
19f2a8f
wip
35C4n0r Dec 4, 2025
d718c3b
wip
35C4n0r Dec 5, 2025
2ed4be2
wip
35C4n0r Dec 5, 2025
8664ded
wip
35C4n0r Dec 5, 2025
b32b2d4
wip
35C4n0r Dec 5, 2025
0d0bfa7
wip
35C4n0r Dec 5, 2025
78a0d14
wip
35C4n0r Dec 5, 2025
250b64e
wip
35C4n0r Dec 5, 2025
63eff43
wip
35C4n0r Dec 12, 2025
c2fa87a
wip
35C4n0r Dec 12, 2025
d0ef4f4
wip
35C4n0r Dec 13, 2025
4856462
Merge branch 'main' into feat-conditional-agentapi
35C4n0r Dec 13, 2025
19dc50d
wip
35C4n0r Dec 13, 2025
327f054
wip
35C4n0r Dec 14, 2025
149e65b
wip
35C4n0r Dec 14, 2025
6806985
wip
35C4n0r Dec 14, 2025
5870805
wip
35C4n0r Dec 14, 2025
93f9ec3
wip
35C4n0r Dec 14, 2025
a630ffa
wip
35C4n0r Dec 14, 2025
090fa7d
wip
35C4n0r Dec 14, 2025
aaf2c4e
wip
35C4n0r Dec 14, 2025
ef0f597
wip
35C4n0r Dec 14, 2025
1dee001
wip
35C4n0r Dec 14, 2025
dd86d3d
wip
35C4n0r Dec 14, 2025
d3b5057
wip
35C4n0r Dec 14, 2025
65189bc
wip
35C4n0r Dec 14, 2025
395f170
wip
35C4n0r Dec 14, 2025
c115d86
wip
35C4n0r Dec 14, 2025
73af151
wip
35C4n0r Dec 14, 2025
ae48c10
wip
35C4n0r Dec 14, 2025
4b03bce
wip
35C4n0r Dec 14, 2025
d4efc09
bun fmt
35C4n0r Dec 14, 2025
d8a9643
Merge branch 'main' into feat-conditional-agentapi
35C4n0r Jan 19, 2026
c411657
feat: merge to main changes
35C4n0r Jan 19, 2026
5f4d7bf
feat: merge to main changes
35C4n0r Jan 19, 2026
60ed613
bun fmt
35C4n0r Jan 19, 2026
0ffd71d
wip
35C4n0r Jan 19, 2026
5c4480d
wip
35C4n0r Jan 19, 2026
9258f18
wip
35C4n0r Jan 20, 2026
f257efd
wip
35C4n0r Jan 20, 2026
229056b
wip
35C4n0r Jan 20, 2026
a7e0b09
wip
35C4n0r Jan 20, 2026
6802df9
hopefully final wip
35C4n0r Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 86 additions & 42 deletions registry/coder/modules/claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ variable "enable_aibridge" {
}
}

variable "cli_command" {
type = string
description = "The command to run for the Claude Code CLI app when tasks are disabled."
default = ""
}

Comment on lines +247 to +252
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be moot now that we have claude_binary_path which is meant to be a place you can provide your path to claude-code if you are sourcing it outside the module.

resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
agent_id = var.agent_id
Expand Down Expand Up @@ -344,12 +350,89 @@ locals {
var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "",
local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : ""
)

# Common environment variables for install script
install_env_vars = <<-EOT
export ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}'
export ARG_MCP_APP_STATUS_SLUG='${local.app_slug}'
export ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}'
export ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}'
export ARG_INSTALL_VIA_NPM='${var.install_via_npm}'
export ARG_REPORT_TASKS='${var.report_tasks}'
export ARG_WORKDIR='${local.workdir}'
export ARG_ALLOWED_TOOLS='${var.allowed_tools}'
export ARG_DISALLOWED_TOOLS='${var.disallowed_tools}'
export ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}'
export ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}'
EOT

# Common environment variables for start script
start_env_vars = <<-EOT
export ARG_RESUME_SESSION_ID='${var.resume_session_id}'
export ARG_CONTINUE='${var.continue}'
export ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}'
export ARG_PERMISSION_MODE='${var.permission_mode}'
export ARG_WORKDIR='${local.workdir}'
export ARG_AI_PROMPT='${base64encode(var.ai_prompt)}'
export ARG_REPORT_TASKS='${var.report_tasks}'
export ARG_ENABLE_BOUNDARY='${var.enable_boundary}'
export ARG_BOUNDARY_VERSION='${var.boundary_version}'
export ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}'
export ARG_CODER_HOST='${local.coder_host}'
export ARG_NON_AGENTAPI_CLI='${!var.report_tasks && var.cli_app ? true : false}'
EOT

# Reusable install script command
install_command = <<-EOT
#!/bin/bash
set -o pipefail

echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh

chmod +x /tmp/install.sh
${local.install_env_vars}
/tmp/install.sh
EOT

# Reusable start script command for agentapi module
agentapi_start_command = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh

${local.start_env_vars}
/tmp/start.sh
EOT
}

resource "coder_script" "install_agent" {
count = !var.report_tasks ? 1 : 0

agent_id = var.agent_id
display_name = "Install agent"
run_on_start = true
log_path = "/home/coder/install.log"
script = local.install_command
}

resource "coder_app" "agent_cli" {
count = (!var.report_tasks && var.cli_app) ? 1 : 0

agent_id = var.agent_id
slug = local.app_slug
display_name = var.cli_app_display_name

command = length(trimprefix(var.cli_command, " ")) > 0 ? var.cli_command : local.agentapi_start_command
}


module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "2.0.0"

count = var.report_tasks ? 1 : 0
agent_id = var.agent_id
web_app_slug = local.app_slug
web_app_order = var.order
Expand All @@ -366,49 +449,10 @@ module "agentapi" {
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh

ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
ARG_CONTINUE='${var.continue}' \
ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \
ARG_PERMISSION_MODE='${var.permission_mode}' \
ARG_WORKDIR='${local.workdir}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \
ARG_BOUNDARY_VERSION='${var.boundary_version}' \
ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \
ARG_CODER_HOST='${local.coder_host}' \
/tmp/start.sh
EOT

install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail

echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \
ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \
ARG_INSTALL_VIA_NPM='${var.install_via_npm}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_WORKDIR='${local.workdir}' \
ARG_ALLOWED_TOOLS='${var.allowed_tools}' \
ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \
ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \
/tmp/install.sh
EOT
start_script = local.agentapi_start_command
install_script = local.install_command
}

output "task_app_id" {
value = module.agentapi.task_app_id
value = try(module.agentapi[0].task_app_id, null)
}
93 changes: 53 additions & 40 deletions registry/coder/modules/claude-code/scripts/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

set -euo pipefail

true > "$HOME/start.log"

command_exists() {
command -v "$1" > /dev/null 2>&1
}
Expand All @@ -17,34 +19,41 @@ ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false}
ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"}
ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false}
ARG_CODER_HOST=${ARG_CODER_HOST:-}
ARG_NON_AGENTAPI_CLI=${ARG_NON_AGENTAPI_CLI:-false}

echo "--------------------------------"
log() {
if [[ "${ARG_NON_AGENTAPI_CLI}" = "true" ]]; then
printf -- "$@" >> "$HOME/start.log"
else
printf -- "$@"
fi
}

printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID"
printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE"
printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS"
printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE"
printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT"
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY"
printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION"
printf "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE"
printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
log "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID"
log "ARG_CONTINUE: %s\n" "$ARG_CONTINUE"
log "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS"
log "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE"
log "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT"
log "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
log "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
log "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY"
log "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION"
log "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE"
log "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"

echo "--------------------------------"
log "--------------------------------\n"

function install_boundary() {
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
# Install boundary by compiling from source
echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)"
log "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)\n"

echo "Removing existing boundary directory to allow re-running the script safely"
log "Removing existing boundary directory to allow re-running the script safely\n"
if [ -d boundary ]; then
rm -rf boundary
fi

echo "Clone boundary repository"
log "Clone boundary repository\n"
git clone https://github.com/coder/boundary.git
cd boundary
git checkout "$ARG_BOUNDARY_VERSION"
Expand All @@ -58,16 +67,16 @@ function install_boundary() {
sudo chmod +x /usr/local/bin/boundary-run
else
# Install boundary using official install script
echo "Installing boundary using official install script (version: $ARG_BOUNDARY_VERSION)"
log "Installing boundary using official install script (version: $ARG_BOUNDARY_VERSION)\n"
curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | bash -s -- --version "$ARG_BOUNDARY_VERSION"
fi
}

function validate_claude_installation() {
if command_exists claude; then
printf "Claude Code is installed\n"
log "Claude Code is installed\n"
else
printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n"
log "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n"
exit 1
fi
}
Expand All @@ -91,10 +100,10 @@ task_session_exists() {
session_file=$(get_task_session_file)

if [ -f "$session_file" ]; then
printf "Task session file found: %s\n" "$session_file"
log "Task session file found: %s\n" "$session_file"
return 0
else
printf "Task session file not found: %s\n" "$session_file"
log "Task session file not found: %s\n" "$session_file"
return 1
fi
}
Expand All @@ -105,12 +114,12 @@ is_valid_session() {
# Check if file exists and is not empty
# Empty files indicate the session was created but never used so they need to be removed
if [ ! -f "$session_file" ]; then
printf "Session validation failed: file does not exist\n"
log "Session validation failed: file does not exist\n"
return 1
fi

if [ ! -s "$session_file" ]; then
printf "Session validation failed: file is empty, removing stale file\n"
log "Session validation failed: file is empty, removing stale file\n"
rm -f "$session_file"
return 1
fi
Expand All @@ -120,15 +129,15 @@ is_valid_session() {
local line_count
line_count=$(wc -l < "$session_file")
if [ "$line_count" -lt 2 ]; then
printf "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count"
log "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count"
rm -f "$session_file"
return 1
fi

# Validate JSONL format by checking first 3 lines
# Claude session files use JSONL (JSON Lines) format where each line is valid JSON
if ! head -3 "$session_file" | jq empty 2> /dev/null; then
printf "Session validation failed: invalid JSONL format, removing corrupt file\n"
log "Session validation failed: invalid JSONL format, removing corrupt file\n"
rm -f "$session_file"
return 1
fi
Expand All @@ -137,12 +146,12 @@ is_valid_session() {
# This ensures the file structure matches Claude's session format
if ! grep -q '"sessionId"' "$session_file" \
|| ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then
printf "Session validation failed: no valid sessionId found, removing malformed file\n"
log "Session validation failed: no valid sessionId found, removing malformed file\n"
rm -f "$session_file"
return 1
fi

printf "Session validation passed: %s\n" "$session_file"
log "Session validation passed: %s\n" "$session_file"
return 0
}

Expand All @@ -151,16 +160,21 @@ has_any_sessions() {
project_dir=$(get_project_dir)

if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then
printf "Sessions found in: %s\n" "$project_dir"
log "Sessions found in: %s\n" "$project_dir"
return 0
else
printf "No sessions found in: %s\n" "$project_dir"
log "No sessions found in: %s\n" "$project_dir"
return 1
fi
}

ARGS=()

CORE_COMMAND=()
if [[ "${ARG_REPORT_TASKS}" == "true" ]]; then
CORE_COMMAND+=(agentapi server --type claude --term-width 67 --term-height 1190 --)
fi

function start_agentapi() {
# For Task reporting
export CODER_MCP_ALLOWED_TOOLS="coder_report_task"
Expand All @@ -173,7 +187,7 @@ function start_agentapi() {
fi

if [ -n "$ARG_RESUME_SESSION_ID" ]; then
echo "Resuming specified session: $ARG_RESUME_SESSION_ID"
log "Resuming specified session: $ARG_RESUME_SESSION_ID"
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)

Expand All @@ -184,46 +198,45 @@ function start_agentapi() {
session_file=$(get_task_session_file)

if task_session_exists && is_valid_session "$session_file"; then
echo "Resuming task session: $TASK_SESSION_ID"
log "Resuming task session: $TASK_SESSION_ID"
ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions)
else
echo "Starting new task session: $TASK_SESSION_ID"
log "Starting new task session: $TASK_SESSION_ID"
ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions)
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
fi

else
if has_any_sessions; then
echo "Continuing most recent standalone session"
log "Continuing most recent standalone session"
ARGS+=(--continue)
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
else
echo "No sessions found, starting fresh standalone session"
log "No sessions found, starting fresh standalone session"
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
fi
fi

else
echo "Continue disabled, starting fresh session"
log "Continue disabled, starting fresh session"
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
fi

printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
log "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"

if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then
install_boundary

printf "Starting with coder boundary enabled\n"
log "Starting with coder boundary enabled\n"

BOUNDARY_ARGS+=()

agentapi server --type claude --term-width 67 --term-height 1190 -- \
boundary-run "${BOUNDARY_ARGS[@]}" -- \
"${CORE_COMMAND[@]}" boundary-run "${BOUNDARY_ARGS[@]}" -- \
claude "${ARGS[@]}"
else
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
"${CORE_COMMAND[@]}" claude "${ARGS[@]}"
fi
}

Expand Down
Loading