Compare commits

...

11 Commits

Author SHA1 Message Date
35C4n0r c1c0dec90f chore: bump agentapi module version (#465) 2025-10-07 18:09:44 +00:00
DevCats 59b67c2c98 chore: update display name for copilot module to Copilot CLI (#467)
## Description

update display name for copilot module to Copilot CLI

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [X] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.1.2`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-07 17:40:23 +00:00
DevCats 7abe422e0a fix: Add COPILOT_MODEL to install script args (#464)
Closes #462

## Description

<!-- Briefly describe what this PR does and why -->
Fixes missing COPILOT_MODEL arg from install script

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.1.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-07 12:05:50 -05:00
Susana Ferreira db8217e4e5 fix(claude-code): update inner system prompt to include summary rules (#461)
## Description

Update `report_tasks_system_prompt` to include `coder_report_task`
summary rules.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.0.3`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

Follow-up from: https://github.com/coder/registry/pull/443
Related to: https://github.com/coder/coder/pull/20191/files#r2410441026
2025-10-07 15:26:09 +01:00
DevCats f75afeb0c8 feat: New Copilot-CLI Module (#441)
## Description

New Copilot-CLI Module using AgentAPI

Need to test once AgentAPI Changes are pushed.

## Type of Change

- [X] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder-labs/modules/copilot-cli`  
**New version:** `v0.1.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-10-07 07:47:02 -05:00
Susana Ferreira 182e5548e2 chore: update MAINTAINER.md to check PR version label (#460)
## Description

Update MAINTAINER.md to include a check of the version label on the PRs

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [x] Documentation
- [ ] Other
2025-10-07 10:47:41 +01:00
Susana Ferreira d057a820c1 feat(claude-code): add coder-specific prompt to system_prompt (#443)
## Description

This PR updates the `claude-code` module to automatically include the
Coder task-reporting system prompt whenever `report_tasks = true`, and
to wrap the final system prompt in `<system>…</system>` when non-empty.

Previously, users needed to manually include this content in their
system prompts to enable proper task reporting. When `report_tasks =
true`, the system prompt is prepended with the Coder task-reporting, and
any user `system_prompt` (if provided) is appended after it, ensuring
consistent integration without manual copy/paste.

When `report_tasks = false`, the module includes only the user
`system_prompt` (if any). If both `report_tasks = false` and
`system_prompt` is empty, the system prompt sent to Claude is empty.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.0.2` 
**Breaking change:** [] Yes [x]  No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

Related to internal slack thread:
https://codercom.slack.com/archives/C0992H8HGCS/p1759317555713269

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-07 10:09:49 +01:00
Satbir Chahal b4e9545c35 fix(claude-code): source bashrc file only if it exists (#459) 2025-10-07 07:33:17 +00:00
DevCats 50ac3b31f6 docs: add MAINTAINER.md link to CONTRIBUTING.md and README.md (#453)
## Description

<!-- Briefly describe what this PR does and why -->
Add links to `MAINTAINER.md` in `README.md` and `CONTRIBUTING.md` to
help guide internal contributors.

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [X] Documentation
- [ ] Other

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-06 12:52:58 -05:00
dependabot[bot] 056937a758 chore(deps): bump crate-ci/typos from 1.36.3 to 1.37.2 in the github-actions group (#451)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M Atif Ali <atif@coder.com>
2025-10-06 07:59:23 -05:00
Rowan Smith af8b4f02fd chore: fix for jetbrains gateway agent_id issue (#437)
## Description

Fixes a regression added in #167 which implemented support for multiple
agents by appending the agent id to the URI, however in a single agent
environment it results in the agent id from the template apply (on
upload to Coder from client) being injected, and when a workspace is
later built using the template the agent id is no longer correct.

Resolves the error `The workspace “<name>” does not have an agent with
ID “<id>”` being thrown by Jetbrains Gateway app upon attempting to open
a Jetbrains app from within a Coder workspace.

When wishing to target a specific Coder Agent with the Jetbrains Gateway
module one should use the `agent_name` variable in the module
configuration to specify the desired agent name. This will append the
agent name to the URI.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/jetbrains-gateway`  
**New version:** `v1.2.4`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

Reported by customer on Zendesk ticket 4391
2025-10-06 08:29:33 +11:00
21 changed files with 1460 additions and 27 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.36.3
uses: crate-ci/typos@v1.37.2
with:
config: .github/typos.toml
validate-readme-files:
+4
View File
@@ -495,4 +495,8 @@ When reporting bugs, include:
4. **Breaking changes** without defaults
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
## For Maintainers
Guidelines for reviewing PRs, managing releases, and maintaining the registry. [See the maintainer guide for detailed information.](./MAINTAINER.md)
Happy contributing! 🚀
+3 -1
View File
@@ -23,6 +23,7 @@ Check that PRs have:
- [ ] Working tests (`terraform test`)
- [ ] Formatted code (`bun run fmt`)
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
- [ ] Version label: `version:patch`, `version:minor`, or `version:major`
### Version Guidelines
@@ -32,7 +33,8 @@ When reviewing PRs, ensure the version change follows semantic versioning:
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
PRs should clearly indicate the version change (e.g., `v1.2.3 → v1.2.4`).
PRs should clearly indicate the intended version change (e.g., `v1.2.3 → v1.2.4`) and include the appropriate label: `version:patch`, `version:minor`, or `version:major`.
The “Version Bump” CI uses this label to validate required updates (README version refs, etc.).
### Validate READMEs
+4
View File
@@ -48,3 +48,7 @@ Simply include that snippet inside your Coder template, defining any data depend
## Contributing
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
## For Maintainers
Guidelines for maintainers reviewing PRs and managing releases. [See the maintainer guide for more information.](./MAINTAINER.md)
@@ -0,0 +1,210 @@
---
display_name: Copilot CLI
description: GitHub Copilot CLI agent for AI-powered terminal assistance
icon: ../../../../.icons/github.svg
verified: false
tags: [agent, copilot, ai, github, tasks]
---
# Copilot
Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) in your workspace for AI-powered coding assistance directly from the terminal. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.1.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
}
```
> [!IMPORTANT]
> This example assumes you have [Coder external authentication](https://coder.com/docs/admin/external-auth) configured with `id = "github"`. If not, you can provide a direct token using the `github_token` variable or provide the correct external authentication id for GitHub by setting `external_auth_id = "my-github"`.
> [!NOTE]
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
## Prerequisites
- **Node.js v22+** and **npm v10+**
- **[Active Copilot subscription](https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot)** (GitHub Copilot Pro, Pro+, Business, or Enterprise)
- **GitHub authentication** via one of:
- [Coder external authentication](https://coder.com/docs/admin/external-auth) (recommended)
- Direct token via `github_token` variable
- Interactive login in Copilot
## Examples
### Usage with Tasks
For development environments where you want Copilot to have full access to tools and automatically resume sessions:
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Initial task prompt for Copilot."
mutable = true
}
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.1.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
ai_prompt = data.coder_parameter.ai_prompt.value
copilot_model = "claude-sonnet-4.5"
allow_all_tools = true
resume_session = true
trusted_directories = ["/home/coder/projects", "/tmp"]
}
```
### Advanced Configuration
Customize tool permissions, MCP servers, and Copilot settings:
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.1.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
# Version pinning (defaults to "0.0.334", use "latest" for newest version)
copilot_version = "latest"
# Tool permissions
allow_tools = ["shell(git)", "shell(npm)", "write"]
trusted_directories = ["/home/coder/projects", "/tmp"]
# Custom Copilot configuration
copilot_config = jsonencode({
banner = "never"
theme = "dark"
})
# MCP server configuration
mcp_config = jsonencode({
mcpServers = {
filesystem = {
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
description = "Provides file system access to the workspace"
name = "Filesystem"
timeout = 3000
type = "local"
tools = ["*"]
trust = true
}
playwright = {
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
description = "Browser automation for testing and previewing changes"
name = "Playwright"
timeout = 5000
type = "local"
tools = ["*"]
trust = false
}
}
})
# Pre-install Node.js if needed
pre_install_script = <<-EOT
#!/bin/bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
EOT
}
```
> [!NOTE]
> GitHub Copilot CLI does not automatically install MCP servers. You have two options:
>
> - Use `npx -y` in the MCP config (shown above) to auto-install on each run
> - Pre-install MCP servers in `pre_install_script` for faster startup (e.g., `npm install -g @modelcontextprotocol/server-filesystem`)
### Direct Token Authentication
Use this example when you want to provide a GitHub Personal Access Token instead of using Coder external auth:
```tf
variable "github_token" {
type = string
description = "GitHub Personal Access Token"
sensitive = true
}
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.1.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
github_token = var.github_token
}
```
### Standalone Mode
Run Copilot as a command-line tool without task reporting or web interface. This installs and configures Copilot, making it available as a CLI app in the Coder agent bar that you can launch to interact with Copilot directly from your terminal. Set `report_tasks = false` to disable integration with Coder Tasks.
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.1.2"
agent_id = coder_agent.example.id
workdir = "/home/coder"
report_tasks = false
cli_app = true
}
```
## Authentication
The module supports multiple authentication methods (in priority order):
1. **[Coder External Auth](https://coder.com/docs/admin/external-auth) (Recommended)** - Automatic if GitHub external auth is configured in Coder
2. **Direct Token** - Pass `github_token` variable (OAuth or Personal Access Token)
3. **Interactive** - Copilot prompts for login via `/login` command if no auth found
> [!NOTE]
> OAuth tokens work best with Copilot. Personal Access Tokens may have limited functionality.
## Session Resumption
By default, the module resumes the latest Copilot session when the workspace restarts. Set `resume_session = false` to always start fresh sessions.
> [!NOTE]
> Session resumption requires persistent storage for the home directory or workspace volume. Without persistent storage, sessions will not resume across workspace restarts.
## Troubleshooting
If you encounter any issues, check the log files in the `~/.copilot-module` directory within your workspace for detailed information.
```bash
# Installation logs
cat ~/.copilot-module/install.log
# Startup logs
cat ~/.copilot-module/agentapi-start.log
# Pre/post install script logs
cat ~/.copilot-module/pre_install.log
cat ~/.copilot-module/post_install.log
```
> [!NOTE]
> To use tasks with Copilot, you must have an active GitHub Copilot subscription.
> The `workdir` variable is required and specifies the directory where Copilot will run.
## References
- [GitHub Copilot CLI Documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
- [Installing GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -0,0 +1,236 @@
run "defaults_are_correct" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
}
assert {
condition = var.copilot_model == "claude-sonnet-4.5"
error_message = "Default model should be 'claude-sonnet-4.5'"
}
assert {
condition = var.report_tasks == true
error_message = "Task reporting should be enabled by default"
}
assert {
condition = var.resume_session == true
error_message = "Session resumption should be enabled by default"
}
assert {
condition = var.allow_all_tools == false
error_message = "allow_all_tools should be disabled by default"
}
assert {
condition = resource.coder_env.mcp_app_status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug env var should be created"
}
assert {
condition = resource.coder_env.mcp_app_status_slug.value == "copilot"
error_message = "Status slug value should be 'copilot'"
}
}
run "github_token_creates_env_var" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
github_token = "test_github_token_abc123"
}
assert {
condition = length(resource.coder_env.github_token) == 1
error_message = "github_token env var should be created when token is provided"
}
assert {
condition = resource.coder_env.github_token[0].name == "GITHUB_TOKEN"
error_message = "github_token env var name should be 'GITHUB_TOKEN'"
}
assert {
condition = resource.coder_env.github_token[0].value == "test_github_token_abc123"
error_message = "github_token env var value should match input"
}
}
run "github_token_not_created_when_empty" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
github_token = ""
}
assert {
condition = length(resource.coder_env.github_token) == 0
error_message = "github_token env var should not be created when empty"
}
}
run "copilot_model_env_var_for_non_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "claude-sonnet-4"
}
assert {
condition = length(resource.coder_env.copilot_model) == 1
error_message = "copilot_model env var should be created for non-default model"
}
assert {
condition = resource.coder_env.copilot_model[0].name == "COPILOT_MODEL"
error_message = "copilot_model env var name should be 'COPILOT_MODEL'"
}
assert {
condition = resource.coder_env.copilot_model[0].value == "claude-sonnet-4"
error_message = "copilot_model env var value should match input"
}
}
run "copilot_model_not_created_for_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "claude-sonnet-4.5"
}
assert {
condition = length(resource.coder_env.copilot_model) == 0
error_message = "copilot_model env var should not be created for default model"
}
}
run "model_validation_accepts_valid_models" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "gpt-5"
}
assert {
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
error_message = "Model should be one of the valid options"
}
}
run "copilot_config_merges_with_trusted_directories" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
trusted_directories = ["/workspace", "/data"]
}
assert {
condition = length(local.final_copilot_config) > 0
error_message = "final_copilot_config should be computed"
}
# Verify workdir is trimmed of trailing slash
assert {
condition = local.workdir == "/home/coder/project"
error_message = "workdir should be trimmed of trailing slash"
}
}
run "custom_copilot_config_overrides_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_config = jsonencode({
banner = "always"
theme = "dark"
})
}
assert {
condition = var.copilot_config != ""
error_message = "Custom copilot config should be set"
}
assert {
condition = jsondecode(local.final_copilot_config).banner == "always"
error_message = "Custom banner setting should be applied"
}
assert {
condition = jsondecode(local.final_copilot_config).theme == "dark"
error_message = "Custom theme setting should be applied"
}
}
run "trusted_directories_merged_with_custom_config" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
copilot_config = jsonencode({
banner = "always"
theme = "dark"
trusted_folders = ["/custom"]
})
trusted_directories = ["/workspace", "/data"]
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/custom")
error_message = "Custom trusted folder should be included"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/home/coder/project")
error_message = "Workdir should be included in trusted folders"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/workspace")
error_message = "trusted_directories should be merged into config"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/data")
error_message = "All trusted_directories should be merged into config"
}
}
run "app_slug_is_consistent" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
}
assert {
condition = local.app_slug == "copilot"
error_message = "app_slug should be 'copilot'"
}
assert {
condition = local.module_dir_name == ".copilot-module"
error_message = "module_dir_name should be '.copilot-module'"
}
}
@@ -0,0 +1,136 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("copilot", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
it("creates mcp_app_status_slug env var", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"mcp_app_status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("copilot");
});
it("creates github_token env var with correct value", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
github_token: "test_token_12345",
});
const githubTokenEnv = findResourceInstance(
state,
"coder_env",
"github_token",
);
expect(githubTokenEnv).toBeDefined();
expect(githubTokenEnv.name).toBe("GITHUB_TOKEN");
expect(githubTokenEnv.value).toBe("test_token_12345");
});
it("does not create github_token env var when empty", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
github_token: "",
});
const githubTokenEnvs = state.resources.filter(
(r) => r.type === "coder_env" && r.name === "github_token",
);
expect(githubTokenEnvs.length).toBe(0);
});
it("creates copilot_model env var for non-default models", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4",
});
const modelEnv = findResourceInstance(state, "coder_env", "copilot_model");
expect(modelEnv).toBeDefined();
expect(modelEnv.name).toBe("COPILOT_MODEL");
expect(modelEnv.value).toBe("claude-sonnet-4");
});
it("does not create copilot_model env var for default model", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4.5",
});
const modelEnvs = state.resources.filter(
(r) => r.type === "coder_env" && r.name === "copilot_model",
);
expect(modelEnvs.length).toBe(0);
});
it("creates coder_script resources via agentapi module", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
// The agentapi module should create coder_script resources for install and start
const scripts = state.resources.filter((r) => r.type === "coder_script");
expect(scripts.length).toBeGreaterThan(0);
});
it("validates copilot_model accepts valid values", async () => {
// Test valid models don't throw errors
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "gpt-5",
}),
).resolves.toBeDefined();
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4.5",
}),
).resolves.toBeDefined();
});
it("merges trusted_directories with custom copilot_config", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder/project",
trusted_directories: JSON.stringify(["/workspace", "/data"]),
copilot_config: JSON.stringify({
banner: "always",
theme: "dark",
trusted_folders: ["/custom"],
}),
});
// Verify that the state was created successfully with the merged config
// The actual merging logic is tested in the .tftest.hcl file
expect(state).toBeDefined();
expect(state.resources).toBeDefined();
});
});
+301
View File
@@ -0,0 +1,301 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "workdir" {
type = string
description = "The folder to run Copilot in."
}
variable "external_auth_id" {
type = string
description = "ID of the GitHub external auth provider configured in Coder."
default = "github"
}
variable "github_token" {
type = string
description = "GitHub OAuth token or Personal Access Token. If provided, this will be used instead of auto-detecting authentication."
default = ""
sensitive = true
}
variable "copilot_model" {
type = string
description = "Model to use. Supported values: claude-sonnet-4, claude-sonnet-4.5 (default), gpt-5."
default = "claude-sonnet-4.5"
validation {
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
error_message = "copilot_model must be one of: claude-sonnet-4, claude-sonnet-4.5, gpt-5."
}
}
variable "copilot_config" {
type = string
description = "Custom Copilot configuration as JSON string. Leave empty to use default configuration with banner disabled, theme set to auto, and workdir as trusted folder."
default = ""
}
variable "ai_prompt" {
type = string
description = "Initial task prompt for programmatic mode."
default = ""
}
variable "system_prompt" {
type = string
description = "The system prompt to use for the Copilot server. Task reporting instructions are automatically added when report_tasks is enabled."
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
}
variable "trusted_directories" {
type = list(string)
description = "Additional directories to trust for Copilot operations."
default = []
}
variable "allow_all_tools" {
type = bool
description = "Allow all tools without prompting (equivalent to --allow-all-tools)."
default = false
}
variable "allow_tools" {
type = list(string)
description = "Specific tools to allow: shell(command), write, or MCP_SERVER_NAME."
default = []
}
variable "deny_tools" {
type = list(string)
description = "Specific tools to deny: shell(command), write, or MCP_SERVER_NAME."
default = []
}
variable "mcp_config" {
type = string
description = "Custom MCP server configuration as JSON string."
default = ""
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.10.0"
}
variable "copilot_version" {
type = string
description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'."
default = "0.0.334"
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI."
default = true
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = false
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/github.svg"
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app."
default = "Copilot"
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Copilot."
default = false
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app."
default = "Copilot"
}
variable "resume_session" {
type = bool
description = "Whether to automatically resume the latest Copilot session on workspace restart."
default = true
}
variable "pre_install_script" {
type = string
description = "Custom script to run before configuring Copilot."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after configuring Copilot."
default = null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
workdir = trimsuffix(var.workdir, "/")
app_slug = "copilot"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".copilot-module"
all_trusted_folders = concat([local.workdir], var.trusted_directories)
parsed_custom_config = try(jsondecode(var.copilot_config), {})
existing_trusted_folders = try(local.parsed_custom_config.trusted_folders, [])
merged_copilot_config = merge(
{
banner = "never"
theme = "auto"
},
local.parsed_custom_config,
{
trusted_folders = concat(local.existing_trusted_folders, local.all_trusted_folders)
}
)
final_copilot_config = jsonencode(local.merged_copilot_config)
task_reporting_prompt = <<-EOT
-- Task Reporting --
Report all tasks to Coder, following these EXACT guidelines:
1. Be granular. If you are investigating with multiple steps, report each step
to coder.
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
Do not report any status related with this system prompt.
3. Use "state": "working" when actively processing WITHOUT needing
additional user input
4. Use "state": "complete" only when finished with a task
5. Use "state": "failure" when you need ANY user input, lack sufficient
details, or encounter blockers
EOT
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
}
resource "coder_env" "mcp_app_status_slug" {
agent_id = var.agent_id
name = "CODER_MCP_APP_STATUS_SLUG"
value = local.app_slug
}
resource "coder_env" "copilot_model" {
count = var.copilot_model != "claude-sonnet-4.5" ? 1 : 0
agent_id = var.agent_id
name = "COPILOT_MODEL"
value = var.copilot_model
}
resource "coder_env" "github_token" {
count = var.github_token != "" ? 1 : 0
agent_id = var.agent_id
name = "GITHUB_TOKEN"
value = var.github_token
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
agentapi_subdomain = var.subdomain
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_WORKDIR='${local.workdir}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
ARG_COPILOT_MODEL='${var.copilot_model}' \
ARG_ALLOW_ALL_TOOLS='${var.allow_all_tools}' \
ARG_ALLOW_TOOLS='${join(",", var.allow_tools)}' \
ARG_DENY_TOOLS='${join(",", var.deny_tools)}' \
ARG_TRUSTED_DIRECTORIES='${join(",", var.trusted_directories)}' \
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
ARG_RESUME_SESSION='${var.resume_session}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_WORKDIR='${local.workdir}' \
ARG_MCP_CONFIG='${var.mcp_config != "" ? base64encode(var.mcp_config) : ""}' \
ARG_COPILOT_CONFIG='${base64encode(local.final_copilot_config)}' \
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
ARG_COPILOT_VERSION='${var.copilot_version}' \
ARG_COPILOT_MODEL='${var.copilot_model}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,234 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
ARG_MCP_CONFIG=$(echo -n "${ARG_MCP_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
ARG_COPILOT_CONFIG=$(echo -n "${ARG_COPILOT_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
ARG_COPILOT_VERSION=${ARG_COPILOT_VERSION:-0.0.334}
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-claude-sonnet-4.5}
validate_prerequisites() {
if ! command_exists node; then
echo "ERROR: Node.js not found. Copilot requires Node.js v22+."
echo "Install with: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"
exit 1
fi
if ! command_exists npm; then
echo "ERROR: npm not found. Copilot requires npm v10+."
exit 1
fi
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
if [ "$node_version" -lt 22 ]; then
echo "WARNING: Node.js v$node_version detected. Copilot requires v22+."
fi
}
install_copilot() {
if ! command_exists copilot; then
echo "Installing GitHub Copilot CLI (version: ${ARG_COPILOT_VERSION})..."
if [ "$ARG_COPILOT_VERSION" = "latest" ]; then
npm install -g @github/copilot
else
npm install -g "@github/copilot@${ARG_COPILOT_VERSION}"
fi
if ! command_exists copilot; then
echo "ERROR: Failed to install Copilot"
exit 1
fi
echo "GitHub Copilot CLI installed successfully"
else
echo "GitHub Copilot CLI already installed"
fi
}
check_github_authentication() {
echo "Checking GitHub authentication..."
if [ -n "${GITHUB_TOKEN:-}" ]; then
echo "✓ GitHub token provided via module configuration"
return 0
fi
if command_exists coder; then
if coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" > /dev/null 2>&1; then
echo "✓ GitHub OAuth authentication via Coder external auth"
return 0
fi
fi
if command_exists gh && gh auth status > /dev/null 2>&1; then
echo "✓ GitHub OAuth authentication via GitHub CLI"
return 0
fi
echo "⚠ No GitHub authentication detected"
echo " Copilot will prompt for authentication when started"
echo " For seamless experience, configure GitHub external auth in Coder or run 'gh auth login'"
return 0
}
setup_copilot_configurations() {
mkdir -p "$ARG_WORKDIR"
local module_path="$HOME/.copilot-module"
mkdir -p "$module_path"
setup_copilot_config
echo "$ARG_WORKDIR" > "$module_path/trusted_directories"
}
setup_copilot_config() {
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
local copilot_config_dir="$XDG_CONFIG_HOME/.copilot"
local copilot_config_file="$copilot_config_dir/config.json"
local mcp_config_file="$copilot_config_dir/mcp-config.json"
mkdir -p "$copilot_config_dir"
if [ -n "$ARG_COPILOT_CONFIG" ]; then
echo "Setting up Copilot configuration..."
if command_exists jq; then
echo "$ARG_COPILOT_CONFIG" | jq 'del(.mcpServers)' > "$copilot_config_file"
else
echo "$ARG_COPILOT_CONFIG" > "$copilot_config_file"
fi
echo "Setting up MCP server configuration..."
setup_mcp_config "$mcp_config_file"
else
echo "ERROR: No Copilot configuration provided"
exit 1
fi
}
setup_mcp_config() {
local mcp_config_file="$1"
echo '{"mcpServers": {}}' > "$mcp_config_file"
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
echo "Adding Coder MCP server for task reporting..."
setup_coder_mcp_server "$mcp_config_file"
fi
if [ -n "$ARG_MCP_CONFIG" ]; then
echo "Adding custom MCP servers..."
add_custom_mcp_servers "$mcp_config_file"
fi
echo "MCP configuration completed: $mcp_config_file"
}
setup_coder_mcp_server() {
local mcp_config_file="$1"
local coder_mcp_wrapper_script
coder_mcp_wrapper_script=$(
cat << EOF
#!/usr/bin/env bash
set -e
export CODER_MCP_APP_STATUS_SLUG="${ARG_MCP_APP_STATUS_SLUG}"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
export CODER_AGENT_URL="${CODER_AGENT_URL}"
export CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN}"
exec coder exp mcp server
EOF
)
echo "$coder_mcp_wrapper_script" > "/tmp/coder-mcp-server.sh"
chmod +x /tmp/coder-mcp-server.sh
local coder_mcp_config
coder_mcp_config=$(
cat << EOF
{
"mcpServers": {
"coder": {
"command": "/tmp/coder-mcp-server.sh",
"args": [],
"description": "CRITICAL: Call this tool immediately after receiving ANY user message to report task status. Rules: (1) Call FIRST before doing work - report what you will do with state='working'. (2) Be granular - report each step separately. (3) State 'working' = actively processing without needing user input. (4) State 'complete' = task 100% finished. (5) State 'failure' = need user input, missing info, or blocked. Example: User says 'fix the bug' -> call with state='working', description='Investigating authentication bug'. When done -> call with state='complete', description='Fixed token validation'. You MUST report on every interaction.",
"name": "Coder",
"timeout": 3000,
"type": "local",
"tools": ["*"],
"trust": true
}
}
}
EOF
)
echo "$coder_mcp_config" > "$mcp_config_file"
}
add_custom_mcp_servers() {
local mcp_config_file="$1"
if command_exists jq; then
local custom_servers
custom_servers=$(echo "$ARG_MCP_CONFIG" | jq '.mcpServers // {}')
local updated_config
updated_config=$(jq --argjson custom "$custom_servers" '.mcpServers += $custom' "$mcp_config_file")
echo "$updated_config" > "$mcp_config_file"
elif command_exists node; then
node -e "
const fs = require('fs');
const existing = JSON.parse(fs.readFileSync('$mcp_config_file', 'utf8'));
const input = JSON.parse(\`$ARG_MCP_CONFIG\`);
const custom = input.mcpServers || {};
existing.mcpServers = {...existing.mcpServers, ...custom};
fs.writeFileSync('$mcp_config_file', JSON.stringify(existing, null, 2));
"
else
echo "WARNING: jq and node not available, cannot merge custom MCP servers"
fi
}
configure_copilot_model() {
if [ -n "$ARG_COPILOT_MODEL" ] && [ "$ARG_COPILOT_MODEL" != "claude-sonnet-4.5" ]; then
echo "Setting Copilot model to: $ARG_COPILOT_MODEL"
copilot config model "$ARG_COPILOT_MODEL" || {
echo "WARNING: Failed to set model via copilot config, will use environment variable fallback"
export COPILOT_MODEL="$ARG_COPILOT_MODEL"
}
fi
}
configure_coder_integration() {
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
echo "Configuring Copilot task reporting..."
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
echo "✓ Coder MCP server configured for task reporting"
else
echo "Task reporting disabled or no app status slug provided."
export CODER_MCP_APP_STATUS_SLUG=""
export CODER_MCP_AI_AGENTAPI_URL=""
fi
}
validate_prerequisites
install_copilot
check_github_authentication
setup_copilot_configurations
configure_copilot_model
configure_coder_integration
echo "Copilot module setup completed."
@@ -0,0 +1,157 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-}
ARG_ALLOW_ALL_TOOLS=${ARG_ALLOW_ALL_TOOLS:-false}
ARG_ALLOW_TOOLS=${ARG_ALLOW_TOOLS:-}
ARG_DENY_TOOLS=${ARG_DENY_TOOLS:-}
ARG_TRUSTED_DIRECTORIES=${ARG_TRUSTED_DIRECTORIES:-}
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
ARG_RESUME_SESSION=${ARG_RESUME_SESSION:-true}
validate_copilot_installation() {
if ! command_exists copilot; then
echo "ERROR: Copilot not installed. Run: npm install -g @github/copilot"
exit 1
fi
}
build_initial_prompt() {
local initial_prompt=""
if [ -n "$ARG_AI_PROMPT" ]; then
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
initial_prompt="$ARG_SYSTEM_PROMPT
$ARG_AI_PROMPT"
else
initial_prompt="$ARG_AI_PROMPT"
fi
fi
echo "$initial_prompt"
}
build_copilot_args() {
COPILOT_ARGS=()
if [ "$ARG_ALLOW_ALL_TOOLS" = "true" ]; then
COPILOT_ARGS+=(--allow-all-tools)
fi
if [ -n "$ARG_ALLOW_TOOLS" ]; then
IFS=',' read -ra ALLOW_ARRAY <<< "$ARG_ALLOW_TOOLS"
for tool in "${ALLOW_ARRAY[@]}"; do
if [ -n "$tool" ]; then
COPILOT_ARGS+=(--allow-tool "$tool")
fi
done
fi
if [ -n "$ARG_DENY_TOOLS" ]; then
IFS=',' read -ra DENY_ARRAY <<< "$ARG_DENY_TOOLS"
for tool in "${DENY_ARRAY[@]}"; do
if [ -n "$tool" ]; then
COPILOT_ARGS+=(--deny-tool "$tool")
fi
done
fi
}
check_existing_session() {
if [ "$ARG_RESUME_SESSION" = "true" ]; then
if copilot --help > /dev/null 2>&1; then
local session_dir="$HOME/.copilot/history-session-state"
if [ -d "$session_dir" ] && [ -n "$(ls "$session_dir"/session_*_*.json 2> /dev/null)" ]; then
echo "Found existing Copilot session. Will continue latest session." >&2
return 0
fi
fi
fi
return 1
}
setup_github_authentication() {
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
echo "Setting up GitHub authentication..."
if [ -n "${GITHUB_TOKEN:-}" ]; then
export GH_TOKEN="$GITHUB_TOKEN"
echo "✓ Using GitHub token from module configuration"
return 0
fi
if command_exists coder; then
local github_token
if github_token=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null); then
if [ -n "$github_token" ] && [ "$github_token" != "null" ]; then
export GITHUB_TOKEN="$github_token"
export GH_TOKEN="$github_token"
echo "✓ Using Coder external auth OAuth token"
return 0
fi
fi
fi
if command_exists gh && gh auth status > /dev/null 2>&1; then
echo "✓ Using GitHub CLI OAuth authentication"
return 0
fi
echo "⚠ No GitHub authentication available"
echo " Copilot will prompt for login during first use"
echo " Use the '/login' command in Copilot to authenticate"
return 0
}
start_agentapi() {
echo "Starting in directory: $ARG_WORKDIR"
cd "$ARG_WORKDIR"
build_copilot_args
if check_existing_session; then
echo "Continuing latest Copilot session..."
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue "${COPILOT_ARGS[@]}"
else
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue
fi
else
echo "Starting new Copilot session..."
local initial_prompt
initial_prompt=$(build_initial_prompt)
if [ -n "$initial_prompt" ]; then
echo "Using initial prompt with system context"
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
else
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot
fi
else
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
else
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot
fi
fi
fi
}
setup_github_authentication
validate_copilot_installation
start_agentapi
@@ -0,0 +1,12 @@
#!/bin/bash
set -euo pipefail
if [[ "$1" == "--version" ]]; then
echo "GitHub Copilot CLI v1.0.0"
exit 0
fi
while true; do
echo "$(date) - Copilot mock running..."
sleep 15
done
+1 -1
View File
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
+1 -1
View File
@@ -117,7 +117,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.3"
default = "v0.10.0"
}
variable "agentapi_port" {
+4 -4
View File
@@ -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.0.1"
version = "3.0.3"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -49,7 +49,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.0.1"
version = "3.0.3"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
@@ -85,7 +85,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.0.1"
version = "3.0.3"
agent_id = coder_agent.example.id
workdir = "/home/coder"
install_claude_code = true
@@ -108,7 +108,7 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.0.1"
version = "3.0.3"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
+33 -4
View File
@@ -183,7 +183,7 @@ variable "claude_code_oauth_token" {
variable "system_prompt" {
type = string
description = "The system prompt to use for the Claude Code server."
default = "Send a task status update to notify the user that you are ready for input, and then wait for user input."
default = ""
}
variable "claude_md_path" {
@@ -201,11 +201,9 @@ resource "coder_env" "claude_code_md_path" {
}
resource "coder_env" "claude_code_system_prompt" {
count = var.system_prompt == "" ? 0 : 1
agent_id = var.agent_id
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = var.system_prompt
value = local.final_system_prompt
}
resource "coder_env" "claude_code_oauth_token" {
@@ -231,6 +229,37 @@ 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"))
# Required prompts for the module to properly report task status to Coder
report_tasks_system_prompt = <<-EOT
-- Tool Selection --
- coder_report_task: providing status updates or requesting user input.
-- 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
In your summary on coder_report_task:
- Be specific about what you're doing
- Clearly indicate what information you need from the user when in "failure" state
- Keep it under 160 characters
- Make it actionable
EOT
# Only include coder system prompts if report_tasks is enabled
custom_system_prompt = trimspace(try(var.system_prompt, ""))
final_system_prompt = format("<system>%s%s</system>",
var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "",
local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : ""
)
}
module "agentapi" {
@@ -187,3 +187,84 @@ run "test_claude_code_permission_mode_validation" {
error_message = "Permission mode should be one of the valid options"
}
}
run "test_claude_code_system_prompt" {
command = plan
variables {
agent_id = "test-agent-system-prompt"
workdir = "/home/coder/test"
system_prompt = "Custom addition"
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
assert {
condition = length(regexall("Custom addition", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have system_prompt variable value"
}
}
run "test_claude_report_tasks_default" {
command = plan
variables {
agent_id = "test-agent-report-tasks"
workdir = "/home/coder/test"
# report_tasks: default is true
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
# Ensure system prompt is wrapped by <system>
assert {
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
error_message = "System prompt should start with <system>"
}
assert {
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
error_message = "System prompt should end with </system>"
}
# Ensure Coder sections are injected when report_tasks=true (default)
assert {
condition = length(regexall("-- Tool Selection --", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have Tool Selection section"
}
assert {
condition = length(regexall("-- Task Reporting --", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have Task Reporting section"
}
}
run "test_claude_report_tasks_disabled" {
command = plan
variables {
agent_id = "test-agent-report-tasks"
workdir = "/home/coder/test"
report_tasks = false
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
# Ensure system prompt is wrapped by <system>
assert {
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
error_message = "System prompt should start with <system>"
}
assert {
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
error_message = "System prompt should end with </system>"
}
}
@@ -1,7 +1,9 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
BOLD='\033[0;1m'
@@ -1,7 +1,9 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
@@ -19,7 +19,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
@@ -37,7 +37,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -51,7 +51,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -66,7 +66,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -91,7 +91,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -109,7 +109,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.3"
version = "1.2.4"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -20,7 +20,7 @@ describe("jetbrains-gateway", async () => {
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent_id=foo",
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent=",
);
const coder_app = state.resources.find(
@@ -40,4 +40,28 @@ describe("jetbrains-gateway", async () => {
});
expect(state.outputs.identifier.value).toBe("IU");
});
it("optionally includes agent when an agent name is provided", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: "main",
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent=main",
);
});
it("includes the agent parameter even when the provided value is blank", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: " ",
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent= ",
);
});
});
@@ -30,15 +30,14 @@ variable "agent_id" {
variable "slug" {
type = string
description = "The slug for the coder_app. Allows resuing the module with the same template."
description = "The slug for the coder_app. Allows reusing the module with the same template."
default = "gateway"
}
variable "agent_name" {
type = string
description = "Agent name. (unused). Will be removed in a future version"
default = ""
description = "Agent name."
default = ""
}
variable "folder" {
@@ -348,8 +347,8 @@ resource "coder_app" "gateway" {
local.build_number,
"&ide_download_link=",
local.download_link,
"&agent_id=",
var.agent_id,
"&agent=",
var.agent_name,
])
}