diff --git a/registry/coder-labs/modules/copilot/README.md b/registry/coder-labs/modules/copilot/README.md
new file mode 100644
index 000000000..c6e8dea26
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/README.md
@@ -0,0 +1,210 @@
+---
+display_name: Copilot
+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.0"
+ 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.0"
+ 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.0"
+ 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.0"
+ 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.0"
+ 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)
diff --git a/registry/coder-labs/modules/copilot/copilot.tftest.hcl b/registry/coder-labs/modules/copilot/copilot.tftest.hcl
new file mode 100644
index 000000000..185c019ba
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/copilot.tftest.hcl
@@ -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'"
+ }
+}
diff --git a/registry/coder-labs/modules/copilot/main.test.ts b/registry/coder-labs/modules/copilot/main.test.ts
new file mode 100644
index 000000000..1d438e33b
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/main.test.ts
@@ -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();
+ });
+});
diff --git a/registry/coder-labs/modules/copilot/main.tf b/registry/coder-labs/modules/copilot/main.tf
new file mode 100644
index 000000000..fd93b048f
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/main.tf
@@ -0,0 +1,300 @@
+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 ? "\n${var.system_prompt}${local.task_reporting_prompt}\n" : "\n${var.system_prompt}\n"
+}
+
+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}' \
+ /tmp/install.sh
+ EOT
+}
\ No newline at end of file
diff --git a/registry/coder-labs/modules/copilot/scripts/install.sh b/registry/coder-labs/modules/copilot/scripts/install.sh
new file mode 100644
index 000000000..f44d50873
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/scripts/install.sh
@@ -0,0 +1,233 @@
+#!/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}
+
+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"
+ mkdir -p "$HOME/.config"
+
+ setup_copilot_config
+
+ echo "$ARG_WORKDIR" > "$module_path/trusted_directories"
+}
+
+setup_copilot_config() {
+ local copilot_config_dir="$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."
diff --git a/registry/coder-labs/modules/copilot/scripts/start.sh b/registry/coder-labs/modules/copilot/scripts/start.sh
new file mode 100644
index 000000000..91a404094
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/scripts/start.sh
@@ -0,0 +1,156 @@
+#!/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() {
+ 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
diff --git a/registry/coder-labs/modules/copilot/testdata/copilot-mock.sh b/registry/coder-labs/modules/copilot/testdata/copilot-mock.sh
new file mode 100644
index 000000000..f1daa15f1
--- /dev/null
+++ b/registry/coder-labs/modules/copilot/testdata/copilot-mock.sh
@@ -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