diff --git a/.icons/omnigent.svg b/.icons/omnigent.svg new file mode 100644 index 000000000..b70b60659 --- /dev/null +++ b/.icons/omnigent.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/coder-labs/modules/omnigent/README.md b/registry/coder-labs/modules/omnigent/README.md new file mode 100644 index 000000000..710189b90 --- /dev/null +++ b/registry/coder-labs/modules/omnigent/README.md @@ -0,0 +1,144 @@ +--- +display_name: Omnigent +icon: ../../../../.icons/omnigent.svg +description: Run a private Omnigent multi-agent coding server in your workspace. +verified: false +tags: [agent, omnigent, ai, multi-agent] +--- + +# Omnigent + +Run a private [Omnigent](https://github.com/omnigent-dev) multi-agent coding orchestrator server inside your Coder workspace. Each workspace gets its own isolated Omnigent instance with a stable, derived admin password — no shared credentials, no manual password management. + +The module installs Omnigent via the [official install script](https://omnigent.ai/install.sh), starts the server on a configurable port, waits for the health endpoint, and registers the local workspace as a host. The admin password is derived from the workspace ID at runtime and never stored in Terraform state. + +```tf +module "omnigent" { + source = "registry.coder.com/coder-labs/omnigent/coder" + version = "1.0.0" + agent_id = coder_agent.main.id +} +``` + +## Examples + +### With a custom port + +```tf +module "omnigent" { + source = "registry.coder.com/coder-labs/omnigent/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + port = 7878 +} +``` + +### With AI tools (Omnigent + Claude Code + Codex) + +Compose Omnigent alongside other AI agent modules to create a full multi-agent workspace. This example authenticates Claude Code and Codex through Coder AI Gateway. + +```tf +module "codex" { + source = "registry.coder.com/coder-labs/codex/coder" + version = "5.0.0" + + agent_id = coder_agent.main.id + enable_ai_gateway = true +} + +module "claude_code" { + source = "registry.coder.com/coder/claude-code/coder" + version = ">= 4.0.0" + + agent_id = coder_agent.main.id + enable_ai_gateway = true +} + +module "omnigent" { + source = "registry.coder.com/coder-labs/omnigent/coder" + version = "1.0.0" + + agent_id = coder_agent.main.id +} +``` + +### Policies (server-wide) + +```tf +module "omnigent" { + source = "registry.coder.com/coder-labs/omnigent/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + + server_config = <<-YAML + policies: + cap_tool_calls: + type: function + handler: omnigent.policies.builtins.safety.max_tool_calls_per_session + factory_params: + limit: 50 + require_approval: + type: function + handler: omnigent.policies.builtins.safety.ask_on_os_tools + YAML +} +``` + +### Custom agents + +```tf +module "omnigent" { + source = "registry.coder.com/coder-labs/omnigent/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + + agents = [ + { + name = "reviewer" + content = <<-YAML + name: reviewer + instructions: You are an expert code reviewer. Focus on correctness, security, and clarity. + executor: + harness: claude-sdk + model: claude-sonnet-4-5 + YAML + } + ] +} +``` + +### Bring-your-own config file + +```tf +module "omnigent" { + source = "registry.coder.com/coder-labs/omnigent/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + server_config_path = "/home/coder/.omnigent/server_config.yaml" +} +``` + +## Troubleshooting + +Script logs are written to `~/.coder-modules/coder-labs/omnigent/logs/`. If the Omnigent app shows as unhealthy or the server fails to start, check: + +```bash +cat ~/.coder-modules/coder-labs/omnigent/logs/server.log +cat ~/.coder-modules/coder-labs/omnigent/logs/start.log +cat ~/.coder-modules/coder-labs/omnigent/logs/install.log +cat ~/.coder-modules/coder-labs/omnigent/logs/host.log +``` + +The health endpoint is available at `http://localhost:/health`. You can check it directly: + +```bash +curl -sf http://localhost:6767/health && echo "healthy" || echo "not ready" +``` + +### Finding the admin password + +The admin password is derived from the workspace ID at runtime. To retrieve it inside the workspace: + +```bash +echo -n "$CODER_WORKSPACE_ID" | tr -d '-' | cut -c1-16 +``` diff --git a/registry/coder-labs/modules/omnigent/main.tf b/registry/coder-labs/modules/omnigent/main.tf new file mode 100644 index 000000000..7b758a3d1 --- /dev/null +++ b/registry/coder-labs/modules/omnigent/main.tf @@ -0,0 +1,165 @@ +terraform { + required_version = ">= 1.9" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13" + } + } +} + +variable "agent_id" { + description = "The ID of a Coder agent." + type = string +} + +variable "icon" { + description = "Icon for Omnigent scripts and app." + type = string + default = "../../../../.icons/omnigent.svg" +} + +variable "port" { + description = "Port the Omnigent server listens on inside the workspace." + type = number + default = 6767 + validation { + condition = var.port > 1024 && var.port < 65536 + error_message = "port must be between 1025 and 65535." + } +} + +variable "omnigent_version" { + description = "Omnigent version to install. 'latest' installs the newest release." + type = string + default = "latest" +} + +variable "share" { + description = "Coder app share level." + type = string + default = "owner" + validation { + condition = contains(["owner", "authenticated", "public"], var.share) + error_message = "share must be one of: owner, authenticated, public." + } +} + +variable "order" { + description = "Order for the Omnigent app in the Coder UI." + type = number + default = null +} + +variable "server_config" { + description = "Inline server_config.yaml content for the Omnigent server. Supports policies, policy_modules, admins, and allowed_domains keys. When set, written to the module directory and passed as -c to the server. Mutually exclusive with server_config_path." + type = string + default = null + validation { + condition = !(var.server_config != null && var.server_config_path != null) + error_message = "Only one of server_config or server_config_path may be set." + } +} + +variable "server_config_path" { + description = "Path to an existing server_config.yaml in the workspace. When set, passed directly as -c to the server; no config file is written by this module. Mutually exclusive with server_config." + type = string + default = null +} + +variable "agents" { + description = "Custom agent YAML definitions to pre-register at server startup. Each entry is written to the module directory and passed as --agent flags." + type = list(object({ + name = string + content = string + })) + default = [] +} + +variable "pre_install_script" { + description = "Custom script to run before installing Omnigent." + type = string + default = null +} + +variable "post_install_script" { + description = "Custom script to run after installing Omnigent." + type = string + default = null +} + +locals { + module_dir = "$HOME/.coder-modules/coder-labs/omnigent" + server_config_file = "${local.module_dir}/config/server.yaml" + agents_dir = "${local.module_dir}/agents" + + effective_server_config_path = ( + var.server_config_path != null ? var.server_config_path : + var.server_config != null ? local.server_config_file : + null + ) + + install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_OMNIGENT_VERSION_B64 = var.omnigent_version != "latest" ? base64encode(var.omnigent_version) : "" + ARG_PORT = tostring(var.port) + ARG_WRITE_SERVER_CONFIG = tostring(var.server_config != null) + ARG_SERVER_CONFIG_B64 = var.server_config != null ? base64encode(var.server_config) : "" + ARG_SERVER_CONFIG_FILE = local.server_config_file + ARG_SERVER_CONFIG_DIR = "${local.module_dir}/config" + ARG_AGENTS_B64 = length(var.agents) > 0 ? base64encode(join("\n", [for a in var.agents : "${a.name}\t${base64encode(a.content)}"])) : "" + ARG_AGENTS_DIR = local.agents_dir + }) + + start_script = templatefile("${path.module}/scripts/start.sh.tftpl", { + ARG_PORT = tostring(var.port) + ARG_EFFECTIVE_SERVER_CONFIG_PATH = local.effective_server_config_path != null ? local.effective_server_config_path : "" + ARG_AGENTS_DIR = local.agents_dir + }) +} + +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "0.0.1" + + agent_id = var.agent_id + module_directory = local.module_dir + display_name_prefix = "Omnigent" + icon = var.icon + pre_install_script = var.pre_install_script + post_install_script = var.post_install_script + install_script = local.install_script + start_script = local.start_script +} + +resource "coder_app" "omnigent" { + agent_id = var.agent_id + slug = "omnigent" + display_name = "Omnigent" + url = "http://localhost:${var.port}" + icon = var.icon + subdomain = true + share = var.share + order = var.order + + healthcheck { + url = "http://localhost:${var.port}/health" + interval = 15 + threshold = 3 + } +} + +output "scripts" { + description = "Ordered list of coder exp sync names produced by this module, in run order." + value = module.coder_utils.scripts +} + +output "port" { + description = "Port the Omnigent server is listening on." + value = var.port +} + +output "server_config_path" { + description = "Effective path to the server config file, or empty string if no config is used." + value = local.effective_server_config_path != null ? local.effective_server_config_path : "" +} diff --git a/registry/coder-labs/modules/omnigent/main.tftest.hcl b/registry/coder-labs/modules/omnigent/main.tftest.hcl new file mode 100644 index 000000000..341703165 --- /dev/null +++ b/registry/coder-labs/modules/omnigent/main.tftest.hcl @@ -0,0 +1,239 @@ +run "test_defaults" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = var.port == 6767 + error_message = "port should default to 6767" + } + + assert { + condition = var.share == "owner" + error_message = "share should default to owner" + } + + assert { + condition = var.omnigent_version == "latest" + error_message = "omnigent_version should default to latest" + } + + assert { + condition = coder_app.omnigent.url == "http://localhost:6767" + error_message = "coder_app url should use default port 6767" + } + + assert { + condition = coder_app.omnigent.share == "owner" + error_message = "coder_app share should default to owner" + } +} + +run "test_custom_port" { + command = plan + + variables { + agent_id = "test-agent" + port = 8080 + } + + assert { + condition = var.port == 8080 + error_message = "port should be set to 8080" + } + + assert { + condition = coder_app.omnigent.url == "http://localhost:8080" + error_message = "coder_app url should use custom port 8080" + } +} + +run "test_custom_share" { + command = plan + + variables { + agent_id = "test-agent" + share = "authenticated" + } + + assert { + condition = var.share == "authenticated" + error_message = "share should be set to authenticated" + } + + assert { + condition = coder_app.omnigent.share == "authenticated" + error_message = "coder_app share should be authenticated" + } +} + +run "test_custom_version" { + command = plan + + variables { + agent_id = "test-agent" + omnigent_version = "0.1.0" + } + + assert { + condition = var.omnigent_version == "0.1.0" + error_message = "omnigent_version should be set to 0.1.0" + } +} + +run "test_scripts_output" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = length(output.scripts) > 0 + error_message = "scripts output should be non-empty" + } +} + +run "test_install_script_installs_uv" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = strcontains(local.install_script, "https://astral.sh/uv/install.sh") + error_message = "install script should install uv when it is missing" + } + + assert { + condition = strcontains(local.install_script, "command -v uv") + error_message = "install script should check whether uv is available" + } +} + +run "test_start_script_backgrounds_host" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = strcontains(local.start_script, "nohup omnigent host") + error_message = "start script should run the Omnigent host in the background" + } + + assert { + condition = strcontains(local.start_script, "host.log") + error_message = "start script should write Omnigent host logs to host.log" + } +} + +run "test_port_output" { + command = plan + + variables { + agent_id = "test-agent" + port = 7777 + } + + assert { + condition = output.port == 7777 + error_message = "port output should match the configured port" + } +} + +run "test_invalid_port_low" { + command = plan + + variables { + agent_id = "test-agent" + port = 80 + } + + expect_failures = [var.port] +} + +run "test_invalid_port_high" { + command = plan + + variables { + agent_id = "test-agent" + port = 65536 + } + + expect_failures = [var.port] +} + +run "test_invalid_share" { + command = plan + + variables { + agent_id = "test-agent" + share = "invalid" + } + + expect_failures = [var.share] +} + +run "test_server_config" { + command = plan + + variables { + agent_id = "test-agent" + server_config = "policies: {}" + } + + assert { + condition = var.server_config == "policies: {}" + error_message = "server_config should be set" + } +} + +run "test_server_config_path" { + command = plan + + variables { + agent_id = "test-agent" + server_config_path = "/home/coder/.omnigent/server.yaml" + } + + assert { + condition = output.server_config_path == "/home/coder/.omnigent/server.yaml" + error_message = "server_config_path output should match the provided path" + } +} + +run "test_server_config_mutual_exclusion" { + command = plan + + variables { + agent_id = "test-agent" + server_config = "policies: {}" + server_config_path = "/home/coder/.omnigent/server.yaml" + } + + expect_failures = [var.server_config] +} + +run "test_agents" { + command = plan + + variables { + agent_id = "test-agent" + agents = [ + { + name = "reviewer" + content = "name: reviewer\ninstructions: You are a reviewer." + } + ] + } + + assert { + condition = length(var.agents) == 1 + error_message = "agents should have one entry" + } +} diff --git a/registry/coder-labs/modules/omnigent/scripts/install.sh.tftpl b/registry/coder-labs/modules/omnigent/scripts/install.sh.tftpl new file mode 100644 index 000000000..b042b025b --- /dev/null +++ b/registry/coder-labs/modules/omnigent/scripts/install.sh.tftpl @@ -0,0 +1,74 @@ +#!/bin/bash +set -euo pipefail + +BOLD='\033[0;1m' + +ARG_OMNIGENT_VERSION=$(echo -n '${ARG_OMNIGENT_VERSION_B64}' | base64 -d) +# Empty = latest; non-empty = pinned version +ARG_PORT='${ARG_PORT}' +ARG_WRITE_SERVER_CONFIG='${ARG_WRITE_SERVER_CONFIG}' +ARG_SERVER_CONFIG=$(echo -n '${ARG_SERVER_CONFIG_B64}' | base64 -d) +ARG_SERVER_CONFIG_FILE='${ARG_SERVER_CONFIG_FILE}' +ARG_SERVER_CONFIG_DIR='${ARG_SERVER_CONFIG_DIR}' +ARG_AGENTS=$(echo -n '${ARG_AGENTS_B64}' | base64 -d) +ARG_AGENTS_DIR='${ARG_AGENTS_DIR}' + +export PATH="$${HOME}/.local/bin:$${PATH}" + +echo "--------------------------------" +printf "omnigent_version: %s\n" "$${ARG_OMNIGENT_VERSION:-latest}" +printf "port: %s\n" "$${ARG_PORT}" +printf "write_server_config: %s\n" "$${ARG_WRITE_SERVER_CONFIG}" +echo "--------------------------------" + +if ! command -v curl >/dev/null 2>&1; then + echo "ERROR: curl is required to install uv and Omnigent." >&2 + exit 1 +fi + +if ! command -v uv >/dev/null 2>&1; then + printf "%s Installing uv\n" "$${BOLD}" + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$${HOME}/.local/bin:$${PATH}" +fi + +if ! command -v uv >/dev/null 2>&1; then + echo "ERROR: uv installation failed. Install from https://docs.astral.sh/uv/getting-started/installation/, then rerun." >&2 + exit 1 +fi + +printf "%s Found uv: %s\n" "$${BOLD}" "$(uv --version)" + +# Install omnigent via official installer +INSTALL_ARGS="--non-interactive" +if [ -n "$${ARG_OMNIGENT_VERSION}" ]; then + INSTALL_ARGS="$${INSTALL_ARGS} --version $${ARG_OMNIGENT_VERSION}" +fi +printf "%s Installing omnigent%s\n" "$${BOLD}" "$${ARG_OMNIGENT_VERSION:+ $${ARG_OMNIGENT_VERSION}}" +# shellcheck disable=SC2086 +curl -fsSL https://omnigent.ai/install.sh | sh -s -- $${INSTALL_ARGS} + +export PATH="$${HOME}/.local/bin:$${PATH}" + +printf "%s Installed omnigent: %s\n" "$${BOLD}" "$(omnigent --version)" + +# Configure client to point to the local server +omnigent config set server=http://localhost:$${ARG_PORT} + +# Write server config file if provided +if [ "$${ARG_WRITE_SERVER_CONFIG}" = "true" ]; then + mkdir -p "$${ARG_SERVER_CONFIG_DIR}" + printf "%s Writing server config to %s\n" "$${BOLD}" "$${ARG_SERVER_CONFIG_FILE}" + echo "$${ARG_SERVER_CONFIG}" > "$${ARG_SERVER_CONFIG_FILE}" +fi + +# Write agent YAML files +if [ -n "$${ARG_AGENTS}" ]; then + mkdir -p "$${ARG_AGENTS_DIR}" + while IFS=$'\t' read -r agent_name agent_content_b64; do + [ -z "$${agent_name}" ] && continue + agent_file="$${ARG_AGENTS_DIR}/$${agent_name}.yaml" + printf "%s Writing agent: %s -> %s\n" "$${BOLD}" "$${agent_name}" "$${agent_file}" + echo -n "$${agent_content_b64}" | base64 -d > "$${agent_file}" + done <<< "$${ARG_AGENTS}" +fi diff --git a/registry/coder-labs/modules/omnigent/scripts/start.sh.tftpl b/registry/coder-labs/modules/omnigent/scripts/start.sh.tftpl new file mode 100644 index 000000000..f569fbc6d --- /dev/null +++ b/registry/coder-labs/modules/omnigent/scripts/start.sh.tftpl @@ -0,0 +1,68 @@ +#!/bin/bash +set -euo pipefail + +export PATH="$${HOME}/.local/bin:$${PATH}" + +MODULE_DIR="$${HOME}/.coder-modules/coder-labs/omnigent" +START_LOG="$${MODULE_DIR}/logs/start.log" +SERVER_LOG="$${MODULE_DIR}/logs/server.log" +HOST_LOG="$${MODULE_DIR}/logs/host.log" +ARG_PORT='${ARG_PORT}' +ARG_EFFECTIVE_SERVER_CONFIG_PATH='${ARG_EFFECTIVE_SERVER_CONFIG_PATH}' +ARG_AGENTS_DIR='${ARG_AGENTS_DIR}' + +mkdir -p "$${MODULE_DIR}/logs" + +# Derive a stable admin password from the workspace ID (first 16 hex chars) +OMNIGENT_ADMIN_PASSWORD=$(echo -n "$${CODER_WORKSPACE_ID}" | tr -d '-' | cut -c1-16) + +if ! curl -sf "http://localhost:$${ARG_PORT}/health" &>/dev/null; then + echo "Starting Omnigent server on port $${ARG_PORT}..." + + # Build server flags + SERVER_FLAGS="--host 127.0.0.1 --port $${ARG_PORT} --no-open" + + # Add config file if set and present + if [ -n "$${ARG_EFFECTIVE_SERVER_CONFIG_PATH}" ] && [ -f "$${ARG_EFFECTIVE_SERVER_CONFIG_PATH}" ]; then + SERVER_FLAGS="$${SERVER_FLAGS} -c $${ARG_EFFECTIVE_SERVER_CONFIG_PATH}" + echo "Using server config: $${ARG_EFFECTIVE_SERVER_CONFIG_PATH}" + fi + + # Add pre-registered agent YAML files + if [ -d "$${ARG_AGENTS_DIR}" ]; then + for agent_file in "$${ARG_AGENTS_DIR}"/*.yaml; do + [ -f "$${agent_file}" ] || continue + SERVER_FLAGS="$${SERVER_FLAGS} --agent $${agent_file}" + echo "Registering agent: $${agent_file}" + done + fi + + export OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD="$${OMNIGENT_ADMIN_PASSWORD}" + # shellcheck disable=SC2086 + nohup omnigent server $${SERVER_FLAGS} >> "$${SERVER_LOG}" 2>&1 & +else + echo "Omnigent server already running on port $${ARG_PORT}, skipping start." +fi + +echo "Waiting for Omnigent server..." +for i in $(seq 1 90); do + if curl -sf "http://localhost:$${ARG_PORT}/health" &>/dev/null; then + echo "Omnigent server is ready." + break + fi + if [ "$${i}" -eq 90 ]; then + echo "ERROR: Omnigent server did not start within 90 seconds." >&2 + cat "$${SERVER_LOG}" >&2 || true + exit 1 + fi + sleep 1 +done + +# Register local workspace as a host. `omnigent host` stays attached, so run it +# in the background to let the Coder startup script finish. +if ! pgrep -f "[o]mnigent host" >/dev/null 2>&1; then + echo "Starting Omnigent host..." + nohup omnigent host "" >> "$${HOST_LOG}" 2>&1 & +else + echo "Omnigent host already running, skipping start." +fi diff --git a/registry/coder-labs/templates/omnigent-workspace/README.md b/registry/coder-labs/templates/omnigent-workspace/README.md new file mode 100644 index 000000000..8cef9480b --- /dev/null +++ b/registry/coder-labs/templates/omnigent-workspace/README.md @@ -0,0 +1,49 @@ +--- +display_name: Omnigent Workspace +icon: ../../../../.icons/omnigent.svg +description: Docker workspace with Omnigent, Claude Code, and Codex pre-installed. +verified: false +tags: [docker, omnigent, claude-code, codex, ai, multi-agent] +--- + +# Omnigent Workspace + +A Docker-based workspace that combines three AI agent modules: + +- **[Omnigent](https://registry.coder.com/modules/coder-labs/omnigent)** — private multi-agent coding orchestrator server +- **[Claude Code](https://registry.coder.com/modules/coder/claude-code)** — Anthropic's Claude in your terminal, authenticated through Coder AI Gateway +- **[Codex](https://registry.coder.com/modules/coder-labs/codex)** — OpenAI's Codex CLI, authenticated through Coder AI Gateway + +Each workspace runs its own isolated Omnigent server. The admin password is derived from the workspace ID at runtime and never stored in Terraform state. + +```tf +module "codex" { + source = "registry.coder.com/coder-labs/codex/coder" + version = "5.2.0" + + agent_id = coder_agent.main.id + enable_ai_gateway = true +} + +module "claude_code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "5.2.0" + + agent_id = coder_agent.main.id + enable_ai_gateway = true +} + +module "omnigent" { + source = "registry.coder.com/coder-labs/omnigent/coder" + version = "1.0.0" + + agent_id = coder_agent.main.id +} +``` + +## Prerequisites + +- Docker with `sysbox-runc` runtime installed on the Coder host +- Coder Premium with AI Gateway enabled + +The template installs `jq`, `tmux`, and `bubblewrap` before the AI tools start because the Claude Code module uses `jq` for setup and Omnigent launches the Claude Code and Codex harnesses through local terminal sessions. diff --git a/registry/coder-labs/templates/omnigent-workspace/main.tf b/registry/coder-labs/templates/omnigent-workspace/main.tf new file mode 100644 index 000000000..a3727f068 --- /dev/null +++ b/registry/coder-labs/templates/omnigent-workspace/main.tf @@ -0,0 +1,189 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 4.0" + } + } +} + +provider "coder" {} +provider "docker" {} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} +data "coder_provisioner" "me" {} + +locals { + ai_tools_pre_install_script = <<-EOT + #!/bin/bash + set -euo pipefail + + if command -v apt-get >/dev/null 2>&1; then + ( + flock 9 + sudo apt-get update + sudo apt-get install -y curl ca-certificates jq tmux bubblewrap + ) 9>/tmp/coder-ai-tools-apt.lock + fi + EOT +} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = <<-EOT + #!/bin/bash + set -e + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ 2>/dev/null || true + touch ~/.init_done + fi + EOT + + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email + } + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "2_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } +} + +module "codex" { + source = "registry.coder.com/coder-labs/codex/coder" + version = "5.2.0" + + agent_id = coder_agent.main.id + enable_ai_gateway = true + pre_install_script = local.ai_tools_pre_install_script +} + +module "claude_code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "5.2.0" + + agent_id = coder_agent.main.id + enable_ai_gateway = true + pre_install_script = local.ai_tools_pre_install_script +} + +module "omnigent" { + source = "registry.coder.com/coder-labs/omnigent/coder" + version = "1.0.0" + + agent_id = coder_agent.main.id + + # Omnigent snapshots the host's available tools when the host starts. Wait for + # Claude Code and Codex to install and configure AI Gateway first, otherwise + # the Omnigent UI shows these harnesses as needing setup until the host restarts. + pre_install_script = <<-EOT + #!/bin/bash + set -euo pipefail + coder exp sync want coder-labs-omnigent-ai-tools ${join(" ", concat(module.claude_code.scripts, module.codex.scripts))} + coder exp sync start coder-labs-omnigent-ai-tools + coder exp sync complete coder-labs-omnigent-ai-tools + EOT +} + +resource "docker_volume" "home_volume" { + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}-home" + lifecycle { + ignore_changes = all + } + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name_at_creation" + value = data.coder_workspace.me.name + } +} + +data "docker_registry_image" "workspace" { + name = "codercom/enterprise-base:ubuntu" +} + +resource "docker_image" "workspace" { + name = "codercom/enterprise-base@${data.docker_registry_image.workspace.sha256_digest}" + pull_triggers = [data.docker_registry_image.workspace.sha256_digest] + keep_locally = true +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.workspace.image_id + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + hostname = lower(data.coder_workspace.me.name) + runtime = "sysbox-runc" + + entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")] + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.main.token}", + ] + + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + volumes { + container_path = "/home/coder" + volume_name = docker_volume.home_volume.name + read_only = false + } + + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +}