diff --git a/.icons/openai.svg b/.icons/openai.svg
new file mode 100644
index 000000000..ba36fc2aa
--- /dev/null
+++ b/.icons/openai.svg
@@ -0,0 +1,15 @@
+
diff --git a/.icons/proxmox.svg b/.icons/proxmox.svg
new file mode 100755
index 000000000..c18256e23
--- /dev/null
+++ b/.icons/proxmox.svg
@@ -0,0 +1,137 @@
+
+
diff --git a/.icons/sourcegraph-amp.svg b/.icons/sourcegraph-amp.svg
new file mode 100644
index 000000000..83777bd2d
--- /dev/null
+++ b/.icons/sourcegraph-amp.svg
@@ -0,0 +1,5 @@
+
diff --git a/registry/coder-labs/README.md b/registry/coder-labs/README.md
index c9a7d8ef7..58c707088 100644
--- a/registry/coder-labs/README.md
+++ b/registry/coder-labs/README.md
@@ -5,7 +5,7 @@ github: coder
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/company/coderhq
website: https://discord.gg/coder
-status: community
+status: official
---
å
diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md
new file mode 100644
index 000000000..62c63a328
--- /dev/null
+++ b/registry/coder-labs/modules/codex/README.md
@@ -0,0 +1,146 @@
+---
+display_name: Codex CLI
+icon: ../../../../.icons/openai.svg
+description: Run Codex CLI in your workspace with AgentAPI integration
+verified: true
+tags: [agent, codex, ai, openai, tasks]
+---
+
+# Codex CLI
+
+Run Codex CLI in your workspace to access OpenAI's models through the Codex interface, with custom pre/post install scripts. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for Coder Tasks compatibility.
+
+```tf
+module "codex" {
+ source = "registry.coder.com/coder-labs/codex/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ openai_api_key = var.openai_api_key
+ folder = "/home/coder/project"
+}
+```
+
+## Prerequisites
+
+- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
+- OpenAI API key for Codex access
+
+## Examples
+
+### Run standalone
+
+```tf
+module "codex" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder-labs/codex/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ openai_api_key = "..."
+ folder = "/home/coder/project"
+}
+```
+
+### Tasks integration
+
+```tf
+data "coder_parameter" "ai_prompt" {
+ type = "string"
+ name = "AI Prompt"
+ default = ""
+ description = "Initial prompt for the Codex CLI"
+ mutable = true
+}
+
+module "coder-login" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/coder-login/coder"
+ version = "1.0.31"
+ agent_id = coder_agent.example.id
+}
+
+module "codex" {
+ source = "registry.coder.com/coder-labs/codex/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ openai_api_key = "..."
+ ai_prompt = data.coder_parameter.ai_prompt.value
+ folder = "/home/coder/project"
+
+ # Custom configuration for full auto mode
+ base_config_toml = <<-EOT
+ approval_policy = "never"
+ preferred_auth_method = "apikey"
+ EOT
+}
+```
+
+> [!WARNING]
+> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified folder. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
+
+## How it Works
+
+- **Install**: The module installs Codex CLI and sets up the environment
+- **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory
+- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI
+- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
+
+## Configuration
+
+### Default Configuration
+
+When no custom `base_config_toml` is provided, the module uses these secure defaults:
+
+```toml
+sandbox_mode = "workspace-write"
+approval_policy = "never"
+preferred_auth_method = "apikey"
+
+[sandbox_workspace_write]
+network_access = true
+```
+
+### Custom Configuration
+
+For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_servers`:
+
+```tf
+module "codex" {
+ source = "registry.coder.com/coder-labs/codex/coder"
+ version = "2.0.0"
+ # ... other variables ...
+
+ # Override default configuration
+ base_config_toml = <<-EOT
+ sandbox_mode = "danger-full-access"
+ approval_policy = "never"
+ preferred_auth_method = "apikey"
+ EOT
+
+ # Add extra MCP servers
+ additional_mcp_servers = <<-EOT
+ [mcp_servers.GitHub]
+ command = "npx"
+ args = ["-y", "@modelcontextprotocol/server-github"]
+ type = "stdio"
+ EOT
+}
+```
+
+> [!NOTE]
+> If no custom configuration is provided, the module uses secure defaults. The Coder MCP server is always included automatically. For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md).
+
+## Troubleshooting
+
+- Check installation and startup logs in `~/.codex-module/`
+- Ensure your OpenAI API key has access to the specified model
+
+> [!IMPORTANT]
+> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set, and **you create a `coder_parameter` named `"AI Prompt"` and pass its value to the codex module's `ai_prompt` variable**. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker).
+> The module automatically configures Codex with your API key and model preferences.
+> folder is a required variable for the module to function correctly.
+
+## References
+
+- [Codex CLI Documentation](https://github.com/openai/codex)
+- [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/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts
new file mode 100644
index 000000000..55d0b879e
--- /dev/null
+++ b/registry/coder-labs/modules/codex/main.test.ts
@@ -0,0 +1,368 @@
+import {
+ test,
+ afterEach,
+ describe,
+ setDefaultTimeout,
+ beforeAll,
+ expect,
+} from "bun:test";
+import { execContainer, readFileContainer, runTerraformInit } from "~test";
+import {
+ loadTestFile,
+ writeExecutable,
+ setup as setupUtil,
+ execModuleScript,
+ expectAgentAPIStarted,
+} from "../../../coder/modules/agentapi/test-util";
+import dedent from "dedent";
+
+let cleanupFunctions: (() => Promise)[] = [];
+const registerCleanup = (cleanup: () => Promise) => {
+ cleanupFunctions.push(cleanup);
+};
+afterEach(async () => {
+ const cleanupFnsCopy = cleanupFunctions.slice().reverse();
+ cleanupFunctions = [];
+ for (const cleanup of cleanupFnsCopy) {
+ try {
+ await cleanup();
+ } catch (error) {
+ console.error("Error during cleanup:", error);
+ }
+ }
+});
+
+interface SetupProps {
+ skipAgentAPIMock?: boolean;
+ skipCodexMock?: boolean;
+ moduleVariables?: Record;
+ agentapiMockScript?: string;
+}
+
+const setup = async (props?: SetupProps): Promise<{ id: string }> => {
+ const projectDir = "/home/coder/project";
+ const { id } = await setupUtil({
+ moduleDir: import.meta.dir,
+ moduleVariables: {
+ install_codex: props?.skipCodexMock ? "true" : "false",
+ install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
+ codex_model: "gpt-4-turbo",
+ folder: "/home/coder",
+ ...props?.moduleVariables,
+ },
+ registerCleanup,
+ projectDir,
+ skipAgentAPIMock: props?.skipAgentAPIMock,
+ agentapiMockScript: props?.agentapiMockScript,
+ });
+ if (!props?.skipCodexMock) {
+ await writeExecutable({
+ containerId: id,
+ filePath: "/usr/bin/codex",
+ content: await loadTestFile(import.meta.dir, "codex-mock.sh"),
+ });
+ }
+ return { id };
+};
+
+setDefaultTimeout(60 * 1000);
+
+describe("codex", async () => {
+ beforeAll(async () => {
+ await runTerraformInit(import.meta.dir);
+ });
+
+ test("happy-path", async () => {
+ const { id } = await setup();
+ await execModuleScript(id);
+ await expectAgentAPIStarted(id);
+ });
+
+ test("install-codex-version", async () => {
+ const version_to_install = "0.10.0";
+ const { id } = await setup({
+ skipCodexMock: true,
+ moduleVariables: {
+ install_codex: "true",
+ codex_version: version_to_install,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await execContainer(id, [
+ "bash",
+ "-c",
+ `cat /home/coder/.codex-module/install.log`,
+ ]);
+ expect(resp.stdout).toContain(version_to_install);
+ });
+
+ test("check-latest-codex-version-works", async () => {
+ const { id } = await setup({
+ skipCodexMock: true,
+ skipAgentAPIMock: true,
+ moduleVariables: {
+ install_codex: "true",
+ },
+ });
+ await execModuleScript(id);
+ await expectAgentAPIStarted(id);
+ });
+
+ test("base-config-toml", async () => {
+ const baseConfig = dedent`
+ sandbox_mode = "danger-full-access"
+ approval_policy = "never"
+ preferred_auth_method = "apikey"
+
+ [custom_section]
+ new_feature = true
+ `.trim();
+ const { id } = await setup({
+ moduleVariables: {
+ base_config_toml: baseConfig,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
+ expect(resp).toContain("sandbox_mode = \"danger-full-access\"");
+ expect(resp).toContain("preferred_auth_method = \"apikey\"");
+ expect(resp).toContain("[custom_section]");
+ expect(resp).toContain("[mcp_servers.Coder]");
+ });
+
+ test("codex-api-key", async () => {
+ const apiKey = "test-api-key-123";
+ const { id } = await setup({
+ moduleVariables: {
+ openai_api_key: apiKey,
+ },
+ });
+ await execModuleScript(id);
+
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.codex-module/agentapi-start.log",
+ );
+ expect(resp).toContain("OpenAI API Key: Provided");
+ });
+
+ test("pre-post-install-scripts", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
+ post_install_script: "#!/bin/bash\necho 'post-install-script'",
+ },
+ });
+ await execModuleScript(id);
+ const preInstallLog = await readFileContainer(
+ id,
+ "/home/coder/.codex-module/pre_install.log",
+ );
+ expect(preInstallLog).toContain("pre-install-script");
+ const postInstallLog = await readFileContainer(
+ id,
+ "/home/coder/.codex-module/post_install.log",
+ );
+ expect(postInstallLog).toContain("post-install-script");
+ });
+
+ test("folder-variable", async () => {
+ const folder = "/tmp/codex-test-folder";
+ const { id } = await setup({
+ skipCodexMock: false,
+ moduleVariables: {
+ folder,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.codex-module/install.log",
+ );
+ expect(resp).toContain(folder);
+ });
+
+ test("additional-mcp-servers", async () => {
+ const additional = dedent`
+ [mcp_servers.GitHub]
+ command = "npx"
+ args = ["-y", "@modelcontextprotocol/server-github"]
+ type = "stdio"
+ description = "GitHub integration"
+
+ [mcp_servers.FileSystem]
+ command = "npx"
+ args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
+ type = "stdio"
+ description = "File system access"
+ `.trim();
+ const { id } = await setup({
+ moduleVariables: {
+ additional_mcp_servers: additional,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
+ expect(resp).toContain("[mcp_servers.GitHub]");
+ expect(resp).toContain("[mcp_servers.FileSystem]");
+ expect(resp).toContain("[mcp_servers.Coder]");
+ expect(resp).toContain("GitHub integration");
+ });
+
+ test("full-custom-config", async () => {
+ const baseConfig = dedent`
+ sandbox_mode = "read-only"
+ approval_policy = "untrusted"
+ preferred_auth_method = "chatgpt"
+ custom_setting = "test-value"
+
+ [advanced_settings]
+ timeout = 30000
+ debug = true
+ logging_level = "verbose"
+ `.trim();
+
+ const additionalMCP = dedent`
+ [mcp_servers.CustomTool]
+ command = "/usr/local/bin/custom-tool"
+ args = ["--serve", "--port", "8080"]
+ type = "stdio"
+ description = "Custom development tool"
+
+ [mcp_servers.DatabaseMCP]
+ command = "python"
+ args = ["-m", "database_mcp_server"]
+ type = "stdio"
+ description = "Database query interface"
+ `.trim();
+
+ const { id } = await setup({
+ moduleVariables: {
+ base_config_toml: baseConfig,
+ additional_mcp_servers: additionalMCP,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
+
+ // Check base config
+ expect(resp).toContain("sandbox_mode = \"read-only\"");
+ expect(resp).toContain("preferred_auth_method = \"chatgpt\"");
+ expect(resp).toContain("custom_setting = \"test-value\"");
+ expect(resp).toContain("[advanced_settings]");
+ expect(resp).toContain("logging_level = \"verbose\"");
+
+ // Check MCP servers
+ expect(resp).toContain("[mcp_servers.Coder]");
+ expect(resp).toContain("[mcp_servers.CustomTool]");
+ expect(resp).toContain("[mcp_servers.DatabaseMCP]");
+ expect(resp).toContain("Custom development tool");
+ expect(resp).toContain("Database query interface");
+ });
+
+ test("minimal-default-config", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ // No base_config_toml or additional_mcp_servers - should use defaults
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
+
+ // Check default base config
+ expect(resp).toContain("sandbox_mode = \"workspace-write\"");
+ expect(resp).toContain("approval_policy = \"never\"");
+ expect(resp).toContain("[sandbox_workspace_write]");
+ expect(resp).toContain("network_access = true");
+
+ // Check only Coder MCP server is present
+ expect(resp).toContain("[mcp_servers.Coder]");
+ expect(resp).toContain("Report ALL tasks and statuses");
+
+ // Ensure no additional MCP servers
+ const mcpServerCount = (resp.match(/\[mcp_servers\./g) || []).length;
+ expect(mcpServerCount).toBe(1);
+ });
+
+ test("codex-system-prompt", async () => {
+ const prompt = "This is a system prompt for Codex.";
+ const { id } = await setup({
+ moduleVariables: {
+ codex_system_prompt: prompt,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
+ expect(resp).toContain(prompt);
+ });
+
+ test("codex-system-prompt-skip-append-if-exists", async () => {
+ const prompt_1 = "This is a system prompt for Codex.";
+ const prompt_2 = "This is a system prompt for Goose.";
+ const prompt_3 = dedent`
+ This is a system prompt for Codex.
+ This is a system prompt for Gemini.
+ `.trim();
+ const pre_install_script = dedent`
+ #!/bin/bash
+ mkdir -p /home/coder/.codex
+ echo -e "${prompt_3}" >> /home/coder/.codex/AGENTS.md
+ `.trim();
+
+ const { id } = await setup({
+ moduleVariables: {
+ pre_install_script,
+ codex_system_prompt: prompt_2,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
+ expect(resp).toContain(prompt_1);
+ expect(resp).toContain(prompt_2);
+
+ // Re-run with a prompt that already exists, it should not append again
+ const { id: id_2 } = await setup({
+ moduleVariables: {
+ pre_install_script,
+ codex_system_prompt: prompt_1,
+ },
+ });
+ await execModuleScript(id_2);
+ const resp_2 = await readFileContainer(id_2, "/home/coder/.codex/AGENTS.md");
+ expect(resp_2).toContain(prompt_1);
+ const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length;
+ expect(count).toBe(1);
+ });
+
+ test("codex-ai-task-prompt", async () => {
+ const prompt = "This is a system prompt for Codex.";
+ const { id } = await setup({
+ moduleVariables: {
+ ai_prompt: prompt,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await execContainer(id, [
+ "bash",
+ "-c",
+ `cat /home/coder/.codex-module/agentapi-start.log`,
+ ]);
+ expect(resp.stdout).toContain(prompt);
+ });
+
+ test("start-without-prompt", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ codex_system_prompt: "", // Explicitly disable system prompt
+ },
+ });
+ await execModuleScript(id);
+ const prompt = await execContainer(id, [
+ "ls",
+ "-l",
+ "/home/coder/.codex/AGENTS.md",
+ ]);
+ expect(prompt.exitCode).not.toBe(0);
+ expect(prompt.stderr).toContain("No such file or directory");
+ });
+});
diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf
new file mode 100644
index 000000000..9fdec4011
--- /dev/null
+++ b/registry/coder-labs/modules/codex/main.tf
@@ -0,0 +1,176 @@
+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."
+}
+
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+variable "order" {
+ type = number
+ description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
+ 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/openai.svg"
+}
+
+variable "folder" {
+ type = string
+ description = "The folder to run Codex in."
+}
+
+variable "install_codex" {
+ type = bool
+ description = "Whether to install Codex."
+ default = true
+}
+
+variable "codex_version" {
+ type = string
+ description = "The version of Codex to install."
+ default = "" # empty string means the latest available version
+}
+
+variable "base_config_toml" {
+ type = string
+ description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration with workspace-write sandbox mode and never approval policy. For advanced options, see https://github.com/openai/codex/blob/main/codex-rs/config.md"
+ default = ""
+}
+
+variable "additional_mcp_servers" {
+ type = string
+ description = "Additional MCP servers configuration in TOML format. These will be merged with the required Coder MCP server in the [mcp_servers] section."
+ default = ""
+}
+
+variable "openai_api_key" {
+ type = string
+ description = "OpenAI API key for Codex CLI"
+ 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.5.0"
+}
+
+variable "codex_model" {
+ type = string
+ description = "The model for Codex to use. Defaults to gpt-5."
+ default = ""
+}
+
+variable "pre_install_script" {
+ type = string
+ description = "Custom script to run before installing Codex."
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Custom script to run after installing Codex."
+ default = null
+}
+
+variable "ai_prompt" {
+ type = string
+ description = "Initial task prompt for Codex CLI when launched via Tasks"
+ default = ""
+}
+
+variable "codex_system_prompt" {
+ type = string
+ description = "System instructions written to AGENTS.md in the ~/.codex directory"
+ default = "You are a helpful coding assistant. Start every response with `Codex says:`"
+}
+
+resource "coder_env" "openai_api_key" {
+ agent_id = var.agent_id
+ name = "OPENAI_API_KEY"
+ value = var.openai_api_key
+}
+
+locals {
+ app_slug = "codex"
+ install_script = file("${path.module}/scripts/install.sh")
+ start_script = file("${path.module}/scripts/start.sh")
+ module_dir_name = ".codex-module"
+}
+
+module "agentapi" {
+ source = "registry.coder.com/coder/agentapi/coder"
+ version = "1.1.1"
+
+ agent_id = var.agent_id
+ web_app_slug = local.app_slug
+ web_app_order = var.order
+ web_app_group = var.group
+ web_app_icon = var.icon
+ web_app_display_name = "Codex"
+ cli_app_slug = "${local.app_slug}-cli"
+ cli_app_display_name = "Codex CLI"
+ 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_OPENAI_API_KEY='${var.openai_api_key}' \
+ ARG_CODEX_MODEL='${var.codex_model}' \
+ ARG_CODEX_START_DIRECTORY='${var.folder}' \
+ ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
+ /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_INSTALL='${var.install_codex}' \
+ ARG_CODEX_VERSION='${var.codex_version}' \
+ ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
+ ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
+ ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
+ ARG_CODEX_START_DIRECTORY='${var.folder}' \
+ ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
+ /tmp/install.sh
+ EOT
+}
\ No newline at end of file
diff --git a/registry/coder-labs/modules/codex/scripts/install.sh b/registry/coder-labs/modules/codex/scripts/install.sh
new file mode 100644
index 000000000..d725f6108
--- /dev/null
+++ b/registry/coder-labs/modules/codex/scripts/install.sh
@@ -0,0 +1,165 @@
+#!/bin/bash
+source "$HOME"/.bashrc
+
+BOLD='\033[0;1m'
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+set -o errexit
+set -o pipefail
+set -o nounset
+
+ARG_BASE_CONFIG_TOML=$(echo -n "$ARG_BASE_CONFIG_TOML" | base64 -d)
+ARG_ADDITIONAL_MCP_SERVERS=$(echo -n "$ARG_ADDITIONAL_MCP_SERVERS" | base64 -d)
+ARG_CODEX_INSTRUCTION_PROMPT=$(echo -n "$ARG_CODEX_INSTRUCTION_PROMPT" | base64 -d)
+
+echo "=== Codex Module Configuration ==="
+printf "Install Codex: %s\n" "$ARG_INSTALL"
+printf "Codex Version: %s\n" "$ARG_CODEX_VERSION"
+printf "App Slug: %s\n" "$ARG_CODER_MCP_APP_STATUS_SLUG"
+printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
+printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")"
+printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")"
+printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")"
+echo "======================================"
+
+set +o nounset
+
+function install_node() {
+ if ! command_exists npm; then
+ printf "npm not found, checking for Node.js installation...\n"
+ if ! command_exists node; then
+ printf "Node.js not found, installing Node.js via NVM...\n"
+ export NVM_DIR="$HOME/.nvm"
+ if [ ! -d "$NVM_DIR" ]; then
+ mkdir -p "$NVM_DIR"
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+ else
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+ fi
+
+ nvm install --lts
+ nvm use --lts
+ nvm alias default node
+
+ printf "Node.js installed: %s\n" "$(node --version)"
+ printf "npm installed: %s\n" "$(npm --version)"
+ else
+ printf "Node.js is installed but npm is not available. Please install npm manually.\n"
+ exit 1
+ fi
+ fi
+}
+
+function install_codex() {
+ if [ "${ARG_INSTALL}" = "true" ]; then
+ install_node
+
+ if ! command_exists nvm; then
+ printf "which node: %s\n" "$(which node)"
+ printf "which npm: %s\n" "$(which npm)"
+
+ mkdir -p "$HOME"/.npm-global
+
+ npm config set prefix "$HOME/.npm-global"
+
+ export PATH="$HOME/.npm-global/bin:$PATH"
+
+ if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
+ echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
+ fi
+ fi
+
+ printf "%s Installing Codex CLI\n" "${BOLD}"
+
+ if [ -n "$ARG_CODEX_VERSION" ]; then
+ npm install -g "@openai/codex@$ARG_CODEX_VERSION"
+ else
+ npm install -g "@openai/codex"
+ fi
+ printf "%s Successfully installed Codex CLI. Version: %s\n" "${BOLD}" "$(codex --version)"
+ fi
+}
+
+write_minimal_default_config() {
+ local config_path="$1"
+ cat << EOF > "$config_path"
+# Minimal Default Codex Configuration
+sandbox_mode = "workspace-write"
+approval_policy = "never"
+preferred_auth_method = "apikey"
+
+[sandbox_workspace_write]
+network_access = true
+
+EOF
+}
+
+append_mcp_servers_section() {
+ local config_path="$1"
+
+ cat << EOF >> "$config_path"
+
+# MCP Servers Configuration
+[mcp_servers.Coder]
+command = "coder"
+args = ["exp", "mcp", "server"]
+env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284", "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
+description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
+type = "stdio"
+
+EOF
+
+ if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
+ printf "Adding additional MCP servers\n"
+ echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
+ fi
+}
+
+function populate_config_toml() {
+ CONFIG_PATH="$HOME/.codex/config.toml"
+ mkdir -p "$(dirname "$CONFIG_PATH")"
+
+ if [ -n "$ARG_BASE_CONFIG_TOML" ]; then
+ printf "Using provided base configuration\n"
+ echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH"
+ else
+ printf "Using minimal default configuration\n"
+ write_minimal_default_config "$CONFIG_PATH"
+ fi
+
+ append_mcp_servers_section "$CONFIG_PATH"
+}
+
+function add_instruction_prompt_if_exists() {
+ if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then
+ AGENTS_PATH="$HOME/.codex/AGENTS.md"
+ printf "Creating AGENTS.md in .codex directory: %s\\n" "${AGENTS_PATH}"
+
+ mkdir -p "$HOME/.codex"
+
+ if [ -f "${AGENTS_PATH}" ] && grep -Fq "${ARG_CODEX_INSTRUCTION_PROMPT}" "${AGENTS_PATH}"; then
+ printf "AGENTS.md already contains the instruction prompt. Skipping append.\n"
+ else
+ printf "Appending instruction prompt to AGENTS.md in .codex directory\n"
+ echo -e "\n${ARG_CODEX_INSTRUCTION_PROMPT}" >> "${AGENTS_PATH}"
+ fi
+
+ if [ ! -d "${ARG_CODEX_START_DIRECTORY}" ]; then
+ printf "Creating start directory '%s'\\n" "${ARG_CODEX_START_DIRECTORY}"
+ mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
+ printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ exit 1
+ }
+ fi
+ else
+ printf "AGENTS.md instruction prompt is not set.\n"
+ fi
+}
+
+install_codex
+codex --version
+populate_config_toml
+add_instruction_prompt_if_exists
diff --git a/registry/coder-labs/modules/codex/scripts/start.sh b/registry/coder-labs/modules/codex/scripts/start.sh
new file mode 100644
index 000000000..29a8d741c
--- /dev/null
+++ b/registry/coder-labs/modules/codex/scripts/start.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+
+source "$HOME"/.bashrc
+set -o errexit
+set -o pipefail
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+if [ -f "$HOME/.nvm/nvm.sh" ]; then
+ source "$HOME"/.nvm/nvm.sh
+else
+ export PATH="$HOME/.npm-global/bin:$PATH"
+fi
+
+printf "Version: %s\n" "$(codex --version)"
+set -o nounset
+ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
+
+echo "=== Codex Launch Configuration ==="
+printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
+printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
+printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
+printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
+echo "======================================"
+set +o nounset
+CODEX_ARGS=()
+
+if command_exists codex; then
+ printf "Codex is installed\n"
+else
+ printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
+ exit 1
+fi
+
+if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
+ printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ cd "${ARG_CODEX_START_DIRECTORY}" || {
+ printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ exit 1
+ }
+else
+ printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
+ printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ exit 1
+ }
+ cd "${ARG_CODEX_START_DIRECTORY}" || {
+ printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
+ exit 1
+ }
+fi
+
+if [ -n "$ARG_CODEX_MODEL" ]; then
+ CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL")
+fi
+
+
+
+if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
+ printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT"
+ PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
+ CODEX_ARGS+=("$PROMPT")
+else
+ printf "No task prompt given.\n"
+fi
+
+
+# Terminal dimensions optimized for Coder Tasks UI sidebar:
+# - Width 67: fits comfortably in sidebar
+# - Height 1190: adjusted due to Codex terminal height bug
+printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
+agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}"
diff --git a/registry/coder-labs/modules/codex/testdata/codex-mock.sh b/registry/coder-labs/modules/codex/testdata/codex-mock.sh
new file mode 100644
index 000000000..8c1c7366d
--- /dev/null
+++ b/registry/coder-labs/modules/codex/testdata/codex-mock.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+if [[ "$1" == "--version" ]]; then
+ echo "HELLO: $(bash -c env)"
+ echo "codex version v1.0.0"
+ exit 0
+fi
+
+set -e
+
+while true; do
+ echo "$(date) - codex-mock"
+ sleep 15
+done
diff --git a/registry/coder-labs/modules/cursor-cli/README.md b/registry/coder-labs/modules/cursor-cli/README.md
new file mode 100644
index 000000000..0d3cd7536
--- /dev/null
+++ b/registry/coder-labs/modules/cursor-cli/README.md
@@ -0,0 +1,135 @@
+---
+display_name: Cursor CLI
+icon: ../../../../.icons/cursor.svg
+description: Run Cursor Agent CLI in your workspace for AI pair programming
+verified: true
+tags: [agent, cursor, ai, tasks]
+---
+
+# Cursor CLI
+
+Run the Cursor Agent CLI in your workspace for interactive coding assistance and automated task execution.
+
+```tf
+module "cursor_cli" {
+ source = "registry.coder.com/coder-labs/cursor-cli/coder"
+ version = "0.1.1"
+ agent_id = coder_agent.example.id
+ folder = "/home/coder/project"
+}
+```
+
+## Basic setup
+
+A full example with MCP, rules, and pre/post install scripts:
+
+```tf
+
+data "coder_parameter" "ai_prompt" {
+ type = "string"
+ name = "AI Prompt"
+ default = ""
+ description = "Build a Minesweeper in Python."
+ mutable = true
+}
+
+module "coder-login" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/coder-login/coder"
+ version = "1.0.31"
+ agent_id = coder_agent.main.id
+}
+
+module "cursor_cli" {
+ source = "registry.coder.com/coder-labs/cursor-cli/coder"
+ version = "0.1.1"
+ agent_id = coder_agent.example.id
+ folder = "/home/coder/project"
+
+ # Optional
+ install_cursor_cli = true
+ force = true
+ model = "gpt-5"
+ ai_prompt = data.coder_parameter.ai_prompt.value
+ api_key = "xxxx-xxxx-xxxx" # Required while using tasks, see note below
+
+ # Minimal MCP server (writes `folder/.cursor/mcp.json`):
+ mcp = jsonencode({
+ mcpServers = {
+ playwright = {
+ command = "npx"
+ args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--no-sandbox"]
+ }
+ desktop-commander = {
+ command = "npx"
+ args = ["-y", "@wonderwhy-er/desktop-commander"]
+ }
+ }
+ })
+
+ # Use a pre_install_script to install the CLI
+ pre_install_script = <<-EOT
+ #!/usr/bin/env bash
+ set -euo pipefail
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
+ apt-get install -y nodejs
+ EOT
+
+ # Use post_install_script to wait for the repo to be ready
+ post_install_script = <<-EOT
+ #!/usr/bin/env bash
+ set -euo pipefail
+ TARGET="$${FOLDER}/.git/config"
+ echo "[cursor-cli] waiting for $${TARGET}..."
+ for i in $(seq 1 600); do
+ [ -f "$TARGET" ] && { echo "ready"; exit 0; }
+ sleep 1
+ done
+ echo "timeout waiting for $${TARGET}" >&2
+ EOT
+
+ # Provide a map of file name to content; files are written to `folder/.cursor/rules/`.
+ rules_files = {
+ "python.mdc" = <<-EOT
+ ---
+ description: RPC Service boilerplate
+ globs:
+ alwaysApply: false
+ ---
+
+ - Use our internal RPC pattern when defining services
+ - Always use snake_case for service names.
+
+ @service-template.ts
+ EOT
+
+ "frontend.mdc" = <<-EOT
+ ---
+ description: RPC Service boilerplate
+ globs:
+ alwaysApply: false
+ ---
+
+ - Use our internal RPC pattern when defining services
+ - Always use snake_case for service names.
+
+ @service-template.ts
+ EOT
+ }
+}
+```
+
+> [!NOTE]
+> A `.cursor` directory will be created in the specified `folder`, containing the MCP configuration, rules.
+> To use this module with tasks, please pass the API Key obtained from Cursor to the `api_key` variable. To obtain the api key follow the instructions [here](https://docs.cursor.com/en/cli/reference/authentication#step-1%3A-generate-an-api-key)
+
+## References
+
+- See Cursor CLI docs: `https://docs.cursor.com/en/cli/overview`
+- For MCP project config, see `https://docs.cursor.com/en/context/mcp#using-mcp-json`. This module writes your `mcp_json` into `folder/.cursor/mcp.json`.
+- For Rules, see `https://docs.cursor.com/en/context/rules#project-rules`. Provide `rules_files` (map of file name to content) to populate `folder/.cursor/rules/`.
+
+## Troubleshooting
+
+- Ensure the CLI is installed (enable `install_cursor_cli = true` or preinstall it in your image)
+- Logs are written to `~/.cursor-cli-module/`
diff --git a/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl b/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl
new file mode 100644
index 000000000..f8e917a1f
--- /dev/null
+++ b/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl
@@ -0,0 +1,152 @@
+run "test_cursor_cli_basic" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-123"
+ folder = "/home/coder/projects"
+ }
+
+ assert {
+ condition = coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
+ error_message = "Status slug environment variable should be set correctly"
+ }
+
+ assert {
+ condition = coder_env.status_slug.value == "cursorcli"
+ error_message = "Status slug value should be 'cursorcli'"
+ }
+
+ assert {
+ condition = var.folder == "/home/coder/projects"
+ error_message = "Folder variable should be set correctly"
+ }
+
+ assert {
+ condition = var.agent_id == "test-agent-123"
+ error_message = "Agent ID variable should be set correctly"
+ }
+}
+
+run "test_cursor_cli_with_api_key" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-456"
+ folder = "/home/coder/workspace"
+ api_key = "test-api-key-123"
+ }
+
+ assert {
+ condition = coder_env.cursor_api_key[0].name == "CURSOR_API_KEY"
+ error_message = "Cursor API key environment variable should be set correctly"
+ }
+
+ assert {
+ condition = coder_env.cursor_api_key[0].value == "test-api-key-123"
+ error_message = "Cursor API key value should match the input"
+ }
+}
+
+run "test_cursor_cli_with_custom_options" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-789"
+ folder = "/home/coder/custom"
+ order = 5
+ group = "development"
+ icon = "/icon/custom.svg"
+ model = "sonnet-4"
+ ai_prompt = "Help me write better code"
+ force = false
+ install_cursor_cli = false
+ install_agentapi = false
+ }
+
+ assert {
+ condition = var.order == 5
+ error_message = "Order variable should be set to 5"
+ }
+
+ assert {
+ condition = var.group == "development"
+ error_message = "Group variable should be set to 'development'"
+ }
+
+ assert {
+ condition = var.icon == "/icon/custom.svg"
+ error_message = "Icon variable should be set to custom icon"
+ }
+
+ assert {
+ condition = var.model == "sonnet-4"
+ error_message = "Model variable should be set to 'sonnet-4'"
+ }
+
+ assert {
+ condition = var.ai_prompt == "Help me write better code"
+ error_message = "AI prompt variable should be set correctly"
+ }
+
+ assert {
+ condition = var.force == false
+ error_message = "Force variable should be set to false"
+ }
+}
+
+run "test_cursor_cli_with_mcp_and_rules" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-mcp"
+ folder = "/home/coder/mcp-test"
+ mcp = jsonencode({
+ mcpServers = {
+ test = {
+ command = "test-server"
+ args = ["--config", "test.json"]
+ }
+ }
+ })
+ rules_files = {
+ "general.md" = "# General coding rules\n- Write clean code\n- Add comments"
+ "security.md" = "# Security rules\n- Never commit secrets\n- Validate inputs"
+ }
+ }
+
+ assert {
+ condition = var.mcp != null
+ error_message = "MCP configuration should be provided"
+ }
+
+ assert {
+ condition = var.rules_files != null
+ error_message = "Rules files should be provided"
+ }
+
+ assert {
+ condition = length(var.rules_files) == 2
+ error_message = "Should have 2 rules files"
+ }
+}
+
+run "test_cursor_cli_with_scripts" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-scripts"
+ folder = "/home/coder/scripts"
+ pre_install_script = "echo 'Pre-install script'"
+ post_install_script = "echo 'Post-install script'"
+ }
+
+ assert {
+ condition = var.pre_install_script == "echo 'Pre-install script'"
+ error_message = "Pre-install script should be set correctly"
+ }
+
+ assert {
+ condition = var.post_install_script == "echo 'Post-install script'"
+ error_message = "Post-install script should be set correctly"
+ }
+}
diff --git a/registry/coder-labs/modules/cursor-cli/main.test.ts b/registry/coder-labs/modules/cursor-cli/main.test.ts
new file mode 100644
index 000000000..52bc993f0
--- /dev/null
+++ b/registry/coder-labs/modules/cursor-cli/main.test.ts
@@ -0,0 +1,212 @@
+import { afterEach, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
+import { execContainer, runTerraformInit, writeFileContainer } from "~test";
+import {
+ execModuleScript,
+ expectAgentAPIStarted,
+ loadTestFile,
+ setup as setupUtil
+} from "../../../coder/modules/agentapi/test-util";
+import { setupContainer, writeExecutable } from "../../../coder/modules/agentapi/test-util";
+
+let cleanupFns: (() => Promise)[] = [];
+const registerCleanup = (fn: () => Promise) => cleanupFns.push(fn);
+
+afterEach(async () => {
+ const fns = cleanupFns.slice().reverse();
+ cleanupFns = [];
+ for (const fn of fns) {
+ try {
+ await fn();
+ } catch (err) {
+ console.error(err);
+ }
+ }
+});
+
+interface SetupProps {
+ skipAgentAPIMock?: boolean;
+ skipCursorCliMock?: boolean;
+ moduleVariables?: Record;
+ agentapiMockScript?: string;
+}
+
+const setup = async (props?: SetupProps): Promise<{ id: string }> => {
+ const projectDir = "/home/coder/project";
+ const { id } = await setupUtil({
+ moduleDir: import.meta.dir,
+ moduleVariables: {
+ enable_agentapi: "true",
+ install_cursor_cli: props?.skipCursorCliMock ? "true" : "false",
+ install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
+ folder: projectDir,
+ ...props?.moduleVariables,
+ },
+ registerCleanup,
+ projectDir,
+ skipAgentAPIMock: props?.skipAgentAPIMock,
+ agentapiMockScript: props?.agentapiMockScript,
+ });
+ if (!props?.skipCursorCliMock) {
+ await writeExecutable({
+ containerId: id,
+ filePath: "/usr/bin/cursor-agent",
+ content: await loadTestFile(import.meta.dir, "cursor-cli-mock.sh"),
+ });
+ }
+ return { id };
+};
+
+setDefaultTimeout(180 * 1000);
+
+describe("cursor-cli", async () => {
+ beforeAll(async () => {
+ await runTerraformInit(import.meta.dir);
+ });
+
+ test("agentapi-happy-path", async () => {
+ const { id } = await setup({});
+ const resp = await execModuleScript(id);
+ expect(resp.exitCode).toBe(0);
+
+ await expectAgentAPIStarted(id);
+ });
+
+ test("agentapi-mcp-json", async () => {
+ const mcpJson = '{"mcpServers": {"test": {"command": "test-cmd", "type": "stdio"}}}';
+ const { id } = await setup({
+ moduleVariables: {
+ mcp: mcpJson,
+ }
+ });
+ const resp = await execModuleScript(id);
+ expect(resp.exitCode).toBe(0);
+
+ const mcpContent = await execContainer(id, [
+ "bash",
+ "-c",
+ `cat '/home/coder/project/.cursor/mcp.json'`,
+ ]);
+ expect(mcpContent.exitCode).toBe(0);
+ expect(mcpContent.stdout).toContain("mcpServers");
+ expect(mcpContent.stdout).toContain("test");
+ expect(mcpContent.stdout).toContain("test-cmd");
+ expect(mcpContent.stdout).toContain("/tmp/mcp-hack.sh");
+ expect(mcpContent.stdout).toContain("coder");
+ });
+
+ test("agentapi-rules-files", async () => {
+ const rulesContent = "Always use TypeScript";
+ const { id } = await setup({
+ moduleVariables: {
+ rules_files: JSON.stringify({ "typescript.md": rulesContent }),
+ }
+ });
+ const resp = await execModuleScript(id);
+ expect(resp.exitCode).toBe(0);
+
+ const rulesFile = await execContainer(id, [
+ "bash",
+ "-c",
+ `cat '/home/coder/project/.cursor/rules/typescript.md'`,
+ ]);
+ expect(rulesFile.exitCode).toBe(0);
+ expect(rulesFile.stdout).toContain(rulesContent);
+ });
+
+ test("agentapi-api-key", async () => {
+ const apiKey = "test-cursor-api-key-123";
+ const { id } = await setup({
+ moduleVariables: {
+ api_key: apiKey,
+ }
+ });
+ const resp = await execModuleScript(id);
+ expect(resp.exitCode).toBe(0);
+
+ const envCheck = await execContainer(id, [
+ "bash",
+ "-c",
+ `env | grep CURSOR_API_KEY || echo "CURSOR_API_KEY not found"`,
+ ]);
+ expect(envCheck.stdout).toContain("CURSOR_API_KEY");
+ });
+
+ test("agentapi-model-and-force-flags", async () => {
+ const model = "sonnet-4";
+ const { id } = await setup({
+ moduleVariables: {
+ model: model,
+ force: "true",
+ ai_prompt: "test prompt",
+ }
+ });
+ const resp = await execModuleScript(id);
+ expect(resp.exitCode).toBe(0);
+
+ const startLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true",
+ ]);
+ expect(startLog.stdout).toContain(`-m ${model}`);
+ expect(startLog.stdout).toContain("-f");
+ expect(startLog.stdout).toContain("test prompt");
+ });
+
+ test("agentapi-pre-post-install-scripts", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ pre_install_script: "#!/bin/bash\necho 'cursor-pre-install-script'",
+ post_install_script: "#!/bin/bash\necho 'cursor-post-install-script'",
+ }
+ });
+ const resp = await execModuleScript(id);
+ expect(resp.exitCode).toBe(0);
+
+ const preInstallLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.cursor-cli-module/pre_install.log || true",
+ ]);
+ expect(preInstallLog.stdout).toContain("cursor-pre-install-script");
+
+ const postInstallLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.cursor-cli-module/post_install.log || true",
+ ]);
+ expect(postInstallLog.stdout).toContain("cursor-post-install-script");
+ });
+
+ test("agentapi-folder-variable", async () => {
+ const folder = "/tmp/cursor-test-folder";
+ const { id } = await setup({
+ moduleVariables: {
+ folder: folder,
+ }
+ });
+ const resp = await execModuleScript(id);
+ expect(resp.exitCode).toBe(0);
+
+ const installLog = await execContainer(id, [
+ "bash",
+ "-c",
+ "cat /home/coder/.cursor-cli-module/install.log || true",
+ ]);
+ expect(installLog.stdout).toContain(folder);
+ });
+
+ test("install-test-cursor-cli-latest", async () => {
+ const { id } = await setup({
+ skipCursorCliMock: true,
+ skipAgentAPIMock: true,
+ });
+ const resp = await execModuleScript(id);
+ expect(resp.exitCode).toBe(0);
+
+ await expectAgentAPIStarted(id);
+ })
+
+});
+
+
diff --git a/registry/coder-labs/modules/cursor-cli/main.tf b/registry/coder-labs/modules/cursor-cli/main.tf
new file mode 100644
index 000000000..482103718
--- /dev/null
+++ b/registry/coder-labs/modules/cursor-cli/main.tf
@@ -0,0 +1,179 @@
+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."
+}
+
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+variable "order" {
+ type = number
+ description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
+ 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/cursor.svg"
+}
+
+variable "folder" {
+ type = string
+ description = "The folder to run Cursor CLI in."
+}
+
+variable "install_cursor_cli" {
+ type = bool
+ description = "Whether to install Cursor CLI."
+ default = true
+}
+
+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.5.0"
+}
+
+variable "force" {
+ type = bool
+ description = "Force allow commands unless explicitly denied"
+ default = true
+}
+
+variable "model" {
+ type = string
+ description = "Model to use (e.g., sonnet-4, sonnet-4-thinking, gpt-5)"
+ default = ""
+}
+
+variable "ai_prompt" {
+ type = string
+ description = "AI prompt/task passed to cursor-agent."
+ default = ""
+}
+
+variable "api_key" {
+ type = string
+ description = "API key for Cursor CLI."
+ default = ""
+ sensitive = true
+}
+
+variable "mcp" {
+ type = string
+ description = "Workspace-specific MCP JSON to write to folder/.cursor/mcp.json. See https://docs.cursor.com/en/context/mcp#using-mcp-json"
+ default = null
+}
+
+variable "rules_files" {
+ type = map(string)
+ description = "Optional map of rule file name to content. Files will be written to folder/.cursor/rules/. See https://docs.cursor.com/en/context/rules#project-rules"
+ default = null
+}
+
+variable "pre_install_script" {
+ type = string
+ description = "Optional script to run before installing Cursor CLI."
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Optional script to run after installing Cursor CLI."
+ default = null
+}
+
+locals {
+ app_slug = "cursorcli"
+ install_script = file("${path.module}/scripts/install.sh")
+ start_script = file("${path.module}/scripts/start.sh")
+ module_dir_name = ".cursor-cli-module"
+}
+
+# Expose status slug and API key to the agent environment
+resource "coder_env" "status_slug" {
+ agent_id = var.agent_id
+ name = "CODER_MCP_APP_STATUS_SLUG"
+ value = local.app_slug
+}
+
+resource "coder_env" "cursor_api_key" {
+ count = var.api_key != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "CURSOR_API_KEY"
+ value = var.api_key
+}
+
+module "agentapi" {
+ source = "registry.coder.com/coder/agentapi/coder"
+ version = "1.1.1"
+
+ agent_id = var.agent_id
+ web_app_slug = local.app_slug
+ web_app_order = var.order
+ web_app_group = var.group
+ web_app_icon = var.icon
+ web_app_display_name = "Cursor CLI"
+ cli_app_slug = local.app_slug
+ cli_app_display_name = "Cursor CLI"
+ 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_FORCE='${var.force}' \
+ ARG_MODEL='${var.model}' \
+ ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
+ ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
+ ARG_FOLDER='${var.folder}' \
+ /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_INSTALL='${var.install_cursor_cli}' \
+ ARG_WORKSPACE_MCP_JSON='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
+ ARG_WORKSPACE_RULES_JSON='${var.rules_files != null ? base64encode(jsonencode(var.rules_files)) : ""}' \
+ ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
+ ARG_FOLDER='${var.folder}' \
+ ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
+ /tmp/install.sh
+ EOT
+}
diff --git a/registry/coder-labs/modules/cursor-cli/scripts/install.sh b/registry/coder-labs/modules/cursor-cli/scripts/install.sh
new file mode 100644
index 000000000..5477f07dd
--- /dev/null
+++ b/registry/coder-labs/modules/cursor-cli/scripts/install.sh
@@ -0,0 +1,122 @@
+#!/bin/bash
+
+set -o errexit
+set -o pipefail
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+# Inputs
+ARG_INSTALL=${ARG_INSTALL:-true}
+ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.cursor-cli-module}
+ARG_FOLDER=${ARG_FOLDER:-$HOME}
+ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
+
+mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
+
+ARG_WORKSPACE_MCP_JSON=$(echo -n "$ARG_WORKSPACE_MCP_JSON" | base64 -d)
+ARG_WORKSPACE_RULES_JSON=$(echo -n "$ARG_WORKSPACE_RULES_JSON" | base64 -d)
+
+echo "--------------------------------"
+echo "install: $ARG_INSTALL"
+echo "folder: $ARG_FOLDER"
+echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
+echo "module_dir_name: $ARG_MODULE_DIR_NAME"
+echo "--------------------------------"
+
+# Install Cursor via official installer if requested
+function install_cursor_cli() {
+ if [ "$ARG_INSTALL" = "true" ]; then
+ echo "Installing Cursor via official installer..."
+ set +e
+ curl https://cursor.com/install -fsS | bash 2>&1
+ CURL_EXIT=${PIPESTATUS[0]}
+ set -e
+ if [ $CURL_EXIT -ne 0 ]; then
+ echo "Cursor installer failed with exit code $CURL_EXIT"
+ fi
+
+ # Ensure binaries are discoverable; create stable symlink to cursor-agent
+ CANDIDATES=(
+ "$(command -v cursor-agent || true)"
+ "$HOME/.cursor/bin/cursor-agent"
+ )
+ FOUND_BIN=""
+ for c in "${CANDIDATES[@]}"; do
+ if [ -n "$c" ] && [ -x "$c" ]; then
+ FOUND_BIN="$c"
+ break
+ fi
+ done
+ mkdir -p "$HOME/.local/bin"
+ if [ -n "$FOUND_BIN" ]; then
+ ln -sf "$FOUND_BIN" "$HOME/.local/bin/cursor-agent"
+ fi
+ echo "Installed cursor-agent at: $(command -v cursor-agent || true) (resolved: $FOUND_BIN)"
+ fi
+}
+
+# Write MCP config to user's home if provided (ARG_FOLDER/.cursor/mcp.json)
+function write_mcp_config() {
+ TARGET_DIR="$ARG_FOLDER/.cursor"
+ TARGET_FILE="$TARGET_DIR/mcp.json"
+ mkdir -p "$TARGET_DIR"
+
+ CURSOR_MCP_HACK_SCRIPT=$(
+ cat << EOF
+#!/usr/bin/env bash
+set -e
+
+# --- Set environment variables ---
+export CODER_MCP_APP_STATUS_SLUG="${ARG_CODER_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}"
+
+# --- Launch the MCP server ---
+exec coder exp mcp server
+EOF
+ )
+ echo "$CURSOR_MCP_HACK_SCRIPT" > "/tmp/mcp-hack.sh"
+ chmod +x /tmp/mcp-hack.sh
+
+ CODER_MCP=$(
+ cat << EOF
+{
+ "coder": {
+ "args": [],
+ "command": "/tmp/mcp-hack.sh",
+ "description": "Report ALL tasks and statuses (in progress, done, failed) you are working on.",
+ "name": "Coder",
+ "timeout": 3000,
+ "type": "stdio",
+ "trust": true
+ }
+}
+EOF
+ )
+
+ echo "${ARG_WORKSPACE_MCP_JSON:-{}}" | jq --argjson base "$CODER_MCP" \
+ '.mcpServers = ((.mcpServers // {}) + $base)' > "$TARGET_FILE"
+ echo "Wrote workspace MCP to $TARGET_FILE"
+}
+
+# Write rules files to user's home (FOLDER/.cursor/rules)
+function write_rules_file() {
+ if [ -n "$ARG_WORKSPACE_RULES_JSON" ]; then
+ RULES_DIR="$ARG_FOLDER/.cursor/rules"
+ mkdir -p "$RULES_DIR"
+ echo "$ARG_WORKSPACE_RULES_JSON" | jq -r 'to_entries[] | @base64' | while read -r entry; do
+ _jq() { echo "${entry}" | base64 -d | jq -r ${1}; }
+ NAME=$(_jq '.key')
+ CONTENT=$(_jq '.value')
+ echo "$CONTENT" > "$RULES_DIR/$NAME"
+ echo "Wrote rule: $RULES_DIR/$NAME"
+ done
+ fi
+}
+
+install_cursor_cli
+write_mcp_config
+write_rules_file
diff --git a/registry/coder-labs/modules/cursor-cli/scripts/start.sh b/registry/coder-labs/modules/cursor-cli/scripts/start.sh
new file mode 100644
index 000000000..1bbc493bb
--- /dev/null
+++ b/registry/coder-labs/modules/cursor-cli/scripts/start.sh
@@ -0,0 +1,67 @@
+#!/bin/bash
+
+set -o errexit
+set -o pipefail
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
+ARG_FORCE=${ARG_FORCE:-false}
+ARG_MODEL=${ARG_MODEL:-}
+ARG_OUTPUT_FORMAT=${ARG_OUTPUT_FORMAT:-json}
+ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.cursor-cli-module}
+ARG_FOLDER=${ARG_FOLDER:-$HOME}
+
+echo "--------------------------------"
+echo "install: $ARG_INSTALL"
+echo "version: $ARG_VERSION"
+echo "folder: $ARG_FOLDER"
+echo "ai_prompt: $ARG_AI_PROMPT"
+echo "force: $ARG_FORCE"
+echo "model: $ARG_MODEL"
+echo "output_format: $ARG_OUTPUT_FORMAT"
+echo "module_dir_name: $ARG_MODULE_DIR_NAME"
+echo "folder: $ARG_FOLDER"
+echo "--------------------------------"
+
+mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
+
+# Find cursor agent cli
+if command_exists cursor-agent; then
+ CURSOR_CMD=cursor-agent
+elif [ -x "$HOME/.local/bin/cursor-agent" ]; then
+ CURSOR_CMD="$HOME/.local/bin/cursor-agent"
+else
+ echo "Error: cursor-agent not found. Install it or set install_cursor_cli=true."
+ exit 1
+fi
+
+# Ensure working directory exists
+if [ -d "$ARG_FOLDER" ]; then
+ cd "$ARG_FOLDER"
+else
+ mkdir -p "$ARG_FOLDER"
+ cd "$ARG_FOLDER"
+fi
+
+ARGS=()
+
+# global flags
+if [ -n "$ARG_MODEL" ]; then
+ ARGS+=("-m" "$ARG_MODEL")
+fi
+if [ "$ARG_FORCE" = "true" ]; then
+ ARGS+=("-f")
+fi
+
+if [ -n "$ARG_AI_PROMPT" ]; then
+ printf "AI prompt provided\n"
+ ARGS+=("Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT")
+fi
+
+# Log and run in background, redirecting all output to the log file
+printf "Running: %q %s\n" "$CURSOR_CMD" "$(printf '%q ' "${ARGS[@]}")"
+
+agentapi server --type cursor --term-width 67 --term-height 1190 -- "$CURSOR_CMD" "${ARGS[@]}"
diff --git a/registry/coder-labs/modules/cursor-cli/testdata/cursor-cli-mock.sh b/registry/coder-labs/modules/cursor-cli/testdata/cursor-cli-mock.sh
new file mode 100644
index 000000000..acf637d06
--- /dev/null
+++ b/registry/coder-labs/modules/cursor-cli/testdata/cursor-cli-mock.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+if [[ "$1" == "--version" ]]; then
+ echo "HELLO: $(bash -c env)"
+ echo "cursor-agent version v2.5.0"
+ exit 0
+fi
+
+set -e
+
+while true; do
+ echo "$(date) - cursor-agent-mock"
+ sleep 15
+done
\ No newline at end of file
diff --git a/registry/coder-labs/modules/sourcegraph_amp/README.md b/registry/coder-labs/modules/sourcegraph_amp/README.md
new file mode 100644
index 000000000..45d175d24
--- /dev/null
+++ b/registry/coder-labs/modules/sourcegraph_amp/README.md
@@ -0,0 +1,90 @@
+---
+display_name: Sourcegraph AMP
+icon: ../../../../.icons/sourcegraph-amp.svg
+description: Run Sourcegraph AMP CLI in your workspace with AgentAPI integration
+verified: false
+tags: [agent, sourcegraph, amp, ai, tasks]
+---
+
+# Sourcegraph AMP CLI
+
+Run [Sourcegraph AMP CLI](https://sourcegraph.com/amp) in your workspace to access Sourcegraph's AI-powered code search and analysis tools, with AgentAPI integration for seamless Coder Tasks support.
+
+```tf
+module "sourcegraph_amp" {
+ source = "registry.coder.com/coder-labs/sourcegraph_amp/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+ sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
+ install_sourcegraph_amp = true
+ agentapi_version = "latest"
+}
+```
+
+## Prerequisites
+
+- Include the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module in your template
+- Node.js and npm are automatically installed (via NVM) if not already available
+
+## Usage Example
+
+```tf
+data "coder_parameter" "ai_prompt" {
+ name = "AI Prompt"
+ description = "Write an initial prompt for AMP to work on."
+ type = "string"
+ default = ""
+ mutable = true
+
+}
+
+# Set system prompt for Sourcegraph Amp via environment variables
+resource "coder_agent" "main" {
+ # ...
+ env = {
+ SOURCEGRAPH_AMP_SYSTEM_PROMPT = <<-EOT
+ You are an AMP assistant that helps developers debug and write code efficiently.
+
+ Always log task status to Coder.
+ EOT
+ SOURCEGRAPH_AMP_TASK_PROMPT = data.coder_parameter.ai_prompt.value
+ }
+}
+
+variable "sourcegraph_amp_api_key" {
+ type = string
+ description = "Sourcegraph AMP API key"
+ sensitive = true
+}
+
+module "sourcegraph_amp" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder-labs/sourcegraph_amp/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.example.id
+ sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
+ install_sourcegraph_amp = true
+}
+```
+
+## How it Works
+
+- **Install**: Installs Sourcegraph AMP CLI using npm (installs Node.js via NVM if required)
+- **Start**: Launches AMP CLI in the specified directory, wrapped with AgentAPI to enable tasks and AI interactions
+- **Environment Variables**: Sets `SOURCEGRAPH_AMP_API_KEY` and `SOURCEGRAPH_AMP_START_DIRECTORY` for the CLI execution
+
+## Troubleshooting
+
+- If `amp` is not found, ensure `install_sourcegraph_amp = true` and your API key is valid
+- Logs are written under `/home/coder/.sourcegraph-amp-module/` (`install.log`, `agentapi-start.log`) for debugging
+- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
+
+> [!IMPORTANT]
+> For using **Coder Tasks** with Sourcegraph AMP, make sure to pass the `AI Prompt` parameter and set `sourcegraph_amp_api_key`.
+> This ensures task reporting and status updates work seamlessly.
+
+## References
+
+- [Sourcegraph AMP Documentation](https://ampcode.com/manual)
+- [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/sourcegraph_amp/main.test.ts b/registry/coder-labs/modules/sourcegraph_amp/main.test.ts
new file mode 100644
index 000000000..a08497087
--- /dev/null
+++ b/registry/coder-labs/modules/sourcegraph_amp/main.test.ts
@@ -0,0 +1,157 @@
+import {
+ test,
+ afterEach,
+ describe,
+ setDefaultTimeout,
+ beforeAll,
+ expect,
+} from "bun:test";
+import { execContainer, readFileContainer, runTerraformInit } from "~test";
+import {
+ loadTestFile,
+ writeExecutable,
+ setup as setupUtil,
+ execModuleScript,
+ expectAgentAPIStarted,
+} from "../../../coder/modules/agentapi/test-util";
+
+let cleanupFunctions: (() => Promise)[] = [];
+const registerCleanup = (cleanup: () => Promise) => {
+ cleanupFunctions.push(cleanup);
+};
+afterEach(async () => {
+ const cleanupFnsCopy = cleanupFunctions.slice().reverse();
+ cleanupFunctions = [];
+ for (const cleanup of cleanupFnsCopy) {
+ try {
+ await cleanup();
+ } catch (error) {
+ console.error("Error during cleanup:", error);
+ }
+ }
+});
+
+interface SetupProps {
+ skipAgentAPIMock?: boolean;
+ skipAmpMock?: boolean;
+ moduleVariables?: Record;
+ agentapiMockScript?: string;
+}
+
+const setup = async (props?: SetupProps): Promise<{ id: string }> => {
+ const projectDir = "/home/coder/project";
+ const { id } = await setupUtil({
+ moduleDir: import.meta.dir,
+ moduleVariables: {
+ install_sourcegraph_amp: props?.skipAmpMock ? "true" : "false",
+ install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
+ sourcegraph_amp_model: "test-model",
+ ...props?.moduleVariables,
+ },
+ registerCleanup,
+ projectDir,
+ skipAgentAPIMock: props?.skipAgentAPIMock,
+ agentapiMockScript: props?.agentapiMockScript,
+ });
+
+ // Place the AMP mock CLI binary inside the container
+ if (!props?.skipAmpMock) {
+ await writeExecutable({
+ containerId: id,
+ filePath: "/usr/bin/amp",
+ content: await loadTestFile(`${import.meta.dir}`, "amp-mock.sh"),
+ });
+ }
+
+ return { id };
+};
+
+setDefaultTimeout(60 * 1000);
+
+describe("sourcegraph-amp", async () => {
+ beforeAll(async () => {
+ await runTerraformInit(import.meta.dir);
+ });
+
+ test("happy-path", async () => {
+ const { id } = await setup();
+ await execModuleScript(id);
+ await expectAgentAPIStarted(id);
+ });
+
+ test("api-key", async () => {
+ const apiKey = "test-api-key-123";
+ const { id } = await setup({
+ moduleVariables: {
+ sourcegraph_amp_api_key: apiKey,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.sourcegraph-amp-module/agentapi-start.log",
+ );
+ expect(resp).toContain("sourcegraph_amp_api_key provided !");
+ });
+
+ test("custom-folder", async () => {
+ const folder = "/tmp/sourcegraph-amp-test";
+ const { id } = await setup({
+ moduleVariables: {
+ folder,
+ },
+ });
+ await execModuleScript(id);
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.sourcegraph-amp-module/install.log",
+ );
+ expect(resp).toContain(folder);
+ });
+
+ test("pre-post-install-scripts", async () => {
+ const { id } = await setup({
+ moduleVariables: {
+ pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
+ post_install_script: "#!/bin/bash\necho 'post-install-script'",
+ },
+ });
+ await execModuleScript(id);
+ const preLog = await readFileContainer(
+ id,
+ "/home/coder/.sourcegraph-amp-module/pre_install.log",
+ );
+ expect(preLog).toContain("pre-install-script");
+ const postLog = await readFileContainer(
+ id,
+ "/home/coder/.sourcegraph-amp-module/post_install.log",
+ );
+ expect(postLog).toContain("post-install-script");
+ });
+
+ test("system-prompt", async () => {
+ const prompt = "this is a system prompt for AMP";
+ const { id } = await setup();
+ await execModuleScript(id, {
+ SOURCEGRAPH_AMP_SYSTEM_PROMPT: prompt,
+ });
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.sourcegraph-amp-module/SYSTEM_PROMPT.md",
+ );
+ expect(resp).toContain(prompt);
+ });
+
+ test("task-prompt", async () => {
+ const prompt = "this is a task prompt for AMP";
+ const { id } = await setup();
+ await execModuleScript(id, {
+ SOURCEGRAPH_AMP_TASK_PROMPT: prompt,
+ });
+ const resp = await readFileContainer(
+ id,
+ "/home/coder/.sourcegraph-amp-module/agentapi-start.log",
+ );
+ expect(resp).toContain(`sourcegraph amp task prompt provided : ${prompt}`);
+ });
+});
diff --git a/registry/coder-labs/modules/sourcegraph_amp/main.tf b/registry/coder-labs/modules/sourcegraph_amp/main.tf
new file mode 100644
index 000000000..033fc84ed
--- /dev/null
+++ b/registry/coder-labs/modules/sourcegraph_amp/main.tf
@@ -0,0 +1,195 @@
+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."
+}
+
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+variable "order" {
+ type = number
+ description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
+ 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/sourcegraph-amp.svg"
+}
+
+variable "folder" {
+ type = string
+ description = "The folder to run sourcegraph_amp in."
+ default = "/home/coder"
+}
+
+variable "install_sourcegraph_amp" {
+ type = bool
+ description = "Whether to install sourcegraph-amp."
+ default = true
+}
+
+variable "sourcegraph_amp_api_key" {
+ type = string
+ description = "sourcegraph-amp API Key"
+ default = ""
+}
+
+resource "coder_env" "sourcegraph_amp_api_key" {
+ agent_id = var.agent_id
+ name = "SOURCEGRAPH_AMP_API_KEY"
+ value = var.sourcegraph_amp_api_key
+}
+
+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.3.0"
+}
+
+variable "pre_install_script" {
+ type = string
+ description = "Custom script to run before installing sourcegraph_amp"
+ default = null
+}
+
+variable "post_install_script" {
+ type = string
+ description = "Custom script to run after installing sourcegraph_amp."
+ default = null
+}
+
+variable "base_amp_config" {
+ type = string
+ description = <<-EOT
+ Base AMP configuration in JSON format. Can be overridden to customize AMP settings.
+
+ If empty, defaults enable thinking and todos for autonomous operation. Additional options include:
+ - "amp.permissions": [] (tool permissions)
+ - "amp.tools.stopTimeout": 600 (extend timeout for long operations)
+ - "amp.terminal.commands.nodeSpawn.loadProfile": "daily" (environment loading)
+ - "amp.tools.disable": ["builtin:open"] (disable tools for containers)
+ - "amp.git.commit.ampThread.enabled": true (link commits to threads)
+ - "amp.git.commit.coauthor.enabled": true (add Amp as co-author)
+
+ Reference: https://ampcode.com/manual
+ EOT
+ default = ""
+}
+
+variable "additional_mcp_servers" {
+ type = string
+ description = "Additional MCP servers configuration in JSON format to append to amp.mcpServers."
+ default = null
+}
+
+locals {
+ app_slug = "amp"
+
+ default_base_config = {
+ "amp.anthropic.thinking.enabled" = true
+ "amp.todos.enabled" = true
+ }
+
+ # Use provided config or default, then extract base settings (excluding mcpServers)
+ user_config = var.base_amp_config != "" ? jsondecode(var.base_amp_config) : local.default_base_config
+ base_amp_settings = { for k, v in local.user_config : k => v if k != "amp.mcpServers" }
+
+ coder_mcp = {
+ "coder" = {
+ "command" = "coder"
+ "args" = ["exp", "mcp", "server"]
+ "env" = {
+ "CODER_MCP_APP_STATUS_SLUG" = local.app_slug
+ "CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284"
+ }
+ "type" = "stdio"
+ }
+ }
+
+ additional_mcp = var.additional_mcp_servers != null ? jsondecode(var.additional_mcp_servers) : {}
+
+ merged_mcp_servers = merge(
+ lookup(local.user_config, "amp.mcpServers", {}),
+ local.coder_mcp,
+ local.additional_mcp
+ )
+
+ final_config = merge(local.base_amp_settings, {
+ "amp.mcpServers" = local.merged_mcp_servers
+ })
+
+ install_script = file("${path.module}/scripts/install.sh")
+ start_script = file("${path.module}/scripts/start.sh")
+ module_dir_name = ".sourcegraph-amp-module"
+}
+
+module "agentapi" {
+ source = "registry.coder.com/coder/agentapi/coder"
+ version = "1.0.1"
+
+ agent_id = var.agent_id
+ web_app_slug = local.app_slug
+ web_app_order = var.order
+ web_app_group = var.group
+ web_app_icon = var.icon
+ web_app_display_name = "Sourcegraph Amp"
+ cli_app_slug = "${local.app_slug}-cli"
+ cli_app_display_name = "Sourcegraph Amp CLI"
+ 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
+ SOURCEGRAPH_AMP_API_KEY='${var.sourcegraph_amp_api_key}' \
+ SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
+ /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_INSTALL_SOURCEGRAPH_AMP='${var.install_sourcegraph_amp}' \
+ SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
+ ARG_AMP_CONFIG="$(echo -n '${base64encode(jsonencode(local.final_config))}' | base64 -d)" \
+ /tmp/install.sh
+ EOT
+}
+
+
diff --git a/registry/coder-labs/modules/sourcegraph_amp/scripts/install.sh b/registry/coder-labs/modules/sourcegraph_amp/scripts/install.sh
new file mode 100644
index 000000000..61e498b7b
--- /dev/null
+++ b/registry/coder-labs/modules/sourcegraph_amp/scripts/install.sh
@@ -0,0 +1,96 @@
+#!/bin/bash
+set -euo pipefail
+
+# ANSI colors
+BOLD='\033[1m'
+
+echo "--------------------------------"
+echo "Install flag: $ARG_INSTALL_SOURCEGRAPH_AMP"
+echo "Workspace: $SOURCEGRAPH_AMP_START_DIRECTORY"
+echo "--------------------------------"
+
+# Helper function to check if a command exists
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+function install_node() {
+ if ! command_exists npm; then
+ printf "npm not found, checking for Node.js installation...\n"
+ if ! command_exists node; then
+ printf "Node.js not found, installing Node.js via NVM...\n"
+ export NVM_DIR="$HOME/.nvm"
+ if [ ! -d "$NVM_DIR" ]; then
+ mkdir -p "$NVM_DIR"
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+ else
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+ fi
+
+ # Temporarily disable nounset (-u) for nvm to avoid PROVIDED_VERSION error
+ set +u
+ nvm install --lts
+ nvm use --lts
+ nvm alias default node
+ set -u
+
+ printf "Node.js installed: %s\n" "$(node --version)"
+ printf "npm installed: %s\n" "$(npm --version)"
+ else
+ printf "Node.js is installed but npm is not available. Please install npm manually.\n"
+ exit 1
+ fi
+ fi
+}
+
+function install_sourcegraph_amp() {
+ if [ "${ARG_INSTALL_SOURCEGRAPH_AMP}" = "true" ]; then
+ install_node
+
+ # If nvm is not used, set up user npm global directory
+ if ! command_exists nvm; then
+ mkdir -p "$HOME/.npm-global"
+ npm config set prefix "$HOME/.npm-global"
+ export PATH="$HOME/.npm-global/bin:$PATH"
+ if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
+ echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
+ fi
+ fi
+
+ printf "%s Installing Sourcegraph AMP CLI...\n" "${BOLD}"
+ npm install -g @sourcegraph/amp@0.0.1754179307-gba1f97
+ printf "%s Successfully installed Sourcegraph AMP CLI. Version: %s\n" "${BOLD}" "$(amp --version)"
+ fi
+}
+
+function setup_system_prompt() {
+ if [ -n "${SOURCEGRAPH_AMP_SYSTEM_PROMPT:-}" ]; then
+ echo "Setting Sourcegraph AMP system prompt..."
+ mkdir -p "$HOME/.sourcegraph-amp-module"
+ echo "$SOURCEGRAPH_AMP_SYSTEM_PROMPT" > "$HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
+ echo "System prompt saved to $HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
+ else
+ echo "No system prompt provided for Sourcegraph AMP."
+ fi
+}
+
+function configure_amp_settings() {
+ echo "Configuring AMP settings..."
+ SETTINGS_PATH="$HOME/.config/amp/settings.json"
+ mkdir -p "$(dirname "$SETTINGS_PATH")"
+
+ if [ -z "${ARG_AMP_CONFIG:-}" ]; then
+ echo "No AMP config provided, skipping configuration"
+ return
+ fi
+
+ echo "Writing AMP configuration to $SETTINGS_PATH"
+ printf '%s\n' "$ARG_AMP_CONFIG" > "$SETTINGS_PATH"
+
+ echo "AMP configuration complete"
+}
+
+install_sourcegraph_amp
+setup_system_prompt
+configure_amp_settings
diff --git a/registry/coder-labs/modules/sourcegraph_amp/scripts/start.sh b/registry/coder-labs/modules/sourcegraph_amp/scripts/start.sh
new file mode 100644
index 000000000..252b343f8
--- /dev/null
+++ b/registry/coder-labs/modules/sourcegraph_amp/scripts/start.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+set -euo pipefail
+
+# Load user environment
+# shellcheck source=/dev/null
+source "$HOME/.bashrc"
+# shellcheck source=/dev/null
+if [ -f "$HOME/.nvm/nvm.sh" ]; then
+ source "$HOME"/.nvm/nvm.sh
+else
+ export PATH="$HOME/.npm-global/bin:$PATH"
+fi
+
+function ensure_command() {
+ command -v "$1" &> /dev/null || {
+ echo "Error: '$1' not found." >&2
+ exit 1
+ }
+}
+
+ensure_command amp
+echo "AMP version: $(amp --version)"
+
+dir="$SOURCEGRAPH_AMP_START_DIRECTORY"
+if [[ -d "$dir" ]]; then
+ echo "Using existing directory: $dir"
+else
+ echo "Creating directory: $dir"
+ mkdir -p "$dir"
+fi
+cd "$dir"
+
+if [ -n "$SOURCEGRAPH_AMP_API_KEY" ]; then
+ printf "sourcegraph_amp_api_key provided !\n"
+ export AMP_API_KEY=$SOURCEGRAPH_AMP_API_KEY
+else
+ printf "sourcegraph_amp_api_key not provided\n"
+fi
+
+if [ -n "${SOURCEGRAPH_AMP_TASK_PROMPT:-}" ]; then
+ printf "sourcegraph amp task prompt provided : $SOURCEGRAPH_AMP_TASK_PROMPT"
+ PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $SOURCEGRAPH_AMP_TASK_PROMPT"
+
+ # Pipe the prompt into amp, which will be run inside agentapi
+ agentapi server --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
+else
+ printf "No task prompt given.\n"
+ agentapi server --term-width=67 --term-height=1190 -- amp
+fi
diff --git a/registry/coder-labs/modules/sourcegraph_amp/testdata/amp-mock.sh b/registry/coder-labs/modules/sourcegraph_amp/testdata/amp-mock.sh
new file mode 100644
index 000000000..259db57ad
--- /dev/null
+++ b/registry/coder-labs/modules/sourcegraph_amp/testdata/amp-mock.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# Mock behavior of the AMP CLI
+if [[ "$1" == "--version" ]]; then
+ echo "AMP CLI mock version v1.0.0"
+ exit 0
+fi
+
+# Simulate AMP running in a loop for AgentAPI to connect
+set -e
+while true; do
+ echo "$(date) - AMP mock is running..."
+ sleep 15
+done
diff --git a/registry/coder-labs/templates/tasks-docker/main.tf b/registry/coder-labs/templates/tasks-docker/main.tf
index ad232317d..35a64540a 100644
--- a/registry/coder-labs/templates/tasks-docker/main.tf
+++ b/registry/coder-labs/templates/tasks-docker/main.tf
@@ -181,7 +181,7 @@ resource "coder_env" "claude_task_prompt" {
resource "coder_env" "app_status_slug" {
agent_id = coder_agent.main.id
name = "CODER_MCP_APP_STATUS_SLUG"
- value = "claude-code"
+ value = "ccw"
}
resource "coder_env" "claude_system_prompt" {
agent_id = coder_agent.main.id
diff --git a/registry/coder/.images/amazon-q-new.png b/registry/coder/.images/amazon-q-new.png
new file mode 100644
index 000000000..6024135e8
Binary files /dev/null and b/registry/coder/.images/amazon-q-new.png differ
diff --git a/registry/coder/modules/amazon-q/README.md b/registry/coder/modules/amazon-q/README.md
index 2d6b081f4..41b347ed9 100644
--- a/registry/coder/modules/amazon-q/README.md
+++ b/registry/coder/modules/amazon-q/README.md
@@ -1,121 +1,376 @@
---
display_name: Amazon Q
-description: Run Amazon Q in your workspace to access Amazon's AI coding assistant.
+description: Run Amazon Q in your workspace to access Amazon's AI coding assistant with MCP integration and task reporting.
icon: ../../../../.icons/amazon-q.svg
verified: true
-tags: [agent, ai, aws, amazon-q]
+tags: [agent, ai, aws, amazon-q, mcp, agentapi]
---
# Amazon Q
-Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module installs and launches Amazon Q, with support for background operation, task reporting, and custom pre/post install scripts.
+Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts.
+
+## Quick Start
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
- version = "1.1.2"
+ version = "2.0.0"
agent_id = coder_agent.example.id
- # Required: see below for how to generate
- experiment_auth_tarball = var.amazon_q_auth_tarball
+ # Required: Authentication tarball (see below for generation)
+ auth_tarball = var.amazon_q_auth_tarball
}
```
-
+
+
+## Features
+
+- **🚀 Automatic Installation**: Downloads and installs Amazon Q CLI automatically
+- **🔐 Authentication**: Supports pre-authenticated tarball for seamless login
+- **📊 Task Reporting**: Built-in MCP integration for reporting progress to Coder
+- **🎯 AI Prompts**: Support for initial task prompts and custom system prompts
+- **🔧 Customization**: Pre/post install scripts for custom setup
+- **🌐 AgentAPI Integration**: Web and CLI app integration through AgentAPI
+- **🛠️ Tool Trust**: Configurable tool trust settings
+- **📁 Flexible Deployment**: Configurable working directory and module structure
## Prerequisites
-- You must generate an authenticated Amazon Q tarball on another machine:
- ```sh
- cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0
- ```
- Paste the result into the `experiment_auth_tarball` variable.
-- To run in the background, your workspace must have `screen` or `tmux` installed.
+### Authentication Tarball (Required)
-
-How to generate the Amazon Q auth tarball (step-by-step)
+You must generate an authenticated Amazon Q tarball on another machine where you have successfully logged in:
-**1. Install and authenticate Amazon Q on your local machine:**
+```bash
+# 1. Install Amazon Q and login on your local machine
+q login
-- Download and install Amazon Q from the [official site](https://aws.amazon.com/q/developer/).
-- Run `q login` and complete the authentication process in your terminal.
+# 2. Generate the authentication tarball
+cd ~/.local/share/amazon-q
+tar -c . | zstd | base64 -w 0
+```
-**2. Locate your Amazon Q config directory:**
+Copy the output and use it as the `auth_tarball` variable.
-- The config is typically stored at `~/.local/share/amazon-q`.
+
+Detailed Authentication Setup
-**3. Generate the tarball:**
+**Step 1: Install Amazon Q locally**
-- Run the following command in your terminal:
- ```sh
- cd ~/.local/share/amazon-q
- tar -c . | zstd | base64 -w 0
- ```
+- Download from [AWS Amazon Q Developer](https://aws.amazon.com/q/developer/)
+- Follow the installation instructions for your platform
-**4. Copy the output:**
+**Step 2: Authenticate**
-- The command will output a long string. Copy this entire string.
+```bash
+q login
+```
+
+Complete the authentication process in your browser.
-**5. Paste into your Terraform variable:**
+**Step 3: Generate tarball**
-- Assign the string to the `experiment_auth_tarball` variable in your Terraform configuration, for example:
- ```tf
- variable "amazon_q_auth_tarball" {
- type = string
- default = "PASTE_LONG_STRING_HERE"
- }
- ```
+```bash
+cd ~/.local/share/amazon-q
+tar -c . | zstd | base64 -w 0 > /tmp/amazon-q-auth.txt
+```
-**Note:**
+**Step 4: Use in Terraform**
+
+```tf
+variable "amazon_q_auth_tarball" {
+ type = string
+ sensitive = true
+ default = "PASTE_YOUR_TARBALL_HERE"
+}
+```
-- You must re-generate the tarball if you log out or re-authenticate Amazon Q on your local machine.
-- This process is required for each user who wants to use Amazon Q in their workspace.
+**Important Notes:**
-[Reference: Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/generate-docs.html)
+- Regenerate the tarball if you logout or re-authenticate
+- Each user needs their own authentication tarball
+- Keep the tarball secure as it contains authentication credentials
-## Examples
+## Configuration Variables
+
+### Required Variables
+
+| Variable | Type | Description |
+| ---------- | -------- | ----------------------- |
+| `agent_id` | `string` | The ID of a Coder agent |
+
+### Optional Variables
+
+| Variable | Type | Default | Description |
+| --------------------- | -------- | --------------- | ----------------------------------------------------------------------------------------------------- |
+| `auth_tarball` | `string` | `""` | Base64 encoded, zstd compressed tarball of authenticated Amazon Q directory |
+| `amazon_q_version` | `string` | `"latest"` | Version of Amazon Q to install |
+| `install_amazon_q` | `bool` | `true` | Whether to install Amazon Q CLI |
+| `install_agentapi` | `bool` | `true` | Whether to install AgentAPI for web integration |
+| `agentapi_version` | `string` | `"v0.5.0"` | Version of AgentAPI to install |
+| `folder` | `string` | `"/home/coder"` | Working directory for Amazon Q |
+| `trust_all_tools` | `bool` | `true` | Whether to trust all tools in Amazon Q |
+| `ai_prompt` | `string` | `""` | Initial task prompt to send to Amazon Q |
+| `system_prompt` | `string` | _See below_ | System prompt for task reporting behavior |
+| `pre_install_script` | `string` | `null` | Script to run before installing Amazon Q |
+| `post_install_script` | `string` | `null` | Script to run after installing Amazon Q |
+| `agent_config` | `string` | `null` | Custom agent configuration JSON (See the [Default Agent configuration](#default-agent-configuration)) |
+
+### UI Configuration
+
+| Variable | Type | Default | Description |
+| -------- | -------- | ---------------------- | ------------------------------------------- |
+| `order` | `number` | `null` | Position in UI (lower numbers appear first) |
+| `group` | `string` | `null` | Group name for organizing apps |
+| `icon` | `string` | `"/icon/amazon-q.svg"` | Icon to display in UI |
+
+### Default System Prompt
+
+The module includes a comprehensive system prompt that instructs Amazon Q:
+
+```
+You are a helpful Coding assistant. Aim to autonomously investigate
+and solve issues the user gives you and test your work, whenever possible.
+Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
+but opt for autonomy.
+
+YOU MUST REPORT ALL TASKS TO CODER.
+When reporting tasks, you MUST follow these EXACT instructions:
+- IMMEDIATELY report status after receiving ANY user message.
+- Be granular. If you are investigating with multiple steps, report each step to coder.
+
+Task state MUST be one of the following:
+- Use "state": "working" when actively processing WITHOUT needing additional user input.
+- Use "state": "complete" only when finished with a task.
+- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
+
+Task summaries MUST:
+- Include specifics about what you're doing.
+- Include clear and actionable steps for the user.
+- Be less than 160 characters in length.
+```
+
+### System Prompt Features:
+
+- **Autonomous Operation:** Encourages Amazon Q to work independently and test solutions
+- **Task Reporting:** Mandatory reporting to Coder via MCP integration
+- **Granular Updates:** Step-by-step progress reporting for complex tasks
+- **Clear State Management:** Three distinct states (working, complete, failure)
+- **Concise Summaries:** 160-character limit for actionable task summaries
+- **User Interaction:** Clear guidelines on when to ask for user input
+
+You can customize this behavior by providing your own system prompt via the `system_prompt` variable.
+
+## Default Agent Configuration
+
+The module includes a default agent configuration template that provides a comprehensive setup for Amazon Q integration:
+
+```json
+{
+ "name": "agent",
+ "description": "This is an default agent config",
+ "prompt": "${system_prompt}",
+ "mcpServers": {},
+ "tools": ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
+ "toolAliases": {},
+ "allowedTools": ["fs_read"],
+ "resources": [
+ "file://AmazonQ.md",
+ "file://README.md",
+ "file://.amazonq/rules/**/*.md"
+ ],
+ "hooks": {},
+ "toolsSettings": {},
+ "useLegacyMcpJson": true
+}
+```
+
+### Configuration Details:
+
+- **Tools Available:** File operations, bash execution, AWS CLI, and knowledge base access
+- **Allowed Tools:** By default, only `fs_read` is allowed (can be customized)
+- **Resources:** Access to documentation and rule files in the workspace
+- **MCP Servers:** Empty by default, can be configured via `agent_config` variable
+- **System Prompt:** Dynamically populated from the `system_prompt` variable
+
+You can override this configuration by providing your own JSON via the `agent_config` variable.
+
+## Usage Examples
+
+### Basic Usage
+
+```tf
+module "amazon-q" {
+ source = "registry.coder.com/coder/amazon-q/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ auth_tarball = var.amazon_q_auth_tarball
+}
+```
+
+### With Custom AI Prompt
+
+```tf
+module "amazon-q" {
+ source = "registry.coder.com/coder/amazon-q/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ auth_tarball = var.amazon_q_auth_tarball
+ ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
+}
+```
+
+### With Custom Pre/Post Install Scripts
+
+```tf
+module "amazon-q" {
+ source = "registry.coder.com/coder/amazon-q/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ auth_tarball = var.amazon_q_auth_tarball
+
+ pre_install_script = <<-EOT
+ #!/bin/bash
+ echo "Setting up custom environment..."
+ # Install additional dependencies
+ sudo apt-get update && sudo apt-get install -y zstd
+ EOT
+
+ post_install_script = <<-EOT
+ #!/bin/bash
+ echo "Configuring Amazon Q settings..."
+ # Custom configuration commands
+ q settings chat.model claude-3-sonnet
+ EOT
+}
+```
-### Run Amazon Q in the background with tmux
+### Specific Version Installation
```tf
module "amazon-q" {
- source = "registry.coder.com/coder/amazon-q/coder"
- version = "1.1.2"
- agent_id = coder_agent.example.id
- experiment_auth_tarball = var.amazon_q_auth_tarball
- experiment_use_tmux = true
+ source = "registry.coder.com/coder/amazon-q/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ auth_tarball = var.amazon_q_auth_tarball
+ amazon_q_version = "1.14.0" # Specific version
+ install_amazon_q = true
}
```
-### Enable task reporting (experimental)
+### Custom Agent Configuration
```tf
module "amazon-q" {
- source = "registry.coder.com/coder/amazon-q/coder"
- version = "1.1.2"
- agent_id = coder_agent.example.id
- experiment_auth_tarball = var.amazon_q_auth_tarball
- experiment_report_tasks = true
+ source = "registry.coder.com/coder/amazon-q/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ auth_tarball = var.amazon_q_auth_tarball
+
+ agent_config = jsonencode({
+ name = "custom-agent"
+ description = "Custom Amazon Q agent for my workspace"
+ prompt = "You are a specialized DevOps assistant..."
+ tools = ["fs_read", "fs_write", "execute_bash", "use_aws"]
+ })
}
```
-### Run custom scripts before/after install
+### UI Customization
```tf
module "amazon-q" {
- source = "registry.coder.com/coder/amazon-q/coder"
- version = "1.1.2"
- agent_id = coder_agent.example.id
- experiment_auth_tarball = var.amazon_q_auth_tarball
- experiment_pre_install_script = "echo Pre-install!"
- experiment_post_install_script = "echo Post-install!"
+ source = "registry.coder.com/coder/amazon-q/coder"
+ version = "2.0.0"
+ agent_id = coder_agent.example.id
+ auth_tarball = var.amazon_q_auth_tarball
+
+ # UI configuration
+ order = 1
+ group = "AI Tools"
+ icon = "/icon/custom-amazon-q.svg"
}
```
-## Notes
+## Architecture
+
+### Components
+
+1. **AgentAPI Module**: Provides web and CLI app integration
+2. **Install Script**: Handles Amazon Q CLI installation and configuration
+3. **Start Script**: Manages Amazon Q startup with proper environment
+4. **MCP Integration**: Enables task reporting back to Coder
+5. **Agent Configuration**: Customizable AI agent behavior
+
+### Installation Process
+
+1. **Pre-install**: Execute custom pre-install script (if provided)
+2. **Download**: Fetch Amazon Q CLI for the appropriate architecture
+3. **Install**: Install Amazon Q CLI to `~/.local/bin/q`
+4. **Authenticate**: Extract and apply authentication tarball
+5. **Configure**: Set up MCP integration and agent configuration
+6. **Post-install**: Execute custom post-install script (if provided)
+
+### Runtime Behavior
+
+- Amazon Q runs in the specified working directory
+- MCP integration reports task progress to Coder
+- AgentAPI provides web interface integration
+- All tools are trusted by default (configurable)
+- Initial AI prompt is sent if provided
+
+## Troubleshooting
+
+### Common Issues
+
+**Amazon Q not found after installation:**
+
+```bash
+# Check if Amazon Q is in PATH
+which q
+# If not found, add to PATH
+export PATH="$PATH:$HOME/.local/bin"
+```
+
+**Authentication issues:**
+
+- Regenerate the auth tarball on your local machine
+- Ensure the tarball is properly base64 encoded
+- Check that the original authentication is still valid
+
+**MCP integration not working:**
+
+- Verify that AgentAPI is installed (`install_agentapi = true`)
+- Check that the Coder agent is properly configured
+- Review the system prompt configuration
+
+### Debug Mode
+
+Enable verbose logging by setting environment variables:
+
+```bash
+export DEBUG=1
+export VERBOSE=1
+```
+
+## Security Considerations
+
+- **Authentication Tarball**: Contains sensitive authentication data - mark as sensitive in Terraform
+- **Tool Trust**: By default, all tools are trusted - review for security requirements
+- **Pre/Post Scripts**: Custom scripts run with user permissions - validate content
+- **Network Access**: Amazon Q requires internet access for AI model communication
+
+## Contributing
+
+For issues, feature requests, or contributions, please visit the [module repository](https://github.com/coder/registry).
+
+## License
+
+This module is provided under the same license as the Coder registry.
+
+---
-- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
-- If neither is set, Amazon Q runs in the foreground.
-- For more details, see the [main.tf](./main.tf) source.
+**Note**: This module requires Coder v2.7+ and is designed to work with the AgentAPI integration system.
diff --git a/registry/coder/modules/amazon-q/amazon-q.tftest.hcl b/registry/coder/modules/amazon-q/amazon-q.tftest.hcl
new file mode 100644
index 000000000..34c0d8c27
--- /dev/null
+++ b/registry/coder/modules/amazon-q/amazon-q.tftest.hcl
@@ -0,0 +1,144 @@
+run "required_variables" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-id"
+ }
+}
+
+run "minimal_config" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-id"
+ auth_tarball = "dGVzdA==" # base64 "test"
+ }
+
+ assert {
+ condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
+ error_message = "Status slug environment variable not configured correctly"
+ }
+
+ assert {
+ condition = resource.coder_env.status_slug.value == "amazonq"
+ error_message = "Status slug value should be 'amazonq'"
+ }
+}
+
+run "full_config" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-id"
+ folder = "/home/coder/project"
+ install_amazon_q = true
+ install_agentapi = true
+ agentapi_version = "v0.5.0"
+ amazon_q_version = "latest"
+ trust_all_tools = true
+ ai_prompt = "Build a web application"
+ auth_tarball = "dGVzdA=="
+ order = 1
+ group = "AI Tools"
+ icon = "/icon/custom-amazon-q.svg"
+ pre_install_script = "echo 'pre-install'"
+ post_install_script = "echo 'post-install'"
+ agent_config = jsonencode({
+ name = "test-agent"
+ description = "Test agent configuration"
+ })
+ }
+
+ assert {
+ condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
+ error_message = "Status slug environment variable not configured correctly"
+ }
+
+ assert {
+ condition = resource.coder_env.status_slug.value == "amazonq"
+ error_message = "Status slug value should be 'amazonq'"
+ }
+
+ assert {
+ condition = length(resource.coder_env.auth_tarball) == 1
+ error_message = "Auth tarball environment variable should be created when provided"
+ }
+}
+
+run "auth_tarball_environment" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-id"
+ auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
+ }
+
+ assert {
+ condition = resource.coder_env.auth_tarball[0].name == "AMAZON_Q_AUTH_TARBALL"
+ error_message = "Auth tarball environment variable name should be 'AMAZON_Q_AUTH_TARBALL'"
+ }
+
+ assert {
+ condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
+ error_message = "Auth tarball environment variable value should match input"
+ }
+}
+
+run "empty_auth_tarball" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-id"
+ auth_tarball = ""
+ }
+
+ assert {
+ condition = length(resource.coder_env.auth_tarball) == 0
+ error_message = "Auth tarball environment variable should not be created when empty"
+ }
+}
+
+run "custom_system_prompt" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-id"
+ system_prompt = "Custom system prompt for testing"
+ }
+
+ # Test that the system prompt is used in the agent config template
+ assert {
+ condition = length(local.agent_config) > 0
+ error_message = "Agent config should be generated with custom system prompt"
+ }
+}
+
+run "install_options" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-id"
+ install_amazon_q = false
+ install_agentapi = false
+ }
+
+ assert {
+ condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
+ error_message = "Status slug should still be configured even when install options are disabled"
+ }
+}
+
+run "version_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent-id"
+ amazon_q_version = "2.15.0"
+ agentapi_version = "v0.4.0"
+ }
+
+ assert {
+ condition = resource.coder_env.status_slug.value == "amazonq"
+ error_message = "Status slug value should remain 'amazonq' regardless of version"
+ }
+}
diff --git a/registry/coder/modules/amazon-q/main.test.ts b/registry/coder/modules/amazon-q/main.test.ts
deleted file mode 100644
index 54553da87..000000000
--- a/registry/coder/modules/amazon-q/main.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { describe, it, expect } from "bun:test";
-import {
- runTerraformApply,
- runTerraformInit,
- testRequiredVariables,
- findResourceInstance,
-} from "~test";
-import path from "path";
-
-const moduleDir = path.resolve(__dirname);
-
-const requiredVars = {
- agent_id: "dummy-agent-id",
-};
-
-describe("amazon-q module", async () => {
- await runTerraformInit(moduleDir);
-
- // 1. Required variables
- testRequiredVariables(moduleDir, requiredVars);
-
- // 2. coder_script resource is created
- it("creates coder_script resource", async () => {
- const state = await runTerraformApply(moduleDir, requiredVars);
- const scriptResource = findResourceInstance(state, "coder_script");
- expect(scriptResource).toBeDefined();
- expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
- // Optionally, check that the script contains expected lines
- expect(scriptResource.script).toContain("Installing Amazon Q");
- });
-
- // 3. coder_app resource is created
- it("creates coder_app resource", async () => {
- const state = await runTerraformApply(moduleDir, requiredVars);
- const appResource = findResourceInstance(state, "coder_app", "amazon_q");
- expect(appResource).toBeDefined();
- expect(appResource.agent_id).toBe(requiredVars.agent_id);
- });
-
- // Add more state-based tests as needed
-});
diff --git a/registry/coder/modules/amazon-q/main.tf b/registry/coder/modules/amazon-q/main.tf
index dcc03156a..9925c3975 100644
--- a/registry/coder/modules/amazon-q/main.tf
+++ b/registry/coder/modules/amazon-q/main.tf
@@ -1,10 +1,12 @@
+# Improved amazon-q module main.tf
+
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
- version = ">= 2.5"
+ version = ">= 2.7" # Updated to match cursor-cli
}
}
}
@@ -15,7 +17,6 @@ variable "agent_id" {
}
data "coder_workspace" "me" {}
-
data "coder_workspace_owner" "me" {}
variable "order" {
@@ -48,46 +49,34 @@ variable "install_amazon_q" {
default = true
}
-variable "amazon_q_version" {
- type = string
- description = "The version of Amazon Q to install."
- default = "latest"
-}
-
-variable "experiment_use_screen" {
- type = bool
- description = "Whether to use screen for running Amazon Q in the background."
- default = false
-}
-
-variable "experiment_use_tmux" {
+variable "install_agentapi" {
type = bool
- description = "Whether to use tmux instead of screen for running Amazon Q in the background."
- default = false
+ description = "Whether to install AgentAPI."
+ default = true
}
-variable "experiment_report_tasks" {
- type = bool
- description = "Whether to enable task reporting."
- default = false
+variable "agentapi_version" {
+ type = string
+ description = "The version of AgentAPI to install."
+ default = "v0.5.0"
}
-variable "experiment_pre_install_script" {
+variable "amazon_q_version" {
type = string
- description = "Custom script to run before installing Amazon Q."
- default = null
+ description = "The version of Amazon Q to install."
+ default = "latest"
}
-variable "experiment_post_install_script" {
- type = string
- description = "Custom script to run after installing Amazon Q."
- default = null
+variable "trust_all_tools" {
+ type = bool
+ description = "Whether to trust all tools in Amazon Q."
+ default = true
}
-variable "experiment_auth_tarball" {
+variable "ai_prompt" {
type = string
- description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory. After running `q login` on another machine, you may generate it with: `cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0`"
- default = "tarball"
+ description = "The initial task prompt to send to Amazon Q."
+ default = ""
}
variable "system_prompt" {
@@ -116,204 +105,106 @@ variable "system_prompt" {
EOT
}
-variable "ai_prompt" {
+variable "auth_tarball" {
type = string
- description = "The initial task prompt to send to Amazon Q."
- default = "Please help me with my coding tasks. I'll provide specific instructions as needed."
+ description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory. After running `q login` on another machine, you may generate it with: `cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0`"
+ default = ""
+ sensitive = true
}
-locals {
- encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
- encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
- full_prompt = <<-EOT
- ${var.system_prompt}
+variable "pre_install_script" {
+ type = string
+ description = "Optional script to run before installing Amazon Q."
+ default = null
+}
- Your first task is:
+variable "post_install_script" {
+ type = string
+ description = "Optional script to run after installing Amazon Q."
+ default = null
+}
- ${var.ai_prompt}
- EOT
+variable "agent_config" {
+ type = string
+ description = "Optional Agent configuration JSON for Amazon Q."
+ default = null
}
-resource "coder_script" "amazon_q" {
- agent_id = var.agent_id
- display_name = "Amazon Q"
- icon = var.icon
- script = <<-EOT
+# Expose status slug to the agent environment
+resource "coder_env" "status_slug" {
+ agent_id = var.agent_id
+ name = "CODER_MCP_APP_STATUS_SLUG"
+ value = local.app_slug
+}
+
+# Expose auth tarball as environment variable for install script
+resource "coder_env" "auth_tarball" {
+ count = var.auth_tarball != "" ? 1 : 0
+ agent_id = var.agent_id
+ name = "AMAZON_Q_AUTH_TARBALL"
+ value = var.auth_tarball
+}
+
+locals {
+ app_slug = "amazonq"
+ install_script = file("${path.module}/scripts/install.sh")
+ start_script = file("${path.module}/scripts/start.sh")
+ module_dir_name = ".amazonq"
+ agent_config = var.agent_config == null ? templatefile("${path.module}/templates/agent-config.json.tpl", {
+ system_prompt = var.system_prompt
+ }) : var.agent_config
+ full_prompt = var.ai_prompt != null ? "${var.ai_prompt}" : ""
+}
+
+
+module "agentapi" {
+ source = "registry.coder.com/coder/agentapi/coder"
+ version = "1.1.1"
+
+ agent_id = var.agent_id
+ web_app_slug = local.app_slug
+ web_app_order = var.order
+ web_app_group = var.group
+ web_app_icon = var.icon
+ web_app_display_name = "Amazon Q"
+ cli_app_slug = local.app_slug
+ cli_app_display_name = "Amazon Q"
+ 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
- command_exists() {
- command -v "$1" >/dev/null 2>&1
- }
-
- if [ -n "${local.encoded_pre_install_script}" ]; then
- echo "Running pre-install script..."
- echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
- chmod +x /tmp/pre_install.sh
- /tmp/pre_install.sh
- fi
-
- if [ "${var.install_amazon_q}" = "true" ]; then
- echo "Installing Amazon Q..."
- PREV_DIR="$PWD"
- TMP_DIR="$(mktemp -d)"
- cd "$TMP_DIR"
-
- ARCH="$(uname -m)"
- case "$ARCH" in
- "x86_64")
- Q_URL="https://desktop-release.q.us-east-1.amazonaws.com/${var.amazon_q_version}/q-x86_64-linux.zip"
- ;;
- "aarch64"|"arm64")
- Q_URL="https://desktop-release.codewhisperer.us-east-1.amazonaws.com/${var.amazon_q_version}/q-aarch64-linux.zip"
- ;;
- *)
- echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
- exit 1
- ;;
- esac
-
- echo "Downloading Amazon Q for $ARCH..."
- curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
- unzip q.zip
- ./q/install.sh --no-confirm
- cd "$PREV_DIR"
- export PATH="$PATH:$HOME/.local/bin"
- echo "Installed Amazon Q version: $(q --version)"
- fi
-
- echo "Extracting auth tarball..."
- PREV_DIR="$PWD"
- echo "${var.experiment_auth_tarball}" | base64 -d > /tmp/auth.tar.zst
- rm -rf ~/.local/share/amazon-q
- mkdir -p ~/.local/share/amazon-q
- cd ~/.local/share/amazon-q
- tar -I zstd -xf /tmp/auth.tar.zst
- rm /tmp/auth.tar.zst
- cd "$PREV_DIR"
- echo "Extracted auth tarball"
-
- if [ "${var.experiment_report_tasks}" = "true" ]; then
- echo "Configuring Amazon Q to report tasks via Coder MCP..."
- q mcp add --name coder --command "coder" --args "exp,mcp,server,--allowed-tools,coder_report_task" --env "CODER_MCP_APP_STATUS_SLUG=amazon-q" --scope global --force
- echo "Added Coder MCP server to Amazon Q configuration"
- fi
-
- if [ -n "${local.encoded_post_install_script}" ]; then
- echo "Running post-install script..."
- echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
- chmod +x /tmp/post_install.sh
- /tmp/post_install.sh
- fi
-
- if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
- echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
- echo "Please set only one of them to true."
- exit 1
- fi
-
- if [ "${var.experiment_use_tmux}" = "true" ]; then
- echo "Running Amazon Q in the background with tmux..."
-
- if ! command_exists tmux; then
- echo "Error: tmux is not installed. Please install tmux manually."
- exit 1
- fi
-
- touch "$HOME/.amazon-q.log"
-
- export LANG=en_US.UTF-8
- export LC_ALL=en_US.UTF-8
-
- tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
-
- tmux send-keys -t amazon-q "${local.full_prompt}"
- sleep 5
- tmux send-keys -t amazon-q Enter
- fi
-
- if [ "${var.experiment_use_screen}" = "true" ]; then
- echo "Running Amazon Q in the background..."
-
- if ! command_exists screen; then
- echo "Error: screen is not installed. Please install screen manually."
- exit 1
- fi
-
- touch "$HOME/.amazon-q.log"
-
- if [ ! -f "$HOME/.screenrc" ]; then
- echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.log"
- echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
- fi
-
- if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
- echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
- echo "multiuser on" >> "$HOME/.screenrc"
- fi
-
- if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
- echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
- echo "acladd $(whoami)" >> "$HOME/.screenrc"
- fi
- export LANG=en_US.UTF-8
- export LC_ALL=en_US.UTF-8
-
- screen -U -dmS amazon-q bash -c '
- cd ${var.folder}
- q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
- exec bash
- '
- # Extremely hacky way to send the prompt to the screen session
- # This will be fixed in the future, but `amazon-q` was not sending MCP
- # tasks when an initial prompt is provided.
- screen -S amazon-q -X stuff "${local.full_prompt}"
- sleep 5
- screen -S amazon-q -X stuff "^M"
- else
- if ! command_exists q; then
- echo "Error: Amazon Q is not installed. Please enable install_amazon_q or install it manually."
- exit 1
- fi
- fi
- EOT
- run_on_start = true
-}
+ echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
+ chmod +x /tmp/start.sh
+ ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
+ ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
+ ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
+ ARG_FOLDER='${var.folder}' \
+ SERVER_PARAMETERS="/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" \
+ /tmp/start.sh
+ EOT
-resource "coder_app" "amazon_q" {
- slug = "amazon-q"
- display_name = "Amazon Q"
- agent_id = var.agent_id
- command = <<-EOT
+ install_script = <<-EOT
#!/bin/bash
- set -e
-
- export LANG=en_US.UTF-8
- export LC_ALL=en_US.UTF-8
-
- if [ "${var.experiment_use_tmux}" = "true" ]; then
- if tmux has-session -t amazon-q 2>/dev/null; then
- echo "Attaching to existing Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
- tmux attach-session -t amazon-q
- else
- echo "Starting a new Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
- tmux new-session -s amazon-q -c ${var.folder} "q chat --trust-all-tools | tee -a \"$HOME/.amazon-q.log\"; exec bash"
- fi
- elif [ "${var.experiment_use_screen}" = "true" ]; then
- if screen -list | grep -q "amazon-q"; then
- echo "Attaching to existing Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
- screen -xRR amazon-q
- else
- echo "Starting a new Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
- screen -S amazon-q bash -c 'q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log"; exec bash'
- fi
- else
- cd ${var.folder}
- q chat --trust-all-tools
- fi
- EOT
- icon = var.icon
- order = var.order
- group = var.group
+ set -o errexit
+ set -o pipefail
+
+ echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
+ chmod +x /tmp/install.sh
+ ARG_INSTALL='${var.install_amazon_q}' \
+ ARG_VERSION='${var.amazon_q_version}' \
+ ARG_AUTH_TARBALL='${var.auth_tarball}' \
+ ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \
+ ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
+ ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
+ ARG_PRE_INSTALL_SCRIPT='${var.pre_install_script != null ? base64encode(var.pre_install_script) : ""}' \
+ ARG_POST_INSTALL_SCRIPT='${var.post_install_script != null ? base64encode(var.post_install_script) : ""}' \
+ /tmp/install.sh
+ EOT
}
diff --git a/registry/coder/modules/amazon-q/scripts/install.sh b/registry/coder/modules/amazon-q/scripts/install.sh
new file mode 100644
index 000000000..3553f1fb4
--- /dev/null
+++ b/registry/coder/modules/amazon-q/scripts/install.sh
@@ -0,0 +1,168 @@
+#!/bin/bash
+# Install script for amazon-q module
+
+set -o errexit
+set -o pipefail
+
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# Inputs
+ARG_INSTALL=${ARG_INSTALL:-true}
+ARG_VERSION=${ARG_VERSION:-latest}
+ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-}
+ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-}
+ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/.amazonq}
+ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
+ARG_PRE_INSTALL_SCRIPT=${ARG_PRE_INSTALL_SCRIPT:-}
+ARG_POST_INSTALL_SCRIPT=${ARG_POST_INSTALL_SCRIPT:-}
+
+mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
+
+# Decode base64 inputs
+ARG_AGENT_CONFIG_DECODED=""
+if [ -n "$ARG_AGENT_CONFIG" ]; then
+ ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d)
+fi
+
+echo "--------------------------------"
+echo "install: $ARG_INSTALL"
+echo "version: $ARG_VERSION"
+echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
+echo "module_dir_name: $ARG_MODULE_DIR_NAME"
+echo "auth_tarball_provided: $([ -n "$ARG_AUTH_TARBALL" ] && echo "yes" || echo "no")"
+echo "pre_install_script_provided: $([ -n "$ARG_PRE_INSTALL_SCRIPT" ] && echo "yes" || echo "no")"
+echo "post_install_script_provided: $([ -n "$ARG_POST_INSTALL_SCRIPT" ] && echo "yes" || echo "no")"
+echo "--------------------------------"
+
+# Execute pre-install script if provided
+function pre_install() {
+ if [ -n "$ARG_PRE_INSTALL_SCRIPT" ]; then
+ echo "Executing pre-install script..."
+ # Decode base64 encoded script and execute it
+ echo "$ARG_PRE_INSTALL_SCRIPT" | base64 -d > /tmp/pre_install.sh
+ chmod +x /tmp/pre_install.sh
+ /tmp/pre_install.sh
+ rm -f /tmp/pre_install.sh
+ echo "Pre-install script completed successfully."
+ else
+ echo "No pre-install script provided, skipping..."
+ fi
+}
+
+# Install Amazon Q if requested
+function install_amazon_q() {
+ if [ "$ARG_INSTALL" = "true" ]; then
+ echo "Installing Amazon Q..."
+ PREV_DIR="$PWD"
+ TMP_DIR="$(mktemp -d)"
+ cd "$TMP_DIR"
+
+ ARCH="$(uname -m)"
+ case "$ARCH" in
+ "x86_64")
+ Q_URL="https://desktop-release.q.us-east-1.amazonaws.com/${ARG_VERSION}/q-x86_64-linux.zip"
+ ;;
+ "aarch64" | "arm64")
+ Q_URL="https://desktop-release.codewhisperer.us-east-1.amazonaws.com/${ARG_VERSION}/q-aarch64-linux.zip"
+ ;;
+ *)
+ echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
+ exit 1
+ ;;
+ esac
+
+ echo "Downloading Amazon Q for $ARCH from $Q_URL..."
+ curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
+ unzip q.zip
+ ./q/install.sh --no-confirm
+ cd "$PREV_DIR"
+ rm -rf "$TMP_DIR"
+
+ # Ensure binaries are discoverable; create stable symlink to q
+ CANDIDATES=(
+ "$(command -v q || true)"
+ "$HOME/.local/bin/q"
+ )
+ FOUND_BIN=""
+ for c in "${CANDIDATES[@]}"; do
+ if [ -n "$c" ] && [ -x "$c" ]; then
+ FOUND_BIN="$c"
+ break
+ fi
+ done
+ export PATH="$PATH:$HOME/.local/bin"
+ echo "Installed Amazon Q at: $(command -v q || true) (resolved: $FOUND_BIN)"
+ fi
+}
+
+# Extract authentication tarball
+function extract_auth_tarball() {
+ if [ -n "$ARG_AUTH_TARBALL" ]; then
+ echo "Extracting auth tarball..."
+ PREV_DIR="$PWD"
+ echo "$ARG_AUTH_TARBALL" | base64 -d >/tmp/auth.tar.zst
+ rm -rf ~/.local/share/amazon-q
+ mkdir -p ~/.local/share/amazon-q
+ cd ~/.local/share/amazon-q
+ tar -I zstd -xf /tmp/auth.tar.zst
+ rm /tmp/auth.tar.zst
+ cd "$PREV_DIR"
+ echo "Extracted auth tarball to ~/.local/share/amazon-q"
+ else
+ echo "Warning: No auth tarball provided. Amazon Q may require manual authentication."
+ fi
+}
+
+# Configure MCP integration and create agent
+function configure_agent() {
+ # Create Amazon Q agent configuration directory
+ AGENT_CONFIG_DIR="$HOME/.aws/amazonq/cli-agents"
+ mkdir -p "$AGENT_CONFIG_DIR"
+ if [ -n "$ARG_CODER_MCP_APP_STATUS_SLUG" ]; then
+ echo "Configuring Amazon Q to report tasks via Coder MCP..."
+ # Apply custom MCP configuration if provided
+ if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then
+ echo "Applying custom MCP configuration..."
+ # Parse and apply MCP config - implementation depends on Amazon Q's MCP config format
+ echo "$ARG_AGENT_CONFIG_DECODED" >"$AGENT_CONFIG_DIR/agent.json"
+ echo "Custom configuration saved to $AGENT_CONFIG_DIR/agent.json"
+ fi
+ q mcp add --name coder \
+ --command "coder" \
+ --args "exp,mcp,server,--allowed-tools,coder_report_task" \
+ --env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \
+ --env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \
+ --env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
+ --env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
+ --agent agent \
+ --force || echo "Warning: Failed to add Coder MCP server"
+ echo "Added Coder MCP server into agent in Amazon Q configuration"
+ q settings chat.defaultAgent agent
+ fi
+}
+
+# Execute post-install script if provided
+function post_install() {
+ if [ -n "$ARG_POST_INSTALL_SCRIPT" ]; then
+ echo "Executing post-install script..."
+ # Decode base64 encoded script and execute it
+ echo "$ARG_POST_INSTALL_SCRIPT" | base64 -d > /tmp/post_install.sh
+ chmod +x /tmp/post_install.sh
+ /tmp/post_install.sh
+ rm -f /tmp/post_install.sh
+ echo "Post-install script completed successfully."
+ else
+ echo "No post-install script provided, skipping..."
+ fi
+}
+
+# Main execution
+pre_install
+install_amazon_q
+extract_auth_tarball
+configure_agent
+post_install
+
+echo "Amazon Q installation and configuration complete!"
diff --git a/registry/coder/modules/amazon-q/scripts/start.sh b/registry/coder/modules/amazon-q/scripts/start.sh
new file mode 100644
index 000000000..2bf2d6372
--- /dev/null
+++ b/registry/coder/modules/amazon-q/scripts/start.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+# Start script for amazon-q module
+
+set -o errexit
+set -o pipefail
+
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# Decode inputs
+ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
+ARG_TRUST_ALL_TOOLS=${ARG_TRUST_ALL_TOOLS:-true}
+ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/amazonq}
+ARG_FOLDER=${ARG_FOLDER:-$HOME}
+
+echo "--------------------------------"
+echo "folder: $ARG_FOLDER"
+echo "ai_prompt: $ARG_AI_PROMPT"
+echo "trust_all_tools: $ARG_TRUST_ALL_TOOLS"
+echo "module_dir_name: $ARG_MODULE_DIR_NAME"
+echo "--------------------------------"
+
+mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
+
+# Find Amazon Q CLI
+if command_exists q; then
+ Q_CMD=q
+elif [ -x "$HOME/.local/bin/q" ]; then
+ Q_CMD="$HOME/.local/bin/q"
+else
+ echo "Error: Amazon Q CLI not found. Install it or set install_amazon_q=true."
+ exit 1
+fi
+
+# Ensure working directory exists
+if [ -d "$ARG_FOLDER" ]; then
+ cd "$ARG_FOLDER"
+else
+ mkdir -p "$ARG_FOLDER"
+ cd "$ARG_FOLDER"
+fi
+
+# Set up environment
+export LANG=en_US.UTF-8
+export LC_ALL=en_US.UTF-8
+
+# Build command arguments
+ARGS=("chat")
+
+if [ "$ARG_TRUST_ALL_TOOLS" = "true" ]; then
+ ARGS+=("--trust-all-tools")
+fi
+
+# Log and run with agentapi integration
+printf "Running: %q %s\n" "$Q_CMD" "$(printf '%q ' "${ARGS[@]}")"
+
+# If we have an AI prompt, we need to handle it specially
+if [ -n "$ARG_AI_PROMPT" ]; then
+ printf "AI prompt provided\n"
+ ARGS+=("\"Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool through coder MCP with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT\"")
+fi
+# Use agentapi to manage the interactive session with initial prompt
+agentapi server -c "$SERVER_PARAMETERS" -- "$Q_CMD" "${ARGS[@]}"
diff --git a/registry/coder/modules/amazon-q/templates/agent-config.json.tpl b/registry/coder/modules/amazon-q/templates/agent-config.json.tpl
new file mode 100644
index 000000000..b21810497
--- /dev/null
+++ b/registry/coder/modules/amazon-q/templates/agent-config.json.tpl
@@ -0,0 +1,26 @@
+{
+ "name": "agent",
+ "description": "This is an default agent config",
+ "prompt": "${system_prompt}",
+ "mcpServers": {},
+ "tools": [
+ "fs_read",
+ "fs_write",
+ "execute_bash",
+ "use_aws",
+ "knowledge"
+ ],
+ "toolAliases": {},
+ "allowedTools": [
+ "fs_read"
+ ],
+ "resources": [
+ "file://AmazonQ.md",
+ "file://README.md",
+ "file://.amazonq/rules/**/*.md"
+ ],
+ "hooks": {},
+ "toolsSettings": {},
+ "useLegacyMcpJson": true
+}
+
diff --git a/registry/coder/modules/kasmvnc/README.md b/registry/coder/modules/kasmvnc/README.md
index 1f65aa295..b9b16b269 100644
--- a/registry/coder/modules/kasmvnc/README.md
+++ b/registry/coder/modules/kasmvnc/README.md
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
- version = "1.2.1"
+ version = "1.2.2"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
diff --git a/registry/coder/modules/kasmvnc/run.sh b/registry/coder/modules/kasmvnc/run.sh
index 67a8a310c..5223fb9f0 100644
--- a/registry/coder/modules/kasmvnc/run.sh
+++ b/registry/coder/modules/kasmvnc/run.sh
@@ -240,7 +240,7 @@ get_http_dir() {
# Check the home directory for overriding values
if [[ -e "$HOME/.vnc/kasmvnc.yaml" ]]; then
- d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
+ d=($(grep -E "^\s*httpd_directory:.*$" "$HOME/.vnc/kasmvnc.yaml"))
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
httpd_directory="$${d[1]}"
fi
diff --git a/registry/coder/modules/vscode-web/README.md b/registry/coder/modules/vscode-web/README.md
index 474578577..d4ca481e0 100644
--- a/registry/coder/modules/vscode-web/README.md
+++ b/registry/coder/modules/vscode-web/README.md
@@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
- version = "1.3.1"
+ version = "1.4.1"
agent_id = coder_agent.example.id
accept_license = true
}
@@ -30,7 +30,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
- version = "1.3.1"
+ version = "1.4.1"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
@@ -44,7 +44,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
- version = "1.3.1"
+ version = "1.4.1"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
@@ -59,7 +59,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
- version = "1.3.1"
+ version = "1.4.1"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -77,9 +77,24 @@ By default, this module installs the latest. To pin a specific version, retrieve
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
- version = "1.3.1"
+ version = "1.4.1"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
}
```
+
+### Open an existing workspace on startup
+
+To open an existing workspace on startup the `workspace` parameter can be used to represent a path on disk to a `code-workspace` file.
+Note: Either `workspace` or `folder` can be used, but not both simultaneously. The `code-workspace` file must already be present on disk.
+
+```tf
+module "vscode-web" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/vscode-web/coder"
+ version = "1.4.1"
+ agent_id = coder_agent.example.id
+ workspace = "/home/coder/coder.code-workspace"
+}
+```
diff --git a/registry/coder/modules/vscode-web/main.tf b/registry/coder/modules/vscode-web/main.tf
index c00724544..7a2029c87 100644
--- a/registry/coder/modules/vscode-web/main.tf
+++ b/registry/coder/modules/vscode-web/main.tf
@@ -158,6 +158,12 @@ variable "platform" {
}
}
+variable "workspace" {
+ type = string
+ description = "Path to a .code-workspace file to open in vscode-web."
+ default = ""
+}
+
data "coder_workspace_owner" "me" {}
data "coder_workspace" "me" {}
@@ -178,6 +184,7 @@ resource "coder_script" "vscode-web" {
DISABLE_TRUST : var.disable_trust,
EXTENSIONS_DIR : var.extensions_dir,
FOLDER : var.folder,
+ WORKSPACE : var.workspace,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
SERVER_BASE_PATH : local.server_base_path,
COMMIT_ID : var.commit_id,
@@ -195,6 +202,11 @@ resource "coder_script" "vscode-web" {
condition = !var.offline || !var.use_cached
error_message = "Offline and Use Cached can not be used together"
}
+
+ precondition {
+ condition = (var.workspace == "" || var.folder == "")
+ error_message = "Set only one of `workspace` or `folder`."
+ }
}
}
@@ -218,6 +230,12 @@ resource "coder_app" "vscode-web" {
locals {
server_base_path = var.subdomain ? "" : format("/@%s/%s/apps/%s/", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug)
- url = var.folder == "" ? "http://localhost:${var.port}${local.server_base_path}" : "http://localhost:${var.port}${local.server_base_path}?folder=${var.folder}"
- healthcheck_url = var.subdomain ? "http://localhost:${var.port}/healthz" : "http://localhost:${var.port}${local.server_base_path}/healthz"
+ url = (
+ var.workspace != "" ?
+ "http://localhost:${var.port}${local.server_base_path}?workspace=${urlencode(var.workspace)}" :
+ var.folder != "" ?
+ "http://localhost:${var.port}${local.server_base_path}?folder=${urlencode(var.folder)}" :
+ "http://localhost:${var.port}${local.server_base_path}"
+ )
+ healthcheck_url = var.subdomain ? "http://localhost:${var.port}/healthz" : "http://localhost:${var.port}${local.server_base_path}/healthz"
}
diff --git a/registry/coder/modules/vscode-web/run.sh b/registry/coder/modules/vscode-web/run.sh
index 9346b4bdb..98881d721 100644
--- a/registry/coder/modules/vscode-web/run.sh
+++ b/registry/coder/modules/vscode-web/run.sh
@@ -109,18 +109,27 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
else
- WORKSPACE_DIR="$HOME"
- if [ -n "${FOLDER}" ]; then
- WORKSPACE_DIR="${FOLDER}"
- fi
-
- if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
- printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
- # Use sed to remove single-line comments before parsing with jq
- extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR"/.vscode/extensions.json | jq -r '.recommendations[]')
+ # Prefer WORKSPACE if set and points to a file
+ if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
+ printf "🧩 Installing extensions from %s...\n" "${WORKSPACE}"
+ # Strip single-line comments then parse .extensions.recommendations[]
+ extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
done
+ else
+ # Fallback to folder-based .vscode/extensions.json (existing behavior)
+ WORKSPACE_DIR="$HOME"
+ if [ -n "${FOLDER}" ]; then
+ WORKSPACE_DIR="${FOLDER}"
+ fi
+ if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
+ printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
+ extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
+ for extension in $extensions; do
+ $VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
+ done
+ fi
fi
fi
fi
diff --git a/registry/umair/.images/avatar.jpeg b/registry/umair/.images/avatar.jpeg
new file mode 100644
index 000000000..6495306b9
Binary files /dev/null and b/registry/umair/.images/avatar.jpeg differ
diff --git a/registry/umair/README.md b/registry/umair/README.md
new file mode 100644
index 000000000..c579b28bf
--- /dev/null
+++ b/registry/umair/README.md
@@ -0,0 +1,13 @@
+---
+display_name: "Muhammad Uamir Ali"
+bio: "Cloud Engineer | Infrastructure as code, Kubernetes | SRE"
+github: "m4rrypro"
+avatar: "./.images/avatar.jpeg"
+linkedin: "https://www.linkedin.com/in/m4rry"
+support_email: "m.umair.ali200@gmail.com"
+status: "community"
+---
+
+# Muhammad Umair Ali
+
+Cloud Engineer | Infrastructure as code, Kubernetes | SRE
diff --git a/registry/umair/templates/proxmox-vm/README.md b/registry/umair/templates/proxmox-vm/README.md
new file mode 100644
index 000000000..0f2f44ea5
--- /dev/null
+++ b/registry/umair/templates/proxmox-vm/README.md
@@ -0,0 +1,161 @@
+---
+display_name: Proxmox VM
+description: Provision VMs on Proxmox VE as Coder workspaces
+icon: ../../../../.icons/proxmox.svg
+verified: false
+tags: [proxmox, vm, cloud-init, qemu]
+---
+
+# Proxmox VM Template for Coder
+
+Provision Linux VMs on Proxmox as [Coder workspaces](https://coder.com/docs/workspaces). The template clones a cloud‑init base image, injects user‑data via Snippets, and runs the Coder agent under the workspace owner's Linux user.
+
+## Prerequisites
+
+- Proxmox VE 8/9
+- Proxmox API token with access to nodes and storages
+- SSH access from Coder provisioner to Proxmox VE
+- Storage with "Snippets" content enabled
+- Ubuntu cloud‑init image/template on Proxmox
+ - Latest images: https://cloud-images.ubuntu.com/ ([source](https://cloud-images.ubuntu.com/))
+
+## Prepare a Proxmox Cloud‑Init Template (once)
+
+Run on the Proxmox node. This uses a RELEASE variable so you always pull a current image.
+
+```bash
+# Choose a release (e.g., jammy or noble)
+RELEASE=jammy
+IMG_URL="https://cloud-images.ubuntu.com/${RELEASE}/current/${RELEASE}-server-cloudimg-amd64.img"
+IMG_PATH="/var/lib/vz/template/iso/${RELEASE}-server-cloudimg-amd64.img"
+
+# Download cloud image
+wget "$IMG_URL" -O "$IMG_PATH"
+
+# Create base VM (example ID 999), enable QGA, correct boot order
+NAME="ubuntu-${RELEASE}-cloudinit"
+qm create 999 --name "$NAME" --memory 4096 --cores 2 \
+ --net0 virtio,bridge=vmbr0 --agent enabled=1
+qm set 999 --scsihw virtio-scsi-pci
+qm importdisk 999 "$IMG_PATH" local-lvm
+qm set 999 --scsi0 local-lvm:vm-999-disk-0
+qm set 999 --ide2 local-lvm:cloudinit
+qm set 999 --serial0 socket --vga serial0
+qm set 999 --boot 'order=scsi0;ide2;net0'
+
+# Enable Snippets on storage 'local' (one‑time)
+pvesm set local --content snippets,vztmpl,backup,iso
+
+# Convert to template
+qm template 999
+```
+
+Verify:
+
+```bash
+qm config 999 | grep -E 'template:|agent:|boot:|ide2:|scsi0:'
+```
+
+### Enable Snippets via GUI
+
+- Datacenter → Storage → select `local` → Edit → Content → check "Snippets" → OK
+- Ensure `/var/lib/vz/snippets/` exists on the node for snippet files
+- Template page → Cloud‑Init → Snippet Storage: `local` → File: your yml → Apply
+
+## Configure this template
+
+Edit `terraform.tfvars` with your environment:
+
+```hcl
+# Proxmox API
+proxmox_api_url = "https://:8006/api2/json"
+proxmox_api_token_id = "!"
+proxmox_api_token_secret = ""
+
+# SSH to the node (for snippet upload)
+proxmox_host = ""
+proxmox_password = ""
+proxmox_ssh_user = "root"
+
+# Infra defaults
+proxmox_node = "pve"
+disk_storage = "local-lvm"
+snippet_storage = "local"
+bridge = "vmbr0"
+vlan = 0
+clone_template_vmid = 999
+```
+
+### Variables (terraform.tfvars)
+
+- These values are standard Terraform variables that the template reads at apply time.
+- Place secrets (e.g., `proxmox_api_token_secret`, `proxmox_password`) in `terraform.tfvars` or inject with environment variables using `TF_VAR_*` (e.g., `TF_VAR_proxmox_api_token_secret`).
+- You can also override with `-var`/`-var-file` if you run Terraform directly. With Coder, the repo's `terraform.tfvars` is bundled when pushing the template.
+
+Variables expected:
+
+- `proxmox_api_url`, `proxmox_api_token_id`, `proxmox_api_token_secret` (sensitive)
+- `proxmox_host`, `proxmox_password` (sensitive), `proxmox_ssh_user`
+- `proxmox_node`, `disk_storage`, `snippet_storage`, `bridge`, `vlan`, `clone_template_vmid`
+- Coder parameters: `cpu_cores`, `memory_mb`, `disk_size_gb`
+
+## Proxmox API Token (GUI/CLI)
+
+Docs: https://pve.proxmox.com/wiki/User_Management#pveum_tokens
+
+GUI:
+
+1. (Optional) Create automation user: Datacenter → Permissions → Users → Add (e.g., `terraform@pve`)
+2. Permissions: Datacenter → Permissions → Add → User Permission
+ - Path: `/` (or narrower covering your nodes/storages)
+ - Role: `PVEVMAdmin` + `PVEStorageAdmin` (or `PVEAdmin` for simplicity)
+3. Token: Datacenter → Permissions → API Tokens → Add → copy Token ID and Secret
+4. Test:
+
+```bash
+curl -k -H "Authorization: PVEAPIToken=!=" \
+ https:// < PVE_HOST > :8006/api2/json/version
+```
+
+CLI:
+
+```bash
+pveum user add terraform@pve --comment 'Terraform automation user'
+pveum aclmod / -user terraform@pve -role PVEAdmin
+pveum user token add terraform@pve terraform --privsep 0
+```
+
+## Use
+
+```bash
+# From this directory
+coder templates push --yes proxmox-cloudinit --directory . | cat
+```
+
+Create a workspace from the template in the Coder UI. First boot usually takes 60–120s while cloud‑init runs.
+
+## How it works
+
+- Uploads rendered cloud‑init user‑data to `:snippets/.yml` via the provider's `proxmox_virtual_environment_file`
+- VM config: `virtio-scsi-pci`, boot order `scsi0, ide2, net0`, QGA enabled
+- Linux user equals Coder workspace owner (sanitized). To avoid collisions, reserved names (`admin`, `root`, etc.) get a suffix (e.g., `admin1`). User is created with `primary_group: adm`, `groups: [sudo]`, `no_user_group: true`
+- systemd service runs as that user:
+ - `coder-agent.service`
+
+## Troubleshooting quick hits
+
+- iPXE boot loop: ensure template has bootable root disk and boot order `scsi0,ide2,net0`
+- QGA not responding: install/enable QGA in template; allow 60–120s on first boot
+- Snippet upload errors: storage must include `Snippets`; token needs Datastore permissions; path format `:snippets/` handled by provider
+- Permissions errors: ensure the token's role covers the target node(s) and storages
+- Verify snippet/QGA: `qm config | egrep 'cicustom|ide2|ciuser'`
+
+## References
+
+- Ubuntu Cloud Images (latest): https://cloud-images.ubuntu.com/ ([source](https://cloud-images.ubuntu.com/))
+- Proxmox qm(1) manual: https://pve.proxmox.com/pve-docs/qm.1.html
+- Proxmox Cloud‑Init Support: https://pve.proxmox.com/wiki/Cloud-Init_Support
+- Terraform Proxmox provider (bpg): `bpg/proxmox` on the Terraform Registry
+- Coder – Best practices & templates:
+ - https://coder.com/docs/tutorials/best-practices/speed-up-templates
+ - https://coder.com/docs/tutorials/template-from-scratch
diff --git a/registry/umair/templates/proxmox-vm/cloud-init/user-data.tftpl b/registry/umair/templates/proxmox-vm/cloud-init/user-data.tftpl
new file mode 100644
index 000000000..2cd34d0ac
--- /dev/null
+++ b/registry/umair/templates/proxmox-vm/cloud-init/user-data.tftpl
@@ -0,0 +1,53 @@
+#cloud-config
+hostname: ${hostname}
+
+users:
+ - name: ${linux_user}
+ groups: [sudo]
+ shell: /bin/bash
+ sudo: ["ALL=(ALL) NOPASSWD:ALL"]
+
+package_update: false
+package_upgrade: false
+packages:
+ - curl
+ - ca-certificates
+ - git
+ - jq
+
+write_files:
+ - path: /opt/coder/init.sh
+ permissions: "0755"
+ owner: root:root
+ encoding: b64
+ content: |
+ ${coder_init_script_b64}
+
+ - path: /etc/systemd/system/coder-agent.service
+ permissions: "0644"
+ owner: root:root
+ content: |
+ [Unit]
+ Description=Coder Agent
+ Wants=network-online.target
+ After=network-online.target
+
+ [Service]
+ Type=simple
+ User=${linux_user}
+ WorkingDirectory=/home/${linux_user}
+ Environment=HOME=/home/${linux_user}
+ Environment=CODER_AGENT_TOKEN=${coder_token}
+ ExecStart=/opt/coder/init.sh
+ OOMScoreAdjust=-1000
+ Restart=always
+ RestartSec=5
+
+ [Install]
+ WantedBy=multi-user.target
+
+runcmd:
+ - systemctl daemon-reload
+ - systemctl enable --now coder-agent.service
+
+final_message: "Cloud-init complete on ${hostname}"
\ No newline at end of file
diff --git a/registry/umair/templates/proxmox-vm/main.tf b/registry/umair/templates/proxmox-vm/main.tf
new file mode 100644
index 000000000..86da81b6c
--- /dev/null
+++ b/registry/umair/templates/proxmox-vm/main.tf
@@ -0,0 +1,283 @@
+terraform {
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ }
+ proxmox = {
+ source = "bpg/proxmox"
+ }
+ }
+}
+
+provider "coder" {}
+
+provider "proxmox" {
+ endpoint = var.proxmox_api_url
+ api_token = "${var.proxmox_api_token_id}=${var.proxmox_api_token_secret}"
+ insecure = true
+
+ # SSH is needed for file uploads to Proxmox
+ ssh {
+ username = var.proxmox_ssh_user
+ password = var.proxmox_password
+
+ node {
+ name = var.proxmox_node
+ address = var.proxmox_host
+ }
+ }
+}
+
+variable "proxmox_api_url" {
+ type = string
+}
+
+variable "proxmox_api_token_id" {
+ type = string
+ sensitive = true
+}
+
+variable "proxmox_api_token_secret" {
+ type = string
+ sensitive = true
+}
+
+
+variable "proxmox_host" {
+ description = "Proxmox node IP or DNS for SSH"
+ type = string
+}
+
+variable "proxmox_password" {
+ description = "Proxmox password (used for SSH)"
+ type = string
+ sensitive = true
+}
+
+variable "proxmox_ssh_user" {
+ description = "SSH username on Proxmox node"
+ type = string
+ default = "root"
+}
+
+variable "proxmox_node" {
+ description = "Target Proxmox node"
+ type = string
+ default = "pve"
+}
+variable "disk_storage" {
+ description = "Disk storage (e.g., local-lvm)"
+ type = string
+ default = "local-lvm"
+}
+
+variable "snippet_storage" {
+ description = "Storage with Snippets content"
+ type = string
+ default = "local"
+}
+
+variable "bridge" {
+ description = "Bridge (e.g., vmbr0)"
+ type = string
+ default = "vmbr0"
+}
+
+variable "vlan" {
+ description = "VLAN tag (0 none)"
+ type = number
+ default = 0
+}
+
+variable "clone_template_vmid" {
+ description = "VMID of the cloud-init base template to clone"
+ type = number
+}
+
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
+data "coder_parameter" "cpu_cores" {
+ name = "cpu_cores"
+ display_name = "CPU Cores"
+ type = "number"
+ default = 2
+ mutable = true
+}
+
+data "coder_parameter" "memory_mb" {
+ name = "memory_mb"
+ display_name = "Memory (MB)"
+ type = "number"
+ default = 4096
+ mutable = true
+}
+
+data "coder_parameter" "disk_size_gb" {
+ name = "disk_size_gb"
+ display_name = "Disk Size (GB)"
+ type = "number"
+ default = 20
+ mutable = true
+ validation {
+ min = 10
+ max = 100
+ monotonic = "increasing"
+ }
+}
+
+resource "coder_agent" "dev" {
+ arch = "amd64"
+ os = "linux"
+
+ env = {
+ GIT_AUTHOR_NAME = data.coder_workspace_owner.me.name
+ GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email
+ }
+
+ startup_script_behavior = "non-blocking"
+ startup_script = <<-EOT
+ set -e
+ # Add any startup scripts here
+ EOT
+
+ metadata {
+ display_name = "CPU Usage"
+ key = "cpu_usage"
+ script = "coder stat cpu"
+ interval = 10
+ timeout = 1
+ order = 1
+ }
+
+ metadata {
+ display_name = "RAM Usage"
+ key = "ram_usage"
+ script = "coder stat mem"
+ interval = 10
+ timeout = 1
+ order = 2
+ }
+
+ metadata {
+ display_name = "Disk Usage"
+ key = "disk_usage"
+ script = "coder stat disk"
+ interval = 600
+ timeout = 30
+ order = 3
+ }
+}
+
+locals {
+ hostname = lower(data.coder_workspace.me.name)
+ vm_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${local.hostname}"
+ snippet_filename = "${local.vm_name}.yml"
+ base_user = replace(replace(replace(lower(data.coder_workspace_owner.me.name), " ", "-"), "/", "-"), "@", "-") # to avoid special characters in the username
+ linux_user = contains(["root", "admin", "daemon", "bin", "sys"], local.base_user) ? "${local.base_user}1" : local.base_user # to avoid conflict with system users
+
+ rendered_user_data = templatefile("${path.module}/cloud-init/user-data.tftpl", {
+ coder_token = coder_agent.dev.token
+ coder_init_script_b64 = base64encode(coder_agent.dev.init_script)
+ hostname = local.vm_name
+ linux_user = local.linux_user
+ })
+}
+
+resource "proxmox_virtual_environment_file" "cloud_init_user_data" {
+ content_type = "snippets"
+ datastore_id = var.snippet_storage
+ node_name = var.proxmox_node
+
+ source_raw {
+ data = local.rendered_user_data
+ file_name = local.snippet_filename
+ }
+}
+
+resource "proxmox_virtual_environment_vm" "workspace" {
+ name = local.vm_name
+ node_name = var.proxmox_node
+
+ clone {
+ node_name = var.proxmox_node
+ vm_id = var.clone_template_vmid
+ full = false
+ retries = 5
+ }
+
+ agent {
+ enabled = true
+ }
+
+ on_boot = true
+ started = true
+
+ startup {
+ order = 1
+ }
+
+ scsi_hardware = "virtio-scsi-pci"
+ boot_order = ["scsi0", "ide2"]
+
+ memory {
+ dedicated = data.coder_parameter.memory_mb.value
+ }
+
+ cpu {
+ cores = data.coder_parameter.cpu_cores.value
+ sockets = 1
+ type = "host"
+ }
+
+ network_device {
+ bridge = var.bridge
+ model = "virtio"
+ vlan_id = var.vlan == 0 ? null : var.vlan
+ }
+
+ vga {
+ type = "serial0"
+ }
+
+ serial_device {
+ device = "socket"
+ }
+
+ disk {
+ interface = "scsi0"
+ datastore_id = var.disk_storage
+ size = data.coder_parameter.disk_size_gb.value
+ }
+
+ initialization {
+ type = "nocloud"
+ datastore_id = var.disk_storage
+
+ user_data_file_id = proxmox_virtual_environment_file.cloud_init_user_data.id
+
+ ip_config {
+ ipv4 {
+ address = "dhcp"
+ }
+ }
+ }
+
+ tags = ["coder", "workspace", local.vm_name]
+
+ depends_on = [proxmox_virtual_environment_file.cloud_init_user_data]
+}
+
+module "code-server" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/code-server/coder"
+ version = "1.3.1"
+ agent_id = coder_agent.dev.id
+}
+
+module "cursor" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/cursor/coder"
+ version = "1.3.0"
+ agent_id = coder_agent.dev.id
+}
\ No newline at end of file
diff --git a/scripts/tag_release.sh b/scripts/tag_release.sh
index c73f93e2b..d2d85f60b 100755
--- a/scripts/tag_release.sh
+++ b/scripts/tag_release.sh
@@ -28,7 +28,6 @@ JSON_OUTPUT='{
readonly EXIT_SUCCESS=0
readonly EXIT_ERROR=1
readonly EXIT_NO_ACTION_NEEDED=2
-readonly EXIT_VALIDATION_FAILED=3
usage() {
cat << EOF
@@ -52,7 +51,7 @@ EXAMPLES:
$0 -m code-server -d # Target specific module
$0 -n coder -m code-server -d # Target module in namespace
-Exit codes: 0=success, 1=error, 2=no action needed, 3=validation failed
+Exit codes: 0=success, 1=error, 2=no action needed
EOF
exit 0
}
@@ -230,29 +229,38 @@ extract_version_from_readme() {
return 1
}
- local version_line
- version_line=$(grep -E "source[[:space:]]*=[[:space:]]*\"registry\.coder\.com/${namespace}/${module_name}" "$readme_path" | head -1 || echo "")
+ local version
+ version=$(extract_version_from_module_block "$readme_path" "$namespace" "$module_name")
- if [ -n "$version_line" ]; then
- local version
- version=$(echo "$version_line" | sed -n 's/.*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p')
- if [ -n "$version" ]; then
- log "DEBUG" "Found version '$version' from source line: $version_line"
- echo "$version"
- return 0
- fi
+ if [ -n "$version" ]; then
+ log "DEBUG" "Found version '$version' from module block for $namespace/$module_name"
+ echo "$version"
+ return 0
fi
- local fallback_version
- fallback_version=$(grep -E 'version[[:space:]]*=[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+"' "$readme_path" | head -1 | sed 's/.*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/' || echo "")
+ log "DEBUG" "No version found in module block for $namespace/$module_name in $readme_path"
+ return 1
+}
+
+extract_version_from_module_block() {
+ local readme_path="$1"
+ local namespace="$2"
+ local module_name="$3"
+
+ local version
+ version=$(grep -A 10 "source[[:space:]]*=[[:space:]]*\"registry\.coder\.com/${namespace}/${module_name}/coder" "$readme_path" \
+ | sed '/^[[:space:]]*}/q' \
+ | grep -E "version[[:space:]]*=[[:space:]]*\"[^\"]+\"" \
+ | head -1 \
+ | sed 's/.*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/')
- if [ -n "$fallback_version" ]; then
- log "DEBUG" "Found fallback version '$fallback_version'"
- echo "$fallback_version"
+ if [ -n "$version" ]; then
+ log "DEBUG" "Found version '$version' for $namespace/$module_name"
+ echo "$version"
return 0
fi
- log "DEBUG" "No version found in $readme_path"
+ log "DEBUG" "No version found within module block for $namespace/$module_name"
return 1
}
@@ -300,7 +308,9 @@ detect_modules_needing_tags() {
fi
local all_modules
- all_modules=$(find registry -mindepth 3 -maxdepth 3 -type d -path "*/modules/*" | sort -u || echo "")
+ # Find all module directories, excluding hidden directories
+ # This works on both macOS and Linux
+ all_modules=$(find registry -mindepth 3 -maxdepth 3 -type d -path "*/modules/*" ! -name ".*" | sort -u || echo "")
[ -z "$all_modules" ] && {
log "ERROR" "No modules found to check"
@@ -546,7 +556,7 @@ create_and_push_tags() {
echo ""
fi
- if [ $pushed_tags -gt 0 ]; then
+ if [ "$pushed_tags" -gt 0 ]; then
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
log "SUCCESS" "🎉 Successfully created and pushed $pushed_tags release tags!"
echo ""
@@ -606,7 +616,7 @@ main() {
detect_exit_code=$?
case $detect_exit_code in
- $EXIT_NO_ACTION_NEEDED)
+ "$EXIT_NO_ACTION_NEEDED")
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
finalize_json_output "$@"
else
@@ -614,7 +624,7 @@ main() {
fi
exit $EXIT_SUCCESS
;;
- $EXIT_ERROR)
+ "$EXIT_ERROR")
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "scan_failed"')
finalize_json_output "$@"