Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 19 additions & 3 deletions registry/coder-labs/modules/copilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ 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.4.0"
version = "0.5.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
}
```

> [!WARNING]
> **Security Notice**: This module runs Copilot with `--allow-all` by default, which enables all permissions (equivalent to `--allow-all-tools --allow-all-paths --allow-all-urls`). This bypasses permission prompts and allows Copilot unrestricted access to tools, file paths, and URLs. Use this module _only_ in trusted environments.

> [!IMPORTANT]
> This example assumes you have [Coder external authentication](https://coder.com/docs/admin/external-auth) configured with `id = "github"`. If not, you can provide a direct token using the `github_token` variable or provide the correct external authentication id for GitHub by setting `external_auth_id = "my-github"`.

Expand Down Expand Up @@ -51,7 +54,7 @@ data "coder_parameter" "ai_prompt" {

module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.4.0"
version = "0.5.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"

Expand All @@ -71,7 +74,7 @@ Customize tool permissions, MCP servers, and Copilot settings:
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.4.0"
version = "0.5.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"

Expand Down Expand Up @@ -215,6 +218,19 @@ By default, the module resumes the latest Copilot session when the workspace res
> [!NOTE]
> Session resumption requires persistent storage for the home directory or workspace volume. Without persistent storage, sessions will not resume across workspace restarts.

## State Persistence

AgentAPI can save and restore its conversation state to disk across workspace restarts. This complements `resume_session` (which resumes the Copilot CLI session) by also preserving the AgentAPI-level context. Enabled by default, requires agentapi >= v0.12.0 (older versions skip it with a warning).

To disable:

```tf
module "copilot" {
# ... other config
enable_state_persistence = false
}
```

## Troubleshooting

If you encounter any issues, check the log files in the `~/.copilot-module` directory within your workspace for detailed information.
Expand Down
29 changes: 29 additions & 0 deletions registry/coder-labs/modules/copilot/copilot.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,32 @@ run "aibridge_proxy_with_copilot_config" {
error_message = "copilot_model environment variable should be set alongside proxy"
}
}

run "enable_state_persistence_default" {
command = plan

variables {
agent_id = "test-agent"
workdir = "/home/coder"
}

assert {
condition = var.enable_state_persistence == true
error_message = "enable_state_persistence should default to true"
}
}

run "disable_state_persistence" {
command = plan

variables {
agent_id = "test-agent"
workdir = "/home/coder"
enable_state_persistence = false
}

assert {
condition = var.enable_state_persistence == false
error_message = "enable_state_persistence should be false when explicitly disabled"
}
}
52 changes: 33 additions & 19 deletions registry/coder-labs/modules/copilot/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ variable "subdomain" {
default = false
}

variable "enable_state_persistence" {
type = bool
description = "Enable AgentAPI conversation state persistence across restarts."
default = true
}

variable "order" {
type = number
description = "The order determines the position of app in the UI presentation."
Expand Down Expand Up @@ -155,6 +161,12 @@ variable "cli_app_display_name" {
default = "Copilot"
}

variable "allow_all" {
type = bool
description = "Allow all tools without prompting (equivalent to --allow-all)."
default = true
}

variable "resume_session" {
type = bool
description = "Whether to automatically resume the latest Copilot session on workspace restart."
Expand Down Expand Up @@ -271,25 +283,26 @@ resource "coder_env" "github_token" {

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

agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_icon = var.cli_app ? var.icon : 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
version = "2.2.0"

agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_icon = var.cli_app ? var.icon : 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
enable_state_persistence = var.enable_state_persistence
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script

start_script = <<-EOT
#!/bin/bash
Expand All @@ -299,6 +312,7 @@ module "agentapi" {
chmod +x /tmp/start.sh

ARG_WORKDIR='${local.workdir}' \
ARG_ALLOW_ALL='${var.allow_all}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
ARG_COPILOT_MODEL='${var.copilot_model}' \
Expand Down
54 changes: 9 additions & 45 deletions registry/coder-labs/modules/copilot/scripts/install.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
#!/bin/bash

if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi

set -euo pipefail

export PATH="$HOME/.local/bin:$PATH"
Copy link
Member

Choose a reason for hiding this comment

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

Should we not check if it exists before prepending the $PATH?


command_exists() {
command -v "$1" > /dev/null 2>&1
}
Expand All @@ -19,34 +17,13 @@ ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
ARG_COPILOT_VERSION=${ARG_COPILOT_VERSION:-0.0.334}
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-claude-sonnet-4.5}

validate_prerequisites() {
if ! command_exists node; then
echo "ERROR: Node.js not found. Copilot requires Node.js v22+."
echo "Install with: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"
exit 1
fi

if ! command_exists npm; then
echo "ERROR: npm not found. Copilot requires npm v10+."
exit 1
fi

node_version=$(node --version | sed 's/v//' | cut -d. -f1)
if [ "$node_version" -lt 22 ]; then
echo "WARNING: Node.js v$node_version detected. Copilot requires v22+."
fi
}

install_copilot() {
if ! command_exists copilot; then
echo "Installing GitHub Copilot CLI (version: ${ARG_COPILOT_VERSION})..."
if [ "$ARG_COPILOT_VERSION" = "latest" ]; then
npm install -g @github/copilot
else
npm install -g "@github/copilot@${ARG_COPILOT_VERSION}"
fi
curl -fsSL https://gh.io/copilot-install | VERSION="${ARG_COPILOT_VERSION}" bash

if ! command_exists copilot; then
echo "PATH after installation: $PATH"
echo "ERROR: Failed to install Copilot"
exit 1
fi
Expand Down Expand Up @@ -95,7 +72,7 @@ setup_copilot_configurations() {
}

setup_copilot_config() {
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@DevelopmentCats I changed the paths here, any reason why we'd want to keep it as $HOME/.config ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we have had a lot of modules that store their configuration, and logs in random places like the home root, or /tmp.

There has been a push to normalize this across all modules. so you are probably fine to just leave as is and I will take care of it in another PR

#782

export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME}"
local copilot_config_dir="$XDG_CONFIG_HOME/.copilot"
local copilot_config_file="$copilot_config_dir/config.json"
local mcp_config_file="$copilot_config_dir/mcp-config.json"
Expand Down Expand Up @@ -190,27 +167,15 @@ add_custom_mcp_servers() {
local updated_config
updated_config=$(jq --argjson custom "$custom_servers" '.mcpServers += $custom' "$mcp_config_file")
echo "$updated_config" > "$mcp_config_file"
elif command_exists node; then
node -e "
const fs = require('fs');
const existing = JSON.parse(fs.readFileSync('$mcp_config_file', 'utf8'));
const input = JSON.parse(\`$ARG_MCP_CONFIG\`);
const custom = input.mcpServers || {};
existing.mcpServers = {...existing.mcpServers, ...custom};
fs.writeFileSync('$mcp_config_file', JSON.stringify(existing, null, 2));
"
else
echo "WARNING: jq and node not available, cannot merge custom MCP servers"
echo "WARNING: jq not available, cannot merge custom MCP servers"
fi
}

configure_copilot_model() {
if [ -n "$ARG_COPILOT_MODEL" ] && [ "$ARG_COPILOT_MODEL" != "claude-sonnet-4.5" ]; then
echo "Setting Copilot model to: $ARG_COPILOT_MODEL"
copilot config model "$ARG_COPILOT_MODEL" || {
echo "WARNING: Failed to set model via copilot config, will use environment variable fallback"
export COPILOT_MODEL="$ARG_COPILOT_MODEL"
}
if [[ -n "${ARG_COPILOT_MODEL}" ]]; then
echo "Setting Copilot model to: ${ARG_COPILOT_MODEL}"
export COPILOT_MODEL="${ARG_COPILOT_MODEL}"
fi
}

Expand All @@ -227,7 +192,6 @@ configure_coder_integration() {
fi
}

validate_prerequisites
install_copilot
check_github_authentication
setup_copilot_configurations
Expand Down
53 changes: 20 additions & 33 deletions registry/coder-labs/modules/copilot/scripts/start.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
#!/bin/bash

if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi

set -euo pipefail

export PATH="$HOME/.local/bin:$PATH"
Expand All @@ -13,6 +9,7 @@ command_exists() {
}

ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_ALLOW_ALL=${ARG_ALLOW_ALL:-true}
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-}
Expand All @@ -28,7 +25,7 @@ ARG_AIBRIDGE_PROXY_CERT_PATH=${ARG_AIBRIDGE_PROXY_CERT_PATH:-}

validate_copilot_installation() {
if ! command_exists copilot; then
echo "ERROR: Copilot not installed. Run: npm install -g @github/copilot"
echo "ERROR: Copilot not installed or not in PATH. Please ensure Copilot CLI is installed and accessible."
exit 1
fi
}
Expand Down Expand Up @@ -73,6 +70,20 @@ build_copilot_args() {
fi
done
fi

if [ "$ARG_ALLOW_ALL" = "true" ]; then
COPILOT_ARGS+=(--allow-all)
fi

if check_existing_session; then
COPILOT_ARGS+=(--continue)
else
local initial_prompt
initial_prompt=$(build_initial_prompt)
if [[ -n "${initial_prompt}" ]]; then
COPILOT_ARGS+=(-i "${initial_prompt}")
fi
fi
}

check_existing_session() {
Expand Down Expand Up @@ -169,35 +180,11 @@ start_agentapi() {

build_copilot_args

if check_existing_session; then
echo "Continuing latest Copilot session..."
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue "${COPILOT_ARGS[@]}"
else
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue
fi
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 67 --term-height 1190 -- copilot "${COPILOT_ARGS[@]}"
else
echo "Starting new Copilot session..."
local initial_prompt
initial_prompt=$(build_initial_prompt)

if [ -n "$initial_prompt" ]; then
echo "Using initial prompt with system context"
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
else
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot
fi
else
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
else
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot
fi
fi
agentapi server --type copilot --term-width 67 --term-height 1190 -- copilot
fi
}

Expand Down
Loading