diff --git a/.icons/poolside.svg b/.icons/poolside.svg new file mode 100644 index 000000000..c83f3bbff --- /dev/null +++ b/.icons/poolside.svg @@ -0,0 +1,4 @@ + + + + diff --git a/registry/coder-labs/modules/pool/README.md b/registry/coder-labs/modules/pool/README.md new file mode 100644 index 000000000..e654df1b3 --- /dev/null +++ b/registry/coder-labs/modules/pool/README.md @@ -0,0 +1,102 @@ +--- +display_name: Pool CLI +icon: ../../../../.icons/poolside.svg +description: Install and configure Poolside's Pool coding agent in your workspace. +verified: false +tags: [agent, poolside, pool, ai, ai-gateway] +--- + +# Pool CLI + +Install and configure [Pool](https://docs.poolside.ai/cli/pool), Poolside's coding agent, in your workspace. + +```tf +module "pool" { + source = "registry.coder.com/coder-labs/pool/coder" + version = "0.1.0" + agent_id = coder_agent.main.id + + poolside_api_key = var.poolside_api_key +} +``` + +The module accepts Pool's EULA noninteractively during installation, installs the CLI in `~/.local/bin` by default, and makes `pool` available to Coder scripts and interactive shells. + +## AI Gateway + +[AI Gateway](https://coder.com/docs/ai-coder/ai-gateway) is a Premium Coder feature that provides centralized LLM proxy management. Requires Coder >= 2.30.0. + +Pool supports OpenAI-compatible endpoints through `POOLSIDE_STANDALONE_BASE_URL`. Set `enable_ai_gateway = true` to configure the Coder AI Gateway endpoint and authenticate Pool with the workspace owner's Coder session token. + +```tf +module "pool" { + source = "registry.coder.com/coder-labs/pool/coder" + version = "0.1.0" + agent_id = coder_agent.main.id + + enable_ai_gateway = true + model = "gpt-5" +} +``` + +> [!CAUTION] +> `enable_ai_gateway = true` is mutually exclusive with `poolside_api_key` and `standalone_base_url`. AI Gateway supplies both the endpoint and authentication. + +## OpenAI-compatible endpoints + +Use `standalone_base_url` to configure another OpenAI-compatible proxy or local inference server. Provide `poolside_api_key` when that endpoint requires authentication; `model` selects the model if the endpoint does not provide a default. + +```tf +module "pool" { + source = "registry.coder.com/coder-labs/pool/coder" + version = "0.1.0" + agent_id = coder_agent.main.id + + poolside_api_key = var.gateway_api_key + standalone_base_url = "https://gateway.example.com/v1" + model = "my-coding-model" +} +``` + +For a Poolside deployment, use `poolside_api_url` to set `POOLSIDE_API_URL` instead. + +## Existing installations + +Set `install_pool = false` when Pool CLI is already present in your workspace image. Configure `pool_binary_path` if the binary is in a directory other than `~/.local/bin`. + +## Serialize a downstream `coder_script` after installation + +The `scripts` output is an ordered list of `coder exp sync` names created by the module. + +```tf +resource "coder_script" "verify_pool" { + agent_id = coder_agent.main.id + display_name = "Verify Pool CLI" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -euo pipefail + trap 'coder exp sync complete verify-pool' EXIT + coder exp sync want verify-pool ${join(" ", module.pool.scripts)} + coder exp sync start verify-pool + + pool --version + EOT +} +``` + +## Troubleshooting + +The module's script logs are under `~/.coder-modules/coder-labs/pool/logs/`. + +```bash +cat ~/.coder-modules/coder-labs/pool/logs/install.log +cat ~/.coder-modules/coder-labs/pool/logs/pre_install.log +cat ~/.coder-modules/coder-labs/pool/logs/post_install.log +``` + +## References + +- [Pool CLI documentation](https://docs.poolside.ai/cli/pool) +- [Pool CLI installation](https://docs.poolside.ai/cli/install) +- [AI Gateway](https://coder.com/docs/ai-coder/ai-gateway) diff --git a/registry/coder-labs/modules/pool/main.tf b/registry/coder-labs/modules/pool/main.tf new file mode 100644 index 000000000..d38cbbd24 --- /dev/null +++ b/registry/coder-labs/modules/pool/main.tf @@ -0,0 +1,159 @@ +terraform { + required_version = ">= 1.9" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.12" + } + } +} + +variable "agent_id" { + description = "The ID of a Coder agent." + type = string +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +variable "icon" { + description = "The icon to use for the app." + type = string + default = "/icon/poolside.svg" +} + +variable "pre_install_script" { + description = "Custom script to run before installing Pool CLI." + type = string + default = null +} + +variable "post_install_script" { + description = "Custom script to run after installing Pool CLI." + type = string + default = null +} + +variable "install_pool" { + description = "Whether to install Pool CLI. Set false when Pool is already installed in the workspace image." + type = bool + default = true +} + +variable "pool_binary_path" { + description = "Directory containing the Pool CLI binary. The installer uses this directory when install_pool is true." + type = string + default = "$HOME/.local/bin" +} + +variable "poolside_api_key" { + description = "Poolside API key passed to Pool CLI via POOLSIDE_API_KEY." + type = string + default = "" + sensitive = true +} + +variable "poolside_api_url" { + description = "Optional Poolside deployment API URL passed to Pool CLI via POOLSIDE_API_URL." + type = string + default = "" +} + +variable "standalone_base_url" { + description = "Optional OpenAI-compatible API base URL passed to Pool CLI via POOLSIDE_STANDALONE_BASE_URL. Use this for a non-Coder gateway or local inference server." + type = string + default = "" + + validation { + condition = !(var.enable_ai_gateway && var.standalone_base_url != "") + error_message = "standalone_base_url cannot be provided when enable_ai_gateway is true." + } +} + +variable "model" { + description = "Optional model passed to Pool CLI via POOLSIDE_STANDALONE_MODEL when using an OpenAI-compatible API or Coder AI Gateway." + type = string + default = "" +} + +variable "enable_ai_gateway" { + description = "Use Coder AI Gateway through its OpenAI-compatible endpoint. https://coder.com/docs/ai-coder/ai-gateway" + type = bool + default = false + + validation { + condition = !(var.enable_ai_gateway && var.poolside_api_key != "") + error_message = "poolside_api_key cannot be provided when enable_ai_gateway is true. AI Gateway automatically authenticates Pool CLI using Coder credentials." + } +} + +resource "coder_env" "poolside_api_key" { + count = var.poolside_api_key != "" ? 1 : 0 + agent_id = var.agent_id + name = "POOLSIDE_API_KEY" + value = var.poolside_api_key +} + +resource "coder_env" "poolside_api_url" { + count = var.poolside_api_url != "" ? 1 : 0 + agent_id = var.agent_id + name = "POOLSIDE_API_URL" + value = var.poolside_api_url +} + +resource "coder_env" "standalone_base_url" { + count = var.standalone_base_url != "" ? 1 : 0 + agent_id = var.agent_id + name = "POOLSIDE_STANDALONE_BASE_URL" + value = var.standalone_base_url +} + +resource "coder_env" "model" { + count = var.model != "" ? 1 : 0 + agent_id = var.agent_id + name = "POOLSIDE_STANDALONE_MODEL" + value = var.model +} + +# Pool CLI uses POOLSIDE_API_KEY for OpenAI-compatible API authentication. +# Coder AI Gateway accepts the workspace owner's session token as a bearer token. +resource "coder_env" "ai_gateway_session_token" { + count = var.enable_ai_gateway ? 1 : 0 + agent_id = var.agent_id + name = "POOLSIDE_API_KEY" + value = data.coder_workspace_owner.me.session_token +} + +resource "coder_env" "ai_gateway_base_url" { + count = var.enable_ai_gateway ? 1 : 0 + agent_id = var.agent_id + name = "POOLSIDE_STANDALONE_BASE_URL" + value = "${trimsuffix(data.coder_workspace.me.access_url, "/")}/api/v2/ai-gateway/openai/v1" +} + +locals { + install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_INSTALL_POOL = tostring(var.install_pool) + ARG_POOL_BINARY_PATH = var.pool_binary_path + }) +} + +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "0.0.1" + + agent_id = var.agent_id + module_directory = "$HOME/.coder-modules/coder-labs/pool" + display_name_prefix = "Pool CLI" + icon = var.icon + pre_install_script = var.pre_install_script + install_script = local.install_script + post_install_script = var.post_install_script +} + +output "scripts" { + description = "Ordered list of coder exp sync names produced by this module, in run order." + value = module.coder_utils.scripts +} diff --git a/registry/coder-labs/modules/pool/main.tftest.hcl b/registry/coder-labs/modules/pool/main.tftest.hcl new file mode 100644 index 000000000..ffaf7cd3e --- /dev/null +++ b/registry/coder-labs/modules/pool/main.tftest.hcl @@ -0,0 +1,122 @@ +run "test_pool_defaults" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = var.install_pool == true + error_message = "install_pool should default to true" + } + + assert { + condition = var.pool_binary_path == "$HOME/.local/bin" + error_message = "pool_binary_path should default to $HOME/.local/bin" + } +} + +run "test_pool_with_api_key" { + command = plan + + variables { + agent_id = "test-agent" + poolside_api_key = "test-key" + } + + assert { + condition = coder_env.poolside_api_key[0].value == "test-key" + error_message = "POOLSIDE_API_KEY should use the supplied key" + } +} + +run "test_standalone_endpoint" { + command = plan + + variables { + agent_id = "test-agent" + standalone_base_url = "https://gateway.example.com/v1" + model = "test-model" + } + + assert { + condition = coder_env.standalone_base_url[0].value == "https://gateway.example.com/v1" + error_message = "POOLSIDE_STANDALONE_BASE_URL should use the supplied endpoint" + } + + assert { + condition = coder_env.model[0].value == "test-model" + error_message = "POOLSIDE_STANDALONE_MODEL should use the supplied model" + } +} + +run "test_ai_gateway_enabled" { + command = plan + + variables { + agent_id = "test-agent" + enable_ai_gateway = true + model = "gpt-5" + } + + override_data { + target = data.coder_workspace_owner.me + values = { + session_token = "mock-session-token" + } + } + + assert { + condition = coder_env.ai_gateway_session_token[0].value == data.coder_workspace_owner.me.session_token + error_message = "AI Gateway should use the workspace owner's session token" + } + + assert { + condition = coder_env.ai_gateway_base_url[0].name == "POOLSIDE_STANDALONE_BASE_URL" + error_message = "AI Gateway should configure Pool's OpenAI-compatible endpoint" + } + + assert { + condition = length(coder_env.poolside_api_key) == 0 + error_message = "A direct Poolside API key should not be set when AI Gateway is enabled" + } +} + +run "test_ai_gateway_rejects_api_key" { + command = plan + + variables { + agent_id = "test-agent" + enable_ai_gateway = true + poolside_api_key = "test-key" + } + + expect_failures = [var.enable_ai_gateway] +} + +run "test_ai_gateway_rejects_standalone_endpoint" { + command = plan + + variables { + agent_id = "test-agent" + enable_ai_gateway = true + standalone_base_url = "https://gateway.example.com/v1" + } + + expect_failures = [var.standalone_base_url] +} + +run "test_scripts_output" { + command = plan + + variables { + agent_id = "test-agent" + pre_install_script = "echo pre" + post_install_script = "echo post" + } + + assert { + condition = length(output.scripts) == 3 + error_message = "scripts should include pre-install, install, and post-install scripts" + } +} diff --git a/registry/coder-labs/modules/pool/scripts/install.sh.tftpl b/registry/coder-labs/modules/pool/scripts/install.sh.tftpl new file mode 100644 index 000000000..55cb730a8 --- /dev/null +++ b/registry/coder-labs/modules/pool/scripts/install.sh.tftpl @@ -0,0 +1,61 @@ +#!/bin/bash + +set -euo pipefail + +ARG_INSTALL_POOL='${ARG_INSTALL_POOL}' +ARG_POOL_BINARY_PATH='${ARG_POOL_BINARY_PATH}' +ARG_POOL_BINARY_PATH="$${ARG_POOL_BINARY_PATH/#\~/$HOME}" +ARG_POOL_BINARY_PATH="$${ARG_POOL_BINARY_PATH//\$HOME/$HOME}" + +add_path_to_shell_profiles() { + local path_dir="$1" + + for profile in "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.zprofile" "$HOME/.zshrc"; do + if [ -f "$${profile}" ] && ! grep -qF "$${path_dir}" "$${profile}" 2> /dev/null; then + echo "export PATH=\"$${path_dir}:\$PATH\"" >> "$${profile}" + fi + done +} + +ensure_pool_in_path() { + local pool_bin="" + if command -v pool > /dev/null 2>&1; then + pool_bin=$(command -v pool) + elif [ -x "$${ARG_POOL_BINARY_PATH}/pool" ]; then + pool_bin="$${ARG_POOL_BINARY_PATH}/pool" + fi + + if [ -z "$${pool_bin}" ]; then + echo "Warning: Could not find the pool binary" + return + fi + + local pool_dir + pool_dir=$(dirname "$${pool_bin}") + if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$${CODER_SCRIPT_BIN_DIR}/pool" ]; then + ln -s "$${pool_bin}" "$${CODER_SCRIPT_BIN_DIR}/pool" + fi + + add_path_to_shell_profiles "$${pool_dir}" +} + +install_pool() { + if [ "$${ARG_INSTALL_POOL}" != "true" ]; then + echo "Skipping Pool CLI installation as configured." + ensure_pool_in_path + return + fi + + echo "Installing Pool CLI" + mkdir -p "$${ARG_POOL_BINARY_PATH}" + POOL_INSTALL_ACCEPT_EULA=1 \ + POOL_INSTALL_DIR="$${ARG_POOL_BINARY_PATH}" \ + POOL_INSTALL_UPDATE_PATH=1 \ + curl -fsSL https://downloads.poolside.ai/pool/install.sh | sh + + export PATH="$${ARG_POOL_BINARY_PATH}:$PATH" + echo "Installed Pool CLI: $(pool --version)" + ensure_pool_in_path +} + +install_pool